OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      d63e516e0799
DATE      2026-07-18
SUBJECT   web: /git section, repos rendered by the site itself, forgejo demoted to
          plumbing
FILES     20 CHANGED
HASH      d63e516e0799
DATE      2026-07-18
SUBJECT   web: /git section, repos rendered by
          the site itself, forgejo demoted to
          plumbing
FILES     20 CHANGED
 

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fb0f028
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# ooknet.org
+
+Personal website. A text-mode knowledge base and the ascii design
+system that renders it - every border, chart, and frame on the site is
+a real character on a strict monospace grid.
+
+## Layout
+
+| PATH             | WHAT                                        |
+| ---------------- | ------------------------------------------- |
+| `ooknet-design/` | the astro site and the ascii design system  |
+| `nix/`           | dev shell                                   |
+
+## The rules
+
+- og ascii: no em dashes, no ellipsis glyphs, no curly quotes.
+  `checkGrid` fails the build on off-grid characters.
+- every frame renders twice: 86 cells wide for desktop, 48 for mobile.
+- no smooth animation. motion is discrete, like a terminal.
+- components are pure builders with tests; chrome is text, not css
+  borders.
+
+## Working on it
+
+```sh
+cd ooknet-design
+npm run dev     # dev server on :4321
+npm test        # vitest
+npm run build   # static build + grid checks across every page
+```
+
+The dev shell comes from the flake; `direnv allow` and the tooling is
+there.
+
+## Content
+
+Three collections under `ooknet-design/src/content/`: `notes/`
+(personal writing, by subject), `kb/` (vendor docs, credited), and
+`docs/` (project documentation). All three cross-link with
+`[[wikilinks]]` and feed a REFERENCED BY index on every page.

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fb0f028
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# ooknet.org
+
+Personal website. A text-mode knowledge bas
e and the ascii design
+system that renders it - every border, char
t, and frame on the site is
+a real character on a strict monospace grid
.
+
+## Layout
+
+| PATH             | WHAT                  
                      |
+| ---------------- | ----------------------
--------------------- |
+| `ooknet-design/` | the astro site and the
 ascii design system  |
+| `nix/`           | dev shell             
                      |
+
+## The rules
+
+- og ascii: no em dashes, no ellipsis glyph
s, no curly quotes.
+  `checkGrid` fails the build on off-grid c
haracters.
+- every frame renders twice: 86 cells wide 
for desktop, 48 for mobile.
+- no smooth animation. motion is discrete, 
like a terminal.
+- components are pure builders with tests; 
chrome is text, not css
+  borders.
+
+## Working on it
+
+```sh
+cd ooknet-design
+npm run dev     # dev server on :4321
+npm test        # vitest
+npm run build   # static build + grid check
s across every page
+```
+
+The dev shell comes from the flake; `direnv
 allow` and the tooling is
+there.
+
+## Content
+
+Three collections under `ooknet-design/src/
content/`: `notes/`
+(personal writing, by subject), `kb/` (vend
or docs, credited), and
+`docs/` (project documentation). All three 
cross-link with
+`[[wikilinks]]` and feed a REFERENCED BY in
dex on every page.
 

diff --git a/ooknet-design/package-lock.json b/ooknet-design/package-lock.json
index d17bd9b..d58ec8a 100644
--- a/ooknet-design/package-lock.json
+++ b/ooknet-design/package-lock.json
@@ -10,7 +10,8 @@
       "dependencies": {
         "@fontsource/cascadia-mono": "^5.2.3",
         "@fontsource/ibm-plex-mono": "^5.2.7",
-        "astro": "^6.1.9"
+        "astro": "^6.1.9",
+        "marked": "^18.0.6"
       },
       "devDependencies": {
         "sass": "^1.99.0",
@@ -3291,6 +3292,18 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/marked": {
+      "version": "18.0.6",
+      "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.6.tgz",
+      "integrity": "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y
7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
     "node_modules/mdast-util-definitions": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util
-definitions-6.0.0.tgz",

diff --git a/ooknet-design/package-lock.json
 b/ooknet-design/package-lock.json
index d17bd9b..d58ec8a 100644
--- a/ooknet-design/package-lock.json
+++ b/ooknet-design/package-lock.json
@@ -10,7 +10,8 @@
       "dependencies": {
         "@fontsource/cascadia-mono": "^5.2.
3",
         "@fontsource/ibm-plex-mono": "^5.2.
7",
-        "astro": "^6.1.9"
+        "astro": "^6.1.9",
+        "marked": "^18.0.6"
       },
       "devDependencies": {
         "sass": "^1.99.0",
@@ -3291,6 +3292,18 @@
         "url": "https://github.com/sponsors
/wooorm"
       }
     },
