Same tokens as style.css, the forge's three fonts (copied in when the guide is deployed), square corners, no dot grid, lowercase chrome.
- commit
- 150d28b4bf72a7bcd82c62569ea3879bfa881e7e
- parent
- 590fe7bb3cf907c66fe24c329cb83e29d9755888
- author
- cat <admin@dev.site>
- ci
- ✓ passed in 73s, signed by cat
- kind
- chore risk score 2.0 of 3, Jev's estimate
- review
- #13 A design system: tokens, three fonts, no caps, real controls
ci attestation
forge-attestation 1 type ci commit 150d28b4bf72a7bcd82c62569ea3879bfa881e7e tree ec8b00285589c0c236a9a0f1d72a85ab51a19ec3 script .forge/ci 426c1fc44b8183300deb721e9a5b028b89003788 result pass exit 0 started 2026-10-19T02:59:12Z duration 73s runner (not shown here; refs/notes/ci has it) system Darwin 25.5.0 arm64 env host log sha256:5b74131aba9a63a8bf5f3f7bc7c9f3a5a15701bbd82f257c643df05416b2a2bc 254 lines |pushed wip/i18n |89a4752 running .forge/ci: greet.sh: say what it is | | sh: /bin/sh | | ok greet |89a4752 pass in 0s, signed |pushed main |remote: review #4: /dami/greet/reviews/4/ |pushed wip/i18n |cc775ed running .forge/ci: wave at the world | | sh: /bin/sh | | FAIL greet : got 'hello, world o/', want 'hello, world' |cc775ed fail in 0s, signed |forge: 1 commit(s) did not pass |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |e4e1705 running .forge/ci: update the test for the wave | | sh: /bin/sh | | ok greet |e4e1705 pass in 0s, signed |remote: review #6: /dami/greet/reviews/6/ |pushed ana/wave |warning: You appear to have cloned an empty repository. |site home: deployed 1a8c372 -> /sites/ana/dotfiles/home/ |created ana/notes |remote: review #1: /ana/notes/reviews/1/ |pushed ana/todo |landing #1 A todo list: ana/todo -> main |6d7043e running .forge/ci: web, server and Python versions of greet | | sh: /bin/sh | | ok greet |6d7043e pass in 0s, signed |pushed main |b7b1d70 running .forge/ci: messages: notes for the later regions | | sh: /bin/sh | | ok greet |b7b1d70 pass in 0s, signed |remote: review #7: /dami/greet/reviews/7/ |pushed dami/regions |created dami/forge |warning: You appear to have cloned an empty repository. |site guide: mkdir -p guide/fonts && cp site/assets/fonts/*.woff2 guide/fonts/ |site guide: deployed d75a249 -> /sites/dami/forge/guide/ |(node:12757) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/dotfiles in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.15Vfwu/wt/demo/root/.forge/www |(node:12881) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built ana/notes in 0s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.15Vfwu/wt/demo/root/.forge/www |(node:13022) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/forge in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.15Vfwu/wt/demo/root/.forge/www |(node:13153) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built dami/greet in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.15Vfwu/wt/demo/root/.forge/www |(node:13822) ExperimentalWarning: stripTypeScriptTypes is an experimental feature and might change at any time |(Use `node --trace-warnings ...` to show where the warning was created) |built - in 1s -> /var/folders/b4/mv8w4c8j0zj0_mldppxdbdw00000gn/T/forge-ci.15Vfwu/wt/demo/root/.forge/www |233 of 233 pages render the same without 11ty |guide: 8 files, links ok |ok
Signature: good, cat
| M | .forge/ | 3 | + |
| M | .forge/ | 2 | +- |
| M | .gitignore | 1 | + |
| M | demo/ | 3 | +- |
| M | guide/ | 498 | +++++++++++++++++------------- |
| M | guide/ | 43 | +- |
| M | guide/ | 31 | +- |
@@ -81,7 +81,10 @@ ls $g/objects/pack/multi-pack-index-*.bitmap >/dev/null
8181 grep -q ' refs/forge/keep/' $g/packed-refs8282 git --git-dir=$g range-diff --no-color $(awk '$1 == "rev" { r = p; p = $5 ".." $3 } END { print r, p }' $g/forge/reviews/4) | grep -q .8383 # The guide: there, deployed in the demo, and no broken internal links.84+# Its fonts are the forge's, copied in when it is deployed (.forge/sites).85+mkdir -p guide/fonts && cp site/assets/fonts/*.woff2 guide/fonts/8486 node lib/check-links.ts guide87+rm -rf guide/fonts8588 test -f "$w/sites/dami/forge/guide/index.html"8689 # Backups: stage the demo forge the way forge backup does, restore that into8790 # an empty root, and check the forge comes back whole: hooks, keys, refs,@@ -504,12 +504,13 @@ commit "messages: notes for the later regions" "Long lines on purpose: the revie
504504 "$forge" attest505505 "$forge" push -t "Regional notes"506506 507-step "dami: forge's own guide, a site with no build step"507+step "dami: forge's own guide, a site whose build copies in the forge's fonts"508508 export FORGE_USER=dami509509 "$forge" repo new dami/forge "a personal git forge"510510 clone dami dami/forge "$work/forge"511511 cd "$work/forge"512512 cp -R "$app/guide" "$app/README.md" . && mkdir .forge && cp "$app/.forge/sites" .forge/513+mkdir -p site/assets/fonts && cp "$app"/site/assets/fonts/*.woff2 site/assets/fonts/513514 commit "the guide"514515 git push -q origin main515516 "$forge" site deploy guide@@ -1,126 +1,177 @@
1-/* The forge guide: one stylesheet, light and dark, phone first. The palette2- is the forge's own (site/assets/style.css); the page reads like a git log,3- with each section a commit on one line. */1+/* The forge guide: one stylesheet, light and dark, phone first. The page2+ reads like a git log, with each section a commit on one line.3+4+ The palette, the fonts and the rules are the forge's own5+ (site/assets/style.css, DESIGN.md), and lib/check-css.ts holds this file6+ to them: a plain page, square corners, content between 1px rules rather7+ than in boxes; Open Sans for the interface, Noto Sans Mono for what is8+ copied or compared, Noto Serif for the prose; hierarchy from weight, size9+ and colour, never capitals or tracking; one accent (#3148F6) for links,10+ the rail, the current entry and the primary button. */11+12+/* == tokens == Every colour, size, weight and family is here; nothing13+ below this block names one. */14+/* The forge's fonts, copied into fonts/ when the guide is deployed15+ (.forge/sites). The sans is preloaded. */16+@font-face { font-family: "Open Sans"; src: url(fonts/open-sans.woff2) format("woff2"); font-weight: 300 800; font-style: normal; font-display: swap; }17+@font-face { font-family: "Noto Sans Mono"; src: url(fonts/noto-sans-mono.woff2) format("woff2"); font-weight: 400 700; font-style: normal; font-display: swap; }18+@font-face { font-family: "Noto Serif"; src: url(fonts/noto-serif.woff2) format("woff2"); font-weight: 400 700; font-style: normal; font-display: swap; }19+@font-face { font-family: "Noto Serif"; src: url(fonts/noto-serif-italic.woff2) format("woff2"); font-weight: 400 700; font-style: italic; font-display: swap; }420 :root {5- --bg: #ffffff;6- --fg: #1f2328;7- --muted: #59636e;8- --faint: #8c959f;9- --border: #d1d9e0;10- --subtle: #f6f8fa;11- --hover: #eef1f4;12- --link: #0969da;13- --accent: #2ca9bc;14- --ink: #0e7482;15- --tint: rgba(44, 169, 188, 0.1);16- --grid: rgba(31, 35, 40, 0.09);17- --pass: #1a7f37;18- --fail: #cf222e;19- --warn: #9a6700;20- --merged: #8250df;21- --add: #116329;22- --del: #a40e26;23- --term-bg: #0d1117;24- --term-fg: #e6edf3;25- --term-dim: #8b949e;26- --serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Charter, "Bitstream Charter", Georgia, serif;27- --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;28- --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;29- --radius: 8px;21+ /* colour: the page, surfaces raised over it, ink in three strengths,22+ rules, and the accent */23+ --bg: #e9e9e9;24+ --raised: #ffffff;25+ --hover: #f4f4f4;26+ --fg: #1c2127;27+ --muted: #5b6169;28+ --faint: #7c828a;29+ --border: #d2d4d7;30+ --border-strong: #a9aeb4;31+ --link: #3148f6;32+ --accent: #3148f6;33+ --deep: #1c4b8f;34+ --accent-hover: #1c4b8f;35+ --on-accent: #ffffff;36+ /* meaning: CI results, diffs, a changed line */37+ --pass: #0b7a1d;38+ --fail: #c7162b;39+ --warn: #965800;40+ --add: #0b7a1d;41+ --del: #c7162b;42+ /* the terminal in the hero is dark in both modes */43+ --term-bg: #08090b;44+ --term-fg: #e2e4e7;45+ --term-dim: #8f959d;46+ --term-ok: #4fbf5f;47+ --term-u: #8e9bff;48+ --term-rule: #1f2329;49+50+ /* type: the interface, what is copied or compared, and prose */51+ --sans: "Open Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;52+ --mono: "Noto Sans Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;53+ --serif: "Noto Serif", Georgia, Cambria, "Times New Roman", serif;54+ --text-xs: 12px;55+ --text-code: 12.5px;56+ --text-sm: 13.5px;57+ --text-md: 15px;58+ --text-prose: 17px;59+ --text-lg: 18px;60+ --text-xl: 22px;61+ --text-2xl: 30px;62+ --text-3xl: clamp(2.4rem, 9vw, 4rem);63+ /* mono inline in serif text runs large; this brings it level */64+ --text-inline-mono: 0.88em;65+ --fw-regular: 400;66+ --fw-medium: 500;67+ --fw-strong: 650;68+69+ /* space: a 4px scale */70+ --space-half: 2px;71+ --space-1: 4px;72+ --space-2: 8px;73+ --space-3: 12px;74+ --space-4: 16px;75+ --space-5: 20px;76+ --space-6: 24px;77+ --space-8: 32px;78+ --space-10: 40px;79+ --hairline: 1px;80+ --bar: 3px;81+ --radius: 0;82+ /* the log's rail: where it runs, and how far the sections sit from it */3083 --rail-x: 7px;3184 --gutter: 30px;3285 color-scheme: light dark;3386 }3487 @media (prefers-color-scheme: dark) {3588 :root {36- --bg: #0d1117;37- --fg: #e6edf3;38- --muted: #9198a1;39- --faint: #6e7681;40- --border: #30363d;41- --subtle: #151b23;42- --hover: #1c232c;43- --link: #4493f8;44- --ink: #3cc0d3;45- --tint: rgba(44, 169, 188, 0.14);46- --grid: rgba(230, 237, 243, 0.07);47- --pass: #3fb950;48- --fail: #ff7b72;49- --warn: #d29922;50- --merged: #ab7df8;51- --add: #3fb950;52- --del: #ff7b72;53- --term-bg: #010409;89+ --bg: #08090b;90+ --raised: #111418;91+ --hover: #15181d;92+ --fg: #e2e4e7;93+ --muted: #8f959d;94+ --faint: #6b7178;95+ --border: #1f2329;96+ --border-strong: #3a4048;97+ --link: #8e9bff;98+ --deep: #b4bcff;99+ --accent-hover: #2a3ee0;100+ --pass: #4fbf5f;101+ --fail: #f0606e;102+ --warn: #e3a33b;103+ --add: #4fbf5f;104+ --del: #f0606e;54105 }55106 }107+/* == end tokens == */56108 57-* { box-sizing: border-box; }58-html { -webkit-text-size-adjust: 100%; scroll-padding-top: 16px; }109+*, ::before, ::after { box-sizing: border-box; border-radius: var(--radius); }110+html { -webkit-text-size-adjust: 100%; scroll-padding-top: var(--space-4); }59111 @media (prefers-reduced-motion: no-preference) { html { scroll-behavior: smooth; } }60-body { margin: 0; background: var(--bg); color: var(--fg); font: 1.0625rem/1.65 var(--serif); font-kerning: normal; text-rendering: optimizeLegibility; }112+body { margin: 0; background: var(--bg); color: var(--fg); font: var(--fw-regular) var(--text-prose)/1.65 var(--serif); font-kerning: normal; text-rendering: optimizeLegibility; }61113 a { color: var(--link); text-underline-offset: 0.18em; text-decoration-thickness: 1px; }62-a:hover { text-decoration-thickness: 2px; }63-:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }114+a:hover { color: var(--deep); text-decoration-thickness: 2px; }115+:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }64116 code, kbd, pre { font-family: var(--mono); }65-code { font-size: 0.84em; background: var(--subtle); border: 1px solid var(--border); border-radius: 5px; padding: 0.05em 0.32em; white-space: nowrap; }66-pre code, .plain code { background: none; border: 0; padding: 0; font-size: inherit; white-space: inherit; }67-kbd { font-size: 0.8em; border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 4px; padding: 0 0.35em; }68-h1, h2, h3 { font-weight: 600; line-height: 1.15; letter-spacing: -0.01em; }69-h3 { font-size: 1.2rem; margin: 2rem 0 0.5rem; }70-p, ul, ol, dl { margin: 0 0 1rem; }71-strong { font-weight: 650; }72-.wrap { max-width: 1180px; margin: 0 auto; padding: 0 18px; }73-.skip { position: absolute; left: -999px; top: 8px; z-index: 5; background: var(--bg); padding: 6px 10px; border: 1px solid var(--border); }74-.skip:focus { left: 8px; }117+code { font-size: var(--text-inline-mono); white-space: nowrap; }118+pre code, .plain code { font-size: inherit; white-space: inherit; }119+kbd { font-size: var(--text-inline-mono); color: var(--muted); background: var(--raised); border: var(--hairline) solid var(--border-strong); padding: 0 var(--space-1); }120+h1, h2, h3 { font-family: var(--sans); font-weight: var(--fw-strong); line-height: 1.2; }121+h3 { font-size: var(--text-lg); margin: var(--space-8) 0 var(--space-2); }122+p, ul, ol, dl { margin: 0 0 var(--space-4); }123+b, strong { font-weight: var(--fw-strong); }124+.wrap { max-width: 1180px; margin: 0 auto; padding: 0 var(--space-4); }125+.skip { position: absolute; left: -999px; top: var(--space-2); z-index: 5; background: var(--raised); padding: var(--space-1) var(--space-3); border: var(--hairline) solid var(--border-strong); font-family: var(--sans); font-size: var(--text-sm); }126+.skip:focus { left: var(--space-2); }75127 .mono { font-family: var(--mono); }76128 .muted { color: var(--muted); }77-78-/* top bar */79-.bar { border-bottom: 1px solid var(--border); font-family: var(--sans); font-size: 0.92rem; }80-.bar .wrap { display: flex; align-items: center; gap: 14px; height: 52px; }81-.brand { display: flex; align-items: center; gap: 9px; color: var(--fg); text-decoration: none; font: 600 1rem var(--mono); }82-.brand span { color: var(--muted); font-weight: 400; }129+/* the logo: the ring in ink, the A in the accent */83130 .mark { display: block; width: 26px; height: 26px; flex: none; }84131 .mark circle { stroke: currentColor; }85-.bar nav { margin-left: auto; display: flex; gap: 4px; }86-.bar nav a { color: var(--muted); text-decoration: none; padding: 6px 10px; border-radius: 6px; }87-.bar nav a:hover { background: var(--hover); color: var(--fg); }88-.bar nav a[aria-current="page"] { color: var(--fg); box-shadow: inset 0 -2px 0 var(--accent); border-radius: 0; }132+.mark path { stroke: var(--accent); }133+134+/* top bar: the name, and the two pages as tabs on the rule */135+.bar { border-bottom: var(--hairline) solid var(--border); font-family: var(--sans); }136+.bar .wrap { display: flex; align-items: center; gap: var(--space-3); height: 52px; }137+.brand { display: flex; align-items: center; gap: var(--space-2); color: var(--fg); text-decoration: none; font-size: var(--text-lg); font-weight: var(--fw-strong); }138+.brand:hover { color: var(--fg); text-decoration: none; }139+.brand span { color: var(--muted); font-weight: var(--fw-regular); }140+.bar nav { margin-left: auto; display: flex; gap: var(--space-6); align-self: stretch; }141+.bar nav a { display: flex; align-items: center; color: var(--muted); text-decoration: none; font-size: var(--text-md); font-weight: var(--fw-medium); }142+.bar nav a:hover { color: var(--fg); }143+.bar nav a[aria-current="page"] { color: var(--fg); box-shadow: inset 0 calc(-1 * var(--bar)) 0 var(--accent); }89144 90145 /* hero */91-.hero { position: relative; overflow: hidden; padding: 44px 0 36px; border-bottom: 1px solid var(--border); }92-.hero::before { content: ""; position: absolute; inset: 0; background-image: radial-gradient(var(--grid) 1.1px, transparent 1.4px); background-size: 22px 22px; -webkit-mask-image: linear-gradient(to bottom, #000 30%, transparent 95%); mask-image: linear-gradient(to bottom, #000 30%, transparent 95%); pointer-events: none; }93-.hero::after { content: ""; position: absolute; width: 340px; height: 340px; right: -190px; top: -170px; border: 2px solid var(--accent); border-radius: 50%; opacity: 0.22; pointer-events: none; }94-.hero .wrap { position: relative; display: grid; gap: 30px; }95-.eyebrow, .ref, figcaption, .label { font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.02em; }96-.eyebrow { color: var(--ink); margin: 0 0 10px; }97-.hero h1 { display: flex; align-items: center; gap: 0.18em; margin: 0 0 16px; font: 700 clamp(3.4rem, 15vw, 6.2rem)/0.95 var(--mono); letter-spacing: -0.055em; }98-.hero h1 .mark { width: 0.86em; height: 0.86em; stroke-width: 2.2; }99-.lede { font-size: 1.28rem; line-height: 1.5; max-width: 34rem; margin: 0 0 22px; }146+.hero { padding: var(--space-10) 0 var(--space-8); border-bottom: var(--hairline) solid var(--border); }147+.hero .wrap { display: grid; gap: var(--space-8); }148+.eyebrow { font-family: var(--sans); font-size: var(--text-sm); font-weight: var(--fw-medium); color: var(--muted); margin: 0 0 var(--space-2); }149+.hero h1 { display: flex; align-items: center; gap: 0.2em; margin: 0 0 var(--space-4); font-size: var(--text-3xl); line-height: 1; }150+.hero h1 .mark { width: 0.9em; height: 0.9em; }151+.lede { font-size: var(--text-lg); line-height: 1.5; max-width: 34rem; margin: 0 0 var(--space-6); }152+@media (min-width: 720px) { .lede { font-size: var(--text-xl); } }100153 .lede em { font-style: italic; }101-.cta { display: flex; flex-wrap: wrap; gap: 10px; margin: 0; font-family: var(--sans); font-size: 0.95rem; }102-.btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border-radius: 999px; border: 1px solid var(--border); background: var(--bg); color: var(--fg); text-decoration: none; }103-.btn:hover { border-color: var(--accent); }104-.btn.go { background: var(--fg); color: var(--bg); border-color: var(--fg); }105-.btn.go:hover { background: var(--ink); border-color: var(--ink); }154+.cta { display: flex; flex-wrap: wrap; gap: var(--space-2); margin: 0; }155+/* buttons, as the forge's: secondary is a raised surface, primary is156+ filled with the accent */157+.btn { display: inline-flex; align-items: center; min-height: 36px; padding: var(--space-1) var(--space-4); font: var(--fw-medium) var(--text-sm)/1.2 var(--sans); color: var(--fg); background: var(--raised); border: var(--hairline) solid var(--border-strong); text-decoration: none; }158+.btn:hover { color: var(--fg); border-color: var(--muted); text-decoration: none; }159+.btn.go { color: var(--on-accent); background: var(--accent); border-color: var(--accent); }160+.btn.go:hover { color: var(--on-accent); background: var(--accent-hover); border-color: var(--accent-hover); }106161 @media (min-width: 900px) {107162 .hero { padding: 72px 0 64px; }108- .hero::after { width: 560px; height: 560px; right: -220px; top: -250px; }109163 .hero .wrap { grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); align-items: center; gap: 48px; }110164 }111165 112166 /* terminal */113-.term { margin: 0; border-radius: 10px; background: var(--term-bg); color: var(--term-fg); border: 1px solid #30363d; box-shadow: 0 18px 50px rgba(1, 4, 9, 0.18); overflow: hidden; min-width: 0; }114-.term .chrome { display: flex; align-items: center; gap: 6px; padding: 9px 12px; border-bottom: 1px solid #21262d; font: 0.72rem var(--mono); color: var(--term-dim); }115-.term .chrome i { width: 9px; height: 9px; border-radius: 50%; background: #30363d; }116-.term .chrome span { margin-left: 6px; }117-.term pre { margin: 0; padding: 14px 16px 16px; font-size: 0.78rem; line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }167+.term { margin: 0; background: var(--term-bg); color: var(--term-fg); border: var(--hairline) solid var(--term-rule); overflow: hidden; min-width: 0; }168+.term .chrome { padding: var(--space-2) var(--space-4); border-bottom: var(--hairline) solid var(--term-rule); font: var(--fw-regular) var(--text-xs) var(--mono); color: var(--term-dim); }169+.term pre { margin: 0; padding: var(--space-3) var(--space-4) var(--space-4); font-size: var(--text-code); line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }118170 .term .line { display: block; }119-.term .p { color: var(--accent); user-select: none; -webkit-user-select: none; }120-.term .o { color: var(--term-dim); }121-.term .r { color: #7d8590; }122-.term .ok { color: #3fb950; }123-.term .u { color: #79c0ff; }171+.term .p { color: var(--term-u); user-select: none; -webkit-user-select: none; }172+.term .o, .term .r { color: var(--term-dim); }173+.term .ok { color: var(--term-ok); }174+.term .u { color: var(--term-u); }124175 @media (prefers-reduced-motion: no-preference) {125176 .hero-text > *, .term { animation: rise 0.6s cubic-bezier(0.2, 0.7, 0.2, 1) both; }126177 .hero-text > :nth-child(2) { animation-delay: 0.06s; }@@ -132,59 +183,63 @@ strong { font-weight: 650; }
132183 }133184 134185 /* page: contents, then the log of sections */135-.page { display: grid; gap: 8px; padding-top: 28px; padding-bottom: 40px; }136-.toc { font: 0.85rem/1.4 var(--mono); }137-.toc h2 { font: 600 0.72rem var(--mono); text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin: 0 0 8px; }138-.toc ol { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: 2px 12px; }139-.toc a { display: flex; gap: 8px; padding: 4px 0; color: var(--fg); text-decoration: none; }140-.toc a span { color: var(--muted); }141-.toc a:hover { color: var(--ink); }142-.toc a[aria-current="true"] { color: var(--ink); }143-.toc a[aria-current="true"] span { color: var(--accent); }186+.page { display: grid; gap: var(--space-2); padding-top: var(--space-6); padding-bottom: var(--space-10); }187+.toc { font-family: var(--sans); font-size: var(--text-sm); line-height: 1.4; }188+.toc h2 { font-size: var(--text-xs); font-weight: var(--fw-strong); color: var(--muted); margin: 0 0 var(--space-2); }189+.toc ol { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-half) var(--space-3); }190+.toc a { display: flex; gap: var(--space-2); padding: var(--space-1) 0; color: var(--muted); text-decoration: none; }191+.toc a span { color: var(--faint); font-variant-numeric: tabular-nums; }192+.toc a:hover { color: var(--fg); }193+.toc a[aria-current="true"] { color: var(--fg); font-weight: var(--fw-medium); }194+.toc a[aria-current="true"] span { color: var(--link); }144195 @media (min-width: 1000px) {145196 .page { grid-template-columns: 200px minmax(0, 1fr); gap: 48px; padding-top: 44px; }146- .toc > div { position: sticky; top: 24px; }147- .toc ol { grid-template-columns: 1fr; }148- .toc a { padding: 5px 0 5px 12px; border-left: 2px solid var(--border); }197+ .toc > div { position: sticky; top: var(--space-6); }198+ .toc ol { grid-template-columns: 1fr; gap: 0; }199+ .toc a { padding: var(--space-1) 0 var(--space-1) var(--space-3); border-left: 2px solid var(--border); }149200 .toc a[aria-current="true"] { border-left-color: var(--accent); }150201 }151202 152203 .log { position: relative; padding-left: var(--gutter); min-width: 0; }153-.log::before { content: ""; position: absolute; left: var(--rail-x); top: 10px; bottom: 0; width: 2px; background: linear-gradient(var(--accent), var(--accent) calc(100% - 120px), transparent); opacity: 0.85; }154-.commit { position: relative; padding: 18px 0 30px; }155-.commit > header { position: relative; margin-bottom: 18px; }156-.commit > header::before { content: ""; position: absolute; left: calc(var(--rail-x) - var(--gutter) - 6px); top: 1px; width: 10px; height: 10px; border-radius: 50%; background: var(--bg); border: 2px solid var(--accent); }204+.log::before { content: ""; position: absolute; left: var(--rail-x); top: 10px; bottom: 0; width: 2px; background: linear-gradient(var(--accent), var(--accent) calc(100% - 120px), transparent); }205+.commit { position: relative; padding: var(--space-4) 0 var(--space-8); }206+.commit > header { position: relative; margin-bottom: var(--space-4); }207+/* each section's commit: a small square on the rail, filled when it is208+ the one linked to */209+.commit > header::before { content: ""; position: absolute; left: calc(var(--rail-x) - var(--gutter) - 6px); top: 3px; width: 10px; height: 10px; background: var(--bg); border: 2px solid var(--accent); }157210 .commit:target > header::before, .commit > header:hover::before { background: var(--accent); }158-.ref { margin: 0 0 6px; color: var(--ink); display: flex; gap: 8px; align-items: baseline; }159-.ref b { font-weight: 600; color: var(--muted); }160-.commit h2 { font-size: clamp(1.65rem, 5.2vw, 2.3rem); margin: 0; max-width: 34ch; }211+.ref { margin: 0 0 var(--space-1); display: flex; gap: var(--space-2); align-items: baseline; font-family: var(--mono); font-size: var(--text-xs); color: var(--muted); }212+.ref b { font-weight: var(--fw-medium); color: var(--faint); }213+.commit h2 { font-size: var(--text-xl); margin: 0; max-width: 34ch; }161214 .commit h2 a { color: inherit; text-decoration: none; }215+.commit h2 a:hover { color: var(--deep); }162216 .prose { max-width: 41rem; }163-.prose li { margin: 0.25rem 0; }217+.prose li { margin: var(--space-1) 0; }164218 .prose ul { padding-left: 1.2rem; }165-.aside { margin-top: 16px; font-family: var(--sans); font-size: 0.9rem; color: var(--muted); border-left: 3px solid var(--border); padding: 2px 0 2px 12px; max-width: 41rem; }219+.aside { margin-top: var(--space-4); font-family: var(--sans); font-size: var(--text-sm); color: var(--muted); border-left: var(--bar) solid var(--border); padding: var(--space-half) 0 var(--space-half) var(--space-3); max-width: 41rem; }166220 @media (min-width: 720px) {167221 :root { --gutter: 44px; --rail-x: 9px; }168- .commit { padding: 26px 0 44px; }222+ .commit { padding: var(--space-6) 0 44px; }223+ .commit h2 { font-size: var(--text-2xl); }169224 }170225 171-/* figures */172-figure { margin: 22px 0 26px; }173-.plate { border: 1px solid var(--border); border-radius: var(--radius); background: var(--subtle); padding: 16px; overflow: hidden; }174-figcaption { color: var(--muted); margin-top: 10px; }175-figcaption b { color: var(--ink); font-weight: 600; margin-right: 6px; }176-@media (min-width: 720px) { .plate { padding: 22px 24px; } }226+/* figures: a raised plate on a hairline, a caption under it */227+figure { margin: var(--space-5) 0 var(--space-6); }228+.plate { border: var(--hairline) solid var(--border); background: var(--raised); padding: var(--space-4); overflow: hidden; }229+figcaption { font-family: var(--sans); font-size: var(--text-sm); color: var(--muted); margin-top: var(--space-2); }230+figcaption b { color: var(--fg); font-weight: var(--fw-strong); margin-right: var(--space-2); }231+@media (min-width: 720px) { .plate { padding: 22px var(--space-6); } }177232 178233 /* fig: three parents merged into forge */179234 .merge .parents { list-style: none; margin: 0; padding: 0 0 0 26px; display: grid; gap: 10px; position: relative; }180235 .merge .parents::before { content: ""; position: absolute; left: 6px; top: 18px; bottom: -26px; border-left: 2px solid var(--accent); }181-.merge .parents li { position: relative; background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; }236+.merge .parents li { position: relative; background: var(--raised); border: var(--hairline) solid var(--border-strong); padding: 10px var(--space-3); }182237 .merge .parents li::before { content: ""; position: absolute; left: -21px; top: 16px; width: 20px; border-top: 2px solid var(--accent); }183-.merge .parents b { display: block; font: 600 0.9rem var(--mono); }184-.merge .parents span { display: block; font-size: 0.95rem; color: var(--muted); line-height: 1.45; margin-top: 2px; }185-.merge .child { position: relative; margin: 14px 0 0; display: flex; align-items: center; gap: 10px; padding-left: 0; font: 700 1.15rem var(--mono); letter-spacing: -0.02em; }186-.merge .child .mark { width: 20px; height: 20px; background: var(--subtle); border-radius: 50%; position: relative; }187-.merge .child small { font: 0.78rem var(--mono); color: var(--muted); letter-spacing: 0; font-weight: 400; }238+.merge .parents b { display: block; font-family: var(--sans); font-size: var(--text-md); font-weight: var(--fw-strong); }239+.merge .parents span { display: block; font-size: var(--text-md); color: var(--muted); line-height: 1.5; margin-top: var(--space-half); }240+.merge .child { position: relative; margin: 14px 0 0; display: flex; align-items: center; gap: 10px; font-family: var(--sans); font-size: var(--text-lg); font-weight: var(--fw-strong); }241+.merge .child .mark { width: 22px; height: 22px; background: var(--raised); position: relative; }242+.merge .child small { font-size: var(--text-sm); font-weight: var(--fw-regular); color: var(--muted); }188243 @media (min-width: 720px) {189244 .merge .parents { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 14px; padding: 0 0 28px; }190245 .merge .parents::before { left: calc((100% - 28px) / 6); right: calc((100% - 28px) / 6); top: auto; bottom: 0; border-left: 0; border-top: 2px solid var(--accent); }@@ -196,38 +251,39 @@ figcaption b { color: var(--ink); font-weight: 600; margin-right: 6px; }
196251 /* fig: the push, as a trace down one line */197252 .flow { list-style: none; margin: 0; padding: 0; position: relative; --x: 6px; }198253 .flow::before { content: ""; position: absolute; left: var(--x); top: 8px; bottom: 14px; width: 2px; background: var(--border); }199-.flow > li { position: relative; padding: 0 0 16px 26px; }200-.flow > li::before { content: ""; position: absolute; left: calc(var(--x) - 5px); top: 7px; width: 8px; height: 8px; border-radius: 50%; background: var(--subtle); border: 2px solid var(--accent); }201-.flow .where { display: block; font: 600 0.8rem var(--mono); color: var(--ink); }202-.flow p { margin: 2px 0 0; font-size: 0.98rem; }203-.flow .src { font: 0.72rem var(--mono); color: var(--muted); margin-top: 4px; }204-.flow ul { margin: 6px 0 0; padding: 0; list-style: none; font-size: 0.95rem; }205-.flow ul li { position: relative; padding-left: 16px; margin: 3px 0; }206-.flow ul li::before { content: ""; position: absolute; left: 0; top: 0.75em; width: 10px; border-top: 2px solid var(--accent); opacity: 0.6; }254+.flow > li { position: relative; padding: 0 0 var(--space-4) 26px; }255+.flow > li::before { content: ""; position: absolute; left: calc(var(--x) - 5px); top: 7px; width: 8px; height: 8px; background: var(--raised); border: 2px solid var(--accent); }256+.flow .where { display: block; font-family: var(--mono); font-size: var(--text-code); font-weight: var(--fw-strong); color: var(--fg); }257+.flow p { margin: var(--space-half) 0 0; font-size: var(--text-md); }258+.flow .src { font-family: var(--mono); font-size: var(--text-xs); color: var(--muted); margin-top: var(--space-1); }259+.flow ul { margin: 6px 0 0; padding: 0; list-style: none; font-size: var(--text-md); }260+.flow ul li { position: relative; padding-left: var(--space-4); margin: 3px 0; }261+.flow ul li::before { content: ""; position: absolute; left: 0; top: 0.75em; width: 10px; border-top: 2px solid var(--accent); }207262 .flow .returns { padding-bottom: 18px; }208263 .flow .returns::before { display: none; }209-.flow .returns span { display: block; border-top: 1px dashed var(--faint); padding-top: 6px; font: 0.75rem var(--mono); color: var(--muted); }264+.flow .returns span { display: block; border-top: var(--hairline) dashed var(--border-strong); padding-top: 6px; font-family: var(--sans); font-size: var(--text-sm); color: var(--muted); }210265 @media (min-width: 720px) {211266 .flow { --x: 148px; }212267 .flow > li { display: grid; grid-template-columns: 128px 1fr; gap: 0 40px; padding-left: 0; }213- .flow .where { text-align: right; padding-top: 1px; }268+ .flow .where { text-align: right; padding-top: 3px; }214269 .flow .returns { grid-template-columns: 1fr; padding-left: 168px; }215270 }216271 217272 /* fig: a stack of reviews, as a graph */218-.graph { list-style: none; margin: 0; padding: 0; font-size: 0.95rem; }273+.graph { list-style: none; margin: 0; padding: 0; font-size: var(--text-md); }219274 .graph ol { list-style: none; margin: 0; padding: 0; }220-.graph .layer { position: relative; display: grid; gap: 4px 18px; padding: 6px 0; }275+.graph .layer { position: relative; display: grid; gap: var(--space-1) 18px; padding: 6px 0; }221276 .graph .layer::before { content: ""; position: absolute; left: 6px; top: 0; bottom: 0; width: 2px; background: var(--accent); }222277 .graph > .layer:first-child::before { top: 1.3em; }223278 .graph .base::before { background: var(--faint); bottom: auto; height: 1.3em; }224-.graph .c { position: relative; padding: 5px 0 5px 28px; display: flex; flex-wrap: wrap; gap: 2px 8px; align-items: baseline; }225-.graph .c::after { content: ""; position: absolute; left: 1px; top: 0.85em; width: 8px; height: 8px; border-radius: 50%; background: var(--subtle); border: 2px solid var(--accent); }279+.graph .c { position: relative; padding: 5px 0 5px 28px; display: flex; flex-wrap: wrap; gap: var(--space-half) var(--space-2); align-items: baseline; }280+.graph .c::after { content: ""; position: absolute; left: 1px; top: 0.85em; width: 8px; height: 8px; background: var(--raised); border: 2px solid var(--accent); }226281 .graph .base .c::after { border-color: var(--faint); }227-.graph code { font-size: 0.78rem; color: var(--muted); background: none; border: 0; padding: 0; }228-.pill { font: 0.72rem var(--mono); padding: 1px 7px; border-radius: 999px; border: 1px solid var(--accent); color: var(--ink); background: var(--tint); white-space: nowrap; }229-.pill.main { border-color: var(--faint); color: var(--muted); background: none; }230-.graph .tag { margin: 0 0 4px 28px; font-family: var(--sans); font-size: 0.85rem; color: var(--muted); border-left: 2px solid var(--border); padding: 2px 0 2px 10px; }282+.graph code { font-size: var(--text-xs); color: var(--muted); }283+/* a branch name on a commit: a square label on a hairline */284+.pill { font-family: var(--mono); font-size: var(--text-xs); padding: 0 6px; border: var(--hairline) solid var(--accent); color: var(--link); white-space: nowrap; }285+.pill.main { border-color: var(--border-strong); color: var(--muted); }286+.graph .tag { margin: 0 0 var(--space-1) 28px; font-family: var(--sans); font-size: var(--text-sm); color: var(--muted); border-left: 2px solid var(--border); padding: var(--space-half) 0 var(--space-half) 10px; }231287 .graph .tag b { display: block; color: var(--fg); }232288 @media (min-width: 720px) {233289 .graph .layer { grid-template-columns: minmax(0, 1fr) 210px; align-items: center; }@@ -235,44 +291,42 @@ figcaption b { color: var(--ink); font-weight: 600; margin-right: 6px; }
235291 }236292 237293 /* code-ish figures: range-diff, statements */238-.code { margin: 0; font: 0.76rem/1.55 var(--mono); white-space: pre; overflow-x: auto; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 12px 14px; tab-size: 4; }239-.code .same { color: var(--muted); }240-.code .chg { color: var(--warn); font-weight: 600; }294+.code { margin: 0; font-size: var(--text-code); line-height: 1.55; white-space: pre; overflow-x: auto; background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-3) 14px; tab-size: 4; }295+.code .same, .code .dim { color: var(--muted); }296+.code .chg { color: var(--warn); font-weight: var(--fw-strong); }241297 .code .add { color: var(--add); }242298 .code .del { color: var(--del); }243-.code .dim { color: var(--muted); }244-.code .key { color: var(--ink); }245-.code .n { display: inline-block; min-width: 1.5em; margin-left: 8px; padding: 0 5px; border-radius: 999px; background: var(--accent); color: #fff; font-size: 0.68rem; font-weight: 700; text-align: center; line-height: 1.5; }246-@media (prefers-color-scheme: dark) { .code .n { color: #0d1117; } }299+.code .key { color: var(--link); }300+/* numbered markers, square, white on the accent in both modes */301+.code .n { display: inline-block; min-width: 1.6em; margin-left: var(--space-2); padding: 0 var(--space-1); background: var(--accent); color: var(--on-accent); font-size: var(--text-xs); font-weight: var(--fw-strong); text-align: center; line-height: 1.5; }247302 248-.annotated { display: grid; gap: 16px; }249-.annotated ol { margin: 0; padding: 0; list-style: none; counter-reset: n; font-size: 0.95rem; }250-.annotated ol li { counter-increment: n; position: relative; padding-left: 30px; margin: 0 0 8px; }251-.annotated ol li::before { content: counter(n); position: absolute; left: 0; top: 0.2em; min-width: 1.5em; padding: 0 5px; border-radius: 999px; background: var(--accent); color: #fff; font: 700 0.68rem/1.5 var(--mono); text-align: center; }252-@media (prefers-color-scheme: dark) { .annotated ol li::before { color: #0d1117; } }303+.annotated { display: grid; gap: var(--space-4); }304+.annotated ol { margin: 0; padding: 0; list-style: none; counter-reset: n; font-size: var(--text-md); }305+.annotated ol li { counter-increment: n; position: relative; padding-left: 30px; margin: 0 0 var(--space-2); }306+.annotated ol li::before { content: counter(n); position: absolute; left: 0; top: 0.25em; min-width: 1.6em; padding: 0 var(--space-1); background: var(--accent); color: var(--on-accent); font: var(--fw-strong) var(--text-xs)/1.5 var(--mono); text-align: center; }253307 .annotated figcaption { grid-column: 1 / -1; margin: 0; }254308 @media (min-width: 900px) { .annotated { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); align-items: start; } }255309 256-/* proves / doesn't */257-.verdict { display: grid; gap: 14px; margin: 18px 0 24px; }258-.verdict > div { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px 6px; border-top: 3px solid var(--pass); }310+/* proves / doesn't: two columns under a bar in pass and fail */311+.verdict { display: grid; gap: var(--space-4) var(--space-8); margin: 18px 0 var(--space-6); }312+.verdict > div { padding: var(--space-3) 0 0; border-top: var(--bar) solid var(--pass); }259313 .verdict > div.no { border-top-color: var(--fail); }260-.verdict h3 { margin: 0 0 8px; font-size: 1.02rem; }261-.verdict ul { list-style: none; padding: 0; font-size: 0.97rem; }262-.verdict li { position: relative; padding-left: 24px; margin: 0 0 8px; }263-.verdict li::before { position: absolute; left: 0; font: 700 0.95rem var(--sans); }314+.verdict h3 { margin: 0 0 var(--space-2); font-size: var(--text-md); }315+.verdict ul { list-style: none; padding: 0; font-size: var(--text-md); }316+.verdict li { position: relative; padding-left: var(--space-6); margin: 0 0 var(--space-2); }317+.verdict li::before { position: absolute; left: 0; font: var(--fw-strong) var(--text-md) var(--sans); }264318 .verdict .yes li::before { content: "✓"; color: var(--pass); }265319 .verdict .no li::before { content: "✗"; color: var(--fail); }266320 @media (min-width: 720px) { .verdict { grid-template-columns: 1fr 1fr; } }267321 268322 /* fig: from ref to hostname */269323 .chain { list-style: none; margin: 0; padding: 0; }270-.chain li { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px 12px; align-items: baseline; }271-.chain .k { font: 0.7rem var(--mono); text-transform: uppercase; letter-spacing: 0.06em; color: var(--muted); }272-.chain .v { font: 0.84rem var(--mono); background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; overflow-wrap: anywhere; }273-.chain .v small { display: block; font: 0.85rem var(--serif); color: var(--muted); margin-top: 2px; }324+.chain li { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-half) var(--space-3); align-items: baseline; }325+.chain .k { font-family: var(--sans); font-size: var(--text-xs); font-weight: var(--fw-medium); color: var(--muted); }326+.chain .v { font-family: var(--mono); font-size: var(--text-code); background: var(--raised); border: var(--hairline) solid var(--border-strong); padding: 6px 10px; overflow-wrap: anywhere; }327+.chain .v small { display: block; font-family: var(--serif); font-size: var(--text-sm); color: var(--muted); margin-top: var(--space-half); }274328 .chain .hop { min-height: 34px; }275-.chain .hop span { grid-column: -2; position: relative; padding: 6px 0 6px 26px; font-family: var(--sans); font-size: 0.82rem; color: var(--muted); }329+.chain .hop span { grid-column: -2; position: relative; padding: 6px 0 6px 26px; font-family: var(--sans); font-size: var(--text-xs); color: var(--muted); }276330 .chain .hop span::before { content: ""; position: absolute; left: 11px; top: 0; bottom: 0; border-left: 2px solid var(--accent); }277331 .chain .hop span::after { content: ""; position: absolute; left: 7px; bottom: 0; border: 5px solid transparent; border-top: 6px solid var(--accent); border-bottom: 0; }278332 .chain .hop.up span::after { bottom: auto; top: 0; border-top: 0; border-bottom: 6px solid var(--accent); }@@ -282,92 +336,90 @@ figcaption b { color: var(--ink); font-weight: 600; margin-right: 6px; }
282336 }283337 .split { display: grid; gap: 18px; }284338 @media (min-width: 900px) { .split { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; gap: 28px; } }285-.split > .prose { font-size: 0.98rem; }339+.split > .prose { font-size: var(--text-md); }286340 287-/* tables */288-.table { overflow-x: auto; margin: 18px 0 24px; border: 1px solid var(--border); border-radius: var(--radius); }289-table { width: 100%; border-collapse: collapse; font-family: var(--sans); font-size: 0.88rem; line-height: 1.45; }290-th, td { text-align: left; vertical-align: top; padding: 9px 12px; border-bottom: 1px solid var(--border); }341+/* tables: no outer box, rows between hairlines, headers small and muted */342+.table { overflow-x: auto; margin: 18px 0 var(--space-6); border-top: var(--hairline) solid var(--border); border-bottom: var(--hairline) solid var(--border); }343+table { width: 100%; border-collapse: collapse; font-family: var(--sans); font-size: var(--text-sm); line-height: 1.5; }344+th, td { text-align: left; vertical-align: top; padding: var(--space-2) var(--space-3); border-bottom: var(--hairline) solid var(--border); }345+th:first-child, td:first-child { padding-left: 0; }291346 tr:last-child > * { border-bottom: 0; }292-thead th { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); background: var(--subtle); font-weight: 600; }293-tbody th { font-weight: 500; }294-td.y::before { content: "✓ "; font-weight: 700; color: var(--pass); }295-td.n::before { content: "✗ "; font-weight: 700; color: var(--fail); }347+thead th { font-size: var(--text-xs); font-weight: var(--fw-medium); color: var(--muted); padding-top: var(--space-3); padding-bottom: var(--space-1); }348+tbody th { font-weight: var(--fw-medium); }349+td.y::before { content: "✓ "; font-weight: var(--fw-strong); color: var(--pass); }350+td.n::before { content: "✗ "; font-weight: var(--fw-strong); color: var(--fail); }296351 td.n { color: var(--muted); }297-td code, th code { font-size: 0.82rem; white-space: normal; }298-@media (max-width: 719px) { table { font-size: 0.84rem; } th, td { padding: 8px 9px; } }352+td code, th code { font-size: var(--text-code); white-space: normal; }353+@media (max-width: 719px) { th, td { padding: var(--space-2) var(--space-2); } }299354 /* tables marked .rows restack on phones: the first cell on its own line,300355 then the rest, labelled from data-l when there is more than one */301356 @media (max-width: 719px) {302357 table.rows thead { display: none; }303- table.rows tr { display: grid; grid-template-columns: 1fr 1fr; border-bottom: 1px solid var(--border); }358+ table.rows tr { display: grid; grid-template-columns: 1fr 1fr; border-bottom: var(--hairline) solid var(--border); }304359 table.rows tr:last-child { border-bottom: 0; }305- table.rows th, table.rows td { border: 0; }360+ table.rows th, table.rows td { border: 0; padding-left: 0; }306361 table.rows tr > :first-child { grid-column: 1 / -1; padding-bottom: 0; }307362 table.rows td:not([data-l]):not(:first-child) { grid-column: 1 / -1; padding-top: 3px; }308- table.rows td[data-l] { position: relative; padding-top: 24px; }309- table.rows td[data-l]::after { content: attr(data-l); position: absolute; left: 9px; top: 6px; font: 0.68rem var(--mono); color: var(--muted); }363+ table.rows td[data-l] { position: relative; padding-top: var(--space-6); }364+ table.rows td[data-l]::after { content: attr(data-l); position: absolute; left: 0; top: 6px; font-size: var(--text-xs); font-weight: var(--fw-medium); color: var(--muted); }310365 }311366 312-313367 /* fig: one key, two generated files */314-.fan { display: grid; gap: 12px; }315-.fan .file { display: block; font: 600 0.75rem var(--mono); color: var(--ink); margin-bottom: 4px; }316-.fan .box { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; }317-.fan .box > code { display: block; font-size: 0.76rem; line-height: 1.5; background: none; border: 0; padding: 0; white-space: normal; overflow-wrap: anywhere; }318-.fan .box p { margin: 6px 0 0; font-size: 0.9rem; color: var(--muted); }319-.fan .by { font: 0.75rem var(--mono); color: var(--muted); display: flex; align-items: center; gap: 10px; }320-.fan .by::before, .fan .by::after { content: ""; flex: 1; border-top: 2px solid var(--accent); opacity: 0.6; }321-.fan .outs { display: grid; gap: 12px; }368+.fan { display: grid; gap: var(--space-3); }369+.fan .file { display: block; font-family: var(--mono); font-size: var(--text-xs); font-weight: var(--fw-strong); color: var(--fg); margin-bottom: var(--space-1); }370+.fan .box { background: var(--raised); border: var(--hairline) solid var(--border-strong); padding: 10px var(--space-3); }371+.fan .box > code { display: block; font-size: var(--text-code); line-height: 1.5; white-space: normal; overflow-wrap: anywhere; }372+.fan .box p { margin: 6px 0 0; font-size: var(--text-sm); color: var(--muted); }373+.fan .by { font-family: var(--mono); font-size: var(--text-xs); color: var(--muted); display: flex; align-items: center; gap: 10px; }374+.fan .by::before, .fan .by::after { content: ""; flex: 1; border-top: 2px solid var(--accent); }375+.fan .outs { display: grid; gap: var(--space-3); }322376 @media (min-width: 900px) { .fan .outs { grid-template-columns: 1fr 1fr; } }323377 324378 /* fig: hosting */325-.hosts { display: grid; gap: 18px; margin: 22px 0 26px; }379+.hosts { display: grid; gap: 18px; margin: var(--space-5) 0 var(--space-6); }326380 @media (min-width: 900px) { .hosts { grid-template-columns: 1.25fr 1fr; } }327381 .hosts figure { margin: 0; }328382 .hops { list-style: none; margin: 0; padding: 0; }329-.hops .node { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; }330-.hops .node b { display: block; font: 600 0.88rem var(--mono); }331-.hops .node span { display: block; font-size: 0.9rem; color: var(--muted); line-height: 1.45; margin-top: 2px; }332-.hops .node.home { border-color: var(--accent); box-shadow: inset 3px 0 0 var(--accent); }333-.hops .node ul { margin: 8px 0 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 6px; }334-.hops .node ul li { font: 0.72rem var(--mono); border: 1px solid var(--border); border-radius: 999px; padding: 1px 8px; color: var(--fg); background: var(--subtle); }335-.hops .wire { position: relative; padding: 8px 0 8px 34px; font-family: var(--sans); font-size: 0.8rem; color: var(--muted); }383+.hops .node { background: var(--raised); border: var(--hairline) solid var(--border-strong); padding: 10px var(--space-3); }384+.hops .node b { display: block; font-family: var(--sans); font-size: var(--text-md); font-weight: var(--fw-strong); }385+.hops .node span { display: block; font-size: var(--text-md); color: var(--muted); line-height: 1.5; margin-top: var(--space-half); }386+.hops .node.home { border-color: var(--accent); border-left-width: var(--bar); }387+.hops .node ul { margin: var(--space-2) 0 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 6px; }388+.hops .node ul li { font-family: var(--mono); font-size: var(--text-xs); border: var(--hairline) solid var(--border-strong); padding: 0 6px; color: var(--fg); }389+.hops .wire { position: relative; padding: var(--space-2) 0 var(--space-2) 34px; font-family: var(--sans); font-size: var(--text-xs); color: var(--muted); }336390 .hops .wire::before { content: ""; position: absolute; left: 18px; top: 0; bottom: 0; border-left: 2px solid var(--accent); }337391 .hops .wire.tunnel::before { border-left: 6px double var(--accent); left: 16px; }338392 339-/* cheat sheet */340-.cheat { display: grid; gap: 16px; }393+/* cheat sheet: groups under a strong rule, entries between hairlines */394+.cheat { display: grid; gap: var(--space-6) var(--space-8); }341395 @media (min-width: 900px) { .cheat { grid-template-columns: 1fr 1fr; } }342-.cheat section { border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; }343-.cheat h3 { margin: 0; padding: 9px 14px; font: 600 0.75rem var(--mono); text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); background: var(--subtle); border-bottom: 1px solid var(--border); }396+.cheat section { border-top: var(--hairline) solid var(--border-strong); }397+.cheat h3 { margin: 0; padding: var(--space-2) 0 var(--space-1); font-size: var(--text-sm); font-weight: var(--fw-strong); color: var(--muted); }344398 .cheat dl { margin: 0; }345-.cheat dl > div { padding: 9px 14px; border-bottom: 1px solid var(--border); }399+.cheat dl > div { padding: var(--space-2) 0; border-bottom: var(--hairline) solid var(--border); }346400 .cheat dl > div:last-child { border-bottom: 0; }347-.cheat dt { font: 0.8rem/1.5 var(--mono); overflow-wrap: anywhere; }348-.cheat dt::before { content: "$ "; color: var(--accent); }401+.cheat dt { font-family: var(--mono); font-size: var(--text-code); line-height: 1.5; overflow-wrap: anywhere; }402+.cheat dt::before { content: "$ "; color: var(--link); }349403 .cheat dt.nop::before { content: ""; }350-.cheat dd { margin: 2px 0 0; font-size: 0.9rem; color: var(--muted); line-height: 1.45; }404+.cheat dd { margin: var(--space-half) 0 0; font-size: var(--text-md); color: var(--muted); line-height: 1.5; }351405 352-/* not yet */353-.gaps { list-style: none; padding: 0; display: grid; gap: 10px; max-width: 41rem; }354-.gaps li { border: 1px dashed var(--border); border-radius: var(--radius); padding: 10px 14px; }355-.gaps b { font-weight: 650; }406+/* not yet: a list between hairlines */407+.gaps { list-style: none; padding: 0; max-width: 41rem; border-bottom: var(--hairline) solid var(--border); }408+.gaps li { border-top: var(--hairline) solid var(--border); padding: 10px 0; }356409 357410 /* files and trees */358-.tree { margin: 0; font: 0.76rem/1.6 var(--mono); white-space: pre; overflow-x: auto; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 12px 14px; }411+.tree { margin: 0; font-size: var(--text-code); line-height: 1.6; white-space: pre; overflow-x: auto; background: var(--raised); border: var(--hairline) solid var(--border); padding: var(--space-3) 14px; }359412 .tree .c { color: var(--muted); }360-@media (max-width: 719px) { .tree .c { display: block; white-space: pre-wrap; padding-left: 3.2em; font-size: 0.92em; } }361-.tree .d { color: var(--ink); font-weight: 600; }413+@media (max-width: 719px) { .tree .c { display: block; white-space: pre-wrap; padding-left: 3.2em; } }414+.tree .d { color: var(--fg); font-weight: var(--fw-strong); }362415 363-footer.end { border-top: 1px solid var(--border); padding: 26px 0 40px; font-family: var(--sans); font-size: 0.88rem; color: var(--muted); }364-footer.end .wrap { display: flex; flex-wrap: wrap; gap: 8px 20px; align-items: center; }416+footer.end { border-top: var(--hairline) solid var(--border); padding: var(--space-6) 0 var(--space-10); font-family: var(--sans); font-size: var(--text-sm); color: var(--muted); }417+footer.end .wrap { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-5); align-items: center; }365418 footer.end .mark { width: 20px; height: 20px; color: var(--fg); }366419 footer.end p { margin: 0; }367420 368421 /* the internals page: a shorter hero */369-.hero.short { padding: 34px 0 30px; }370-.hero.short h1 { font-size: clamp(2.6rem, 11vw, 4.4rem); }422+.hero.short { padding: var(--space-8) 0 var(--space-8); }371423 .hero.short .lede { max-width: 40rem; }372424 @media (min-width: 900px) { .hero.short { padding: 56px 0 48px; } .hero.short .wrap { grid-template-columns: 1fr; } }373425 @@ -5,9 +5,10 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">66 <title>forge: a git forge made of plain git</title>77 <meta name="description" content="How forge works: plain git over ssh, static pages built after each push, reviews as stacked branches, CI signed on your own machine, and sites that ship with repos.">8-<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">9-<meta name="theme-color" content="#0d1117" media="(prefers-color-scheme: dark)">10-<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cstyle%3Ecircle%7Bstroke:%23000%7D@media (prefers-color-scheme:dark)%7Bcircle%7Bstroke:%23bdbdbd%7D%7D%3C/style%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M16 15 12 7 8 15Zm-1 0L11.5 8' stroke='%232ca9bc'/%3E%3C/svg%3E">8+<meta name="theme-color" content="#e9e9e9" media="(prefers-color-scheme: light)">9+<meta name="theme-color" content="#08090b" media="(prefers-color-scheme: dark)">10+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cstyle%3Ecircle%7Bstroke:%231c2127%7Dpath%7Bstroke:%233148f6%7D@media (prefers-color-scheme:dark)%7Bcircle%7Bstroke:%23e2e4e7%7D%7D%3C/style%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M16 15 12 7 8 15Zm-1 0L11.5 8'/%3E%3C/svg%3E">11+<link rel="preload" href="fonts/open-sans.woff2" as="font" type="font/woff2" crossorigin>1112 <link rel="stylesheet" href="guide.css">1213 <script src="guide.js" defer></script>1314 </head>@@ -15,8 +16,8 @@
1516 <a class="skip" href="#main">Skip to the guide</a>1617 <header class="bar">1718 <div class="wrap">18-<a class="brand" href="./"><svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8" stroke="#2ca9bc"/></svg>forge <span>guide</span></a>19-<nav aria-label="guide"><a href="./" aria-current="page">Guide</a><a href="internals.html">Internals</a></nav>19+<a class="brand" href="./"><svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8"/></svg>forge <span>guide</span></a>20+<nav aria-label="guide"><a href="./" aria-current="page">guide</a><a href="internals.html">internals</a></nav>2021 </div>2122 </header>2223 @@ -24,12 +25,12 @@
2425 <div class="wrap">2526 <div class="hero-text">2627 <p class="eyebrow">a personal git forge</p>27-<h1 id="top"><svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8" stroke="#2ca9bc"/></svg>forge</h1>28+<h1 id="top"><svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8"/></svg>forge</h1>2829 <p class="lede">Plain git on a server, and web pages written after every push. Reviews are branches, CI runs on <em>your</em> machine and is signed with your SSH key, and any repo can ship a static site.</p>29-<p class="cta"><a class="btn go" href="#push">Follow a push</a><a class="btn" href="#cli">Cheat sheet</a><a class="btn" href="internals.html">Internals</a></p>30+<p class="cta"><a class="btn go" href="#push">follow a push</a><a class="btn" href="#cli">cheat sheet</a><a class="btn" href="internals.html">internals</a></p>3031 </div>3132 <figure class="term" aria-label="A terminal session: attest, push, deploy a preview">32-<div class="chrome" aria-hidden="true"><i></i><i></i><i></i><span>~/greet (dami/flags)</span></div>33+<div class="chrome" aria-hidden="true">~/greet (dami/flags)</div>3334 <pre><span class="line" style="--i:0"><span class="p">$ </span>forge attest</span><span class="line o" style="--i:1">80b9dda running .forge/ci: add --shout</span><span class="line o" style="--i:2"> | ok greet --shout</span><span class="line" style="--i:3">80b9dda <span class="ok">pass</span> in 2s, signed</span><span class="line" style="--i:4"><span class="p">$ </span>forge push -t "Flags"</span><span class="line o" style="--i:5">pushed dami/flags</span><span class="line" style="--i:6"><span class="p">$ </span>forge site deploy docs</span><span class="line o" style="--i:7">site docs: sh docs/build.sh</span><span class="line r" style="--i:8">remote: site docs/dami~flags: <span class="u">https://greet-docs--dami-flags.pages.example.com/</span></span><span class="line o" style="--i:9">site docs: deployed 942ff39 -> https://greet-docs--dami-flags.pages.example.com/</span></pre>3435 </figure>3536 </div>@@ -38,7 +39,7 @@
3839 <div class="wrap page">3940 <nav class="toc" aria-label="On this page">4041 <div>41-<h2>On this page</h2>42+<h2>on this page</h2>4243 <ol>4344 <li><a href="#what"><span>01</span>what and why</a></li>4445 <li><a href="#push"><span>02</span>a push</a></li>@@ -67,7 +68,7 @@
6768 <li><b>stagit</b><span>Log, files, blobs and refs as plain pages, written when something is pushed instead of computed per request.</span></li>6869 <li><b>args.io</b><span>Reviews as stacked branches with revisions, CI attested on your machine, a site per repo.</span></li>6970 </ol>70-<p class="child"><svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8" stroke="#2ca9bc"/></svg>forge <small>a merge of three ideas</small></p>71+<p class="child"><svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8"/></svg>forge <small>a merge of three ideas</small></p>7172 <figcaption><b>fig. 1</b>where it comes from</figcaption>7273 </figure>7374 <div class="prose">@@ -178,7 +179,7 @@ log sha256:6cc79019... 4 lines
178179 <p class="prose">A deploy is a commit on <code>refs/sites/<name></code> whose tree is the built output. So git provides content-addressed storage, dedup across deploys, history, and revert by pushing an older deploy. Trailers record the source commit, the build command and who built it, and the commit carries an SSH signature when your key can sign.</p>179180 <div class="split">180181 <div class="prose">181-<p>Declare sites in <code>.forge/sites</code>, one per line: a name, the output directory, and a build command, or <code>-</code> for none. This guide is <code>guide guide -</code>.</p>182+<p>Declare sites in <code>.forge/sites</code>, one per line: a name, the output directory, and a build command, or <code>-</code> for none. This guide is <code>guide guide</code>, with a build command that copies the forge's fonts in.</p>182183 <p><code>forge site deploy <name></code> builds from a clean worktree of <code>HEAD</code>, with <code>FORGE_SITE_BASE</code> set to the path it will be served from (<code>/</code> on its own hostname). From <code>main</code> it publishes the site. From any other branch it publishes that review's preview, <code>refs/site-previews/<name>/<branch></code>, which the review page links and marks stale once the branch moves on. <code>forge site push <dir> <name></code> publishes a directory you already built.</p>183184 <p>Only the owner and collaborators deploy sites. Anyone can deploy previews of their own branches.</p>184185 </div>@@ -203,7 +204,7 @@ log sha256:6cc79019... 4 lines
203204 </div>204205 <div class="table">205206 <table>206-<thead><tr><th scope="col">Deploy</th><th scope="col">Hostname under the pages domain</th></tr></thead>207+<thead><tr><th scope="col">deploy</th><th scope="col">hostname under the pages domain</th></tr></thead>207208 <tbody>208209 <tr><td>a site of the forge's owner: <code>dami/greet</code>, site <code>docs</code></td><td><code>greet-docs</code></td></tr>209210 <tr><td>another user's site: <code>ana/dotfiles</code>, site <code>home</code></td><td><code>dotfiles-home-ana</code></td></tr>@@ -234,7 +235,7 @@ log sha256:6cc79019... 4 lines
234235 <p class="prose">Repos live at <code>/srv/git/<owner>/<repo>.git</code>, and the directory names the owner. <code>ssh git@host new <repo></code> creates one under your name, and so does pushing to one that isn't there yet. The owner, and anyone listed in <code><repo>.git/forge/collaborators</code>, can push anything; everyone else works under their own name:</p>235236 <div class="table">236237 <table class="rows">237-<thead><tr><th scope="col">Ref</th><th scope="col">Owner, collaborators</th><th scope="col">Anyone else, e.g. ana</th></tr></thead>238+<thead><tr><th scope="col">ref</th><th scope="col">owner, collaborators</th><th scope="col">anyone else, e.g. ana</th></tr></thead>238239 <tbody>239240 <tr><th scope="row"><code>main</code> (the default branch)</th><td class="y" data-l="owner, collaborators">fast-forward only, unless <code>forge.allowforce</code>; never deleted</td><td class="n" data-l="anyone else">no</td></tr>240241 <tr><th scope="row"><code>refs/heads/<wbr>ana/*</code></th><td class="y" data-l="owner, collaborators">yes</td><td class="y" data-l="anyone else">yes, her own prefix</td></tr>@@ -278,14 +279,14 @@ log sha256:6cc79019... 4 lines
278279 <section class="commit" id="cli" aria-labelledby="cli-h">279280 <header><p class="ref"><b>08</b> cheat sheet</p><h2 id="cli-h"><a href="#cli">Day to day</a></h2></header>280281 <div class="cheat">281-<section aria-labelledby="cs-setup"><h3 id="cs-setup">Once</h3><dl>282+<section aria-labelledby="cs-setup"><h3 id="cs-setup">once</h3><dl>282283 <div><dt>git config --global forge.signingkey ~/.ssh/id_ed25519.pub</dt><dd>The key that signs CI results and deploys; a <code>.pub</code> path signs through ssh-agent.</dd></div>283284 <div><dt>ssh git@host signers > ~/.config/forge/allowed_signers</dt><dd>Then point <code>forge.allowedsigners</code> at it, so <code>forge verify</code> can check signatures.</dd></div>284285 <div><dt>ssh git@host new greet "say hello"</dt><dd>Create a repo under your name; prints the remote to add.</dd></div>285286 <div><dt>git push git@host:dami/greet.git main</dt><dd>Or just push: a repo under your name that doesn't exist yet is created first, the same way.</dd></div>286287 <div><dt>forge import https://github.com/you/thing.git</dt><dd>Copy a repo in (set <code>forge.host</code> first). <code>--branches</code> takes every branch.</dd></div>287288 </dl></section>288-<section aria-labelledby="cs-review"><h3 id="cs-review">A review</h3><dl>289+<section aria-labelledby="cs-review"><h3 id="cs-review">a review</h3><dl>289290 <div><dt>git switch -c dami/topic</dt><dd>Branch under your name; once pushed, it is a review.</dd></div>290291 <div><dt>forge attest --stack</dt><dd>CI for every commit on top of <code>main</code>, signed.</dd></div>291292 <div><dt>forge push -t "Title"</dt><dd>Push the branch and the CI notes. <code>--draft</code>, <code>--ready</code>, <code>--target <branch></code>, <code>-d</code> for a description.</dd></div>@@ -296,21 +297,21 @@ log sha256:6cc79019... 4 lines
296297 <div><dt>git push origin dami/topic:main</dt><dd>Land it (or <code>forge land</code>). <code>main</code> only fast-forwards.</dd></div>297298 <div><dt>ssh git@host land dami/greet 7</dt><dd>Have the forge land review 7, no clone needed: it pushes to itself as you, so the same gates apply, and deletes the landed branches. <code>forge land --server</code> does it from a clone.</dd></div>298299 </dl></section>299-<section aria-labelledby="cs-ci"><h3 id="cs-ci">CI</h3><dl>300+<section aria-labelledby="cs-ci"><h3 id="cs-ci">ci</h3><dl>300301 <div><dt>forge attest [rev...]</dt><dd>Attest given commits (default <code>HEAD</code>). <code>--force</code> re-runs a passed tree, <code>--clean</code> also removes ignored files from the worktree.</dd></div>301302 <div><dt>forge verify [rev...]</dt><dd>Show each commit's attestations and who signed them.</dd></div>302303 </dl></section>303-<section aria-labelledby="cs-sites"><h3 id="cs-sites">Sites</h3><dl>304+<section aria-labelledby="cs-sites"><h3 id="cs-sites">sites</h3><dl>304305 <div><dt>forge site deploy [name...]</dt><dd>Build and publish from a clean worktree of <code>HEAD</code>: the site on <code>main</code>, a preview elsewhere. <code>--here</code> builds in your work tree.</dd></div>305306 <div><dt>forge site push <dir> <name></dt><dd>Publish a directory you built yourself.</dd></div>306307 <div><dt>forge site url <name> [branch]</dt><dd>Where a deploy is served.</dd></div>307308 </dl></section>308-<section aria-labelledby="cs-releases"><h3 id="cs-releases">Releases</h3><dl>309+<section aria-labelledby="cs-releases"><h3 id="cs-releases">releases</h3><dl>309310 <div><dt>forge release publish <tag> <file>...</dt><dd>The tag's files with a signed <code>SHA256SUMS</code>, pushed with the tag and your CI notes; again to replace them.</dd></div>310311 <div><dt>forge release verify <file>...</dt><dd>Check downloads against <code>SHA256SUMS</code> and its signature against the forge's keys.</dd></div>311312 <div><dt>forge release ls, rm <tag></dt><dd>This repo's releases; delete one.</dd></div>312313 </dl></section>313-<section aria-labelledby="cs-server"><h3 id="cs-server">On the server, as root</h3><dl>314+<section aria-labelledby="cs-server"><h3 id="cs-server">on the server, as root</h3><dl>314315 <div><dt>forge-admin user add ana 'ssh-ed25519 AAAA...'</dt><dd>Add a user and key; <code>user rm ana</code> revokes their keys.</dd></div>315316 <div><dt>forge-admin repo new dami/greet "description"</dt><dd>Or <code>repo import dami/greet <url></code> to fetch one server side.</dd></div>316317 <div><dt>forge-admin build [owner/repo]</dt><dd>Rebuild pages; <code>metrics</code> shows where builds spend their time.</dd></div>@@ -318,7 +319,7 @@ log sha256:6cc79019... 4 lines
318319 <div><dt>forge-admin backups</dt><dd>How the last backup went, then the snapshots. <code>backup</code> takes one now; the container takes one daily.</dd></div>319320 <div><dt>forge-admin restore latest</dt><dd>Bring the forge back on an empty box, from the newest snapshot or a named one.</dd></div>320321 </dl></section>321-<section aria-labelledby="cs-repo"><h3 id="cs-repo">In a bare repo</h3><dl>322+<section aria-labelledby="cs-repo"><h3 id="cs-repo">in a bare repo</h3><dl>322323 <div><dt>git config forge.requireci tip</dt><dd>Require a verified pass for the new tip of <code>main</code>; <code>all</code> checks every new commit.</dd></div>323324 <div><dt>git config forge.allowforce true</dt><dd>Let the owner and collaborators force-push <code>main</code>.</dd></div>324325 <div><dt class="nop">forge/collaborators</dt><dd>Names, one per line, who can push anything.</dd></div>@@ -342,7 +343,7 @@ log sha256:6cc79019... 4 lines
342343 343344 <footer class="end">344345 <div class="wrap">345-<svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8" stroke="#2ca9bc"/></svg>346+<svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8"/></svg>346347 <p>This guide is <code>guide/</code> in forge's own repo, published with <code>forge site deploy guide</code>. For how the code fits together, read <a href="internals.html">the internals</a>.</p>347348 </div>348349 </footer>@@ -5,9 +5,10 @@
55 <meta name="viewport" content="width=device-width, initial-scale=1">66 <title>forge internals: where things are and how they fit</title>77 <meta name="description" content="For whoever maintains forge next: the source map, state on disk, what runs, how a build works, URLs, settings and upkeep.">8-<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">9-<meta name="theme-color" content="#0d1117" media="(prefers-color-scheme: dark)">10-<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cstyle%3Ecircle%7Bstroke:%23000%7D@media (prefers-color-scheme:dark)%7Bcircle%7Bstroke:%23bdbdbd%7D%7D%3C/style%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M16 15 12 7 8 15Zm-1 0L11.5 8' stroke='%232ca9bc'/%3E%3C/svg%3E">8+<meta name="theme-color" content="#e9e9e9" media="(prefers-color-scheme: light)">9+<meta name="theme-color" content="#08090b" media="(prefers-color-scheme: dark)">10+<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cstyle%3Ecircle%7Bstroke:%231c2127%7Dpath%7Bstroke:%233148f6%7D@media (prefers-color-scheme:dark)%7Bcircle%7Bstroke:%23e2e4e7%7D%7D%3C/style%3E%3Ccircle cx='12' cy='12' r='9'/%3E%3Cpath d='M16 15 12 7 8 15Zm-1 0L11.5 8'/%3E%3C/svg%3E">11+<link rel="preload" href="fonts/open-sans.woff2" as="font" type="font/woff2" crossorigin>1112 <link rel="stylesheet" href="guide.css">1213 <script src="guide.js" defer></script>1314 </head>@@ -15,8 +16,8 @@
1516 <a class="skip" href="#main">Skip to the internals</a>1617 <header class="bar">1718 <div class="wrap">18-<a class="brand" href="./"><svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8" stroke="#2ca9bc"/></svg>forge <span>guide</span></a>19-<nav aria-label="guide"><a href="./">Guide</a><a href="internals.html" aria-current="page">Internals</a></nav>19+<a class="brand" href="./"><svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8"/></svg>forge <span>guide</span></a>20+<nav aria-label="guide"><a href="./">guide</a><a href="internals.html" aria-current="page">internals</a></nav>2021 </div>2122 </header>2223 @@ -33,7 +34,7 @@
3334 <div class="wrap page">3435 <nav class="toc" aria-label="On this page">3536 <div>36-<h2>On this page</h2>37+<h2>on this page</h2>3738 <ol>3839 <li><a href="#map"><span>01</span>source</a></li>3940 <li><a href="#disk"><span>02</span>on disk</a></li>@@ -54,7 +55,7 @@
5455 <p class="prose">Four kinds of code, kept small: one shell script for everything git and ssh touch, a TypeScript data layer that Node 22 runs as it is (types are stripped, never compiled), Nunjucks templates with a base stylesheet, and container and deploy files.</p>5556 <div class="table">5657 <table class="rows">57-<thead><tr><th scope="col">Path</th><th scope="col">What it is</th></tr></thead>58+<thead><tr><th scope="col">path</th><th scope="col">what it is</th></tr></thead>5859 <tbody>5960 <tr><th scope="row"><code>bin/forge</code></th><td>The whole CLI in POSIX sh. Developer commands (<code>attest</code>, <code>verify</code>, <code>push</code>, <code>site</code>, <code>import</code>) run in a clone; server commands (<code>server init</code>, <code>user</code>, <code>keys sync</code>, <code>repo</code>, <code>build</code>, <code>gc</code>, <code>shell</code>, <code>hook</code>) run as the git user. Both hooks are this script.</td></tr>6061 <tr><th scope="row"><code>lib/forge.ts</code></th><td>Finds repos and flattens them into one list per page type for 11ty. Fingerprints every page so unchanged ones are skipped, and holds the filters that render heavy HTML when a page is written.</td></tr>@@ -121,7 +122,7 @@ deleted 1788292812 dami <span class="dim"># only o
121122 <p class="prose">The container's entrypoint is <code>tini</code>, which also reaps the background builds, and then <code>server/container/start.sh</code>. It creates host keys on first boot, runs <code>forge server init</code>, writes <code>.forge/config</code> from the environment, adds the owner's key, syncs keys, writes the Caddyfile with <code>forge server caddyfile</code>, and starts:</p>122123 <div class="table">123124 <table class="rows">124-<thead><tr><th scope="col">Process</th><th scope="col">Listens</th><th scope="col">As</th><th scope="col">Why</th></tr></thead>125+<thead><tr><th scope="col">process</th><th scope="col">listens</th><th scope="col">as</th><th scope="col">why</th></tr></thead>125126 <tbody>126127 <tr><th scope="row">sshd</th><td data-l="listens"><code>:2222</code></td><td data-l="as">root</td><td>git over ssh, only for the git user, keys only.</td></tr>127128 <tr><th scope="row">fcgiwrap</th><td data-l="listens">unix socket</td><td data-l="as">git</td><td>Runs <code>git http-backend</code> for HTTPS clones; Caddy routes only <code>info/refs</code> and <code>git-upload-pack</code> to it.</td></tr>@@ -162,7 +163,7 @@ deleted 1788292812 dami <span class="dim"># only o
162163 <p class="prose">Every page is a directory with an <code>index.html</code>, so URLs read like paths. Branch names may contain <code>/</code>, which would make <code>/tree/<branch>/<path></code> ambiguous, so it is written <code>~</code>, which git forbids in ref names: <code>dami/flags</code> becomes <code>dami~flags</code>.</p>163164 <div class="table">164165 <table class="rows">165-<thead><tr><th scope="col">URL</th><th scope="col">Page</th></tr></thead>166+<thead><tr><th scope="col">url</th><th scope="col">page</th></tr></thead>166167 <tbody>167168 <tr><td><code>/<owner>/<repo>/</code></td><td>open reviews, recent commits, README</td></tr>168169 <tr><td><code>.../log/<branch>/</code></td><td>log for a branch</td></tr>@@ -188,12 +189,12 @@ deleted 1788292812 dami <span class="dim"># only o
188189 <div><dt>FORGE_PAGES_DOMAIN, FORGE_OWNER</dt><dd>Turn on per-site hostnames; the owner's sites get the short ones.</dd></div>189190 <div><dt>FORGE_MAX_COMMITS, FORGE_MAX_FILES, FORGE_MAX_BLOB, FORGE_MAX_DIFF_LINES</dt><dd>Limits per branch log, per tree, per file shown and per diff shown: 1000, 5000, 1 MiB, 20000 lines.</dd></div>190191 </dl></section>191-<section aria-labelledby="k-repo"><h3 id="k-repo">A bare repo's git config</h3><dl>192+<section aria-labelledby="k-repo"><h3 id="k-repo">a bare repo's git config</h3><dl>192193 <div><dt>forge.requireci tip | all</dt><dd>A verified pass before <code>main</code> moves, for the new tip or every new commit.</dd></div>193194 <div><dt>forge.allowforce true</dt><dd>Let admins force-push <code>main</code>.</dd></div>194195 <div><dt>transfer.hideRefs refs/forge</dt><dd>Set by forge on every repo, with <code>receive.advertisePushOptions true</code> and <code>commitGraph.changedPathsVersion 2</code>; see <a href="#upkeep">upkeep</a>.</dd></div>195196 </dl></section>196-<section aria-labelledby="k-dev"><h3 id="k-dev">Your git config</h3><dl>197+<section aria-labelledby="k-dev"><h3 id="k-dev">your git config</h3><dl>197198 <div><dt>forge.signingkey, forge.allowedsigners</dt><dd>Sign with this key; verify against this file.</dd></div>198199 <div><dt>forge.host, forge.remote, forge.base</dt><dd>Where <code>forge import</code> goes; which remote and base branch the other commands use.</dd></div>199200 <div><dt>forge.cidir, forge.nix</dt><dd>Where CI worktrees live; <code>forge.nix false</code> skips <code>nix develop</code>.</dd></div>@@ -203,7 +204,7 @@ deleted 1788292812 dami <span class="dim"># only o
203204 <div><dt>FORGE_BACKUP_AT, _KEEP_DAILY, _KEEP_WEEKLY</dt><dd>When the daily backup runs (UTC) and what retention keeps: 03:30, 7 and 5.</dd></div>204205 <div><dt>FORGE_MIRROR_S3, _KEY_ID, _SECRET, _DELAY</dt><dd>The pages mirror's bucket and keys, and how long it waits after a build: 30 seconds.</dd></div>205206 </dl></section>206-<section aria-labelledby="k-env"><h3 id="k-env">The environment, for tests</h3><dl>207+<section aria-labelledby="k-env"><h3 id="k-env">the environment, for tests</h3><dl>207208 <div><dt>FORGE_NOW</dt><dd>Pins the clock reviews are stamped with; the demo moves it with its commits.</dd></div>208209 <div><dt>FORGE_NO_BUILD, FORGE_SYNC_BUILD</dt><dd>Skip the build after a push, or run it in the foreground.</dd></div>209210 <div><dt>FORGE_STRICT</dt><dd>1 makes a template that prints an undefined or null value fail the build; CI sets it.</dd></div>@@ -235,13 +236,13 @@ deleted 1788292812 dami <span class="dim"># only o
235236 <section class="commit" id="hacking" aria-labelledby="hacking-h">236237 <header><p class="ref"><b>08</b> hacking</p><h2 id="hacking-h"><a href="#hacking">Working on forge</a></h2></header>237238 <div class="cheat">238-<section aria-labelledby="hk-run"><h3 id="hk-run">Run it</h3><dl>239+<section aria-labelledby="hk-run"><h3 id="hk-run">run it</h3><dl>239240 <div><dt>npm install</dt><dd>11ty, markdown-it, sanitize-html, mermaid; TypeScript only for checking.</dd></div>240241 <div><dt>sh demo/make-demo.sh</dt><dd>A throwaway forge in <code>demo/root</code>: two users, stacked reviews with revisions, CI that fails and passes, a wiki, sites and previews, all through the real hooks over file:// pushes.</dd></div>241242 <div><dt>node demo/serve.ts 4817</dt><dd>Serves it with live updates and cache headers, the way Caddy and the events server do.</dd></div>242243 <div><dt>npm run dev</dt><dd>Serves the demo from <code>_site/</code> on :8080 and rebuilds it, reloading the browser, when templates, assets or <code>lib/</code> change. It never writes to <code>demo/root</code>.</dd></div>243244 </dl></section>244-<section aria-labelledby="hk-check"><h3 id="hk-check">Check it</h3><dl>245+<section aria-labelledby="hk-check"><h3 id="hk-check">check it</h3><dl>245246 <div><dt>sh .forge/ci</dt><dd>Lints the shell, type-checks, builds the demo, checks a few pages, and checks this guide's links. It is forge's own CI, so <code>forge attest</code> runs it and signs the result.</dd></div>246247 <div><dt>npx tsc -p .</dt><dd>Type check only. <code>tsc</code> never emits.</dd></div>247248 <div><dt>node lib/check-links.ts guide</dt><dd>Every relative link and anchor in the guide resolves.</dd></div>@@ -259,7 +260,7 @@ deleted 1788292812 dami <span class="dim"># only o
259260 260261 <footer class="end">261262 <div class="wrap">262-<svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8" stroke="#2ca9bc"/></svg>263+<svg class="mark" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M16 15 12 7 8 15Zm-1 0L11.5 8"/></svg>263264 <p>Back to <a href="./">the guide</a>. The README in the repo has the same facts in plain text.</p>264265 </div>265266 </footer>