diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
new file mode 100644
index 0000000..0df9b1e
--- /dev/null
+++ b/.github/workflows/pages.yml
@@ -0,0 +1,73 @@
+name: Pages
+
+on:
+ push:
+ branches: [main, brand]
+ paths:
+ - "site/**"
+ - "brand/**"
+ - "tools/build-docs.mjs"
+ - "tools/docs-build-mbt/**"
+ - ".github/workflows/pages.yml"
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+
+ - name: Install MoonBit toolchain
+ run: |
+ curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash
+ echo "$HOME/.moon/bin" >> "$GITHUB_PATH"
+
+ - name: Build MoonBit docs renderer
+ working-directory: tools/docs-build-mbt
+ run: |
+ moon update
+ moon build --target js --release
+
+ - name: Refresh brand assets in site
+ run: |
+ mkdir -p site/assets/img
+ cp brand/tokens/tokens.css site/assets/tokens.css
+ cp brand/logo/*.svg site/assets/img/
+ touch site/.nojekyll
+
+ - name: Render markdown to HTML
+ run: node tools/build-docs.mjs
+
+ - name: Configure Pages
+ uses: actions/configure-pages@v5
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: ./site
+
+ deploy:
+ needs: build
+ runs-on: ubuntu-latest
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4
diff --git a/brand/README.md b/brand/README.md
new file mode 100644
index 0000000..9496560
--- /dev/null
+++ b/brand/README.md
@@ -0,0 +1,191 @@
+# bit — Brand
+
+A visual identity for **bit**, a distributed Git implementation in MoonBit.
+
+The brand exists to make three ideas legible at a glance:
+
+> **a node, a source, a single bit of communication.**
+
+---
+
+## Concept
+
+bit is distributed. Source is replicated. Peers communicate. The brand makes
+this physical — a triad of geometric primitives, a pink node that always
+represents *source*, an off-cream paper, an ink that is not quite black, and a
+deliberately uncomfortable acid green held in reserve.
+
+Three rules that never bend:
+
+1. **No gradients.** Color is asserted, never blended.
+2. **No shadows.** Depth comes from layout, not light.
+3. **Geometric primitives only.** Circles, rectangles, lines. The wordmark is
+ constructed from the same primitives as the logo — they are literally
+ the same alphabet.
+
+---
+
+## Logo
+
+| Asset | File | Use |
+| --- | --- | --- |
+| Logo | [`logo/logo.svg`](logo/logo.svg) | App icon, avatar, square placements |
+| Logo, inverse | [`logo/logo-inverse.svg`](logo/logo-inverse.svg) | Dark surfaces |
+| Logo, mono | [`logo/logo-mono.svg`](logo/logo-mono.svg) | Single-channel printing, embossing |
+| Wordmark | [`logo/wordmark.svg`](logo/wordmark.svg) | In-line product name |
+| Wordmark, inverse | [`logo/wordmark-inverse.svg`](logo/wordmark-inverse.svg) | Dark surfaces |
+| Lockup, horizontal | [`logo/lockup-horizontal.svg`](logo/lockup-horizontal.svg) | Header, README hero |
+| Lockup, stacked | [`logo/lockup-stack.svg`](logo/lockup-stack.svg) | Square hero, cover art |
+
+**Logo composition** — three nodes form an asymmetric triad:
+
+- **pink filled** — *source*. The origin of truth.
+- **ink outlined** — *peer*. A replica that has not converged.
+- **ink filled** — *peer*. A replica that has.
+
+Edges are thin. The triangle is intentionally non-equilateral. Symmetry is the
+enemy of *distributed*.
+
+**Wordmark construction** — `b`, `i`, `t` are rectangles and circles. The
+tittle of the **i** is the pink dot. This is the brand's signature: a single
+node, a single source, a single bit. Use it as a reductive brand element on
+its own where the full lockup would be too loud.
+
+### Clear space
+
+Minimum margin around any logo asset is **one source-node diameter** on every
+side. The logo must never be cropped by a frame closer than this. When in
+doubt, give it more room.
+
+### Minimum size
+
+- Logo: **24×24 px** on screen, **8 mm** in print.
+- Wordmark: **80 px** wide on screen.
+- Lockup: **160 px** wide on screen.
+
+### Misuse
+
+Do not stretch. Do not recolor (pink stays pink, ink stays ink). Do not add
+strokes to the source node. Do not add a drop shadow. Do not place over a
+photograph without a paper or ink rectangle behind it. Do not rotate beyond
+the supplied compositions.
+
+---
+
+## Color
+
+```
+Bit Pink #FF2D6F Primary. Source, accent, the single bit.
+Bit Ink #0E0E12 Foreground. Off-black with a cool tint.
+Bit Paper #F2EFE7 Background. Warm cream.
+Bit Stone #D9D5CB UI lines, dividers, secondary tracks.
+Bit Acid #D6FF3D Eccentric counterpoint. Use SPARINGLY.
+```
+
+Press / hover states:
+
+```
+Pink Deep #C7124F Pressed pink only. Never as a primary surface.
+Pink Tint #FFD9E6 Chip/tag fill on paper.
+```
+
+**Rules.** Pink is a single state — owned by *source*, primary actions, and
+the i-tittle. The acid green is permitted on one element per view and never
+inside the logo. Ink is not `#000` and paper is not `#FFF` — the warmth in
+paper and the coolness in ink are deliberate.
+
+Full token sources: [`tokens/tokens.css`](tokens/tokens.css) ·
+[`tokens/tokens.json`](tokens/tokens.json) (W3C draft format).
+
+---
+
+## Typography
+
+| Role | Family | Loaded from |
+| --- | --- | --- |
+| Sans (display + body) | **Space Grotesk** — 300 / 400 / 500 / 600 / 700 | Google Fonts |
+| Mono (machine voice, code) | **Space Mono** — 400 / 700 | Google Fonts |
+
+**No italic. Anywhere.** Italic is forbidden in the brand — not in display,
+not in body, not in code. Emphasis is carried by weight, color, and tracking.
+
+### Scale
+
+| Token | px | Use |
+| --- | --: | --- |
+| `--type-001` | 11 | micro label, system meta |
+| `--type-00` | 13 | caption |
+| `--type-0` | 15 | small body |
+| `--type-1` | 17 | body |
+| `--type-2` | 21 | lead paragraph |
+| `--type-3` | 28 | h4 |
+| `--type-4` | 40 | h3 |
+| `--type-5` | 60 | h2 |
+| `--type-6` | 96 | h1 |
+| `--type-7` | 148 | hero, poster |
+
+### Tracking
+
+- Display (≥ 60 px): **−0.035em** — tight, near-touching.
+- Headings (28–60 px): **−0.018em**.
+- Body: **0**.
+- Labels (UPPERCASE, ≤ 13 px): **+0.16em**.
+
+### Use it
+
+- **Display headlines** in Space Grotesk **700** at `--type-5` or larger, tight
+ tracking, leading `0.95`–`1.08`. Let one phrase fill a column. Generous
+ whitespace below.
+- **Body** in Space Grotesk **400** at `--type-1`, leading `1.55`.
+- **Labels & meta** in Space Mono **400** UPPERCASE, `--type-001`,
+ `tracking +0.16em` — used for `// COMMIT`, `NODE.0142`, kicker text, and
+ data captions. This is where the eccentric mode-design voice lives.
+- **Code** in Space Mono **400**, no italic. Comments in `--bit-ink-mute`.
+
+Never pair these with Inter, Roboto, Helvetica, or system defaults. If a
+target environment can only render system fonts, fall back to the platform
+sans — never substitute a "near miss" web font.
+
+---
+
+## Layout principles
+
+- **Asymmetry over symmetry.** Compositions should feel like the triad logo —
+ weighted, intentional, never centered for the sake of it.
+- **Rules, not boxes.** Use a 1 px ink rule to divide content. Cards with
+ rounded corners are forbidden; use rectangular ink-stroked panels with
+ `radius: 0` and stroke `1.5px`.
+- **Mono labels as kicker text.** A small Space Mono UPPERCASE label sits
+ above every editorial heading, e.g. `// 003 — COMMUNICATION`.
+- **One pink per view.** The primary action, or the source node, or the
+ i-tittle in the wordmark — pick one anchor, not three.
+- **Acid is a guest.** One acid element per page maximum. Often zero.
+
+---
+
+## Files
+
+```
+brand/
+├── README.md ← you are here
+├── tokens/
+│ ├── tokens.css ← CSS custom properties + @import for fonts
+│ └── tokens.json ← W3C-draft design tokens
+├── logo/
+│ ├── logo.svg
+│ ├── logo-inverse.svg
+│ ├── logo-mono.svg
+│ ├── wordmark.svg
+│ ├── wordmark-inverse.svg
+│ ├── lockup-horizontal.svg
+│ └── lockup-stack.svg
+├── preview.html ← visual showcase of the system
+└── docs/
+ └── docs-template.html ← documentation page applying the brand
+```
+
+---
+
+**License.** Brand assets are released under the project's Apache-2.0 license.
+The MoonBit name and pink palette are referenced as cultural lineage, not
+ownership.
diff --git a/brand/docs/docs-template.html b/brand/docs/docs-template.html
new file mode 100644
index 0000000..fd82234
--- /dev/null
+++ b/brand/docs/docs-template.html
@@ -0,0 +1,387 @@
+
+
+
+
+
+bit — docs · brand applied
+
+
+
+
+
+
+
+
+
+ docs · v0.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
// guide · 002 · concepts
+
A node, a source, a single bit.
+
bit is a distributed Git implementation. Every clone is a peer. Every commit is a bit of evidence. There is no central authority — only a graph of nodes converging through gossip.
+
+
+
Nodes
+
A node in bit is any process holding a copy of the repository. Your laptop, a CI runner, a relay host — all peers, no master. Each node exposes an address and a set of refs, and accepts handshakes from other peers it trusts.
The pink dot in the bit logo represents the source node — the origin of a particular truth. In a distributed system, every node can be a source. The brand asserts this visually: only one node in the triad is pink at a time, but which one can rotate.
+
+
+
Source
+
Source is whoever wrote the commit you are looking at. bit records the source identity in the commit metadata and reconciles concurrent writes through a conflict-free history graph — not through a server vote.
+
+
Replication
+
When two peers handshake, they exchange ref tips and walk the object graph backwards until they meet a common ancestor. Bytes flow in both directions. This is gossip, not consensus.
+
+
+
+
Mode
Trigger
Direction
+
+
+
push
explicit
local → remote
+
fetch
explicit
remote → local
+
gossip
periodic, on handshake
both
+
relay
via relay host
both, async
+
+
+
+
Communication
+
A single edge between two nodes is the smallest unit of meaning in the system — one bit of exchange. Communication in bit is content-addressed: peers ask for object hashes, not for filenames, so the protocol is naturally idempotent and deduplicating.
bit is experimental. Data corruption can occur in worst-case scenarios. Always keep backups of important repositories. The acid green of this callout exists for one reason: you should not miss it.
bit is a distributed Git implementation. The brand is its physical form — geometric primitives, asserted color, the pink dot as the smallest unit of meaning.
bit stores history as content-addressed objects, replicated across peers without a central authority. Every node is a source. Every commit is a bit of evidence. The graph converges through gossip, not consensus.
+
+
+
+
Label 11 / 500 / mono
+
// node.0142 — commit a8c2f6 — peer wrote 14 bytes
+
+
+
+
+
+
+
+
+
// 003
+
Logo. Three nodes, one asymmetric triangle.
+
+
+
+
+
+
lockup · horizontal · on paper
+
+
+
+
lockup · inverse · on ink
+
+
+
+
lockup · on acid (rare)
+
+
+
+
+
mark · on paper
+
+
+
+
mark · inverse
+
+
+
+
wordmark · the pink dot is the i
+
+
+
+
+
+
+
+
// 004
+
Components. Rectangles. No corners. No shadow.
+
+
+
+
+
+
// node.0142 — origin
+
aurora.local
+
refs14
+
last gossip2s ago
+
heada8c2f6
+
+
+
+
+
+
+
+
// pr.0007 — open
+
Replace gossip backoff with token bucket
+
+ pink · primary
+ ghost · meta
+ acid · 1 per view
+
+
+
+
+
+
+
diff --git a/brand/tokens/tokens.css b/brand/tokens/tokens.css
new file mode 100644
index 0000000..b47b9ac
--- /dev/null
+++ b/brand/tokens/tokens.css
@@ -0,0 +1,164 @@
+/*
+ * bit — Design Tokens (CSS)
+ * Surface-level tokens for color, typography, spacing, motion, and shape.
+ * No gradients. No shadows. Strict palette. Geometric primitives only.
+ */
+
+@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap");
+
+:root {
+ /* ──────────────────────────────────────────────────────────────
+ * COLOR — strict 5-token palette. Never blend, never gradient.
+ * ────────────────────────────────────────────────────────────── */
+
+ /* Primary — Bit Pink. The node, the source, the single bit. */
+ --bit-pink: #FF2D6F;
+ --bit-pink-deep: #C7124F; /* for press/hover states only */
+ --bit-pink-tint: #FFD9E6; /* for filled tag/chip backgrounds */
+
+ /* Ink — off-black with cool tint. Editorial. Never #000. */
+ --bit-ink: #0E0E12;
+ --bit-ink-soft: #2B2B33; /* secondary text */
+ --bit-ink-mute: #6B6B74; /* meta / captions */
+
+ /* Paper — warm cream. Never #FFF. */
+ --bit-paper: #F2EFE7;
+ --bit-paper-deep: #E8E4D8; /* alt surface, panels */
+
+ /* Stone — desaturated neutral. UI lines, dividers, tracks. */
+ --bit-stone: #D9D5CB;
+
+ /* Acid — the eccentric counterpoint. Use SPARINGLY. */
+ --bit-acid: #D6FF3D;
+
+ /* ──────────────────────────────────────────────────────────────
+ * SEMANTIC ALIASES
+ * ────────────────────────────────────────────────────────────── */
+
+ --bg: var(--bit-paper);
+ --bg-alt: var(--bit-paper-deep);
+ --fg: var(--bit-ink);
+ --fg-soft: var(--bit-ink-soft);
+ --fg-mute: var(--bit-ink-mute);
+ --rule: var(--bit-ink); /* rules are ink, full strength */
+ --rule-soft: var(--bit-stone);
+ --accent: var(--bit-pink);
+ --accent-press: var(--bit-pink-deep);
+ --highlight: var(--bit-acid);
+
+ /* ──────────────────────────────────────────────────────────────
+ * TYPOGRAPHY
+ * Sans → Space Grotesk (display + body)
+ * Mono → Space Mono (machine voice, captions, code)
+ * No italic. Ever.
+ * ────────────────────────────────────────────────────────────── */
+
+ --font-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
+ --font-mono: "Space Mono", ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, monospace;
+
+ /* Type scale — modular, tight at display, loose at body */
+ --type-001: 11px; /* micro label */
+ --type-00: 13px; /* caption, meta */
+ --type-0: 15px; /* small body */
+ --type-1: 17px; /* body */
+ --type-2: 21px; /* lead */
+ --type-3: 28px; /* h4 */
+ --type-4: 40px; /* h3 */
+ --type-5: 60px; /* h2 */
+ --type-6: 96px; /* h1 */
+ --type-7: 148px; /* hero, poster */
+
+ /* Line heights — display tight, body comfortable */
+ --leading-tight: 0.95;
+ --leading-snug: 1.08;
+ --leading-body: 1.55;
+ --leading-loose: 1.72;
+
+ /* Tracking — display tightens, micro labels open up */
+ --track-display: -0.035em;
+ --track-heading: -0.018em;
+ --track-body: 0;
+ --track-label: 0.16em; /* UPPERCASE labels only */
+ --track-mono: 0;
+
+ /* ──────────────────────────────────────────────────────────────
+ * SHAPE — geometric primitives, no soft rounding.
+ * ────────────────────────────────────────────────────────────── */
+
+ --radius-none: 0;
+ --radius-dot: 999px; /* circles only — never used on rectangles */
+ --stroke-hair: 1px;
+ --stroke-thin: 1.5px;
+ --stroke-med: 2px;
+ --stroke-thick: 4px;
+
+ /* ──────────────────────────────────────────────────────────────
+ * SPACE — 4px base, prefer larger jumps for editorial whitespace.
+ * ────────────────────────────────────────────────────────────── */
+
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 24px;
+ --space-6: 32px;
+ --space-7: 48px;
+ --space-8: 72px;
+ --space-9: 112px;
+ --space-10: 168px;
+
+ /* ──────────────────────────────────────────────────────────────
+ * MOTION — instant or slow. No "smooth-default".
+ * ────────────────────────────────────────────────────────────── */
+
+ --ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
+ --ease-flat: linear;
+ --dur-fast: 120ms;
+ --dur-slow: 480ms;
+
+ color-scheme: light;
+}
+
+/* ──────────────────────────────────────────────────────────────
+ * DARK MODE — invert paper/ink, keep pink and acid identical.
+ * ────────────────────────────────────────────────────────────── */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: var(--bit-ink);
+ --bg-alt: #17171C;
+ --fg: var(--bit-paper);
+ --fg-soft: #C9C5BB;
+ --fg-mute: #8A867D;
+ --rule: var(--bit-paper);
+ --rule-soft: #3A3A42;
+
+ color-scheme: dark;
+ }
+}
+
+/* Forced dark utility (e.g. previews) */
+[data-theme="dark"] {
+ --bg: var(--bit-ink);
+ --bg-alt: #17171C;
+ --fg: var(--bit-paper);
+ --fg-soft: #C9C5BB;
+ --fg-mute: #8A867D;
+ --rule: var(--bit-paper);
+ --rule-soft: #3A3A42;
+
+ color-scheme: dark;
+}
+
+/* ──────────────────────────────────────────────────────────────
+ * BRAND RULE — NO ITALIC.
+ * Browser defaults render , , , , ,
+ * in italic. We do not ship italic fonts, so the browser would
+ * synthesize a slanted face — on a geometric grotesque that reads
+ * as a deformed serif. We override globally and carry emphasis
+ * through weight + color instead.
+ * ────────────────────────────────────────────────────────────── */
+em, i, cite, dfn, address, var, q {
+ font-style: normal;
+ font-weight: 600;
+ color: var(--fg);
+}
diff --git a/brand/tokens/tokens.json b/brand/tokens/tokens.json
new file mode 100644
index 0000000..25de729
--- /dev/null
+++ b/brand/tokens/tokens.json
@@ -0,0 +1,100 @@
+{
+ "$schema": "https://design-tokens.github.io/community-group/format/",
+ "$description": "bit brand design tokens. Strict palette, no gradients, no shadows.",
+ "color": {
+ "bit": {
+ "pink": { "$value": "#FF2D6F", "$type": "color", "$description": "Primary. The node, the source, the single bit." },
+ "pink-deep": { "$value": "#C7124F", "$type": "color", "$description": "Press/hover only." },
+ "pink-tint": { "$value": "#FFD9E6", "$type": "color", "$description": "Filled chip/tag background." },
+ "ink": { "$value": "#0E0E12", "$type": "color", "$description": "Foreground. Never #000." },
+ "ink-soft": { "$value": "#2B2B33", "$type": "color" },
+ "ink-mute": { "$value": "#6B6B74", "$type": "color" },
+ "paper": { "$value": "#F2EFE7", "$type": "color", "$description": "Background. Never #FFF." },
+ "paper-deep": { "$value": "#E8E4D8", "$type": "color" },
+ "stone": { "$value": "#D9D5CB", "$type": "color", "$description": "UI lines, dividers, tracks." },
+ "acid": { "$value": "#D6FF3D", "$type": "color", "$description": "Eccentric counterpoint. Use sparingly." }
+ },
+ "semantic": {
+ "bg": { "$value": "{color.bit.paper}", "$type": "color" },
+ "bg-alt": { "$value": "{color.bit.paper-deep}", "$type": "color" },
+ "fg": { "$value": "{color.bit.ink}", "$type": "color" },
+ "fg-soft": { "$value": "{color.bit.ink-soft}", "$type": "color" },
+ "fg-mute": { "$value": "{color.bit.ink-mute}", "$type": "color" },
+ "rule": { "$value": "{color.bit.ink}", "$type": "color" },
+ "rule-soft": { "$value": "{color.bit.stone}", "$type": "color" },
+ "accent": { "$value": "{color.bit.pink}", "$type": "color" },
+ "highlight": { "$value": "{color.bit.acid}", "$type": "color" }
+ }
+ },
+ "font": {
+ "family": {
+ "sans": { "$value": "Space Grotesk, ui-sans-serif, system-ui, sans-serif", "$type": "fontFamily" },
+ "mono": { "$value": "Space Mono, ui-monospace, JetBrains Mono, monospace", "$type": "fontFamily" }
+ },
+ "weight": {
+ "light": { "$value": 300, "$type": "fontWeight" },
+ "regular": { "$value": 400, "$type": "fontWeight" },
+ "medium": { "$value": 500, "$type": "fontWeight" },
+ "semi": { "$value": 600, "$type": "fontWeight" },
+ "bold": { "$value": 700, "$type": "fontWeight" }
+ },
+ "size": {
+ "001": { "$value": "11px", "$type": "dimension", "$description": "micro label" },
+ "00": { "$value": "13px", "$type": "dimension", "$description": "caption" },
+ "0": { "$value": "15px", "$type": "dimension" },
+ "1": { "$value": "17px", "$type": "dimension", "$description": "body" },
+ "2": { "$value": "21px", "$type": "dimension", "$description": "lead" },
+ "3": { "$value": "28px", "$type": "dimension" },
+ "4": { "$value": "40px", "$type": "dimension" },
+ "5": { "$value": "60px", "$type": "dimension" },
+ "6": { "$value": "96px", "$type": "dimension" },
+ "7": { "$value": "148px","$type": "dimension", "$description": "hero / poster" }
+ },
+ "tracking": {
+ "display": { "$value": "-0.035em", "$type": "string" },
+ "heading": { "$value": "-0.018em", "$type": "string" },
+ "body": { "$value": "0", "$type": "string" },
+ "label": { "$value": "0.16em", "$type": "string", "$description": "UPPERCASE only" }
+ },
+ "leading": {
+ "tight": { "$value": 0.95, "$type": "number" },
+ "snug": { "$value": 1.08, "$type": "number" },
+ "body": { "$value": 1.55, "$type": "number" },
+ "loose": { "$value": 1.72, "$type": "number" }
+ }
+ },
+ "space": {
+ "1": { "$value": "4px", "$type": "dimension" },
+ "2": { "$value": "8px", "$type": "dimension" },
+ "3": { "$value": "12px", "$type": "dimension" },
+ "4": { "$value": "16px", "$type": "dimension" },
+ "5": { "$value": "24px", "$type": "dimension" },
+ "6": { "$value": "32px", "$type": "dimension" },
+ "7": { "$value": "48px", "$type": "dimension" },
+ "8": { "$value": "72px", "$type": "dimension" },
+ "9": { "$value": "112px", "$type": "dimension" },
+ "10": { "$value": "168px", "$type": "dimension" }
+ },
+ "shape": {
+ "radius": {
+ "none": { "$value": "0", "$type": "dimension" },
+ "dot": { "$value": "999px", "$type": "dimension", "$description": "Circle only. Never on rectangles." }
+ },
+ "stroke": {
+ "hair": { "$value": "1px", "$type": "dimension" },
+ "thin": { "$value": "1.5px", "$type": "dimension" },
+ "med": { "$value": "2px", "$type": "dimension" },
+ "thick": { "$value": "4px", "$type": "dimension" }
+ }
+ },
+ "motion": {
+ "ease": {
+ "snap": { "$value": "cubic-bezier(0.2, 0.8, 0.2, 1)", "$type": "cubicBezier" },
+ "flat": { "$value": "linear", "$type": "string" }
+ },
+ "duration": {
+ "fast": { "$value": "120ms", "$type": "duration" },
+ "slow": { "$value": "480ms", "$type": "duration" }
+ }
+ }
+}
diff --git a/site/.nojekyll b/site/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/site/README.md b/site/README.md
new file mode 100644
index 0000000..c40dadc
--- /dev/null
+++ b/site/README.md
@@ -0,0 +1,151 @@
+# bit — documentation site
+
+The static documentation site for **bit**. Deployed to GitHub Pages from this
+folder. Markdown content under [`content/`](content/) is rendered to HTML at
+build time using [`mizchi/markdown`](https://github.com/mizchi/markdown.mbt)
+(MoonBit) wrapped by a tiny renderer in
+[`tools/docs-build-mbt/`](../tools/docs-build-mbt/) and orchestrated by
+[`tools/build-docs.mjs`](../tools/build-docs.mjs).
+
+## Layout
+
+```
+site/
+├── index.html ← landing page (hand-written, brand-heavy)
+├── content/ ← MARKDOWN SOURCE — author here
+│ ├── learn/
+│ │ ├── index.md
+│ │ ├── concept.md
+│ │ ├── install.md
+│ │ ├── first-commit.md
+│ │ └── distributed.md
+│ └── reference/
+│ ├── index.md
+│ ├── cli.md
+│ ├── library.md
+│ └── env.md
+├── learn/ ← BUILD OUTPUT (overwritten by build-docs.mjs)
+├── reference/ ← BUILD OUTPUT
+├── assets/
+│ ├── tokens.css ← brand design tokens (copied from /brand)
+│ ├── site.css ← site-level layout + components
+│ ├── syntax.js ← MoonBit / bash syntax highlighter
+│ ├── site.js ← nav toggle, copy buttons
+│ └── img/ ← logo, wordmark, lockups (copied from /brand)
+├── .nojekyll ← tell GitHub Pages not to run Jekyll
+└── README.md ← you are here
+```
+
+## Build
+
+The pipeline is two steps. The MoonBit renderer compiles once and is reused
+on every content change.
+
+```sh
+# 1. build the MoonBit renderer (one-time, or after dep bumps)
+cd tools/docs-build-mbt && moon update && moon build --target js --release && cd -
+
+# 2. render markdown to HTML
+node tools/build-docs.mjs
+```
+
+This writes `site/learn/*.html` and `site/reference/*.html`. The
+hand-written `site/index.html` is left alone.
+
+## Preview locally
+
+After building, open `site/index.html` in a browser or serve from a real
+HTTP server (recommended — the clipboard API needs a secure context):
+
+```sh
+python3 -m http.server -d site 8080
+# open http://localhost:8080
+```
+
+## Deploy
+
+Pushed automatically by [`.github/workflows/pages.yml`](../.github/workflows/pages.yml)
+on every push to `main` or `brand` that touches `site/`, `brand/`,
+`tools/build-docs.mjs`, or `tools/docs-build-mbt/`.
+
+The workflow:
+
+1. Installs the MoonBit toolchain.
+2. Builds the MoonBit renderer (`moon build --target js --release`).
+3. Re-syncs brand assets (`tokens.css` + logo SVGs) from `brand/` into
+ `site/assets/`.
+4. Runs `node tools/build-docs.mjs` to render markdown to HTML.
+5. Uploads the `site/` folder to GitHub Pages.
+
+To enable Pages: in the repo's **Settings → Pages**, set the source to
+**GitHub Actions**. The workflow handles the rest.
+
+## Authoring content
+
+Every chapter is a markdown file under `site/content//.md`
+with a frontmatter block. Frontmatter keys are flat (no nesting); structure
+is encoded by named fields like `prev_href` and `next_label`.
+
+### Frontmatter
+
+```yaml
+---
+title: Install bit # and H1 fallback
+section: learn # learn | reference
+slug: install # filename without .md
+order: 1 # sort order in sidenav and chapter list
+nav_label: Install bit # label in sidenav (optional, defaults to title)
+summary: One-line install… # shown on the section index TOC
+meta: ~5 min # right-column meta on the index TOC
+kicker: // guide · 01 # mono kicker above the H1
+h1: Install bit. # H1 text (defaults to title)
+lead: Two install paths … # lead paragraph — markdown is rendered inline
+prev_href: ./ # pager left
+prev_kicker: back
+prev_label: Learning Guide
+next_href: first-commit.html
+next_kicker: next · 02
+next_label: Your first commit
+---
+```
+
+For section index pages, add `template: index` and omit `prev_*` / `next_*`.
+The chapter list is auto-generated from sibling pages.
+
+### Code blocks
+
+Fenced code blocks are post-processed into our `.codeblock` structure with
+a language label and copy button. Supported `data-lang` values for the
+syntax highlighter:
+
+- `moonbit` / `mbt` — MoonBit syntax
+- `bash` / `sh` — shell prompts and comments
+- anything else — plain text
+
+### Callouts
+
+Inline HTML is the simplest path:
+
+```html
+
+
// note
+
Body text. Inline HTML inside is plain HTML — markdown is not parsed here.
+
+
+
+
// caveat
+
Acid variant for warnings.
+
+```
+
+## Updating brand assets
+
+Don't edit `site/assets/tokens.css` or `site/assets/img/*.svg` directly —
+edit the originals in [`brand/`](../brand/) and re-sync:
+
+```sh
+cp brand/tokens/tokens.css site/assets/tokens.css
+cp brand/logo/*.svg site/assets/img/
+```
+
+The Pages workflow does this automatically on every deploy.
diff --git a/site/assets/img/lockup-horizontal.svg b/site/assets/img/lockup-horizontal.svg
new file mode 100644
index 0000000..c482891
--- /dev/null
+++ b/site/assets/img/lockup-horizontal.svg
@@ -0,0 +1,28 @@
+
diff --git a/site/assets/img/lockup-stack.svg b/site/assets/img/lockup-stack.svg
new file mode 100644
index 0000000..4127f11
--- /dev/null
+++ b/site/assets/img/lockup-stack.svg
@@ -0,0 +1,28 @@
+
diff --git a/site/assets/img/logo-inverse.svg b/site/assets/img/logo-inverse.svg
new file mode 100644
index 0000000..1205b9f
--- /dev/null
+++ b/site/assets/img/logo-inverse.svg
@@ -0,0 +1,12 @@
+
diff --git a/site/assets/img/logo-mono.svg b/site/assets/img/logo-mono.svg
new file mode 100644
index 0000000..d498733
--- /dev/null
+++ b/site/assets/img/logo-mono.svg
@@ -0,0 +1,12 @@
+
diff --git a/site/assets/img/logo.svg b/site/assets/img/logo.svg
new file mode 100644
index 0000000..8f30f8c
--- /dev/null
+++ b/site/assets/img/logo.svg
@@ -0,0 +1,16 @@
+
diff --git a/site/assets/img/wordmark-inverse.svg b/site/assets/img/wordmark-inverse.svg
new file mode 100644
index 0000000..a7bba21
--- /dev/null
+++ b/site/assets/img/wordmark-inverse.svg
@@ -0,0 +1,13 @@
+
diff --git a/site/assets/img/wordmark.svg b/site/assets/img/wordmark.svg
new file mode 100644
index 0000000..36208fc
--- /dev/null
+++ b/site/assets/img/wordmark.svg
@@ -0,0 +1,16 @@
+
diff --git a/site/assets/site.css b/site/assets/site.css
new file mode 100644
index 0000000..427ba74
--- /dev/null
+++ b/site/assets/site.css
@@ -0,0 +1,579 @@
+/*
+ * bit — docs site styles (built on brand tokens.css)
+ * No gradients. No shadows. Rectangles only.
+ */
+
+*, *::before, *::after { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+html { scroll-behavior: smooth; scroll-padding-top: 88px; }
+body {
+ background: var(--bg);
+ color: var(--fg);
+ font-family: var(--font-sans);
+ font-size: var(--type-1);
+ line-height: var(--leading-body);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+img, svg { max-width: 100%; }
+
+/* ── links ────────────────────────────────────────────────────── */
+a { color: var(--bit-pink); text-decoration: none; border-bottom: 1px solid var(--bit-pink); }
+a:hover { background: var(--bit-pink); color: var(--bit-paper); }
+a.plain { color: inherit; border: 0; }
+a.plain:hover { background: transparent; color: var(--bit-pink); }
+
+/* ── topbar ───────────────────────────────────────────────────── */
+.topbar {
+ position: sticky; top: 0; z-index: 10;
+ background: var(--bg);
+ border-bottom: 1px solid var(--rule);
+ padding: var(--space-4) var(--space-6);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-5);
+}
+.topbar__brand { display: flex; align-items: center; gap: var(--space-3); }
+.topbar__brand a { border: 0; }
+.topbar__brand img { height: 26px; display: block; }
+.topbar__brand .ver {
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--fg-mute);
+ padding-left: var(--space-3);
+ border-left: 1px solid var(--rule-soft);
+}
+.topbar nav { display: flex; gap: var(--space-5); }
+.topbar nav a {
+ color: var(--fg);
+ text-decoration: none;
+ border: none;
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ padding-bottom: 4px;
+ border-bottom: 1px solid transparent;
+ background: transparent;
+}
+.topbar nav a:hover, .topbar nav a.active {
+ color: var(--bit-pink);
+ border-bottom-color: var(--bit-pink);
+ background: transparent;
+}
+.topbar__menu { display: none; background: transparent; border: 1.5px solid var(--rule); padding: 6px 10px; cursor: pointer; }
+.topbar__menu span { display: block; width: 18px; height: 1.5px; background: var(--fg); margin: 4px 0; }
+
+@media (max-width: 760px) {
+ .topbar nav { display: none; position: absolute; top: 100%; left: 0; right: 0; flex-direction: column; background: var(--bg); border-bottom: 1px solid var(--rule); padding: var(--space-5); gap: var(--space-4); }
+ .topbar nav.open { display: flex; }
+ .topbar__menu { display: block; }
+}
+
+/* ── footer ───────────────────────────────────────────────────── */
+footer.site {
+ border-top: 1px solid var(--rule);
+ padding: var(--space-6);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-5);
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--fg-mute);
+ flex-wrap: wrap;
+}
+footer.site a { color: var(--fg-mute); border: 0; }
+footer.site a:hover { color: var(--bit-pink); background: transparent; }
+
+/* ── shared kicker ────────────────────────────────────────────── */
+.kicker {
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--fg-mute);
+ margin: 0 0 var(--space-4);
+}
+.kicker .pink { color: var(--bit-pink); }
+
+/* ── hero (landing) ───────────────────────────────────────────── */
+.landing {
+ display: flex;
+ flex-direction: column;
+}
+.hero {
+ padding: var(--space-8) var(--space-6) var(--space-9);
+ max-width: 1280px;
+ margin: 0 auto;
+ width: 100%;
+}
+.hero__tag {
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ border-bottom: 1px solid var(--rule);
+ padding-bottom: var(--space-3);
+ margin-bottom: var(--space-6);
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--fg-mute);
+}
+.hero__tag strong { color: var(--fg); font-weight: 400; }
+.hero__grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: var(--space-7); align-items: end; }
+@media (max-width: 900px) { .hero__grid { grid-template-columns: 1fr; gap: var(--space-6); } }
+.hero__head {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: clamp(56px, 11vw, 132px);
+ line-height: var(--leading-tight);
+ letter-spacing: var(--track-display);
+ margin: 0 0 var(--space-5);
+}
+.hero__head .dot {
+ display: inline-block;
+ width: 0.6em; height: 0.6em;
+ border-radius: var(--radius-dot);
+ background: var(--bit-pink);
+ vertical-align: 0.06em;
+ margin: 0 0.03em;
+}
+.hero__sub {
+ font-size: var(--type-2);
+ line-height: var(--leading-snug);
+ margin: 0 0 var(--space-6);
+ max-width: 38ch;
+ color: var(--fg-soft);
+}
+.hero__cta { display: flex; flex-wrap: wrap; gap: var(--space-3); margin: 0 0 var(--space-6); }
+.hero__install {
+ background: var(--bit-ink);
+ color: var(--bit-paper);
+ padding: var(--space-4) var(--space-5);
+ font-family: var(--font-mono);
+ font-size: var(--type-0);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: var(--space-3);
+}
+.hero__install .prompt { color: var(--bit-pink); }
+.hero__install button {
+ background: transparent;
+ color: var(--bit-paper);
+ border: 1px solid var(--bit-paper);
+ padding: 4px 10px;
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ cursor: pointer;
+}
+.hero__install button:hover { background: var(--bit-paper); color: var(--bit-ink); }
+.hero__art { display: flex; align-items: center; justify-content: center; }
+.hero__art img { max-width: 320px; width: 100%; }
+
+/* ── buttons ──────────────────────────────────────────────────── */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
+ padding: 14px 22px;
+ border: 0;
+ font-family: var(--font-sans);
+ font-weight: 500;
+ font-size: var(--type-0);
+ letter-spacing: var(--track-heading);
+ cursor: pointer;
+ text-decoration: none;
+ transition: background var(--dur-fast) var(--ease-snap), color var(--dur-fast) var(--ease-snap);
+ border-bottom: 0 !important;
+}
+.btn--primary { background: var(--bit-pink); color: var(--bit-paper); }
+.btn--primary:hover { background: var(--bit-pink-deep); color: var(--bit-paper); }
+.btn--ghost { background: transparent; color: var(--fg); border: 1.5px solid var(--rule); }
+.btn--ghost:hover { background: var(--fg); color: var(--bg); }
+
+/* ── manifesto strip ─────────────────────────────────────────── */
+.strip {
+ background: var(--bit-ink);
+ color: var(--bit-paper);
+ padding: var(--space-8) var(--space-6);
+ border-top: 1px solid var(--rule);
+ border-bottom: 1px solid var(--rule);
+}
+.strip__wrap { max-width: 1280px; margin: 0 auto; }
+.strip .kicker { color: var(--bit-paper-deep); }
+.strip__head {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: clamp(40px, 7vw, 84px);
+ line-height: var(--leading-tight);
+ letter-spacing: var(--track-display);
+ margin: 0;
+}
+.strip__head .acid { color: var(--bit-acid); }
+
+/* ── feature grid ─────────────────────────────────────────────── */
+.section {
+ padding: var(--space-8) var(--space-6);
+ max-width: 1280px;
+ margin: 0 auto;
+ width: 100%;
+}
+.section__head {
+ display: grid;
+ grid-template-columns: 80px 1fr;
+ align-items: baseline;
+ gap: var(--space-5);
+ margin-bottom: var(--space-7);
+}
+.section__num {
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--bit-pink);
+}
+.section__title {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: clamp(28px, 5vw, var(--type-4));
+ letter-spacing: var(--track-heading);
+ line-height: var(--leading-snug);
+ margin: 0;
+}
+
+.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-5); }
+@media (max-width: 760px) { .feature-grid { grid-template-columns: 1fr; } }
+.feature {
+ border: 1px solid var(--rule);
+ padding: var(--space-6) var(--space-5);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-3);
+ background: var(--bg);
+}
+.feature__no {
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--bit-pink);
+ margin: 0;
+}
+.feature__h {
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: var(--type-3);
+ letter-spacing: var(--track-heading);
+ line-height: var(--leading-snug);
+ margin: 0;
+}
+.feature__p { margin: 0; color: var(--fg-soft); font-size: var(--type-1); line-height: var(--leading-body); }
+.feature__link { margin-top: auto; padding-top: var(--space-3); }
+.feature__link a {
+ color: var(--fg);
+ border-bottom: 1px solid var(--fg);
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+}
+.feature__link a:hover { background: var(--bit-pink); color: var(--bit-paper); border-bottom-color: var(--bit-pink); }
+
+/* ── docs layout (Learn + Reference pages) ────────────────────── */
+.layout {
+ display: grid;
+ grid-template-columns: 240px 1fr 220px;
+ gap: var(--space-7);
+ max-width: 1280px;
+ margin: 0 auto;
+ padding: var(--space-7) var(--space-6) var(--space-9);
+}
+@media (max-width: 1080px) { .layout { grid-template-columns: 220px 1fr; } aside.toc { display: none; } }
+@media (max-width: 760px) { .layout { grid-template-columns: 1fr; padding: var(--space-5); } aside.side { display: none; } }
+
+aside.side {
+ position: sticky; top: 88px; align-self: start;
+ font-family: var(--font-sans); font-size: var(--type-0);
+ max-height: calc(100vh - 100px); overflow-y: auto;
+}
+aside.side h4 {
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--fg-mute);
+ margin: 0 0 var(--space-3);
+ font-weight: 400;
+}
+aside.side ul { list-style: none; padding: 0; margin: 0 0 var(--space-6); }
+aside.side li { padding: 4px 0; }
+aside.side a {
+ color: var(--fg);
+ text-decoration: none;
+ border: none;
+ display: block;
+ padding: 2px 0 2px var(--space-3);
+ border-left: 1px solid var(--rule-soft);
+ background: transparent;
+}
+aside.side a:hover { color: var(--bit-pink); border-left-color: var(--bit-pink); background: transparent; }
+aside.side a.active { color: var(--bit-pink); border-left-color: var(--bit-pink); font-weight: 500; }
+
+aside.toc {
+ position: sticky; top: 88px; align-self: start;
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+}
+aside.toc h4 { color: var(--fg-mute); margin: 0 0 var(--space-3); font-weight: 400; }
+aside.toc ul { list-style: none; padding: 0; margin: 0; }
+aside.toc li { padding: 6px 0; }
+aside.toc a { color: var(--fg); text-decoration: none; border: none; display: flex; gap: var(--space-2); background: transparent; }
+aside.toc a:hover { color: var(--bit-pink); }
+aside.toc .num { color: var(--bit-pink); }
+
+/* ── article ──────────────────────────────────────────────────── */
+article { min-width: 0; }
+article header.title {
+ border-bottom: 1px solid var(--rule);
+ padding-bottom: var(--space-6);
+ margin-bottom: var(--space-7);
+}
+article .kicker { margin: 0 0 var(--space-4); }
+article h1 {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: clamp(40px, 6vw, var(--type-5));
+ line-height: var(--leading-tight);
+ letter-spacing: var(--track-display);
+ margin: 0 0 var(--space-4);
+}
+article .lead { font-size: var(--type-2); line-height: var(--leading-snug); color: var(--fg-soft); max-width: 56ch; margin: 0; }
+
+article h2 {
+ font-family: var(--font-sans);
+ font-weight: 700;
+ font-size: var(--type-4);
+ line-height: var(--leading-snug);
+ letter-spacing: var(--track-heading);
+ margin: var(--space-8) 0 var(--space-4);
+ padding-top: var(--space-3);
+ border-top: 1px solid var(--rule);
+}
+article h2[data-num]::before {
+ content: attr(data-num);
+ display: block;
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--bit-pink);
+ margin-bottom: var(--space-3);
+ font-weight: 400;
+}
+article h3 {
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: var(--type-2);
+ margin: var(--space-6) 0 var(--space-3);
+ letter-spacing: var(--track-heading);
+}
+article h4 {
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: var(--type-1);
+ margin: var(--space-5) 0 var(--space-2);
+ letter-spacing: var(--track-heading);
+}
+article p { margin: 0 0 var(--space-4); max-width: 72ch; }
+article ul, article ol { max-width: 72ch; padding-left: var(--space-5); margin: 0 0 var(--space-4); }
+article li { padding: 4px 0; }
+article li::marker { color: var(--bit-pink); }
+article strong { font-weight: 600; }
+article hr { border: 0; border-top: 1px solid var(--rule-soft); margin: var(--space-6) 0; }
+
+article code {
+ font-family: var(--font-mono);
+ font-size: 0.94em;
+ background: var(--bit-paper-deep);
+ padding: 2px 6px;
+ border: 1px solid var(--rule-soft);
+}
+
+/* ── code blocks ──────────────────────────────────────────────── */
+.codeblock {
+ position: relative;
+ margin: 0 0 var(--space-5);
+}
+.codeblock__bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--bit-ink);
+ color: var(--bit-paper-deep);
+ padding: 8px var(--space-4);
+ border-bottom: 1px solid #2A2A30;
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+}
+.codeblock__bar .lang { color: var(--bit-pink); }
+.codeblock__copy {
+ background: transparent;
+ color: var(--bit-paper-deep);
+ border: 1px solid var(--bit-paper-deep);
+ padding: 2px 8px;
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ cursor: pointer;
+}
+.codeblock__copy:hover { background: var(--bit-paper); color: var(--bit-ink); }
+.codeblock__copy.copied { background: var(--bit-acid); color: var(--bit-ink); border-color: var(--bit-acid); }
+.codeblock pre {
+ background: var(--bit-ink);
+ color: var(--bit-paper);
+ padding: var(--space-5);
+ overflow-x: auto;
+ margin: 0;
+ font-family: var(--font-mono);
+ font-size: var(--type-0);
+ line-height: 1.7;
+}
+.codeblock pre code { background: transparent; border: 0; padding: 0; color: inherit; font-size: inherit; }
+
+/* syntax token colors */
+.tok-cm { color: var(--bit-ink-mute); } /* comment */
+.tok-kw { color: var(--bit-pink); } /* keyword */
+.tok-ty { color: var(--bit-acid); } /* type */
+.tok-str { color: #FFB28A; } /* string — warm peach (still no gradient) */
+.tok-num { color: var(--bit-paper); } /* number */
+.tok-fn { color: var(--bit-paper); font-weight: 500; } /* function name */
+.tok-pm { color: #B89CFF; } /* preamble: pub, extern, import — soft violet */
+
+/* ── tables ───────────────────────────────────────────────────── */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: var(--space-5) 0;
+ font-size: var(--type-0);
+}
+thead th {
+ text-align: left;
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--fg-mute);
+ font-weight: 400;
+ padding: var(--space-2) var(--space-3);
+ border-bottom: 1px solid var(--rule);
+}
+tbody td { padding: var(--space-3); border-bottom: 1px solid var(--rule-soft); vertical-align: top; }
+tbody td:first-child { font-family: var(--font-mono); white-space: nowrap; }
+tbody tr:hover { background: var(--bit-paper-deep); }
+
+/* ── callouts ─────────────────────────────────────────────────── */
+.callout {
+ border: 1px solid var(--rule);
+ border-left: 6px solid var(--bit-pink);
+ padding: var(--space-4) var(--space-5);
+ margin: var(--space-5) 0;
+}
+.callout .kicker { margin: 0 0 var(--space-2); color: var(--bit-pink); }
+.callout p:last-child { margin-bottom: 0; }
+.callout--acid { border-left-color: var(--bit-ink); background: var(--bit-acid); }
+.callout--acid .kicker { color: var(--bit-ink); }
+.callout--acid a { color: var(--bit-ink); border-bottom-color: var(--bit-ink); }
+.callout--acid a:hover { background: var(--bit-ink); color: var(--bit-acid); }
+
+/* ── chapter grid (Learn / Reference index) ───────────────────── */
+.chapters { display: grid; grid-template-columns: 1fr; gap: 0; border-top: 1px solid var(--rule); }
+.chapter {
+ display: grid;
+ grid-template-columns: 80px 1fr 100px;
+ gap: var(--space-5);
+ padding: var(--space-6) var(--space-3);
+ border-bottom: 1px solid var(--rule);
+ align-items: baseline;
+ text-decoration: none;
+ color: inherit;
+ border-bottom: 1px solid var(--rule);
+ background: transparent;
+}
+a.chapter { border: 0; border-bottom: 1px solid var(--rule); }
+.chapter:hover { background: var(--bit-paper-deep); color: var(--bit-ink); }
+.chapter:hover .chapter__h { color: var(--bit-pink); }
+.chapter__num {
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--bit-pink);
+}
+.chapter__h {
+ font-family: var(--font-sans);
+ font-weight: 600;
+ font-size: var(--type-3);
+ letter-spacing: var(--track-heading);
+ line-height: var(--leading-snug);
+ margin: 0 0 var(--space-2);
+ transition: color var(--dur-fast) var(--ease-snap);
+}
+.chapter__p { margin: 0; color: var(--fg-soft); font-size: var(--type-1); }
+.chapter__meta {
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--fg-mute);
+ text-align: right;
+}
+@media (max-width: 760px) {
+ .chapter { grid-template-columns: 60px 1fr; }
+ .chapter__meta { display: none; }
+}
+
+/* ── pager (prev/next at chapter end) ─────────────────────────── */
+.pager {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--space-4);
+ margin-top: var(--space-8);
+ padding-top: var(--space-5);
+ border-top: 1px solid var(--rule);
+}
+.pager a {
+ border: 1px solid var(--rule);
+ padding: var(--space-4) var(--space-5);
+ color: inherit;
+ background: transparent;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+.pager a:hover { background: var(--fg); color: var(--bg); border-color: var(--fg); }
+.pager a:hover .pager__t { color: var(--bg); }
+.pager .pager__k {
+ font-family: var(--font-mono);
+ font-size: var(--type-001);
+ letter-spacing: var(--track-label);
+ text-transform: uppercase;
+ color: var(--fg-mute);
+}
+.pager .pager__t { font-family: var(--font-sans); font-weight: 500; font-size: var(--type-1); }
+.pager .next { text-align: right; align-items: flex-end; }
+@media (max-width: 760px) { .pager { grid-template-columns: 1fr; } .pager .next { text-align: left; align-items: flex-start; } }
diff --git a/site/assets/site.js b/site/assets/site.js
new file mode 100644
index 0000000..271f582
--- /dev/null
+++ b/site/assets/site.js
@@ -0,0 +1,23 @@
+// bit — site-level interactivity
+(function () {
+ // mobile nav toggle
+ const menu = document.querySelector(".topbar__menu");
+ const nav = document.querySelector(".topbar nav");
+ if (menu && nav) {
+ menu.addEventListener("click", () => nav.classList.toggle("open"));
+ }
+
+ // hero install copy (single button)
+ const heroCopy = document.querySelector(".hero__install button");
+ if (heroCopy) {
+ heroCopy.addEventListener("click", () => {
+ const cmd = heroCopy.parentElement.querySelector(".cmd");
+ if (!cmd) return;
+ navigator.clipboard.writeText(cmd.textContent).then(() => {
+ const original = heroCopy.textContent;
+ heroCopy.textContent = "copied";
+ setTimeout(() => { heroCopy.textContent = original; }, 1400);
+ });
+ });
+ }
+})();
diff --git a/site/assets/syntax.js b/site/assets/syntax.js
new file mode 100644
index 0000000..7d751e1
--- /dev/null
+++ b/site/assets/syntax.js
@@ -0,0 +1,100 @@
+// bit — minimal MoonBit syntax highlighter
+// Vanilla JS, no deps. Tokenizes via a single ordered regex (Crockford-style).
+// Classes match site.css: tok-cm, tok-kw, tok-ty, tok-str, tok-num, tok-fn, tok-pm
+(function () {
+ const KEYWORDS = new Set([
+ "let", "fn", "match", "if", "else", "for", "while", "return", "mut",
+ "loop", "break", "continue", "as", "in", "with", "struct", "enum",
+ "trait", "impl", "type", "typealias", "test", "guard", "raise",
+ "try", "catch", "self", "and", "or", "not", "where", "is",
+ "derive", "init", "new"
+ ]);
+ const PREAMBLE = new Set([
+ "pub", "priv", "extern", "import", "package", "fnalias", "traitalias",
+ "typealias", "async"
+ ]);
+ const TYPES = new Set([
+ "Int", "Int64", "UInt", "UInt64", "Double", "Float", "Bool", "String",
+ "Char", "Bytes", "Unit", "Array", "FixedArray", "Map", "HashMap",
+ "HashSet", "Option", "Result", "Iter", "Ref", "BigInt", "Json"
+ ]);
+ const CONSTS = new Set(["true", "false", "None", "Some", "Ok", "Err", "Nil"]);
+
+ // ordered token regex — first hit wins
+ const RE = new RegExp([
+ "(\\/\\/[^\\n]*)", // 1 line comment
+ "(\\/\\*[\\s\\S]*?\\*\\/)", // 2 block comment
+ "(b?\"(?:\\\\.|[^\"\\\\])*\")", // 3 string / b-string
+ "('(?:\\\\.|[^'\\\\])')", // 4 char literal
+ "(0x[0-9a-fA-F_]+|0b[01_]+|0o[0-7_]+|[0-9][0-9_]*(?:\\.[0-9_]+)?(?:[eE][+-]?[0-9_]+)?[a-zA-Z]*)", // 5 number
+ "([A-Za-z_][A-Za-z0-9_]*)", // 6 ident
+ "([^\\s\\w]+)" // 7 punct (passthrough)
+ ].join("|"), "g");
+
+ function esc(s) {
+ return s.replace(/&/g, "&").replace(//g, ">");
+ }
+
+ function highlight(src) {
+ let out = "";
+ let lastIndex = 0;
+ let m;
+ RE.lastIndex = 0;
+ while ((m = RE.exec(src)) !== null) {
+ if (m.index > lastIndex) out += esc(src.slice(lastIndex, m.index));
+ const [_, lc, bc, str, ch, num, id, punct] = m;
+ if (lc) out += `${esc(lc)}`;
+ else if (bc) out += `${esc(bc)}`;
+ else if (str)out += `${esc(str)}`;
+ else if (ch) out += `${esc(ch)}`;
+ else if (num)out += `${esc(num)}`;
+ else if (id) {
+ if (PREAMBLE.has(id)) out += `${id}`;
+ else if (KEYWORDS.has(id)) out += `${id}`;
+ else if (TYPES.has(id)) out += `${id}`;
+ else if (CONSTS.has(id)) out += `${id}`;
+ else {
+ // function call detection: ident followed by '('
+ const next = src[m.index + id.length];
+ if (next === "(") out += `${id}`;
+ else out += id;
+ }
+ } else if (punct) out += esc(punct);
+ lastIndex = RE.lastIndex;
+ }
+ if (lastIndex < src.length) out += esc(src.slice(lastIndex));
+ return out;
+ }
+
+ // bash/shell minimal: comments + strings + prompt
+ function shellHighlight(src) {
+ return esc(src)
+ .replace(/(^|\n)(\$|#) /g, (_, nl, sigil) => `${nl}${sigil} `)
+ .replace(/(#[^\n]*)/g, '$1')
+ .replace(/("[^"]*"|'[^']*')/g, '$1');
+ }
+
+ function init() {
+ document.querySelectorAll('pre code[data-lang]').forEach((el) => {
+ const lang = el.dataset.lang;
+ const src = el.textContent;
+ if (lang === "moonbit" || lang === "mbt") el.innerHTML = highlight(src);
+ else if (lang === "bash" || lang === "sh") el.innerHTML = shellHighlight(src);
+ });
+
+ document.querySelectorAll('.codeblock__copy').forEach((btn) => {
+ btn.addEventListener("click", () => {
+ const code = btn.closest('.codeblock').querySelector('pre code');
+ if (!code) return;
+ navigator.clipboard.writeText(code.textContent).then(() => {
+ btn.textContent = "copied";
+ btn.classList.add("copied");
+ setTimeout(() => { btn.textContent = "copy"; btn.classList.remove("copied"); }, 1400);
+ });
+ });
+ });
+ }
+
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
+ else init();
+})();
diff --git a/site/assets/tokens.css b/site/assets/tokens.css
new file mode 100644
index 0000000..b47b9ac
--- /dev/null
+++ b/site/assets/tokens.css
@@ -0,0 +1,164 @@
+/*
+ * bit — Design Tokens (CSS)
+ * Surface-level tokens for color, typography, spacing, motion, and shape.
+ * No gradients. No shadows. Strict palette. Geometric primitives only.
+ */
+
+@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Space+Mono:wght@400;700&display=swap");
+
+:root {
+ /* ──────────────────────────────────────────────────────────────
+ * COLOR — strict 5-token palette. Never blend, never gradient.
+ * ────────────────────────────────────────────────────────────── */
+
+ /* Primary — Bit Pink. The node, the source, the single bit. */
+ --bit-pink: #FF2D6F;
+ --bit-pink-deep: #C7124F; /* for press/hover states only */
+ --bit-pink-tint: #FFD9E6; /* for filled tag/chip backgrounds */
+
+ /* Ink — off-black with cool tint. Editorial. Never #000. */
+ --bit-ink: #0E0E12;
+ --bit-ink-soft: #2B2B33; /* secondary text */
+ --bit-ink-mute: #6B6B74; /* meta / captions */
+
+ /* Paper — warm cream. Never #FFF. */
+ --bit-paper: #F2EFE7;
+ --bit-paper-deep: #E8E4D8; /* alt surface, panels */
+
+ /* Stone — desaturated neutral. UI lines, dividers, tracks. */
+ --bit-stone: #D9D5CB;
+
+ /* Acid — the eccentric counterpoint. Use SPARINGLY. */
+ --bit-acid: #D6FF3D;
+
+ /* ──────────────────────────────────────────────────────────────
+ * SEMANTIC ALIASES
+ * ────────────────────────────────────────────────────────────── */
+
+ --bg: var(--bit-paper);
+ --bg-alt: var(--bit-paper-deep);
+ --fg: var(--bit-ink);
+ --fg-soft: var(--bit-ink-soft);
+ --fg-mute: var(--bit-ink-mute);
+ --rule: var(--bit-ink); /* rules are ink, full strength */
+ --rule-soft: var(--bit-stone);
+ --accent: var(--bit-pink);
+ --accent-press: var(--bit-pink-deep);
+ --highlight: var(--bit-acid);
+
+ /* ──────────────────────────────────────────────────────────────
+ * TYPOGRAPHY
+ * Sans → Space Grotesk (display + body)
+ * Mono → Space Mono (machine voice, captions, code)
+ * No italic. Ever.
+ * ────────────────────────────────────────────────────────────── */
+
+ --font-sans: "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
+ --font-mono: "Space Mono", ui-monospace, "JetBrains Mono", "SFMono-Regular", Menlo, monospace;
+
+ /* Type scale — modular, tight at display, loose at body */
+ --type-001: 11px; /* micro label */
+ --type-00: 13px; /* caption, meta */
+ --type-0: 15px; /* small body */
+ --type-1: 17px; /* body */
+ --type-2: 21px; /* lead */
+ --type-3: 28px; /* h4 */
+ --type-4: 40px; /* h3 */
+ --type-5: 60px; /* h2 */
+ --type-6: 96px; /* h1 */
+ --type-7: 148px; /* hero, poster */
+
+ /* Line heights — display tight, body comfortable */
+ --leading-tight: 0.95;
+ --leading-snug: 1.08;
+ --leading-body: 1.55;
+ --leading-loose: 1.72;
+
+ /* Tracking — display tightens, micro labels open up */
+ --track-display: -0.035em;
+ --track-heading: -0.018em;
+ --track-body: 0;
+ --track-label: 0.16em; /* UPPERCASE labels only */
+ --track-mono: 0;
+
+ /* ──────────────────────────────────────────────────────────────
+ * SHAPE — geometric primitives, no soft rounding.
+ * ────────────────────────────────────────────────────────────── */
+
+ --radius-none: 0;
+ --radius-dot: 999px; /* circles only — never used on rectangles */
+ --stroke-hair: 1px;
+ --stroke-thin: 1.5px;
+ --stroke-med: 2px;
+ --stroke-thick: 4px;
+
+ /* ──────────────────────────────────────────────────────────────
+ * SPACE — 4px base, prefer larger jumps for editorial whitespace.
+ * ────────────────────────────────────────────────────────────── */
+
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 24px;
+ --space-6: 32px;
+ --space-7: 48px;
+ --space-8: 72px;
+ --space-9: 112px;
+ --space-10: 168px;
+
+ /* ──────────────────────────────────────────────────────────────
+ * MOTION — instant or slow. No "smooth-default".
+ * ────────────────────────────────────────────────────────────── */
+
+ --ease-snap: cubic-bezier(0.2, 0.8, 0.2, 1);
+ --ease-flat: linear;
+ --dur-fast: 120ms;
+ --dur-slow: 480ms;
+
+ color-scheme: light;
+}
+
+/* ──────────────────────────────────────────────────────────────
+ * DARK MODE — invert paper/ink, keep pink and acid identical.
+ * ────────────────────────────────────────────────────────────── */
+@media (prefers-color-scheme: dark) {
+ :root {
+ --bg: var(--bit-ink);
+ --bg-alt: #17171C;
+ --fg: var(--bit-paper);
+ --fg-soft: #C9C5BB;
+ --fg-mute: #8A867D;
+ --rule: var(--bit-paper);
+ --rule-soft: #3A3A42;
+
+ color-scheme: dark;
+ }
+}
+
+/* Forced dark utility (e.g. previews) */
+[data-theme="dark"] {
+ --bg: var(--bit-ink);
+ --bg-alt: #17171C;
+ --fg: var(--bit-paper);
+ --fg-soft: #C9C5BB;
+ --fg-mute: #8A867D;
+ --rule: var(--bit-paper);
+ --rule-soft: #3A3A42;
+
+ color-scheme: dark;
+}
+
+/* ──────────────────────────────────────────────────────────────
+ * BRAND RULE — NO ITALIC.
+ * Browser defaults render , , , , ,
+ * in italic. We do not ship italic fonts, so the browser would
+ * synthesize a slanted face — on a geometric grotesque that reads
+ * as a deformed serif. We override globally and carry emphasis
+ * through weight + color instead.
+ * ────────────────────────────────────────────────────────────── */
+em, i, cite, dfn, address, var, q {
+ font-style: normal;
+ font-weight: 600;
+ color: var(--fg);
+}
diff --git a/site/content/learn/concept.md b/site/content/learn/concept.md
new file mode 100644
index 0000000..ec327da
--- /dev/null
+++ b/site/content/learn/concept.md
@@ -0,0 +1,91 @@
+---
+title: Concept
+section: learn
+slug: concept
+order: 0
+nav_label: Concept
+summary: What bit is, what is interesting about it, and the five-concept mental model that makes everything else fall into place.
+meta: ~7 min
+kicker: // guide · 00 — the idea
+h1: What bit is.
+lead: bit is a Git implementation in MoonBit. It speaks the Git wire protocol, it produces a normal `.git/`, and any Git client can read what it writes. Underneath, bit treats distribution as the default — not a feature bolted onto a central server, but the entire premise.
+prev_href: ./
+prev_kicker: back
+prev_label: Learning Guide
+next_href: install.html
+next_kicker: next · 01
+next_label: Install bit
+---
+
+## What's interesting
+
+Three things distinguish bit from "yet another Git client."
+
+**PRs and issues live inside the repo.** Pull requests, reviews, and issues are written as Git objects under `refs/notes/bit-hub`. Replicate the repo and you replicate the work. GitHub can go away. Your project history — and the conversations around it — survive intact.
+
+**bit is a library before it is a tool.** Every CLI command is a thin shell over a MoonBit function. Drop bit into an agent, an editor plugin, or a CI runner — you do not shell out, you call it. Mount an in-memory filesystem and you have a working repository in five lines.
+
+**Convergence is gossip, not consensus.** Peers reconcile by walking object graphs until they meet a common ancestor. No election, no quorum, no leader. A laptop offline for a week catches up the moment it touches a peer.
+
+## A five-concept mental model
+
+These five words carry the whole system. If you have them straight, the CLI and the library both stop surprising you.
+
+### Node
+
+A node is any process holding a copy of the repository. Your laptop is a node. A CI runner is a node. A relay host is a node. Nodes are symmetric — there is no "primary" — and they are addressed by URI:
+
+```text
+bit://aurora.local:9418/repo
+relay://bit.example.com/u/mizchi/bit
+https://github.com/mizchi/bit.git
+```
+
+### Source
+
+Source is whoever wrote the commit you are looking at. It is a fact, not a place. The pink dot in the bit logo represents the source node; in any given moment, *any* node can become it.
+
+```moonbit
+let commit = @bit.head(fs, ".git")?
+println("author: \{commit.author}")
+println("source: \{commit.committer.tz}@\{commit.committer.email}")
+```
+
+### Bit
+
+A bit is a single edge between two peers — one unit of exchange. Communication is content-addressed: peers ask for object hashes, never filenames. The protocol is therefore naturally idempotent and naturally deduplicating.
+
+### Convergence
+
+Two peers handshake, exchange ref tips, and walk the graph backwards until they meet. Bytes flow in both directions. Disagreement persists as parallel heads until a human merges them. There is no server-side reconciliation step. There is no server.
+
+### Hub
+
+PRs and issues are stored as Git objects. The hub is not a service — it is a particular shape of commit graph. `bit pr create` writes objects. `bit relay sync push` replicates them. The hub survives forks because it has no "outside" to survive against.
+
+## What follows from this
+
+Implications you can feel as soon as you start using it.
+
+- The hub survives forks. Fork a repo, get every PR and issue with it.
+- Agents drive bit without a shell. The same MoonBit functions the CLI calls, your agent can call directly.
+- You can clone a single subdirectory of a 50 GB monorepo with `bit clone user/repo:src/lib`.
+- A merge conflict resolved by AI is just commits plus a note — no third-party state, no external dashboard.
+- You can write your own bit-aware tool in MoonBit in an afternoon. The library API is the surface.
+
+## What bit is not
+
+Equally important — bit refuses several adjacent identities.
+
+- **Not a Git replacement.** It *is* Git. The bytes on disk are valid Git, the wire protocol is Git's, the SHA-1 history is interchangeable. Use it next to vanilla `git` if you want.
+- **Not a blockchain.** No consensus, no tokens, no proof-of-anything. Convergence is gossip.
+- **Not a P2P file share.** bit is collaboration software that happens to be distributed. The unit of meaning is a commit, not a file.
+- **Not an LLM agent.** AI assist is one optional command (`bit ai rebase`), not the architecture.
+
+## What's next
+
+Now that you have the model, the rest of the guide is mostly muscle memory.
+
+- Read [Install bit](install.html) and get the binary on PATH.
+- Walk through [Your first commit](first-commit.html) for the porcelain refresher.
+- Hit [Going distributed](distributed.html) to see two peers converge.
diff --git a/site/content/learn/distributed.md b/site/content/learn/distributed.md
new file mode 100644
index 0000000..b8d0336
--- /dev/null
+++ b/site/content/learn/distributed.md
@@ -0,0 +1,111 @@
+---
+title: Going distributed
+section: learn
+slug: distributed
+order: 3
+nav_label: Going distributed
+summary: Clone over HTTP and over a bit relay. Push, fetch, and watch gossip converge two peers.
+meta: ~10 min
+kicker: // guide · 03
+h1: Going distributed.
+lead: bit was built on the premise that source control should not depend on a central host. In this chapter you will clone a repository over HTTP and over a bit *relay*, push commits in both directions, and watch two peers converge through gossip.
+prev_href: first-commit.html
+prev_kicker: prev · 02
+prev_label: Your first commit
+next_href: hub.html
+next_kicker: next · 04
+next_label: PRs & issues
+---
+
+## Mental model
+
+A bit **node** is any process holding a copy of the repository. Your laptop, a CI runner, a relay host — all peers, no master. Two peers reconcile by exchanging ref tips and walking the object graph until they meet a common ancestor.
+
+| Mode | Trigger | Direction |
+| --- | --- | --- |
+| push | explicit | local → remote |
+| fetch | explicit | remote → local |
+| gossip | periodic, on handshake | both |
+| relay sync | via relay host | both, async |
+
+## Clone over HTTP
+
+This is the standard Git path. bit speaks `git-upload-pack` and `git-receive-pack` over smart HTTP.
+
+```bash
+$ bit clone https://github.com/user/repo
+$ cd repo
+$ bit remote -v
+origin https://github.com/user/repo (fetch)
+origin https://github.com/user/repo (push)
+```
+
+## Clone over a relay
+
+A *relay* is a lightweight bit-aware host. It speaks the bit protocol over HTTP and supports async sync — useful when peers are not online at the same time.
+
+```bash
+$ bit clone relay://bit.example.com/u/me/repo
+$ cd repo
+```
+
+## Two peers, one truth
+
+Open two terminals. In the first, simulate node A:
+
+```bash
+$ bit clone relay://bit.example.com/u/me/repo a
+$ cd a
+$ echo "from A" >> note.txt
+$ bit add . && bit commit -m "A writes"
+$ bit push origin main
+```
+
+In the second terminal, node B:
+
+```bash
+$ bit clone relay://bit.example.com/u/me/repo b
+$ cd b
+$ bit fetch origin
+$ bit log --oneline
+a8c2f6e (origin/main) A writes
+1d0e7c2 root commit
+```
+
+B now holds A's history without ever talking to A directly. The relay carried the bytes asynchronously.
+
+## Concurrent writes
+
+Two peers writing at the same time produce two heads. bit's history graph is content-addressed, so collisions are deterministic — both heads are valid until a merge or rebase reconciles them. There is no server vote.
+
+
+
// recommendation
+
Use bit ai rebase when the conflict is mechanical (formatting drift, import re-orderings). For semantic conflicts, do the merge by hand — the AI assist is wrong often enough that you want to read what it proposes.
+
+
+## Inspect the graph
+
+```bash
+$ bit log --graph --oneline --all
+* 1f02776 (HEAD -> main) merge: reconcile A and B
+|\
+| * e871d71 B writes
+* | a8c2f6e A writes
+|/
+* 1d0e7c2 root commit
+```
+
+## From MoonBit
+
+The same flow scripted against the library. `Node::from_uri` opens a peer session; `fetch` walks the graph diff.
+
+```moonbit
+let peer = @bit.Node::from_uri("bit://aurora.local:9418/repo")
+peer.handshake(timeout=2.s)?
+
+let local_refs = @bit.refs(fs, ".git")
+let wants = local_refs.diff(peer.refs())
+peer.fetch(wants)?
+
+println("now at: \{peer.refs().get(\"refs/heads/main\")}")
+```
diff --git a/site/content/learn/first-commit.md b/site/content/learn/first-commit.md
new file mode 100644
index 0000000..41645a0
--- /dev/null
+++ b/site/content/learn/first-commit.md
@@ -0,0 +1,91 @@
+---
+title: Your first commit
+section: learn
+slug: first-commit
+order: 2
+nav_label: Your first commit
+summary: Initialize a repo, write a file, stage it, commit it, read the log. Git muscle memory works — bit is a drop-in.
+meta: ~5 min
+kicker: // guide · 02
+h1: Your first commit.
+lead: If you have ever used Git, you already know this. bit reuses the porcelain — `init`, `add`, `commit`, `log` all behave the way you expect. The first commit is a sanity check that the binary works.
+prev_href: install.html
+prev_kicker: prev · 01
+prev_label: Install bit
+next_href: distributed.html
+next_kicker: next · 03
+next_label: Going distributed
+---
+
+## Initialize a repository
+
+```bash
+$ mkdir hello-bit
+$ cd hello-bit
+$ bit init
+Initialized empty bit repository in .git/
+```
+
+bit writes a standard `.git/` directory. Any existing Git tool can read it. There is no parallel `.bit/` store.
+
+## Write and stage
+
+```bash
+$ echo "hello, peer" > note.txt
+$ bit add note.txt
+$ bit status
+On branch main
+
+Changes to be committed:
+ new file: note.txt
+```
+
+
+
// note
+
The status output is intentionally identical to Git's. bit emits the same line shapes so your editor's plugins, your hooks, and your grep incantations keep working.
+
+
+## Commit
+
+```bash
+$ bit commit -m "first node speaks"
+[main (root-commit) a8c2f6e] first node speaks
+ 1 file changed, 1 insertion(+)
+ create mode 100644 note.txt
+```
+
+## Read the log
+
+```bash
+$ bit log --graph --oneline
+* a8c2f6e (HEAD -> main) first node speaks
+```
+
+The graph renderer is native — `--graph`, `--stat`, `--name-only`, `--name-status`, and `--topo-order` all run inside bit without shelling out.
+
+## What just happened
+
+bit wrote three Git objects under `.git/objects/`:
+
+- a **blob** for `note.txt` (the file contents)
+- a **tree** describing the directory snapshot
+- a **commit** pointing at the tree, with your author identity and message
+
+These are the same object types every Git client speaks. Anything you commit with bit can be pushed to a Git server, and anything cloned from a Git server can be operated on by bit.
+
+## Try it from MoonBit
+
+The same flow, scripted against an in-memory filesystem — no shell involved. This is how agents drive bit.
+
+```moonbit
+let fs = @bit.TestFs::new()
+let root = "/hello-bit"
+
+run_storage_command(fs, fs, root, "init", ["-q"])
+fs.write_string(root + "/note.txt", "hello, peer")
+run_storage_command(fs, fs, root, "add", ["note.txt"])
+run_storage_command(fs, fs, root, "commit", ["-m", "first node speaks"])
+
+let head = run_storage_command(fs, fs, root, "rev-parse", ["HEAD"])
+println("commit: \{head}")
+```
diff --git a/site/content/learn/index.md b/site/content/learn/index.md
new file mode 100644
index 0000000..fd59b84
--- /dev/null
+++ b/site/content/learn/index.md
@@ -0,0 +1,11 @@
+---
+title: Learning Guide
+section: learn
+slug: index
+order: 0
+template: index
+hero_tag: est. 35 min — six chapters
+kicker: // guide
+h1: Learning Guide.
+lead: A guided path through bit. By the end you will understand the model, install the binary, make commits against a real repository, replicate history to a second peer, and open a local pull request — all without leaving your terminal.
+---
diff --git a/site/content/learn/install.md b/site/content/learn/install.md
new file mode 100644
index 0000000..5079619
--- /dev/null
+++ b/site/content/learn/install.md
@@ -0,0 +1,84 @@
+---
+title: Install bit
+section: learn
+slug: install
+order: 1
+nav_label: Install bit
+summary: One-line install on macOS & Linux, MoonBit toolchain, shell completion. The 30-second on-ramp.
+meta: ~5 min
+kicker: // guide · 01
+h1: Install bit.
+lead: Two install paths — a single curl line, or via the MoonBit toolchain if you already have one. Both produce a `bit` binary on PATH.
+prev_href: ./
+prev_kicker: back
+prev_label: Learning Guide
+next_href: first-commit.html
+next_kicker: next · 02
+next_label: Your first commit
+---
+
+## Supported platforms
+
+- Linux x86_64
+- macOS arm64 & x86_64
+
+Windows is not supported yet. WSL works.
+
+## One-line install
+
+```bash
+$ curl -fsSL https://raw.githubusercontent.com/mizchi/bit-vcs/main/install.sh | bash
+```
+
+The script detects your OS and architecture, downloads the matching release, and drops the binary at `~/.local/bin/bit`. Add it to your shell's `PATH` if it isn't already.
+
+
+
// note
+
Read the script before you pipe it to bash — that's the rule for every install script on the internet, not just this one. Mirror at github.com/mizchi/bit-vcs.
+
+
+## Via the MoonBit toolchain
+
+If you have `moon` installed:
+
+```bash
+$ moon install mizchi/bit/cmd/bit
+```
+
+This builds bit from source against your toolchain. Useful if you want to track main or contribute patches.
+
+## Verify
+
+```bash
+$ bit --version
+bit 0.1.0 — distributed git in moonbit
+```
+
+## Shell completion
+
+bit ships its own completion generator. Pick your shell and source the output from your rc file.
+
+### bash
+
+```bash
+eval "$(bit completion bash)"
+```
+
+### zsh
+
+```bash
+eval "$(bit completion zsh)"
+```
+
+## Uninstall
+
+```bash
+$ rm ~/.local/bin/bit
+```
+
+That's it. bit has no daemons, no system services, no cache outside your repos.
+
+
+
// caveat
+
bit is experimental. Data corruption is possible in worst-case scenarios. Always keep a Git backup of important repositories until you have stress-tested your workflow.
+
diff --git a/site/content/reference/cli.md b/site/content/reference/cli.md
new file mode 100644
index 0000000..f4264a5
--- /dev/null
+++ b/site/content/reference/cli.md
@@ -0,0 +1,124 @@
+---
+title: CLI commands
+section: reference
+slug: cli
+order: 1
+nav_label: CLI commands
+summary: Every subcommand grouped by surface — porcelain, plumbing, hub, relay, extensions, AI. Flags, exit codes, examples.
+meta: 108 commands
+kicker: // reference · R1
+h1: CLI commands.
+lead: bit implements 108 Git commands natively. Anything not listed here falls back to Git's surface — read [going distributed](../learn/distributed.html) for the protocol-level differences.
+prev_href: ./
+prev_kicker: back
+prev_label: Reference
+next_href: library.html
+next_kicker: next · R2
+next_label: Library API
+---
+
+## Porcelain
+
+Everyday commands. Identical surface to Git.
+
+| Command | Description |
+| --- | --- |
+| `bit init` | Create an empty `.git/` in the current directory. |
+| `bit clone ` | Clone over HTTP, relay, or a subdirectory URL (see extensions). |
+| `bit add …` | Stage files. Supports `-A`, `-p`, `--update`. |
+| `bit commit -m ` | Record staged changes. `--amend`, `--no-edit`, `--allow-empty`. |
+| `bit status` | Working tree state in Git's exact line format. |
+| `bit log` | Native graph renderer. `--graph`, `--stat`, `--name-only`, `--topo-order`. |
+| `bit diff` | Worktree vs index, or any two trees / commits. |
+| `bit checkout` | Switch branches, restore files. `-b` creates. |
+| `bit branch` | List, create, delete branches. |
+| `bit merge` | Three-way merge with conflict markers. |
+| `bit rebase` | Native `-i` with editor injection. |
+| `bit cherry-pick ` | Apply commits onto HEAD. |
+| `bit revert ` | Reverse a commit. |
+| `bit fetch` | Download refs and objects from a remote. |
+| `bit pull` | Fetch and merge / rebase. |
+| `bit push` | Upload refs and objects. HTTPS, relay, LFS upload. |
+| `bit remote` | Manage remotes. |
+| `bit tag` | Lightweight and annotated tags. |
+| `bit stash` | Shelve worktree state. |
+| `bit reset` | `--soft`, `--mixed`, `--hard`. |
+
+## Plumbing
+
+Low-level operations on Git objects. Same surface as Git's plumbing.
+
+| Command | Description |
+| --- | --- |
+| `bit hash-object` | Compute the SHA-1 of a file (optionally write to objects). |
+| `bit cat-file` | Print object type, size, or contents. |
+| `bit rev-parse` | Resolve refs and revisions to SHAs. |
+| `bit ls-tree` | List tree contents. |
+| `bit ls-files` | List files in the index. |
+| `bit update-ref` | Move a ref atomically. |
+| `bit pack-objects` | Build a packfile. |
+| `bit unpack-objects` | Explode a packfile into loose objects. |
+| `bit symbolic-ref` | Read or write a symbolic ref like `HEAD`. |
+
+## Hub — PRs and issues
+
+Local, server-less collaboration backed by Git objects. Sync via relay.
+
+| Command | Description |
+| --- | --- |
+| `bit pr init` | Initialize hub metadata in the current repo (writes `.git/hub/policy.toml`). |
+| `bit pr create` | `--title`, `--body`, `--head`, `--base`. Opens a local PR. |
+| `bit pr list` | `--open`, `--closed`, `--all`. |
+| `bit pr review ` | `--approve`, `--request-changes`, `--commit `. |
+| `bit pr merge ` | Merge into base. Honors policy. |
+| `bit pr search ` | Full-text over PR titles and bodies. |
+| `bit issue create` | `--title`, `--body`, `--parent `. |
+| `bit issue list` | `--open`, `--tree`, `--all`, `--parent`. |
+| `bit issue link ` | Cross-link issue ↔ PR. |
+| `bit issue search ` | Full-text over issues. |
+
+## Relay
+
+Sync PR and issue metadata between peers via an HTTP relay.
+
+| Command | Description |
+| --- | --- |
+| `bit relay sync push ` | Upload hub notes to a relay. |
+| `bit relay sync fetch ` | Download hub notes from a relay. |
+| `bit relay serve` | Run a relay host locally. Supports LFS Batch API. |
+
+## Extensions
+
+| Command | Description |
+| --- | --- |
+| `bit subdir-clone ` | Clone a subdirectory as an independent repo. |
+| `bit clone user/repo:path` | Shorthand for subdir-clone via the standard clone command. |
+| `bit hq get ` | `ghq`-compatible repo manager. Default root: `~/bhq`. |
+| `bit hq list` | List all repos under hq. |
+| `bit workspace flow ` | Workspace fingerprint-based task runner. |
+| `bit fingerprint` | Workspace / PR fingerprint helpers. |
+| `bit completion ` | Emit shell completion script. `bash` or `zsh`. |
+
+## AI assist
+
+AI-assisted conflict resolution via OpenRouter. Default model: `moonshotai/kimi-k2`. Requires `OPENROUTER_API_KEY`.
+
+| Command | Description |
+| --- | --- |
+| `bit ai rebase ` | Rebase with AI resolving conflicts. `--continue`, `--abort`, `--skip`. |
+| `bit ai merge ` | Merge with AI assist. |
+| `bit ai commit` | AI commit message (`--split` for multi-commit suggestion). |
+| `bit ai cherry-pick ` | Cherry-pick with AI conflict resolution. |
+| `bit ai revert ` | Revert with AI. |
+
+### Common AI flags
+
+- `--model ` — override the OpenRouter model.
+- `--max-ai-rounds ` — bound conflict-resolution turns.
+- `--agent-loop` — give the AI an agent loop, not single-shot.
+- `--agent-max-steps ` — bound the agent loop.
+
+
+
// see also
+
The Library API exposes the same surface to MoonBit code — drive any of the above from an agent without spawning a process.
+
diff --git a/site/content/reference/env.md b/site/content/reference/env.md
new file mode 100644
index 0000000..2c2be5b
--- /dev/null
+++ b/site/content/reference/env.md
@@ -0,0 +1,53 @@
+---
+title: Environment
+section: reference
+slug: env
+order: 3
+nav_label: Environment
+summary: Every `BIT_*` variable bit reads, and the Git-compat variables it honors (`GIT_EDITOR`, `GIT_CONFIG_GLOBAL`, …).
+meta: 12 vars
+kicker: // reference · R3
+h1: Environment variables.
+lead: Every `BIT_*` variable bit reads, plus the Git-compat variables it honors. Set them in your shell rc, in `.envrc`, or per-command.
+prev_href: library.html
+prev_kicker: prev · R2
+prev_label: Library API
+next_href: ../
+next_kicker: end
+next_label: Back to home
+---
+
+## bit-native
+
+| Variable | Default | Effect |
+| --- | --- | --- |
+| `BIT_BENCH_GIT_DIR` | — | Override `.git` path for `bench_real` (vfs benchmarks). |
+| `BIT_PACK_CACHE_LIMIT` | `2` | Max packfiles kept in memory. `0` disables the cache. |
+| `BIT_RACY_GIT` | unset | When set, rehash even if `stat` matches — avoids racy-git false negatives on fast filesystems. |
+| `BIT_WORKSPACE_FINGERPRINT_MODE` | `git` | `git` for add-all-style snapshots, `fast` for per-node directory hashes. |
+| `BIT_HUB_INIT_PROMPT` | `1` (interactive) | Force-on (`1`) or force-off (`0`) the `bit pr init` / `bit issue init` prompts. |
+
+## Git-compat
+
+bit honors the standard Git environment so existing tooling, hooks, and editors keep working.
+
+| Variable | Effect |
+| --- | --- |
+| `GIT_DIR` | Override the location of the repository's `.git` directory. |
+| `GIT_WORK_TREE` | Override the working tree root. |
+| `GIT_CONFIG_GLOBAL` | Path to global config (default `~/.gitconfig`). |
+| `GIT_EDITOR` | Editor used for commit messages, rebase conflict markers. |
+| `GIT_SEQUENCE_EDITOR` | Editor used for the rebase todo list. |
+| `GIT_AUTHOR_NAME` / `GIT_AUTHOR_EMAIL` | Override commit author identity. |
+| `GIT_COMMITTER_NAME` / `GIT_COMMITTER_EMAIL` | Override committer identity. |
+
+## AI assist
+
+| Variable | Effect |
+| --- | --- |
+| `OPENROUTER_API_KEY` | Required for any `bit ai *` subcommand. |
+
+
+
// note
+
bit never reads secrets from the repository — keys live only in the environment. If you set OPENROUTER_API_KEY via .envrc, make sure that file is git-ignored.
+
diff --git a/site/content/reference/index.md b/site/content/reference/index.md
new file mode 100644
index 0000000..3708816
--- /dev/null
+++ b/site/content/reference/index.md
@@ -0,0 +1,11 @@
+---
+title: Reference
+section: reference
+slug: index
+order: 0
+template: index
+hero_tag: complete · authoritative
+kicker: // reference
+h1: Reference.
+lead: Everything bit exposes — every command, every public type, every environment variable. Sorted, terse, link-anchored. Reach for this when you know what you are looking for.
+---
diff --git a/site/content/reference/library.md b/site/content/reference/library.md
new file mode 100644
index 0000000..89fa3dc
--- /dev/null
+++ b/site/content/reference/library.md
@@ -0,0 +1,178 @@
+---
+title: Library API
+section: reference
+slug: library
+order: 2
+nav_label: Library API
+summary: MoonBit types and functions for embedding bit in your own programs — agents, CI runners, editor plugins.
+meta: moonbit
+kicker: // reference · R2
+h1: Library API.
+lead: bit is a MoonBit library first, a CLI second. Every CLI command is a thin shell around a public function. Embed bit directly in your agent, editor plugin, or CI runner — no subprocess required.
+prev_href: cli.html
+prev_kicker: prev · R1
+prev_label: CLI commands
+next_href: env.html
+next_kicker: next · R3
+next_label: Environment
+---
+
+## Package layout
+
+Source is organized in five layers, dependencies flow downward only.
+
+| Layer | Path | What lives here |
+| --- | --- | --- |
+| core | `src/core/` | Object types, SHA, ref store primitives. |
+| mid | `src/mid/` | Index, pack, merge, diff algorithms. |
+| high | `src/high/` | Porcelain operations (commit, rebase, …). |
+| ext | `src/ext/` | Hub, relay, LFS, AI, workspace. |
+| cmd | `src/cmd/` | CLI entry points. |
+
+## Storage runtime
+
+The entry point for embedding. Any storage that implements `FileSystem` and `RepoFileSystem` can host a repository.
+
+### Traits
+
+```moonbit
+pub trait FileSystem {
+ write_string(Self, String, String) -> Unit
+ write_bytes(Self, String, Bytes) -> Unit
+ mkdir_p(Self, String) -> Unit
+ remove(Self, String) -> Unit
+ rename(Self, String, String) -> Unit
+}
+
+pub trait RepoFileSystem {
+ read_string(Self, String) -> String?
+ read_bytes(Self, String) -> Bytes?
+ exists(Self, String) -> Bool
+ readdir(Self, String) -> Array[String]
+ stat(Self, String) -> FileStat?
+}
+```
+
+### Entry function
+
+```moonbit
+pub fn run_storage_command[F : FileSystem + RepoFileSystem](
+ fs : F,
+ rfs : F,
+ root : String,
+ cmd : String,
+ args : Array[String]
+) -> String
+```
+
+### In-memory backend
+
+`TestFs` is the reference implementation. Use it for agents, tests, and ephemeral repos.
+
+```moonbit
+let fs = @bit.TestFs::new()
+let root = "/agent-repo"
+
+run_storage_command(fs, fs, root, "init", ["-q"])
+fs.write_string(root + "/note.txt", "hello, peer")
+run_storage_command(fs, fs, root, "add", ["note.txt"])
+run_storage_command(fs, fs, root, "commit", ["-m", "agent snapshot"])
+
+let head = run_storage_command(fs, fs, root, "rev-parse", ["HEAD"])
+println("head: \{head}")
+```
+
+## Fs — virtual filesystem
+
+Mount any commit as a read-only filesystem with lazy blob loading.
+
+```moonbit
+let mounted = @bit.Fs::from_commit(fs, ".git", commit_id)
+let entries = mounted.readdir(fs, "src")
+let content = mounted.read_file(fs, "src/main.mbt")?
+
+for name in entries {
+ println("- \{name}")
+}
+```
+
+| Method | Description |
+| --- | --- |
+| `Fs::from_commit(fs, gitdir, sha)` | Open a virtual FS rooted at a commit's tree. |
+| `Fs::from_tree(fs, gitdir, sha)` | Open from a tree object directly. |
+| `readdir(self, fs, path)` | List entries in a directory. |
+| `read_file(self, fs, path)` | Read a blob lazily. |
+| `stat(self, fs, path)` | Get entry metadata (mode, size). |
+
+## Kv — distributed KV store
+
+Git-backed key/value store with gossip sync. Useful when you want a small, replicated config alongside the repository.
+
+```moonbit
+let db = @bit.Kv::init(fs, fs, git_dir, node_id="aurora.local")
+
+db.set(fs, fs, "users/alice/profile", profile_bytes, ts=now())
+let value = db.get(fs, fs, "users/alice/profile")?
+
+db.sync_with_peer(fs, fs, "relay://bit.example.com/u/me/kv")
+```
+
+## Hub — PR / Issue API
+
+Same data the `bit pr` and `bit issue` CLI commands operate on. Drive PRs from an agent, write a custom triage UI, or wire it into your editor.
+
+```moonbit
+let hub = @bit.Hub::init(fs, fs, git_dir)
+
+let pr = hub.create_pr(
+ fs, fs,
+ title = "Replace gossip backoff with token bucket",
+ body = "fixes thrashing under high churn",
+ source_branch = "feature/token-bucket",
+ target_branch = "main",
+ author = "ubugeeei",
+ ts = now()
+)
+
+hub.review(fs, fs, pr.id, status=Approve, commit=Some(pr.head))?
+hub.merge(fs, fs, pr.id)?
+```
+
+| Function | Returns |
+| --- | --- |
+| `Hub::init(fs, rfs, gitdir)` | `Hub` |
+| `hub.create_pr(...)` | `Pr` |
+| `hub.list_prs(state)` | `Array[Pr]` |
+| `hub.review(id, status, commit?)` | `Result[Review, Error]` |
+| `hub.merge(id)` | `Result[Sha, Error]` |
+| `hub.create_issue(...)` | `Issue` |
+| `hub.link(issue_id, pr_id)` | `Unit` |
+
+## Node — peer protocol
+
+Open sessions with other peers, exchange refs, fetch object graphs.
+
+```moonbit
+let peer = @bit.Node::from_uri("bit://aurora.local:9418/repo")
+peer.handshake(timeout=2.s)?
+
+let local_refs = @bit.refs(fs, ".git")
+let wants = local_refs.diff(peer.refs())
+peer.fetch(wants)?
+
+println("now at: \{peer.refs().get(\"refs/heads/main\")}")
+```
+
+| Function | Description |
+| --- | --- |
+| `Node::from_uri(uri)` | Create a peer session (lazy connect). |
+| `peer.handshake(timeout?)` | Open the session, exchange capabilities. |
+| `peer.refs()` | Snapshot of remote refs. |
+| `peer.fetch(wants)` | Download objects reachable from `wants`. |
+| `peer.send(bit)` | Push a single object or pack. |
+| `peer.close()` | Drop the session. |
+
+
+
// see also
+
The CLI Reference documents the same surface, packaged as commands. The mapping is one-to-one: bit pr create ↔ Hub::create_pr, bit fetch ↔ Node::fetch, and so on.
Written in MoonBit. Compiles to native and JS. The MoonBit API is the runtime — agents and CI can drive bit storage directly without going through a CLI.
$ bit clone https://github.com/user/repo
+$ bit checkout -b feature
+$ bit add .
+$ bit commit -m "first node speaks"
+$ bit push origin feature
+
+
+
That's Git. Now flip on the distributed layer:
+
+
+
// shell
+
$ bit pr init
+$ bit pr create --title "first node speaks" --head feature --base main
+$ bit relay sync push https://relay.example.com/u/me/repo
+
+
+
+
+
+
+
// 004 — manifesto
+
no center. no master. only peers.
+
+
+
+
+
+
+
// 005
+
Speaks MoonBit, natively.
+
+
+
bit's core operations run against any storage backend that implements @bit.FileSystem. Agents can drive a repository entirely in memory — no shell, no subprocess.
+
+
+
// moonbit
+
// spin up an in-memory repo and write a commit
+let fs = @bit.TestFs::new()
+let root = "/agent-repo"
+run_storage_command(fs, fs, root, "init", ["-q"])
+fs.write_string(root + "/note.txt", "hello, peer")
+run_storage_command(fs, fs, root, "add", ["note.txt"])
+run_storage_command(fs, fs, root, "commit", ["-m", "agent snapshot"])
bit is a Git implementation in MoonBit. It speaks the Git wire protocol, it produces a normal .git/, and any Git client can read what it writes. Underneath, bit treats distribution as the default — not a feature bolted onto a central server, but the entire premise.
+
+
+
What's interesting
+
Three things distinguish bit from "yet another Git client."
+
PRs and issues live inside the repo. Pull requests, reviews, and issues are written as Git objects under refs/notes/bit-hub. Replicate the repo and you replicate the work. GitHub can go away. Your project history — and the conversations around it — survive intact.
+
bit is a library before it is a tool. Every CLI command is a thin shell over a MoonBit function. Drop bit into an agent, an editor plugin, or a CI runner — you do not shell out, you call it. Mount an in-memory filesystem and you have a working repository in five lines.
+
Convergence is gossip, not consensus. Peers reconcile by walking object graphs until they meet a common ancestor. No election, no quorum, no leader. A laptop offline for a week catches up the moment it touches a peer.
+
A five-concept mental model
+
These five words carry the whole system. If you have them straight, the CLI and the library both stop surprising you.
+
Node
+
A node is any process holding a copy of the repository. Your laptop is a node. A CI runner is a node. A relay host is a node. Nodes are symmetric — there is no "primary" — and they are addressed by URI:
Source is whoever wrote the commit you are looking at. It is a fact, not a place. The pink dot in the bit logo represents the source node; in any given moment, any node can become it.
+
// moonbit
let commit = @bit.head(fs, ".git")?
+println("author: \{commit.author}")
+println("source: \{commit.committer.tz}@\{commit.committer.email}")
+
+
Bit
+
A bit is a single edge between two peers — one unit of exchange. Communication is content-addressed: peers ask for object hashes, never filenames. The protocol is therefore naturally idempotent and naturally deduplicating.
+
Convergence
+
Two peers handshake, exchange ref tips, and walk the graph backwards until they meet. Bytes flow in both directions. Disagreement persists as parallel heads until a human merges them. There is no server-side reconciliation step. There is no server.
+
Hub
+
PRs and issues are stored as Git objects. The hub is not a service — it is a particular shape of commit graph. bit pr create writes objects. bit relay sync push replicates them. The hub survives forks because it has no "outside" to survive against.
+
What follows from this
+
Implications you can feel as soon as you start using it.
+
+
+
The hub survives forks. Fork a repo, get every PR and issue with it.
+
+
+
Agents drive bit without a shell. The same MoonBit functions the CLI calls, your agent can call directly.
+
+
+
You can clone a single subdirectory of a 50 GB monorepo with bit clone user/repo:src/lib.
+
+
+
A merge conflict resolved by AI is just commits plus a note — no third-party state, no external dashboard.
+
+
+
You can write your own bit-aware tool in MoonBit in an afternoon. The library API is the surface.
+
+
+
What bit is not
+
Equally important — bit refuses several adjacent identities.
+
+
+
Not a Git replacement. It is Git. The bytes on disk are valid Git, the wire protocol is Git's, the SHA-1 history is interchangeable. Use it next to vanilla git if you want.
+
+
+
Not a blockchain. No consensus, no tokens, no proof-of-anything. Convergence is gossip.
+
+
+
Not a P2P file share. bit is collaboration software that happens to be distributed. The unit of meaning is a commit, not a file.
+
+
+
Not an LLM agent. AI assist is one optional command (bit ai rebase), not the architecture.
+
+
+
What's next
+
Now that you have the model, the rest of the guide is mostly muscle memory.
bit was built on the premise that source control should not depend on a central host. In this chapter you will clone a repository over HTTP and over a bit relay, push commits in both directions, and watch two peers converge through gossip.
+
+
+
Mental model
+
A bit node is any process holding a copy of the repository. Your laptop, a CI runner, a relay host — all peers, no master. Two peers reconcile by exchanging ref tips and walking the object graph until they meet a common ancestor.
+
+
+
+
Mode
+
Trigger
+
Direction
+
+
+
+
+
push
+
explicit
+
local → remote
+
+
+
fetch
+
explicit
+
remote → local
+
+
+
gossip
+
periodic, on handshake
+
both
+
+
+
relay sync
+
via relay host
+
both, async
+
+
+
+
Clone over HTTP
+
This is the standard Git path. bit speaks git-upload-pack and git-receive-pack over smart HTTP.
+
// bash
$ bit clone https://github.com/user/repo
+$ cd repo
+$ bit remote -v
+origin https://github.com/user/repo (fetch)
+origin https://github.com/user/repo (push)
+
+
Clone over a relay
+
A relay is a lightweight bit-aware host. It speaks the bit protocol over HTTP and supports async sync — useful when peers are not online at the same time.
+
// bash
$ bit clone relay://bit.example.com/u/me/repo
+$ cd repo
+
+
Two peers, one truth
+
Open two terminals. In the first, simulate node A:
+
// bash
$ bit clone relay://bit.example.com/u/me/repo a
+$ cd a
+$ echo "from A" >> note.txt
+$ bit add . && bit commit -m "A writes"
+$ bit push origin main
+
+
In the second terminal, node B:
+
// bash
$ bit clone relay://bit.example.com/u/me/repo b
+$ cd b
+$ bit fetch origin
+$ bit log --oneline
+a8c2f6e (origin/main) A writes
+1d0e7c2 root commit
+
+
B now holds A's history without ever talking to A directly. The relay carried the bytes asynchronously.
+
Concurrent writes
+
Two peers writing at the same time produce two heads. bit's history graph is content-addressed, so collisions are deterministic — both heads are valid until a merge or rebase reconciles them. There is no server vote.
+
+
// recommendation
+
Use bit ai rebase when the conflict is mechanical (formatting drift, import re-orderings). For semantic conflicts, do the merge by hand — the AI assist is wrong often enough that you want to read what it proposes.
+
+
Inspect the graph
+
// bash
$ bit log --graph --oneline --all
+* 1f02776 (HEAD -> main) merge: reconcile A and B
+|\
+| * e871d71 B writes
+* | a8c2f6e A writes
+|/
+* 1d0e7c2 root commit
+
+
From MoonBit
+
The same flow scripted against the library. Node::from_uri opens a peer session; fetch walks the graph diff.
If you have ever used Git, you already know this. bit reuses the porcelain — init, add, commit, log all behave the way you expect. The first commit is a sanity check that the binary works.
+
+
+
Initialize a repository
+
// bash
$ mkdir hello-bit
+$ cd hello-bit
+$ bit init
+Initialized empty bit repository in .git/
+
+
bit writes a standard .git/ directory. Any existing Git tool can read it. There is no parallel .bit/ store.
+
Write and stage
+
// bash
$ echo "hello, peer" > note.txt
+$ bit add note.txt
+$ bit status
+On branch main
+
+Changes to be committed:
+ new file: note.txt
+
+
+
// note
+
The status output is intentionally identical to Git's. bit emits the same line shapes so your editor's plugins, your hooks, and your grep incantations keep working.
$ bit log --graph --oneline
+* a8c2f6e (HEAD -> main) first node speaks
+
+
The graph renderer is native — --graph, --stat, --name-only, --name-status, and --topo-order all run inside bit without shelling out.
+
What just happened
+
bit wrote three Git objects under .git/objects/:
+
+
+
a blob for note.txt (the file contents)
+
+
+
a tree describing the directory snapshot
+
+
+
a commit pointing at the tree, with your author identity and message
+
+
+
These are the same object types every Git client speaks. Anything you commit with bit can be pushed to a Git server, and anything cloned from a Git server can be operated on by bit.
+
Try it from MoonBit
+
The same flow, scripted against an in-memory filesystem — no shell involved. This is how agents drive bit.
A guided path through bit. By the end you will understand the model, install the binary, make commits against a real repository, replicate history to a second peer, and open a local pull request — all without leaving your terminal.
The script detects your OS and architecture, downloads the matching release, and drops the binary at ~/.local/bin/bit. Add it to your shell's PATH if it isn't already.
+
+
// note
+
Read the script before you pipe it to bash — that's the rule for every install script on the internet, not just this one. Mirror at github.com/mizchi/bit-vcs.
+
+
Via the MoonBit toolchain
+
If you have moon installed:
+
// bash
$ moon install mizchi/bit/cmd/bit
+
+
This builds bit from source against your toolchain. Useful if you want to track main or contribute patches.
+
Verify
+
// bash
$ bit --version
+bit 0.1.0 — distributed git in moonbit
+
+
Shell completion
+
bit ships its own completion generator. Pick your shell and source the output from your rc file.
+
bash
+
// bash
eval "$(bit completion bash)"
+
+
zsh
+
// bash
eval "$(bit completion zsh)"
+
+
Uninstall
+
// bash
$ rm ~/.local/bin/bit
+
+
That's it. bit has no daemons, no system services, no cache outside your repos.
+
+
// caveat
+
bit is experimental. Data corruption is possible in worst-case scenarios. Always keep a Git backup of important repositories until you have stress-tested your workflow.
bit implements 108 Git commands natively. Anything not listed here falls back to Git's surface — read going distributed for the protocol-level differences.
+
+
+
Porcelain
+
Everyday commands. Identical surface to Git.
+
+
+
+
Command
+
Description
+
+
+
+
+
bit init
+
Create an empty .git/ in the current directory.
+
+
+
bit clone <url>
+
Clone over HTTP, relay, or a subdirectory URL (see extensions).
+
+
+
bit add <path>…
+
Stage files. Supports -A, -p, --update.
+
+
+
bit commit -m <msg>
+
Record staged changes. --amend, --no-edit, --allow-empty.
Every BIT_* variable bit reads, plus the Git-compat variables it honors. Set them in your shell rc, in .envrc, or per-command.
+
+
+
bit-native
+
+
+
+
Variable
+
Default
+
Effect
+
+
+
+
+
BIT_BENCH_GIT_DIR
+
—
+
Override .git path for bench_real (vfs benchmarks).
+
+
+
BIT_PACK_CACHE_LIMIT
+
2
+
Max packfiles kept in memory. 0 disables the cache.
+
+
+
BIT_RACY_GIT
+
unset
+
When set, rehash even if stat matches — avoids racy-git false negatives on fast filesystems.
+
+
+
BIT_WORKSPACE_FINGERPRINT_MODE
+
git
+
git for add-all-style snapshots, fast for per-node directory hashes.
+
+
+
BIT_HUB_INIT_PROMPT
+
1 (interactive)
+
Force-on (1) or force-off (0) the bit pr init / bit issue init prompts.
+
+
+
+
Git-compat
+
bit honors the standard Git environment so existing tooling, hooks, and editors keep working.
+
+
+
+
Variable
+
Effect
+
+
+
+
+
GIT_DIR
+
Override the location of the repository's .git directory.
+
+
+
GIT_WORK_TREE
+
Override the working tree root.
+
+
+
GIT_CONFIG_GLOBAL
+
Path to global config (default ~/.gitconfig).
+
+
+
GIT_EDITOR
+
Editor used for commit messages, rebase conflict markers.
+
+
+
GIT_SEQUENCE_EDITOR
+
Editor used for the rebase todo list.
+
+
+
GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL
+
Override commit author identity.
+
+
+
GIT_COMMITTER_NAME / GIT_COMMITTER_EMAIL
+
Override committer identity.
+
+
+
+
AI assist
+
+
+
+
Variable
+
Effect
+
+
+
+
+
OPENROUTER_API_KEY
+
Required for any bit ai * subcommand.
+
+
+
+
+
// note
+
bit never reads secrets from the repository — keys live only in the environment. If you set OPENROUTER_API_KEY via .envrc, make sure that file is git-ignored.
Everything bit exposes — every command, every public type, every environment variable. Sorted, terse, link-anchored. Reach for this when you know what you are looking for.
bit is a MoonBit library first, a CLI second. Every CLI command is a thin shell around a public function. Embed bit directly in your agent, editor plugin, or CI runner — no subprocess required.
+
+
+
Package layout
+
Source is organized in five layers, dependencies flow downward only.
+
+
+
+
Layer
+
Path
+
What lives here
+
+
+
+
+
core
+
src/core/
+
Object types, SHA, ref store primitives.
+
+
+
mid
+
src/mid/
+
Index, pack, merge, diff algorithms.
+
+
+
high
+
src/high/
+
Porcelain operations (commit, rebase, …).
+
+
+
ext
+
src/ext/
+
Hub, relay, LFS, AI, workspace.
+
+
+
cmd
+
src/cmd/
+
CLI entry points.
+
+
+
+
Storage runtime
+
The entry point for embedding. Any storage that implements FileSystem and RepoFileSystem can host a repository.
The CLI Reference documents the same surface, packaged as commands. The mapping is one-to-one: bit pr create ↔ Hub::create_pr, bit fetch ↔ Node::fetch, and so on.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/build-docs.mjs b/tools/build-docs.mjs
new file mode 100644
index 0000000..bd43902
--- /dev/null
+++ b/tools/build-docs.mjs
@@ -0,0 +1,412 @@
+#!/usr/bin/env node
+// tools/build-docs.mjs
+// Reads site/content/**/*.md, renders bodies via mizchi/markdown (via the
+// MoonBit→JS module under tools/docs-build-mbt/), and writes site//*.html
+// using inline templates that carry the bit brand layout.
+//
+// Frontmatter format: a leading `---` / `---` fenced block of flat key:value
+// pairs (no nesting; structure encoded in named fields like prev_href / next_href).
+//
+// Usage:
+// 1. (once) build the renderer:
+// cd tools/docs-build-mbt && moon build --target js --release
+// 2. node tools/build-docs.mjs
+
+import fs from "node:fs/promises";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const root = path.resolve(here, "..");
+const contentDir = path.join(root, "site/content");
+const outDir = path.join(root, "site");
+const rendererPath = path.join(
+ root,
+ "tools/docs-build-mbt/_build/js/release/build/render/render.js",
+);
+
+// ── load renderer ────────────────────────────────────────────────────────
+const { render } = await import(rendererPath).catch((err) => {
+ console.error(
+ `\nFailed to load MoonBit renderer at:\n ${rendererPath}\n\n` +
+ `Build it first:\n cd tools/docs-build-mbt && moon build --target js --release\n`,
+ );
+ throw err;
+});
+
+// ── frontmatter parser ────────────────────────────────────────────────────
+function parseFrontmatter(src) {
+ const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
+ if (!m) return { meta: {}, body: src };
+ const meta = {};
+ for (const raw of m[1].split(/\r?\n/)) {
+ const line = raw.replace(/^\s+|\s+$/g, "");
+ if (!line || line.startsWith("#")) continue;
+ const kv = line.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
+ if (!kv) continue;
+ let [, k, v] = kv;
+ v = v.replace(/^["']|["']$/g, "");
+ if (/^-?\d+$/.test(v)) meta[k] = Number(v);
+ else if (v === "true" || v === "false") meta[k] = v === "true";
+ else meta[k] = v;
+ }
+ return { meta, body: m[2] };
+}
+
+// ── walk content tree ────────────────────────────────────────────────────
+async function walk(dir) {
+ const out = [];
+ for (const ent of await fs.readdir(dir, { withFileTypes: true })) {
+ const p = path.join(dir, ent.name);
+ if (ent.isDirectory()) out.push(...(await walk(p)));
+ else if (ent.isFile() && ent.name.endsWith(".md")) out.push(p);
+ }
+ return out;
+}
+
+// ── HTML post-processing ─────────────────────────────────────────────────
+function slugify(s) {
+ return s
+ .toLowerCase()
+ .replace(/<[^>]+>/g, "")
+ .replace(/[^\p{Letter}\p{Number}]+/gu, "-")
+ .replace(/^-|-$/g, "");
+}
+
+function transformHeadings(html) {
+ // Number h2s "// 001" and add id + data-num. Leave h3/h4 alone.
+ let i = 1;
+ const out = html.replace(
+ /
.
+ // Wrap in our .codeblock structure and hand the language off via data-lang
+ // so site/assets/syntax.js can pick it up.
+ return html.replace(
+ /
`
+ );
+ },
+ );
+}
+
+// Render a short string of markdown as inline HTML (no
wrapper).
+// Useful for frontmatter fields like `lead` where we want `code` and **bold**
+// to render but the result is dropped into an inline context.
+function renderInline(md) {
+ if (!md) return "";
+ const html = render(md).trim();
+ return html
+ .replace(/^