+    "node_modules/marked": {
+      "version": "18.0.6",
+      "resolved": "https://registry.npmjs.o
rg/marked/-/marked-18.0.6.tgz",
+      "integrity": "sha512-MrV5puXBfuiy6wl6
DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQ
oEgK/pBgurGqLyAuRgZ2xOjr6w==",
+      "license": "MIT",
+      "bin": {
+        "marked": "bin/marked.js"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
     "node_modules/mdast-util-definitions": 
{
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.o
rg/mdast-util-definitions/-/mdast-util-defin
itions-6.0.0.tgz",
 

diff --git a/ooknet-design/package.json b/ooknet-design/package.json
index 31126d6..112a66e 100644
--- a/ooknet-design/package.json
+++ b/ooknet-design/package.json
@@ -15,7 +15,8 @@
   "dependencies": {
     "@fontsource/cascadia-mono": "^5.2.3",
     "@fontsource/ibm-plex-mono": "^5.2.7",
-    "astro": "^6.1.9"
+    "astro": "^6.1.9",
+    "marked": "^18.0.6"
   },
   "devDependencies": {
     "sass": "^1.99.0",

diff --git a/ooknet-design/package.json b/oo
knet-design/package.json
index 31126d6..112a66e 100644
--- a/ooknet-design/package.json
+++ b/ooknet-design/package.json
@@ -15,7 +15,8 @@
   "dependencies": {
     "@fontsource/cascadia-mono": "^5.2.3",
     "@fontsource/ibm-plex-mono": "^5.2.7",
-    "astro": "^6.1.9"
+    "astro": "^6.1.9",
+    "marked": "^18.0.6"
   },
   "devDependencies": {
     "sass": "^1.99.0",
 

diff --git a/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.ts b/ooknet-desi
gn/src/components/Breadcrumbs/Breadcrumbs.ts
index 3150f9f..1f40af1 100644
--- a/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.ts
+++ b/ooknet-design/src/components/Breadcrumbs/Breadcrumbs.ts
@@ -9,6 +9,7 @@ const ROOTS: Record<string, string> = {
   NOTES: "/notes/",
   KB: "/kb/",
   DOCS: "/docs/",
+  GIT: "/git/",
 };
 
 export interface Crumb {

diff --git a/ooknet-design/src/components/Br
eadcrumbs/Breadcrumbs.ts b/ooknet-design/src
/components/Breadcrumbs/Breadcrumbs.ts
index 3150f9f..1f40af1 100644
--- a/ooknet-design/src/components/Breadcrum
bs/Breadcrumbs.ts
+++ b/ooknet-design/src/components/Breadcrum
bs/Breadcrumbs.ts
@@ -9,6 +9,7 @@ const ROOTS: Record<string, 
string> = {
   NOTES: "/notes/",
   KB: "/kb/",
   DOCS: "/docs/",
+  GIT: "/git/",
 };
 
 export interface Crumb {
 

diff --git a/ooknet-design/src/components/GitListing/GitListing.astro b/ooknet-des
ign/src/components/GitListing/GitListing.astro
new file mode 100644
index 0000000..8c82ca8
--- /dev/null
+++ b/ooknet-design/src/components/GitListing/GitListing.astro
@@ -0,0 +1,21 @@
+---
+import Pre from "../Pre/Pre.astro";
+import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config";
+import { layoutListing, type ListingRow, type Props } from "./GitListing";
+import "./GitListing.scss";
+
+const { entries, treeBase, blobBase, commitBase } = Astro.props as Props;
+const variants: [string, ListingRow[]][] = [
+  ["frame-wide", layoutListing(entries, FRAME_W, treeBase, blobBase, commitBase)]
,
+  ["frame-narrow", layoutListing(entries, MOBILE_FRAME_W, treeBase, blobBase, com
mitBase)],
+];
+---
+<div class="git-listing">
+  {variants.map(([variant, rows]) =>
+    rows.map((r) => (
+      <Pre class={variant}><a class="listing-link" href={r.href}>{r.name}</a>{r.g
ap1}{r.subject
+        ? <a class="listing-subject" href={r.subjectHref}>{r.subject}</a>
+        : ""}{r.gap2}{r.date}</Pre>
+    ))
+  )}
+</div>

diff --git a/ooknet-design/src/components/Gi
tListing/GitListing.astro b/ooknet-design/sr
c/components/GitListing/GitListing.astro
new file mode 100644
index 0000000..8c82ca8
--- /dev/null
+++ b/ooknet-design/src/components/GitListin
g/GitListing.astro
@@ -0,0 +1,21 @@
+---
+import Pre from "../Pre/Pre.astro";
+import { FRAME_W, MOBILE_FRAME_W } from "..
/../lib/config";
+import { layoutListing, type ListingRow, ty
pe Props } from "./GitListing";
+import "./GitListing.scss";
+
+const { entries, treeBase, blobBase, commit
Base } = Astro.props as Props;
+const variants: [string, ListingRow[]][] = 
[
+  ["frame-wide", layoutListing(entries, FRA
ME_W, treeBase, blobBase, commitBase)],
+  ["frame-narrow", layoutListing(entries, M
OBILE_FRAME_W, treeBase, blobBase, commitBas
e)],
+];
+---
+<div class="git-listing">
+  {variants.map(([variant, rows]) =>
+    rows.map((r) => (
+      <Pre class={variant}><a class="listin
g-link" href={r.href}>{r.name}</a>{r.gap1}{r
.subject
+        ? <a class="listing-subject" href={
r.subjectHref}>{r.subject}</a>
+        : ""}{r.gap2}{r.date}</Pre>
+    ))
+  )}
+</div>
 

diff --git a/ooknet-design/src/components/GitListing/GitListing.scss b/ooknet-desi
gn/src/components/GitListing/GitListing.scss
new file mode 100644
index 0000000..556e2cd
--- /dev/null
+++ b/ooknet-design/src/components/GitListing/GitListing.scss
@@ -0,0 +1,28 @@
+.git-listing {
+  display: block;
+
+  .listing-link {
+    color: var(--ink);
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+      text-underline-offset: 3px;
+    }
+
+    &:focus-visible {
+      outline: 1px dashed var(--ink);
+      outline-offset: 1px;
+    }
+  }
+
+  .listing-subject {
+    color: var(--ink-soft);
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+      text-underline-offset: 3px;
+    }
+  }
+}

diff --git a/ooknet-design/src/components/Gi
tListing/GitListing.scss b/ooknet-design/src
/components/GitListing/GitListing.scss
new file mode 100644
index 0000000..556e2cd
--- /dev/null
+++ b/ooknet-design/src/components/GitListin
g/GitListing.scss
@@ -0,0 +1,28 @@
+.git-listing {
+  display: block;
+
+  .listing-link {
+    color: var(--ink);
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+      text-underline-offset: 3px;
+    }
+
+    &:focus-visible {
+      outline: 1px dashed var(--ink);
+      outline-offset: 1px;
+    }
+  }
+
+  .listing-subject {
+    color: var(--ink-soft);
+    text-decoration: none;
+
+    &:hover {
+      text-decoration: underline;
+      text-underline-offset: 3px;
+    }
+  }
+}
 

diff --git a/ooknet-design/src/components/GitListing/GitListing.test.ts b/ooknet-d
esign/src/components/GitListing/GitListing.test.ts
new file mode 100644
index 0000000..fa3ed56
--- /dev/null
+++ b/ooknet-design/src/components/GitListing/GitListing.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vitest";
+import { layoutListing } from "./GitListing";
+import { len } from "../../lib/ascii";
+
+const entries = [
+  { name: "src", path: "src", kind: "tree" as const, hash: "abc1234", date: "2026
-07-18", subject: "web: restructure into collections" },
+  { name: "a-very-long-file-name-indeed.config.ts", path: "a-very-long-file-name-
indeed.config.ts", kind: "blob" as const, hash: "def5678", date: "2026-07-17", sub
ject: "short" },
+];
+
+describe("layoutListing", () => {
+  it("fills the exact width at both frames", () => {
+    for (const w of [86, 48]) {
+      for (const r of layoutListing(entries, w, "/t/", "/b/")) {
+        expect(len(r.name + r.gap1 + r.subject + r.gap2 + r.date)).toBe(w);
+      }
+    }
+  });
+
+  it("marks directories with a slash and links by kind", () => {
+    const rows = layoutListing(entries, 86, "/t/", "/b/");
+    expect(rows[0].name).toBe("src/");
+    expect(rows[0].href).toBe("/t/src/");
+    expect(rows[1].href).toBe("/b/a-very-long-file-name-indeed.config.ts/");
+  });
+
+  it("links subjects to their commit", () => {
+    const rows = layoutListing(entries, 86, "/t/", "/b/", "/c/");
+    expect(rows[0].subjectHref).toBe("/c/abc1234/");
+  });
+
+  it("truncates names and subjects rather than overflowing", () => {
+    const rows = layoutListing(entries, 48, "/t/", "/b/");
+    expect(rows[1].name.endsWith("...")).toBe(true);
+  });
+});

diff --git a/ooknet-design/src/components/Gi
tListing/GitListing.test.ts b/ooknet-design/
src/components/GitListing/GitListing.test.ts
new file mode 100644
index 0000000..fa3ed56
--- /dev/null
+++ b/ooknet-design/src/components/GitListin
g/GitListing.test.ts
@@ -0,0 +1,35 @@
+import { describe, expect, it } from "vites
t";
+import { layoutListing } from "./GitListing
";
+import { len } from "../../lib/ascii";
+
+const entries = [
+  { name: "src", path: "src", kind: "tree" 
as const, hash: "abc1234", date: "2026-07-18
", subject: "web: restructure into collectio
ns" },
+  { name: "a-very-long-file-name-indeed.con
fig.ts", path: "a-very-long-file-name-indeed
.config.ts", kind: "blob" as const, hash: "d
ef5678", date: "2026-07-17", subject: "short
" },
+];
+
+describe("layoutListing", () => {
+  it("fills the exact width at both frames"
, () => {
+    for (const w of [86, 48]) {
+      for (const r of layoutListing(entries
, w, "/t/", "/b/")) {
+        expect(len(r.name + r.gap1 + r.subj
ect + r.gap2 + r.date)).toBe(w);
+      }
+    }
+  });
+
+  it("marks directories with a slash and li
nks by kind", () => {
+    const rows = layoutListing(entries, 86,
 "/t/", "/b/");
+    expect(rows[0].name).toBe("src/");
+    expect(rows[0].href).toBe("/t/src/");
+    expect(rows[1].href).toBe("/b/a-very-lo
ng-file-name-indeed.config.ts/");
+  });
+
+  it("links subjects to their commit", () =
> {
+    const rows = layoutListing(entries, 86,
 "/t/", "/b/", "/c/");
+    expect(rows[0].subjectHref).toBe("/c/ab
c1234/");
+  });
+
+  it("truncates names and subjects rather t
han overflowing", () => {
+    const rows = layoutListing(entries, 48,
 "/t/", "/b/");
+    expect(rows[1].name.endsWith("...")).to
Be(true);
+  });
+});
 

diff --git a/ooknet-design/src/components/GitListing/GitListing.ts b/ooknet-design
/src/components/GitListing/GitListing.ts
new file mode 100644
index 0000000..e2bbc3b
--- /dev/null
+++ b/ooknet-design/src/components/GitListing/GitListing.ts
@@ -0,0 +1,62 @@
+// Directory listing rows, github-shape: NAME  LAST COMMIT  DATE.
+// The name is a link; widths flex per frame and the subject column
+// drops when the narrow frame can't seat it.
+import { checkGrid, len, pad } from "../../lib/ascii";
+import type { DirEntry } from "../../lib/git";
+
+export interface Props {
+  entries: DirEntry[];
+  /** Url prefixes for links. */
+  treeBase: string;
+  blobBase: string;
+  commitBase: string;
+}
+
+export interface ListingRow {
+  name: string;
+  href: string;
+  gap1: string;
+  subject: string;
+  subjectHref: string;
+  gap2: string;
+  date: string;
+}
+
+const DATE_W = 10;
+const GAP = 2;
+
+export function layoutListing(
+  entries: DirEntry[],
+  width: number,
+  treeBase: string,
+  blobBase: string,
+  commitBase = "",
+): ListingRow[] {
+  const nameW = Math.min(
+    Math.max(8, ...entries.map((e) => len(e.name) + (e.kind === "tree" ? 1 : 0)))
,
+    Math.floor(width / 2) - GAP,
+  );
+  const subjectW = Math.max(0, width - nameW - DATE_W - GAP * 2);
+  return entries.map((e) => {
+    const rawName = e.kind === "tree" ? `${e.name}/` : e.name;
+    const name =
+      len(rawName) > nameW ? [...rawName].slice(0, nameW - 3).join("") + "..." : 
rawName;
+    const subject =
+      subjectW < 8
+        ? ""
+        : len(e.subject) > subjectW
+          ? [...e.subject].slice(0, subjectW - 3).join("") + "..."
+          : e.subject;
+    const row: ListingRow = {
+      name,
+      href: `${e.kind === "tree" ? treeBase : blobBase}${e.path}/`,
+      gap1: " ".repeat(nameW - len(name) + GAP),
+      subject,
+      subjectHref: `${commitBase}${e.hash}/`,
+      gap2: " ".repeat(Math.max(GAP, width - nameW - GAP - len(subject) - len(e.d
ate))),
+      date: e.date,
+    };
+    checkGrid(row.name + row.gap1 + row.subject + row.gap2 + row.date, width, "gi
t listing");
+    return row;
+  });
+}

diff --git a/ooknet-design/src/components/Gi
tListing/GitListing.ts b/ooknet-design/src/c
omponents/GitListing/GitListing.ts
new file mode 100644
index 0000000..e2bbc3b
--- /dev/null
+++ b/ooknet-design/src/components/GitListin
g/GitListing.ts
@@ -0,0 +1,62 @@
+// Directory listing rows, github-shape: NA
ME  LAST COMMIT  DATE.
+// The name is a link; widths flex per fram
e and the subject column
+// drops when the narrow frame can't seat i
t.
+import { checkGrid, len, pad } from "../../
lib/ascii";
+import type { DirEntry } from "../../lib/gi
t";
+
+export interface Props {
+  entries: DirEntry[];
+  /** Url prefixes for links. */
+  treeBase: string;
+  blobBase: string;
+  commitBase: string;
+}
+
+export interface ListingRow {
+  name: string;
+  href: string;
+  gap1: string;
+  subject: string;
+  subjectHref: string;
+  gap2: string;
+  date: string;
+}
+
+const DATE_W = 10;
+const GAP = 2;
+
+export function layoutListing(
+  entries: DirEntry[],
+  width: number,
+  treeBase: string,
+  blobBase: string,
+  commitBase = "",
+): ListingRow[] {
+  const nameW = Math.min(
+    Math.max(8, ...entries.map((e) => len(e
.name) + (e.kind === "tree" ? 1 : 0))),
+    Math.floor(width / 2) - GAP,
+  );
+  const subjectW = Math.max(0, width - name
W - DATE_W - GAP * 2);
+  return entries.map((e) => {
+    const rawName = e.kind === "tree" ? `${
e.name}/` : e.name;
+    const name =
+      len(rawName) > nameW ? [...rawName].s
lice(0, nameW - 3).join("") + "..." : rawNam
e;
+    const subject =
+      subjectW < 8
+        ? ""
+        : len(e.subject) > subjectW
+          ? [...e.subject].slice(0, subject
W - 3).join("") + "..."
+          : e.subject;
+    const row: ListingRow = {
+      name,
+      href: `${e.kind === "tree" ? treeBase
 : blobBase}${e.path}/`,
+      gap1: " ".repeat(nameW - len(name) + 
GAP),
+      subject,
+      subjectHref: `${commitBase}${e.hash}/
`,
+      gap2: " ".repeat(Math.max(GAP, width 
- nameW - GAP - len(subject) - len(e.date)))
,
+      date: e.date,
+    };
+    checkGrid(row.name + row.gap1 + row.sub
ject + row.gap2 + row.date, width, "git list
ing");
+    return row;
+  });
+}
 

diff --git a/ooknet-design/src/components/NavBar/NavBar.test.ts b/ooknet-design/sr
c/components/NavBar/NavBar.test.ts
index 6a9fbc9..d895ccf 100644
--- a/ooknet-design/src/components/NavBar/NavBar.test.ts
+++ b/ooknet-design/src/components/NavBar/NavBar.test.ts
@@ -1,10 +1,10 @@
 import { describe, expect, it } from "vitest";
-import { NAV_ITEMS, NAV_LANG_W, itemsWidth, navRows } from "./NavBar";
+import { NAV_GAP, NAV_ITEMS, NAV_LANG_W, itemsWidth, navRows } from "./NavBar";
 import { MOBILE_FRAME_W, FRAME_W } from "../../lib/config";
 
 describe("navRows", () => {
-  it("keeps all five items on one row at desktop width", () => {
-    const rows = navRows(NAV_ITEMS, FRAME_W, 4, NAV_LANG_W + 1);
+  it("keeps all seven items on one row at desktop width", () => {
+    const rows = navRows(NAV_ITEMS, FRAME_W, NAV_GAP, NAV_LANG_W + 1);
     expect(rows).toHaveLength(1);
   });
 

diff --git a/ooknet-design/src/components/Na
vBar/NavBar.test.ts b/ooknet-design/src/comp
onents/NavBar/NavBar.test.ts
index 6a9fbc9..d895ccf 100644
--- a/ooknet-design/src/components/NavBar/Na
vBar.test.ts
+++ b/ooknet-design/src/components/NavBar/Na
vBar.test.ts
@@ -1,10 +1,10 @@
 import { describe, expect, it } from "vites
t";
-import { NAV_ITEMS, NAV_LANG_W, itemsWidth,
 navRows } from "./NavBar";
+import { NAV_GAP, NAV_ITEMS, NAV_LANG_W, it
emsWidth, navRows } from "./NavBar";
 import { MOBILE_FRAME_W, FRAME_W } from "..
/../lib/config";
 
 describe("navRows", () => {
-  it("keeps all five items on one row at de
sktop width", () => {
-    const rows = navRows(NAV_ITEMS, FRAME_W
, 4, NAV_LANG_W + 1);
+  it("keeps all seven items on one row at d
esktop width", () => {
+    const rows = navRows(NAV_ITEMS, FRAME_W
, NAV_GAP, NAV_LANG_W + 1);
     expect(rows).toHaveLength(1);
   });
 
 

diff --git a/ooknet-design/src/components/NavBar/NavBar.ts b/ooknet-design/src/com
ponents/NavBar/NavBar.ts
index 6eb9a15..e0c92fa 100644
--- a/ooknet-design/src/components/NavBar/NavBar.ts
+++ b/ooknet-design/src/components/NavBar/NavBar.ts
@@ -21,12 +21,13 @@ export const NAV_ITEMS: NavItem[] = [
   { label: "NOTES",     href: "/notes/" },
   { label: "KB",        href: "/kb/" },
   { label: "DOCS",      href: "/docs/" },
+  { label: "GIT",       href: "/git/" },
   { label: "PROJECTS",  href: "/projects/" },
   { label: "RESOURCES", href: "/resources/" },
   { label: "ABOUT",     href: "/about/" },
 ];
 
-export const NAV_GAP = 4;       // spaces between buttons
+export const NAV_GAP = 3;       // spaces between buttons - seven items fit one 8
6-cell row
 export const NAV_LANG_W = 2;    // visible width of "EN"
 
 /** Total width of all buttons + inter-button gaps. */

diff --git a/ooknet-design/src/components/Na
vBar/NavBar.ts b/ooknet-design/src/component
s/NavBar/NavBar.ts
index 6eb9a15..e0c92fa 100644
--- a/ooknet-design/src/components/NavBar/Na
vBar.ts
+++ b/ooknet-design/src/components/NavBar/Na
vBar.ts
@@ -21,12 +21,13 @@ export const NAV_ITEMS: 
NavItem[] = [
   { label: "NOTES",     href: "/notes/" },
   { label: "KB",        href: "/kb/" },
   { label: "DOCS",      href: "/docs/" },
+  { label: "GIT",       href: "/git/" },
   { label: "PROJECTS",  href: "/projects/" 
},
   { label: "RESOURCES", href: "/resources/"
 },
   { label: "ABOUT",     href: "/about/" },
 ];
 
-export const NAV_GAP = 4;       // spaces b
etween buttons
+export const NAV_GAP = 3;       // spaces b
etween buttons - seven items fit one 86-cell
 row
 export const NAV_LANG_W = 2;    // visible 
width of "EN"
 
 /** Total width of all buttons + inter-butt
on gaps. */
 

diff --git a/ooknet-design/src/git.config.ts b/ooknet-design/src/git.config.ts
new file mode 100644
index 0000000..e9795bd
--- /dev/null
+++ b/ooknet-design/src/git.config.ts
@@ -0,0 +1,15 @@
+import type { GitRepo } from "./lib/git";
+
+// Repos rendered by /git/. Paths resolve from ooknet-design/ at build
+// time; missing paths are skipped so any machine can build the site.
+// On ooknode these point at forgejo's bare repositories
+// (.../forgejo/data/forgejo-repositories/<owner>/<repo>.git).
+export const REPOS: GitRepo[] = [
+  {
+    slug: "ooknet-org",
+    name: "ooknet.org",
+    path: "..",
+    desc: "personal website - a text-mode knowledge base and the ascii design sys
tem that renders it",
+    clone: "https://git.ooknet.org/ooks/ooknet.org.git",
+  },
+];

diff --git a/ooknet-design/src/git.config.ts
 b/ooknet-design/src/git.config.ts
new file mode 100644
index 0000000..e9795bd
--- /dev/null
+++ b/ooknet-design/src/git.config.ts
@@ -0,0 +1,15 @@
+import type { GitRepo } from "./lib/git";
+
+// Repos rendered by /git/. Paths resolve f
rom ooknet-design/ at build
+// time; missing paths are skipped so any m
achine can build the site.
+// On ooknode these point at forgejo's bare
 repositories
+// (.../forgejo/data/forgejo-repositories/<
owner>/<repo>.git).
+export const REPOS: GitRepo[] = [
+  {
+    slug: "ooknet-org",
+    name: "ooknet.org",
+    path: "..",
+    desc: "personal website - a text-mode k
nowledge base and the ascii design system th
at renders it",
+    clone: "https://git.ooknet.org/ooks/ook
net.org.git",
+  },
+];
 

diff --git a/ooknet-design/src/lib/ascii.ts b/ooknet-design/src/lib/ascii.ts
index 22e7fff..31f24ef 100644
--- a/ooknet-design/src/lib/ascii.ts
+++ b/ooknet-design/src/lib/ascii.ts
@@ -34,6 +34,21 @@ export function checkGrid(text: string, maxW: number, ctx: stri
ng): string {
   return text;
 }
 
+// For text from outside the pipeline (git history, external feeds):
+// transliterate typographic glyphs to their og-ascii forms and blank
+// anything else off-grid, so checkGrid never fails on foreign input.
+const GRID_MAP: Record<string, string> = {
+  "-": "-", "-": "-", "...": "...", "-": "-",
+  "": "", """: '"', """: '"', "'": "'", "'": "'",
+};
+
+export function toGrid(s: string): string {
+  return s
+    .split("\n")
+    .map((line) => [...line].map((c) => GRID_MAP[c] ?? (ON_GRID.test(c) ? c : "?"
)).join(""))
+    .join("\n");
+}
+
 export type PadSide = "start" | "end" | "center";
 export function pad(s: unknown, n: number, side: PadSide = "end"): string {
   const str = String(s ?? "");

diff --git a/ooknet-design/src/lib/ascii.ts 
b/ooknet-design/src/lib/ascii.ts
index 22e7fff..31f24ef 100644
--- a/ooknet-design/src/lib/ascii.ts
+++ b/ooknet-design/src/lib/ascii.ts
@@ -34,6 +34,21 @@ export function checkGrid
(text: string, maxW: number, ctx: string): s
tring {
   return text;
 }
 
+// For text from outside the pipeline (git 
history, external feeds):
+// transliterate typographic glyphs to thei
r og-ascii forms and blank
+// anything else off-grid, so checkGrid nev
er fails on foreign input.
+const GRID_MAP: Record<string, string> = {
+  "-": "-", "-": "-", "...": "...", "-": "-
",
+  "": "", """: '"', """: '"', "'": "'", "'"
: "'",
+};
+
+export function toGrid(s: string): string {
+  return s
+    .split("\n")
+    .map((line) => [...line].map((c) => GRI
D_MAP[c] ?? (ON_GRID.test(c) ? c : "?")).joi
n(""))
+    .join("\n");
+}
+
 export type PadSide = "start" | "end" | "ce
nter";
 export function pad(s: unknown, n: number, 
side: PadSide = "end"): string {
   const str = String(s ?? "");
 

diff --git a/ooknet-design/src/lib/git.test.ts b/ooknet-design/src/lib/git.test.ts
new file mode 100644
index 0000000..fed2655
--- /dev/null
+++ b/ooknet-design/src/lib/git.test.ts
@@ -0,0 +1,42 @@
+import { describe, expect, it } from "vitest";
+import { filesTree, repoAvailable, repoInfo } from "./git";
+import { toGrid } from "./ascii";
+
+describe("toGrid", () => {
+  it("transliterates typographic glyphs to og ascii", () => {
+    expect(toGrid("a - b... "c"")).toBe('a - b... "c"');
+  });
+  it("blanks anything else off-grid", () => {
+    expect(toGrid("na?ve ?")).toBe("na?ve ?");
+  });
+  it("passes frame glyphs and ascii through", () => {
+    expect(toGrid("[ ok ] ─ █")).toBe("[ ok ] ─ █");
+  });
+  it("preserves line structure", () => {
+    expect(toGrid("a\nb - c\n")).toBe("a\nb - c\n");
+  });
+});
+
+describe("filesTree", () => {
+  const tree = filesTree(["flake.nix", "src/lib/ascii.ts", "src/pages/index.astro
"], "/git/x/blob/");
+
+  it("branches directories and links files", () => {
+    expect(tree.map((n) => n.label)).toEqual(["src/", "flake.nix"]);
+    const src = tree[0].children!;
+    expect(src.map((n) => n.label)).toEqual(["lib/", "pages/"]);
+    expect(src[0].children![0]).toEqual({
+      label: "ascii.ts",
+      href: "/git/x/blob/src/lib/ascii.ts/",
+    });
+  });
+});
+
+describe("repoInfo", () => {
+  it("reads this repository", () => {
+    expect(repoAvailable({ slug: "x", name: "x", path: "..", desc: "" })).toBe(tr
ue);
+    const info = repoInfo("..");
+    expect(info.commits).toBeGreaterThan(30);
+    expect(info.branch).toBe("master");
+    expect(info.lastDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
+  });
+});

diff --git a/ooknet-design/src/lib/git.test.
ts b/ooknet-design/src/lib/git.test.ts
new file mode 100644
index 0000000..fed2655
--- /dev/null
+++ b/ooknet-design/src/lib/git.test.ts
@@ -0,0 +1,42 @@
+import { describe, expect, it } from "vites
t";
+import { filesTree, repoAvailable, repoInfo
 } from "./git";
+import { toGrid } from "./ascii";
+
+describe("toGrid", () => {
+  it("transliterates typographic glyphs to 
og ascii", () => {
+    expect(toGrid("a - b... "c"")).toBe('a 
- b... "c"');
+  });
+  it("blanks anything else off-grid", () =>
 {
+    expect(toGrid("na?ve ?")).toBe("na?ve ?
");
+  });
+  it("passes frame glyphs and ascii through
", () => {
+    expect(toGrid("[ ok ] ─ █")).toBe("[ ok
 ] ─ █");
+  });
+  it("preserves line structure", () => {
+    expect(toGrid("a\nb - c\n")).toBe("a\nb
 - c\n");
+  });
+});
+
+describe("filesTree", () => {
+  const tree = filesTree(["flake.nix", "src
/lib/ascii.ts", "src/pages/index.astro"], "/
git/x/blob/");
+
+  it("branches directories and links files"
, () => {
+    expect(tree.map((n) => n.label)).toEqua
l(["src/", "flake.nix"]);
+    const src = tree[0].children!;
+    expect(src.map((n) => n.label)).toEqual
(["lib/", "pages/"]);
+    expect(src[0].children![0]).toEqual({
+      label: "ascii.ts",
+      href: "/git/x/blob/src/lib/ascii.ts/"
,
+    });
+  });
+});
+
+describe("repoInfo", () => {
+  it("reads this repository", () => {
+    expect(repoAvailable({ slug: "x", name:
 "x", path: "..", desc: "" })).toBe(true);
+    const info = repoInfo("..");
+    expect(info.commits).toBeGreaterThan(30
);
+    expect(info.branch).toBe("master");
+    expect(info.lastDate).toMatch(/^\d{4}-\
d{2}-\d{2}$/);
+  });
+});
 

diff --git a/ooknet-design/src/lib/git.ts b/ooknet-design/src/lib/git.ts
new file mode 100644
index 0000000..489be48
--- /dev/null
+++ b/ooknet-design/src/lib/git.ts
@@ -0,0 +1,201 @@
+// Build-time git readers for the /git/ section. Pages are generated
+// from real repositories at build time - stagit-style, but rendered
+// through the site's own components. Anything that can land in a
+// frame passes toGrid() so history written before the og-ascii rule
+// can't fail the build.
+import { execFileSync } from "node:child_process";
+import { existsSync } from "node:fs";
+import { toGrid } from "./ascii";
+import type { TreeNode } from "../components/Tree/Tree";
+import type { TimelineEntry } from "../components/Timeline/Timeline";
+
+export interface GitRepo {
+  /** Url segment. */
+  slug: string;
+  /** Display name. */
+  name: string;
+  /** Local checkout or bare repo path, resolved from ooknet-design/. */
+  path: string;
+  desc: string;
+  clone?: string;
+}
+
+const MAX_BLOB = 200 * 1024;
+
+const git = (path: string, args: string[]): string =>
+  execFileSync("git", ["-C", path, ...args], {
+    encoding: "utf8",
+    maxBuffer: 32 * 1024 * 1024,
+  });
+
+export const repoAvailable = (r: GitRepo): boolean => existsSync(r.path);
+
+export interface RepoInfo {
+  branch: string;
+  commits: number;
+  lastDate: string;
+  lastSubject: string;
+}
+
+export function repoInfo(path: string): RepoInfo {
+  const [lastDate, ...subject] = git(path, ["log", "-1", "--format=%as|%s"])
+    .trim()
+    .split("|");
+  return {
+    branch: git(path, ["symbolic-ref", "--short", "HEAD"]).trim(),
+    commits: parseInt(git(path, ["rev-list", "--count", "HEAD"]).trim(), 10),
+    lastDate,
+    lastSubject: toGrid(subject.join("|")),
+  };
+}
+
+export function repoFiles(path: string): string[] {
+  return git(path, ["ls-tree", "-r", "--name-only", "HEAD"])
+    .split("\n")
+    .filter(Boolean);
+}
+
+/** File list as a Tree: directories branch, files link to blob pages. */
+export function filesTree(files: string[], base: string): TreeNode[] {
+  type Branch = { dirs: Map<string, Branch>; files: string[] };
+  const root: Branch = { dirs: new Map(), files: [] };
+  for (const file of files) {
+    const segments = file.split("/");
+    let branch = root;
+    for (const seg of segments.slice(0, -1)) {
+      if (!branch.dirs.has(seg)) branch.dirs.set(seg, { dirs: new Map(), files: [
] });
+      branch = branch.dirs.get(seg)!;
+    }
+    branch.files.push(file);
+  }
+  const toNodes = (b: Branch): TreeNode[] => [
+    ...[...b.dirs.entries()]
+      .sort(([a], [c]) => a.localeCompare(c))
+      .map(([seg, child]) => ({ label: `${toGrid(seg)}/`, children: toNodes(child
) })),
+    ...b.files
+      .sort()
+      .map((f) => ({
+        label: toGrid(f.split("/").pop()!),
+        href: `${base}${f}/`,
+      })),
+  ];
+  return toNodes(root);
+}
+
+export function repoLog(path: string, limit: number, base: string): TimelineEntry
[] {
+  return git(path, ["log", `-${limit}`, "--format=%h|%as|%s"])
+    .split("\n")
+    .filter(Boolean)
+    .map((line) => {
+      const [hash, date, ...subject] = line.split("|");
+      return {
+        date,
+        title: toGrid(hash),
+        desc: toGrid(subject.join("|")),
+        href: `${base}commit/${hash}/`,
+      };
+    });
+}
+
+export interface DirEntry {
+  name: string;
+  path: string;
+  kind: "tree" | "blob";
+  /** Last commit touching this path. */
+  hash: string;
+  date: string;
+  subject: string;
+}
+
+/** Immediate children of a directory, github-style: dirs first, each
+ *  with the last commit that touched it. */
+export function dirEntries(path: string, dir = ""): DirEntry[] {
+  const spec = dir ? `HEAD:${dir}` : "HEAD";
+  const rows = git(path, ["ls-tree", spec])
+    .split("\n")
+    .filter(Boolean)
+    .map((line) => {
+      const [meta, name] = line.split("\t");
+      const kind = meta.split(" ")[1] as "tree" | "blob" | "commit";
+      return { name, kind };
+    })
+    .filter((e): e is { name: string; kind: "tree" | "blob" } => e.kind !== "comm
it");
+  return rows
+    .sort((a, b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind ==
= "tree" ? -1 : 1))
+    .map(({ name, kind }) => {
+      const full = dir ? `${dir}/${name}` : name;
+      const [hash, date, ...subject] = git(path, ["log", "-1", "--format=%h|%as|%
s", "HEAD", "--", full])
+        .trim()
+        .split("|");
+      return { name: toGrid(name), path: full, kind, hash, date, subject: toGrid(
subject.join("|")) };
+    });
+}
+
+/** Every directory path in the repo, for tree page generation. */
+export function repoDirs(files: string[]): string[] {
+  const dirs = new Set<string>();
+  for (const f of files) {
+    const segments = f.split("/");
+    for (let i = 1; i < segments.length; i++) {
+      dirs.add(segments.slice(0, i).join("/"));
+    }
+  }
+  return [...dirs].sort();
+}
+
+/** Repo README content at HEAD, if present. */
+export function readReadme(path: string): string | null {
+  for (const name of ["README.md", "readme.md", "README"]) {
+    try {
+      return git(path, ["show", `HEAD:${name}`]);
+    } catch {
+      continue;
+    }
+  }
+  return null;
+}
+
+export interface Blob {
+  text: string;
+  binary: boolean;
+  truncated: boolean;
+  lines: number;
+}
+
+export function readBlob(path: string, file: string): Blob {
+  const raw = execFileSync("git", ["-C", path, "show", `HEAD:${file}`], {
+    maxBuffer: 32 * 1024 * 1024,
+  });
+  if (raw.subarray(0, 8000).includes(0)) {
+    return { text: "", binary: true, truncated: false, lines: 0 };
+  }
+  const truncated = raw.length > MAX_BLOB;
+  const text = raw.subarray(0, MAX_BLOB).toString("utf8");
+  return {
+    text,
+    binary: false,
+    truncated,
+    lines: text.split("\n").length,
+  };
+}
+
+export interface Commit {
+  hash: string;
+  date: string;
+  subject: string;
+  body: string;
+  diff: string;
+}
+
+export function readCommit(path: string, hash: string): Commit {
+  const meta = git(path, ["show", "-s", "--format=%H|%as|%s|%b", hash]).trim();
+  const [full, date, subject, ...body] = meta.split("|");
+  const diff = git(path, ["show", "--stat", "--patch", "--format=", hash]);
+  return {
+    hash: full,
+    date,
+    subject: toGrid(subject),
+    body: toGrid(body.join("|").trim()),
+    diff: diff.length > MAX_BLOB ? diff.slice(0, MAX_BLOB) + "\n[diff truncated]"
 : diff,
+  };
+}

diff --git a/ooknet-design/src/lib/git.ts b/
ooknet-design/src/lib/git.ts
new file mode 100644
index 0000000..489be48
--- /dev/null
+++ b/ooknet-design/src/lib/git.ts
@@ -0,0 +1,201 @@
+// Build-time git readers for the /git/ sec
tion. Pages are generated
+// from real repositories at build time - s
tagit-style, but rendered
+// through the site's own components. Anyth
ing that can land in a
+// frame passes toGrid() so history written
 before the og-ascii rule
+// can't fail the build.
+import { execFileSync } from "node:child_pr
ocess";
+import { existsSync } from "node:fs";
+import { toGrid } from "./ascii";
+import type { TreeNode } from "../component
s/Tree/Tree";
+import type { TimelineEntry } from "../comp
onents/Timeline/Timeline";
+
+export interface GitRepo {
+  /** Url segment. */
+  slug: string;
+  /** Display name. */
+  name: string;
+  /** Local checkout or bare repo path, res
olved from ooknet-design/. */
+  path: string;
+  desc: string;
+  clone?: string;
+}
+
+const MAX_BLOB = 200 * 1024;
+
+const git = (path: string, args: string[]):
 string =>
+  execFileSync("git", ["-C", path, ...args]
, {
+    encoding: "utf8",
+    maxBuffer: 32 * 1024 * 1024,
+  });
+
+export const repoAvailable = (r: GitRepo): 
boolean => existsSync(r.path);
+
+export interface RepoInfo {
+  branch: string;
+  commits: number;
+  lastDate: string;
+  lastSubject: string;
+}
+
+export function repoInfo(path: string): Rep
oInfo {
+  const [lastDate, ...subject] = git(path, 
["log", "-1", "--format=%as|%s"])
+    .trim()
+    .split("|");
+  return {
+    branch: git(path, ["symbolic-ref", "--s
hort", "HEAD"]).trim(),
+    commits: parseInt(git(path, ["rev-list"
, "--count", "HEAD"]).trim(), 10),
+    lastDate,
+    lastSubject: toGrid(subject.join("|")),
+  };
+}
+
+export function repoFiles(path: string): st
ring[] {
+  return git(path, ["ls-tree", "-r", "--nam
e-only", "HEAD"])
+    .split("\n")
+    .filter(Boolean);
+}
+
+/** File list as a Tree: directories branch
, files link to blob pages. */
+export function filesTree(files: string[], 
base: string): TreeNode[] {
+  type Branch = { dirs: Map<string, Branch>
; files: string[] };
+  const root: Branch = { dirs: new Map(), f
iles: [] };
+  for (const file of files) {
+    const segments = file.split("/");
+    let branch = root;
+    for (const seg of segments.slice(0, -1)
) {
+      if (!branch.dirs.has(seg)) branch.dir
s.set(seg, { dirs: new Map(), files: [] });
+      branch = branch.dirs.get(seg)!;
+    }
+    branch.files.push(file);
+  }
+  const toNodes = (b: Branch): TreeNode[] =
> [
+    ...[...b.dirs.entries()]
+      .sort(([a], [c]) => a.localeCompare(c
))
+      .map(([seg, child]) => ({ label: `${t
oGrid(seg)}/`, children: toNodes(child) })),
+    ...b.files
+      .sort()
+      .map((f) => ({
+        label: toGrid(f.split("/").pop()!),
+        href: `${base}${f}/`,
+      })),
+  ];
+  return toNodes(root);
+}
+
+export function repoLog(path: string, limit
: number, base: string): TimelineEntry[] {
+  return git(path, ["log", `-${limit}`, "--
format=%h|%as|%s"])
+    .split("\n")
+    .filter(Boolean)
+    .map((line) => {
+      const [hash, date, ...subject] = line
.split("|");
+      return {
+        date,
+        title: toGrid(hash),
+        desc: toGrid(subject.join("|")),
+        href: `${base}commit/${hash}/`,
+      };
+    });
+}
+
+export interface DirEntry {
+  name: string;
+  path: string;
+  kind: "tree" | "blob";
+  /** Last commit touching this path. */
+  hash: string;
+  date: string;
+  subject: string;
+}
+
+/** Immediate children of a directory, gith
ub-style: dirs first, each
+ *  with the last commit that touched it. *
/
+export function dirEntries(path: string, di
r = ""): DirEntry[] {
+  const spec = dir ? `HEAD:${dir}` : "HEAD"
;
+  const rows = git(path, ["ls-tree", spec])
+    .split("\n")
+    .filter(Boolean)
+    .map((line) => {
+      const [meta, name] = line.split("\t")
;
+      const kind = meta.split(" ")[1] as "t
ree" | "blob" | "commit";
+      return { name, kind };
+    })
+    .filter((e): e is { name: string; kind:
 "tree" | "blob" } => e.kind !== "commit");
+  return rows
+    .sort((a, b) => (a.kind === b.kind ? a.
name.localeCompare(b.name) : a.kind === "tre
e" ? -1 : 1))
+    .map(({ name, kind }) => {
+      const full = dir ? `${dir}/${name}` :
 name;
+      const [hash, date, ...subject] = git(
path, ["log", "-1", "--format=%h|%as|%s", "H
EAD", "--", full])
+        .trim()
+        .split("|");
+      return { name: toGrid(name), path: fu
ll, kind, hash, date, subject: toGrid(subjec
t.join("|")) };
+    });
+}
+
+/** Every directory path in the repo, for t
ree page generation. */
+export function repoDirs(files: string[]): 
string[] {
+  const dirs = new Set<string>();
+  for (const f of files) {
+    const segments = f.split("/");
+    for (let i = 1; i < segments.length; i+
+) {
+      dirs.add(segments.slice(0, i).join("/
"));
+    }
+  }
+  return [...dirs].sort();
+}
+
+/** Repo README content at HEAD, if present
. */
+export function readReadme(path: string): s
tring | null {
+  for (const name of ["README.md", "readme.
md", "README"]) {
+    try {
+      return git(path, ["show", `HEAD:${nam
e}`]);
+    } catch {
+      continue;
+    }
+  }
+  return null;
+}
+
+export interface Blob {
+  text: string;
+  binary: boolean;
+  truncated: boolean;
+  lines: number;
+}
+
+export function readBlob(path: string, file
: string): Blob {
+  const raw = execFileSync("git", ["-C", pa
th, "show", `HEAD:${file}`], {
+    maxBuffer: 32 * 1024 * 1024,
+  });
+  if (raw.subarray(0, 8000).includes(0)) {
+    return { text: "", binary: true, trunca
ted: false, lines: 0 };
+  }
+  const truncated = raw.length > MAX_BLOB;
+  const text = raw.subarray(0, MAX_BLOB).to
String("utf8");
+  return {
+    text,
+    binary: false,
+    truncated,
+    lines: text.split("\n").length,
+  };
+}
+
+export interface Commit {
+  hash: string;
+  date: string;
+  subject: string;
+  body: string;
+  diff: string;
+}
+
+export function readCommit(path: string, ha
sh: string): Commit {
+  const meta = git(path, ["show", "-s", "--
format=%H|%as|%s|%b", hash]).trim();
+  const [full, date, subject, ...body] = me
ta.split("|");
+  const diff = git(path, ["show", "--stat",
 "--patch", "--format=", hash]);
+  return {
+    hash: full,
+    date,
+    subject: toGrid(subject),
+    body: toGrid(body.join("|").trim()),
+    diff: diff.length > MAX_BLOB ? diff.sli
ce(0, MAX_BLOB) + "\n[diff truncated]" : dif
f,
+  };
+}
 

diff --git a/ooknet-design/src/pages/git/[repo]/blob/[...path].astro b/ooknet-desi
gn/src/pages/git/[repo]/blob/[...path].astro
new file mode 100644
index 0000000..6f33a4e
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/blob/[...path].astro
@@ -0,0 +1,52 @@
+---
+import ArticleLayout from "../../../../layouts/ArticleLayout/ArticleLayout.astro"
;
+import TopHeader from "../../../../components/TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../../components/ArticleHeader/ArticleHeader.ast
ro";
+import Pre from "../../../../components/Pre/Pre.astro";
+import KeyValue from "../../../../components/KeyValue/KeyValue.astro";
+import Diff from "../../../../components/Diff/Diff.astro";
+import Button from "../../../../components/Button/Button.astro";
+import ArticleFooter from "../../../../components/ArticleFooter/ArticleFooter.ast
ro";
+import { toGrid } from "../../../../lib/ascii";
+import { REPOS } from "../../../../git.config";
+import { repoAvailable, repoFiles, readBlob, type GitRepo } from "../../../../lib
/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).flatMap((r) =>
+    repoFiles(r.path).map((file) => ({
+      params: { repo: r.slug, path: file },
+      props: { repo: r, file },
+    })),
+  );
+}
+
+const { repo, file } = Astro.props as { repo: GitRepo; file: string };
+const blob = readBlob(repo.path, file);
+const name = file.split("/").pop()!;
+// paths are one long "word" - chunk them so KeyValue stays on-grid at
+// the narrow frame (48 - keyW 8 = 40 cells)
+const chunk = (s: string, n: number) => {
+  const cps = [...s];
+  return Array.from({ length: Math.ceil(cps.length / n) }, (_, i) =>
+    cps.slice(i * n, (i + 1) * n).join(""));
+};
+const pathRows = chunk(toGrid(file), 38);
+---
+<ArticleLayout title={`GIT - ${repo.name} - ${name}`} description={`${file} in ${
repo.name}.`}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`REPO: ${repo.name.toUpperCase()}`} />
+  <Pre>{" "}</Pre>
+  <KeyValue entries={[
+    ["FILE", pathRows[0]],
+    ...pathRows.slice(1).map((c) => ["", c] as [string, string]),
+    ["LINES", blob.binary ? "BINARY" : `${blob.lines}`],
+  ]} />
+  <Pre>{" "}</Pre>
+  {blob.binary
+    ? <Pre>*** BINARY FILE - NOT RENDERED ***</Pre>
+    : <Diff code={toGrid(blob.text)} />}
+  {blob.truncated && <Pre>*** TRUNCATED AT 200 KIB ***</Pre>}
+  <Pre>{" "}</Pre>
+  <Pre><Button label="BACK TO REPO" href={`/git/${repo.slug}/`} /></Pre>
+  <ArticleFooter />
+</ArticleLayout>

diff --git a/ooknet-design/src/pages/git/[re
po]/blob/[...path].astro b/ooknet-design/src
/pages/git/[repo]/blob/[...path].astro
new file mode 100644
index 0000000..6f33a4e
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/blo
b/[...path].astro
@@ -0,0 +1,52 @@
+---
+import ArticleLayout from "../../../../layo
uts/ArticleLayout/ArticleLayout.astro";
+import TopHeader from "../../../../componen
ts/TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../../comp
onents/ArticleHeader/ArticleHeader.astro";
+import Pre from "../../../../components/Pre
/Pre.astro";
+import KeyValue from "../../../../component
s/KeyValue/KeyValue.astro";
+import Diff from "../../../../components/Di
ff/Diff.astro";
+import Button from "../../../../components/
Button/Button.astro";
+import ArticleFooter from "../../../../comp
onents/ArticleFooter/ArticleFooter.astro";
+import { toGrid } from "../../../../lib/asc
ii";
+import { REPOS } from "../../../../git.conf
ig";
+import { repoAvailable, repoFiles, readBlob
, type GitRepo } from "../../../../lib/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).flatMa
p((r) =>
+    repoFiles(r.path).map((file) => ({
+      params: { repo: r.slug, path: file },
+      props: { repo: r, file },
+    })),
+  );
+}
+
+const { repo, file } = Astro.props as { rep
o: GitRepo; file: string };
+const blob = readBlob(repo.path, file);
+const name = file.split("/").pop()!;
+// paths are one long "word" - chunk them s
o KeyValue stays on-grid at
+// the narrow frame (48 - keyW 8 = 40 cells
)
+const chunk = (s: string, n: number) => {
+  const cps = [...s];
+  return Array.from({ length: Math.ceil(cps
.length / n) }, (_, i) =>
+    cps.slice(i * n, (i + 1) * n).join(""))
;
+};
+const pathRows = chunk(toGrid(file), 38);
+---
+<ArticleLayout title={`GIT - ${repo.name} -
 ${name}`} description={`${file} in ${repo.n
ame}.`}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`
REPO: ${repo.name.toUpperCase()}`} />
+  <Pre>{" "}</Pre>
+  <KeyValue entries={[
+    ["FILE", pathRows[0]],
+    ...pathRows.slice(1).map((c) => ["", c]
 as [string, string]),
+    ["LINES", blob.binary ? "BINARY" : `${b
lob.lines}`],
+  ]} />
+  <Pre>{" "}</Pre>
+  {blob.binary
+    ? <Pre>*** BINARY FILE - NOT RENDERED *
**</Pre>
+    : <Diff code={toGrid(blob.text)} />}
+  {blob.truncated && <Pre>*** TRUNCATED AT 
200 KIB ***</Pre>}
+  <Pre>{" "}</Pre>
+  <Pre><Button label="BACK TO REPO" href={`
/git/${repo.slug}/`} /></Pre>
+  <ArticleFooter />
+</ArticleLayout>
 

diff --git a/ooknet-design/src/pages/git/[repo]/commit/[hash].astro b/ooknet-desig
n/src/pages/git/[repo]/commit/[hash].astro
new file mode 100644
index 0000000..391cea9
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/commit/[hash].astro
@@ -0,0 +1,41 @@
+---
+import ArticleLayout from "../../../../layouts/ArticleLayout/ArticleLayout.astro"
;
+import TopHeader from "../../../../components/TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../../components/ArticleHeader/ArticleHeader.ast
ro";
+import Pre from "../../../../components/Pre/Pre.astro";
+import KeyValue from "../../../../components/KeyValue/KeyValue.astro";
+import Diff from "../../../../components/Diff/Diff.astro";
+import Button from "../../../../components/Button/Button.astro";
+import ArticleFooter from "../../../../components/ArticleFooter/ArticleFooter.ast
ro";
+import { toGrid } from "../../../../lib/ascii";
+import { REPOS } from "../../../../git.config";
+import { repoAvailable, repoLog, readCommit, type GitRepo } from "../../../../lib
/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).flatMap((r) =>
+    repoLog(r.path, 500, `/git/${r.slug}/`).map((c) => ({
+      params: { repo: r.slug, hash: c.title },
+      props: { repo: r },
+    })),
+  );
+}
+
+const { repo } = Astro.props as { repo: GitRepo };
+const { hash } = Astro.params;
+const commit = readCommit(repo.path, hash!);
+---
+<ArticleLayout title={`GIT - ${repo.name} @ ${hash}`} description={commit.subject
}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`COMMIT: ${hash!.toUpperCase()}`} date=
{commit.date} />
+  <Pre>{" "}</Pre>
+  <KeyValue entries={[
+    ["HASH", commit.hash.slice(0, 12)],
+    ["DATE", commit.date],
+    ["SUBJECT", commit.subject],
+  ]} />
+  <Pre>{" "}</Pre>
+  <Diff code={toGrid(commit.diff)} />
+  <Pre>{" "}</Pre>
+  <Pre><Button label="BACK TO LOG" href={`/git/${repo.slug}/log/`} /></Pre>
+  <ArticleFooter />
+</ArticleLayout>

diff --git a/ooknet-design/src/pages/git/[re
po]/commit/[hash].astro b/ooknet-design/src/
pages/git/[repo]/commit/[hash].astro
new file mode 100644
index 0000000..391cea9
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/com
mit/[hash].astro
@@ -0,0 +1,41 @@
+---
+import ArticleLayout from "../../../../layo
uts/ArticleLayout/ArticleLayout.astro";
+import TopHeader from "../../../../componen
ts/TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../../comp
onents/ArticleHeader/ArticleHeader.astro";
+import Pre from "../../../../components/Pre
/Pre.astro";
+import KeyValue from "../../../../component
s/KeyValue/KeyValue.astro";
+import Diff from "../../../../components/Di
ff/Diff.astro";
+import Button from "../../../../components/
Button/Button.astro";
+import ArticleFooter from "../../../../comp
onents/ArticleFooter/ArticleFooter.astro";
+import { toGrid } from "../../../../lib/asc
ii";
+import { REPOS } from "../../../../git.conf
ig";
+import { repoAvailable, repoLog, readCommit
, type GitRepo } from "../../../../lib/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).flatMa
p((r) =>
+    repoLog(r.path, 500, `/git/${r.slug}/`)
.map((c) => ({
+      params: { repo: r.slug, hash: c.title
 },
+      props: { repo: r },
+    })),
+  );
+}
+
+const { repo } = Astro.props as { repo: Git
Repo };
+const { hash } = Astro.params;
+const commit = readCommit(repo.path, hash!)
;
+---
+<ArticleLayout title={`GIT - ${repo.name} @
 ${hash}`} description={commit.subject}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`
COMMIT: ${hash!.toUpperCase()}`} date={commi
t.date} />
+  <Pre>{" "}</Pre>
+  <KeyValue entries={[
+    ["HASH", commit.hash.slice(0, 12)],
+    ["DATE", commit.date],
+    ["SUBJECT", commit.subject],
+  ]} />
+  <Pre>{" "}</Pre>
+  <Diff code={toGrid(commit.diff)} />
+  <Pre>{" "}</Pre>
+  <Pre><Button label="BACK TO LOG" href={`/
git/${repo.slug}/log/`} /></Pre>
+  <ArticleFooter />
+</ArticleLayout>
 

diff --git a/ooknet-design/src/pages/git/[repo]/index.astro b/ooknet-design/src/pa
ges/git/[repo]/index.astro
new file mode 100644
index 0000000..bb1118f
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/index.astro
@@ -0,0 +1,65 @@
+---
+import { marked } from "marked";
+import ArticleLayout from "../../../layouts/ArticleLayout/ArticleLayout.astro";
+import TopHeader from "../../../components/TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../components/ArticleHeader/ArticleHeader.astro"
;
+import Frame from "../../../components/Frame/Frame.astro";
+import Pre from "../../../components/Pre/Pre.astro";
+import KeyValue from "../../../components/KeyValue/KeyValue.astro";
+import GitListing from "../../../components/GitListing/GitListing.astro";
+import Timeline from "../../../components/Timeline/Timeline.astro";
+import Button from "../../../components/Button/Button.astro";
+import Prose from "../../../components/Prose/Prose.astro";
+import ArticleFooter from "../../../components/ArticleFooter/ArticleFooter.astro"
;
+import { row2, ruleD } from "../../../lib/ascii";
+import { REPOS } from "../../../git.config";
+import { repoAvailable, repoInfo, dirEntries, repoLog, readReadme, type GitRepo }
 from "../../../lib/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).map((r) => ({
+    params: { repo: r.slug },
+    props: { repo: r },
+  }));
+}
+
+const { repo } = Astro.props as { repo: GitRepo };
+const info = repoInfo(repo.path);
+const entries = dirEntries(repo.path);
+const log = repoLog(repo.path, 5, `/git/${repo.slug}/`);
+const readme = readReadme(repo.path);
+const readmeHtml = readme ? marked.parse(readme) : null;
+
+const section = (title: string, right: string) => (w: number) =>
+  [row2(title, right, w), ruleD(w)].join("\n");
+---
+<ArticleLayout title={`GIT - ${repo.name}`} description={repo.desc}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`REPO: ${repo.name.toUpperCase()}`} dat
e={info.lastDate} />
+  <Pre>{" "}</Pre>
+  <KeyValue entries={[
+    ["BRANCH", info.branch],
+    ["COMMITS", `${info.commits}`],
+    ...(repo.clone ? [["CLONE", repo.clone.replace(/^https?:\/\//, "")] as [strin
g, string]] : []),
+  ]} />
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("FILES", `HEAD @ ${info.branch}`)} />
+  <Pre>{" "}</Pre>
+  <GitListing entries={entries} treeBase={`/git/${repo.slug}/tree/`} blobBase={`/
git/${repo.slug}/blob/`} commitBase={`/git/${repo.slug}/commit/`} />
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("LOG", "most recent first")} />
+  <Pre>{" "}</Pre>
+  <Timeline entries={log} />
+  <Pre><Button label="FULL LOG" href={`/git/${repo.slug}/log/`} /></Pre>
+  <Pre>{" "}</Pre>
+
+  {readmeHtml && (
+    <Fragment>
+      <Frame build={section("README", "rendered from HEAD")} />
+      <Prose><div set:html={readmeHtml} /></Prose>
+      <Pre>{" "}</Pre>
+    </Fragment>
+  )}
+  <ArticleFooter />
+</ArticleLayout>

diff --git a/ooknet-design/src/pages/git/[re
po]/index.astro b/ooknet-design/src/pages/gi
t/[repo]/index.astro
new file mode 100644
index 0000000..bb1118f
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/ind
ex.astro
@@ -0,0 +1,65 @@
+---
+import { marked } from "marked";
+import ArticleLayout from "../../../layouts
/ArticleLayout/ArticleLayout.astro";
+import TopHeader from "../../../components/
TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../compone
nts/ArticleHeader/ArticleHeader.astro";
+import Frame from "../../../components/Fram
e/Frame.astro";
+import Pre from "../../../components/Pre/Pr
e.astro";
+import KeyValue from "../../../components/K
eyValue/KeyValue.astro";
+import GitListing from "../../../components
/GitListing/GitListing.astro";
+import Timeline from "../../../components/T
imeline/Timeline.astro";
+import Button from "../../../components/But
ton/Button.astro";
+import Prose from "../../../components/Pros
e/Prose.astro";
+import ArticleFooter from "../../../compone
nts/ArticleFooter/ArticleFooter.astro";
+import { row2, ruleD } from "../../../lib/a
scii";
+import { REPOS } from "../../../git.config"
;
+import { repoAvailable, repoInfo, dirEntrie
s, repoLog, readReadme, type GitRepo } from 
"../../../lib/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).map((r
) => ({
+    params: { repo: r.slug },
+    props: { repo: r },
+  }));
+}
+
+const { repo } = Astro.props as { repo: Git
Repo };
+const info = repoInfo(repo.path);
+const entries = dirEntries(repo.path);
+const log = repoLog(repo.path, 5, `/git/${r
epo.slug}/`);
+const readme = readReadme(repo.path);
+const readmeHtml = readme ? marked.parse(re
adme) : null;
+
+const section = (title: string, right: stri
ng) => (w: number) =>
+  [row2(title, right, w), ruleD(w)].join("\
n");
+---
+<ArticleLayout title={`GIT - ${repo.name}`}
 description={repo.desc}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`
REPO: ${repo.name.toUpperCase()}`} date={inf
o.lastDate} />
+  <Pre>{" "}</Pre>
+  <KeyValue entries={[
+    ["BRANCH", info.branch],
+    ["COMMITS", `${info.commits}`],
+    ...(repo.clone ? [["CLONE", repo.clone.
replace(/^https?:\/\//, "")] as [string, str
ing]] : []),
+  ]} />
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("FILES", `HEAD @ ${
info.branch}`)} />
+  <Pre>{" "}</Pre>
+  <GitListing entries={entries} treeBase={`
/git/${repo.slug}/tree/`} blobBase={`/git/${
repo.slug}/blob/`} commitBase={`/git/${repo.
slug}/commit/`} />
+  <Pre>{" "}</Pre>
+
+  <Frame build={section("LOG", "most recent
 first")} />
+  <Pre>{" "}</Pre>
+  <Timeline entries={log} />
+  <Pre><Button label="FULL LOG" href={`/git
/${repo.slug}/log/`} /></Pre>
+  <Pre>{" "}</Pre>
+
+  {readmeHtml && (
+    <Fragment>
+      <Frame build={section("README", "rend
ered from HEAD")} />
+      <Prose><div set:html={readmeHtml} /><
/Prose>
+      <Pre>{" "}</Pre>
+    </Fragment>
+  )}
+  <ArticleFooter />
+</ArticleLayout>
 

diff --git a/ooknet-design/src/pages/git/[repo]/log.astro b/ooknet-design/src/page
s/git/[repo]/log.astro
new file mode 100644
index 0000000..337ed4a
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/log.astro
@@ -0,0 +1,31 @@
+---
+import ArticleLayout from "../../../layouts/ArticleLayout/ArticleLayout.astro";
+import TopHeader from "../../../components/TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../components/ArticleHeader/ArticleHeader.astro"
;
+import Pre from "../../../components/Pre/Pre.astro";
+import Timeline from "../../../components/Timeline/Timeline.astro";
+import Button from "../../../components/Button/Button.astro";
+import ArticleFooter from "../../../components/ArticleFooter/ArticleFooter.astro"
;
+import { REPOS } from "../../../git.config";
+import { repoAvailable, repoInfo, repoLog, type GitRepo } from "../../../lib/git"
;
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).map((r) => ({
+    params: { repo: r.slug },
+    props: { repo: r },
+  }));
+}
+
+const { repo } = Astro.props as { repo: GitRepo };
+const info = repoInfo(repo.path);
+const log = repoLog(repo.path, 200, `/git/${repo.slug}/`);
+---
+<ArticleLayout title={`GIT - ${repo.name} - log`} description={`Commit log for ${
repo.name}.`}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`LOG: ${repo.name.toUpperCase()}`} date
={info.lastDate} />
+  <Pre>{" "}</Pre>
+  <Timeline entries={log} />
+  <Pre>{" "}</Pre>
+  <Pre><Button label="BACK TO REPO" href={`/git/${repo.slug}/`} /></Pre>
+  <ArticleFooter />
+</ArticleLayout>

diff --git a/ooknet-design/src/pages/git/[re
po]/log.astro b/ooknet-design/src/pages/git/
[repo]/log.astro
new file mode 100644
index 0000000..337ed4a
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/log
.astro
@@ -0,0 +1,31 @@
+---
+import ArticleLayout from "../../../layouts
/ArticleLayout/ArticleLayout.astro";
+import TopHeader from "../../../components/
TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../compone
nts/ArticleHeader/ArticleHeader.astro";
+import Pre from "../../../components/Pre/Pr
e.astro";
+import Timeline from "../../../components/T
imeline/Timeline.astro";
+import Button from "../../../components/But
ton/Button.astro";
+import ArticleFooter from "../../../compone
nts/ArticleFooter/ArticleFooter.astro";
+import { REPOS } from "../../../git.config"
;
+import { repoAvailable, repoInfo, repoLog, 
type GitRepo } from "../../../lib/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).map((r
) => ({
+    params: { repo: r.slug },
+    props: { repo: r },
+  }));
+}
+
+const { repo } = Astro.props as { repo: Git
Repo };
+const info = repoInfo(repo.path);
+const log = repoLog(repo.path, 200, `/git/$
{repo.slug}/`);
+---
+<ArticleLayout title={`GIT - ${repo.name} -
 log`} description={`Commit log for ${repo.n
ame}.`}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`
LOG: ${repo.name.toUpperCase()}`} date={info
.lastDate} />
+  <Pre>{" "}</Pre>
+  <Timeline entries={log} />
+  <Pre>{" "}</Pre>
+  <Pre><Button label="BACK TO REPO" href={`
/git/${repo.slug}/`} /></Pre>
+  <ArticleFooter />
+</ArticleLayout>
 

diff --git a/ooknet-design/src/pages/git/[repo]/tree/[...dir].astro b/ooknet-desig
n/src/pages/git/[repo]/tree/[...dir].astro
new file mode 100644
index 0000000..5ef4a03
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/tree/[...dir].astro
@@ -0,0 +1,42 @@
+---
+import ArticleLayout from "../../../../layouts/ArticleLayout/ArticleLayout.astro"
;
+import TopHeader from "../../../../components/TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../../components/ArticleHeader/ArticleHeader.ast
ro";
+import Frame from "../../../../components/Frame/Frame.astro";
+import Pre from "../../../../components/Pre/Pre.astro";
+import GitListing from "../../../../components/GitListing/GitListing.astro";
+import Button from "../../../../components/Button/Button.astro";
+import ArticleFooter from "../../../../components/ArticleFooter/ArticleFooter.ast
ro";
+import { row2, ruleD, toGrid } from "../../../../lib/ascii";
+import { REPOS } from "../../../../git.config";
+import { repoAvailable, repoFiles, repoDirs, dirEntries, type GitRepo } from "../
../../../lib/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).flatMap((r) =>
+    repoDirs(repoFiles(r.path)).map((dir) => ({
+      params: { repo: r.slug, dir },
+      props: { repo: r, dir },
+    })),
+  );
+}
+
+const { repo, dir } = Astro.props as { repo: GitRepo; dir: string };
+const entries = dirEntries(repo.path, dir);
+const parent = dir.includes("/") ? dir.slice(0, dir.lastIndexOf("/")) : "";
+const upHref = parent ? `/git/${repo.slug}/tree/${parent}/` : `/git/${repo.slug}/
`;
+
+const section = (w: number) =>
+  [row2("FILES", `/${toGrid(dir)}`.length > 40 ? "..." + `/${toGrid(dir)}`.slice(
-37) : `/${toGrid(dir)}`, w), ruleD(w)].join("\n");
+---
+<ArticleLayout title={`GIT - ${repo.name} - ${dir}`} description={`${dir} in ${re
po.name}.`}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`REPO: ${repo.name.toUpperCase()}`} />
+  <Pre>{" "}</Pre>
+  <Frame build={section} />
+  <Pre>{" "}</Pre>
+  <Pre><Button label="UP" href={upHref} /></Pre>
+  <Pre>{" "}</Pre>
+  <GitListing entries={entries} treeBase={`/git/${repo.slug}/tree/`} blobBase={`/
git/${repo.slug}/blob/`} commitBase={`/git/${repo.slug}/commit/`} />
+  <Pre>{" "}</Pre>
+  <ArticleFooter />
+</ArticleLayout>

diff --git a/ooknet-design/src/pages/git/[re
po]/tree/[...dir].astro b/ooknet-design/src/
pages/git/[repo]/tree/[...dir].astro
new file mode 100644
index 0000000..5ef4a03
--- /dev/null
+++ b/ooknet-design/src/pages/git/[repo]/tre
e/[...dir].astro
@@ -0,0 +1,42 @@
+---
+import ArticleLayout from "../../../../layo
uts/ArticleLayout/ArticleLayout.astro";
+import TopHeader from "../../../../componen
ts/TopHeader/TopHeader.astro";
+import ArticleHeader from "../../../../comp
onents/ArticleHeader/ArticleHeader.astro";
+import Frame from "../../../../components/F
rame/Frame.astro";
+import Pre from "../../../../components/Pre
/Pre.astro";
+import GitListing from "../../../../compone
nts/GitListing/GitListing.astro";
+import Button from "../../../../components/
Button/Button.astro";
+import ArticleFooter from "../../../../comp
onents/ArticleFooter/ArticleFooter.astro";
+import { row2, ruleD, toGrid } from "../../
../../lib/ascii";
+import { REPOS } from "../../../../git.conf
ig";
+import { repoAvailable, repoFiles, repoDirs
, dirEntries, type GitRepo } from "../../../
../lib/git";
+
+export function getStaticPaths() {
+  return REPOS.filter(repoAvailable).flatMa
p((r) =>
+    repoDirs(repoFiles(r.path)).map((dir) =
> ({
+      params: { repo: r.slug, dir },
+      props: { repo: r, dir },
+    })),
+  );
+}
+
+const { repo, dir } = Astro.props as { repo
: GitRepo; dir: string };
+const entries = dirEntries(repo.path, dir);
+const parent = dir.includes("/") ? dir.slic
e(0, dir.lastIndexOf("/")) : "";
+const upHref = parent ? `/git/${repo.slug}/
tree/${parent}/` : `/git/${repo.slug}/`;
+
+const section = (w: number) =>
+  [row2("FILES", `/${toGrid(dir)}`.length >
 40 ? "..." + `/${toGrid(dir)}`.slice(-37) :
 `/${toGrid(dir)}`, w), ruleD(w)].join("\n")
;
+---
+<ArticleLayout title={`GIT - ${repo.name} -
 ${dir}`} description={`${dir} in ${repo.nam
e}.`}>
+  <TopHeader />
+  <ArticleHeader left="OOKNET GIT" right={`
REPO: ${repo.name.toUpperCase()}`} />
+  <Pre>{" "}</Pre>
+  <Frame build={section} />
+  <Pre>{" "}</Pre>
+  <Pre><Button label="UP" href={upHref} /><
/Pre>
+  <Pre>{" "}</Pre>
+  <GitListing entries={entries} treeBase={`
/git/${repo.slug}/tree/`} blobBase={`/git/${
repo.slug}/blob/`} commitBase={`/git/${repo.
slug}/commit/`} />
+  <Pre>{" "}</Pre>
+  <ArticleFooter />
+</ArticleLayout>
 

diff --git a/ooknet-design/src/pages/git/index.astro b/ooknet-design/src/pages/git
/index.astro
new file mode 100644
index 0000000..d5b1cbe
--- /dev/null
+++ b/ooknet-design/src/pages/git/index.astro
@@ -0,0 +1,49 @@
+---
+import ArticleLayout from "../../layouts/ArticleLayout/ArticleLayout.astro";
+import TopHeader from "../../components/TopHeader/TopHeader.astro";
+import Frame from "../../components/Frame/Frame.astro";
+import Pre from "../../components/Pre/Pre.astro";
+import Panel from "../../components/Panel/Panel.astro";
+import Button from "../../components/Button/Button.astro";
+import ArticleFooter from "../../components/ArticleFooter/ArticleFooter.astro";
+import { kv, row2, ruleD } from "../../lib/ascii";
+import { REPOS } from "../../git.config";
+import { repoAvailable, repoInfo } from "../../lib/git";
+
+const repos = REPOS.filter(repoAvailable).map((r) => ({ ...r, info: repoInfo(r.pa
th) }));
+const KV_W = 38;
+---
+<ArticleLayout title="OOKNET KB - Git"
+  description="Source on file: the ooknet repositories, browsable in place.">
+  <TopHeader />
+  <Frame build={(w) => [row2("OOKNET GIT", `${repos.length} ON FILE`, w), ruleD(w
)].join("\n")} />
+  <Pre>{" "}</Pre>
+  {repos.map((r) => (
+    <Fragment>
+      <Panel title={r.name.toUpperCase()}>
+        <p class="repo-desc">{r.desc}</p>
+        <Pre>{kv("BRANCH", r.info.branch, 10, KV_W)}</Pre>
+        <Pre>{kv("COMMITS", String(r.info.commits), 10, KV_W)}</Pre>
+        <Pre>{kv("UPDATED", r.info.lastDate, 10, KV_W)}</Pre>
+        {r.clone && <p class="repo-clone">{r.clone}</p>}
+        <Pre><Button label="BROWSE" href={`/git/${r.slug}/`} />{"  "}<Button labe
l="LOG" href={`/git/${r.slug}/log/`} /></Pre>
+      </Panel>
+      <Pre>{" "}</Pre>
+    </Fragment>
+  ))}
+  <ArticleFooter />
+</ArticleLayout>
+
+<style lang="scss">
+  .repo-desc,
+  .repo-clone {
+    margin: 0 0 0.7em;
+    font-family: var(--mono);
+    font-size: var(--fs);
+    line-height: var(--lh);
+    overflow-wrap: anywhere;
+  }
+  .repo-clone {
+    color: var(--ink-soft);
+  }
+</style>

diff --git a/ooknet-design/src/pages/git/ind
ex.astro b/ooknet-design/src/pages/git/index
.astro
new file mode 100644
index 0000000..d5b1cbe
--- /dev/null
+++ b/ooknet-design/src/pages/git/index.astr
o
@@ -0,0 +1,49 @@
+---
+import ArticleLayout from "../../layouts/Ar
ticleLayout/ArticleLayout.astro";
+import TopHeader from "../../components/Top
Header/TopHeader.astro";
+import Frame from "../../components/Frame/F
rame.astro";
+import Pre from "../../components/Pre/Pre.a
stro";
+import Panel from "../../components/Panel/P
anel.astro";
+import Button from "../../components/Button
/Button.astro";
+import ArticleFooter from "../../components
/ArticleFooter/ArticleFooter.astro";
+import { kv, row2, ruleD } from "../../lib/
ascii";
+import { REPOS } from "../../git.config";
+import { repoAvailable, repoInfo } from "..
/../lib/git";
+
+const repos = REPOS.filter(repoAvailable).m
ap((r) => ({ ...r, info: repoInfo(r.path) })
);
+const KV_W = 38;
+---
+<ArticleLayout title="OOKNET KB - Git"
+  description="Source on file: the ooknet r
epositories, browsable in place.">
+  <TopHeader />
+  <Frame build={(w) => [row2("OOKNET GIT", 
`${repos.length} ON FILE`, w), ruleD(w)].joi
n("\n")} />
+  <Pre>{" "}</Pre>
+  {repos.map((r) => (
+    <Fragment>
+      <Panel title={r.name.toUpperCase()}>
+        <p class="repo-desc">{r.desc}</p>
+        <Pre>{kv("BRANCH", r.info.branch, 1
0, KV_W)}</Pre>
+        <Pre>{kv("COMMITS", String(r.info.c
ommits), 10, KV_W)}</Pre>
+        <Pre>{kv("UPDATED", r.info.lastDate
, 10, KV_W)}</Pre>
+        {r.clone && <p class="repo-clone">{
r.clone}</p>}
+        <Pre><Button label="BROWSE" href={`
/git/${r.slug}/`} />{"  "}<Button label="LOG
" href={`/git/${r.slug}/log/`} /></Pre>
+      </Panel>
+      <Pre>{" "}</Pre>
+    </Fragment>
+  ))}
+  <ArticleFooter />
+</ArticleLayout>
+
+<style lang="scss">
+  .repo-desc,
+  .repo-clone {
+    margin: 0 0 0.7em;
+    font-family: var(--mono);
+    font-size: var(--fs);
+    line-height: var(--lh);
+    overflow-wrap: anywhere;
+  }
+  .repo-clone {
+    color: var(--ink-soft);
+  }
+</style>
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET