OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
HASH      3f3defb760c2
DATE      2026-07-16
SUBJECT   web: harden the frame pipeline
FILES     16 CHANGED
HASH      3f3defb760c2
DATE      2026-07-16
SUBJECT   web: harden the frame pipeline
FILES     16 CHANGED
 

diff --git a/ooknet-design/package-lock.json b/ooknet-design/package-lock.json
index ee26c9a..3482424 100644
--- a/ooknet-design/package-lock.json
+++ b/ooknet-design/package-lock.json
@@ -14,7 +14,8 @@
         "fuse.js": "^7.3.0"
       },
       "devDependencies": {
-        "sass": "^1.99.0"
+        "sass": "^1.99.0",
+        "vitest": "^4.1.10"
       },
       "engines": {
         "node": ">=22.12.0"
@@ -1527,6 +1528,24 @@
       "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzG
eUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
       "license": "MIT"
     },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.
tgz",
+      "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU
4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEH
bS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
     "node_modules/@types/debug": {
       "version": "4.1.13",
       "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
@@ -1536,6 +1555,13 @@
         "@types/ms": "*"
       }
     },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tg
z",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk54
1lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/estree": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1587,6 +1613,129 @@
       "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1Ic
nQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
       "license": "ISC"
     },
+    "node_modules/@vitest/expect": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz"
,
+      "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+D
ASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.1.0",
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "chai": "^6.2.2",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz"
,
+      "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxa
YURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "4.1.10",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.21"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/mocker/node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3
.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35
LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-form
at-4.1.10.tgz",
+      "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gko
pdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz"
,
+      "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6O
aVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "4.1.10",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.
tgz",
+      "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhc
FSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "magic-string": "^0.30.21",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
+      "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIB
EHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
+      "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEw
RPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.10",
+        "convert-source-map": "^2.0.0",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
     "node_modules/anymatch": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
@@ -1637,6 +1786,16 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2
.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7j
v12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/astro": {
       "version": "6.1.9",
       "resolved": "https://registry.npmjs.org/astro/-/astro-6.1.9.tgz",
@@ -2206,6 +2365,16 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFo
z5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/character-entities": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/character-entities/-/character-enti
ties-2.0.2.tgz",
@@ -2303,6 +2472,13 @@
         "node": ">= 18"
       }
     },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source
-map-2.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXv
fJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/cookie": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
@@ -2618,6 +2794,16 @@
       "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4K
cci7BS85f93veegeCPkL8R4GLClnjLFw==",
       "license": "MIT"
     },
+    "node_modules/expect-type": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz
",
+      "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJS
uwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -4131,6 +4317,13 @@
         "url": "https://github.com/inikulin/parse5?sponsor=1"
       }
     },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugp
hxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/piccolore": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz",
@@ -4628,6 +4821,13 @@
         "node": ">=20"
       }
     },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK
0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -4665,6 +4865,20 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8
zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/std-env": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
+      "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAs
l2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/stringify-entities": {
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-enti
ties-4.0.4.tgz",
@@ -4710,6 +4924,13 @@
       "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7
nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
       "license": "MIT"
     },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7P
c2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/tinyclip": {
       "version": "0.1.12",
       "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz",
@@ -4744,6 +4965,16 @@
         "url": "https://github.com/sponsors/SuperchupuDev"
       }
     },
+    "node_modules/tinyrainbow": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz
",
+      "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEY
dVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/trim-lines": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -5650,6 +5881,96 @@
         }
       }
     },
+    "node_modules/vitest": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
+      "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0
SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "4.1.10",
+        "@vitest/mocker": "4.1.10",
+        "@vitest/pretty-format": "4.1.10",
+        "@vitest/runner": "4.1.10",
+        "@vitest/snapshot": "4.1.10",
+        "@vitest/spy": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "es-module-lexer": "^2.0.0",
+        "expect-type": "^1.3.0",
+        "magic-string": "^0.30.21",
+        "obug": "^2.1.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "std-env": "^4.0.0-rc.1",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^1.0.2",
+        "tinyglobby": "^0.2.15",
+        "tinyrainbow": "^3.1.0",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@opentelemetry/api": "^1.9.0",
+        "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+        "@vitest/browser-playwright": "4.1.10",
+        "@vitest/browser-preview": "4.1.10",
+        "@vitest/browser-webdriverio": "4.1.10",
+        "@vitest/coverage-istanbul": "4.1.10",
+        "@vitest/coverage-v8": "4.1.10",
+        "@vitest/ui": "4.1.10",
+        "happy-dom": "*",
+        "jsdom": "*",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser-playwright": {
+          "optional": true
+        },
+        "@vitest/browser-preview": {
+          "optional": true
+        },
+        "@vitest/browser-webdriverio": {
+          "optional": true
+        },
+        "@vitest/coverage-istanbul": {
+          "optional": true
+        },
+        "@vitest/coverage-v8": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        },
+        "vite": {
+          "optional": false
+        }
+      }
+    },
     "node_modules/web-namespaces": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0
.1.tgz",
@@ -5669,6 +5990,23 @@
         "node": ">=4"
       }
     },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-r
unning-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7X
vlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/xxhash-wasm": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz
",

diff --git a/ooknet-design/package-lock.json
 b/ooknet-design/package-lock.json
index ee26c9a..3482424 100644
--- a/ooknet-design/package-lock.json
+++ b/ooknet-design/package-lock.json
@@ -14,7 +14,8 @@
         "fuse.js": "^7.3.0"
       },
       "devDependencies": {
-        "sass": "^1.99.0"
+        "sass": "^1.99.0",
+        "vitest": "^4.1.10"
       },
       "engines": {
         "node": ">=22.12.0"
@@ -1527,6 +1528,24 @@
       "integrity": "sha512-83yeghZ2xxin3Nj8
z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9
FC3E/SBEMvzJRwWEOz5gGes9Qg==",
       "license": "MIT"
     },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.o
rg/@standard-schema/spec/-/spec-1.1.0.tgz",
+      "integrity": "sha512-l2aFy5jALhniG5Hg
qrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXav
m/jPityDo5TCvKMnpjKnOriy0w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/chai": {
+      "version": "5.2.3",
+      "resolved": "https://registry.npmjs.o
rg/@types/chai/-/chai-5.2.3.tgz",
+      "integrity": "sha512-Mw558oeA9fFbv65/
y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lah
r+pwZFR8A7GQleRWeI6cGFC2UA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/deep-eql": "*",
+        "assertion-error": "^2.0.1"
+      }
+    },
     "node_modules/@types/debug": {
       "version": "4.1.13",
       "resolved": "https://registry.npmjs.o
rg/@types/debug/-/debug-4.1.13.tgz",
@@ -1536,6 +1555,13 @@
         "@types/ms": "*"
       }
     },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.o
rg/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bw
TvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7A
qFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/estree": {
       "version": "1.0.8",
       "resolved": "https://registry.npmjs.o
rg/@types/estree/-/estree-1.0.8.tgz",
@@ -1587,6 +1613,129 @@
       "integrity": "sha512-WmoN8qaIAo7WTYWb
AZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHq
jDfX8Ft5j4YzDM23f87zBfDe9g==",
       "license": "ISC"
     },
+    "node_modules/@vitest/expect": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.o
rg/@vitest/expect/-/expect-4.1.10.tgz",
+      "integrity": "sha512-YsCn+qAk1GWjQOWF
EsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7i
QB3HGTXkdBwGddzxPlmiql5vlA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@standard-schema/spec": "^1.1.0",
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "chai": "^6.2.2",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/
vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.o
rg/@vitest/mocker/-/mocker-4.1.10.tgz",
+      "integrity": "sha512-v0xaezt+DKEmKfax
g133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivw
V7sWy5fqn3rH5uOrSp07bp44Ow==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "4.1.10",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.21"
+      },
+      "funding": {
+        "url": "https://opencollective.com/
vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0
"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/mocker/node_modul
es/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.o
rg/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6Zu
XmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC
5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.o
rg/@vitest/pretty-format/-/pretty-format-4.1
.10.tgz",
+      "integrity": "sha512-W1HsjSH4MXQ9Yfmm
hLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaV
CFO+JDJMrAJGwa5pRpNpvy4P8Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/
vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.o
rg/@vitest/runner/-/runner-4.1.10.tgz",
+      "integrity": "sha512-IKI6kpIH+LmpROpl
yLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kR
vSEMD870Vd91rSSw0eegtWyLGg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "4.1.10",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/
vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.o
rg/@vitest/snapshot/-/snapshot-4.1.10.tgz",
+      "integrity": "sha512-xRkfOT1qpTAi/Ti4
Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQq
jUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "magic-string": "^0.30.21",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/
vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.o
rg/@vitest/spy/-/spy-4.1.10.tgz",
+      "integrity": "sha512-PLf/Ugvoq5wO/b4r
wYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+
l+cj7mNwAfZHzqXqDyjvOuwbDw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://opencollective.com/
vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.o
rg/@vitest/utils/-/utils-4.1.10.tgz",
+      "integrity": "sha512-fy9am/HWxbaGt/Sa
wrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ
7vKfPTwo0diXifkpPvwH9v7nGA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "4.1.10",
+        "convert-source-map": "^2.0.0",
+        "tinyrainbow": "^3.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/
vitest"
+      }
+    },
     "node_modules/anymatch": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.o
rg/anymatch/-/anymatch-3.1.3.tgz",
@@ -1637,6 +1786,16 @@
         "url": "https://github.com/sponsors
/wooorm"
       }
     },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.o
rg/assertion-error/-/assertion-error-2.0.1.t
gz",
+      "integrity": "sha512-Izi8RQcffqCeNVgF
igKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQ
gmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/astro": {
       "version": "6.1.9",
       "resolved": "https://registry.npmjs.o
rg/astro/-/astro-6.1.9.tgz",
@@ -2206,6 +2365,16 @@
         "url": "https://github.com/sponsors
/wooorm"
       }
     },
+    "node_modules/chai": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.o
rg/chai/-/chai-6.2.2.tgz",
+      "integrity": "sha512-NUPRluOfOiTKBKvW
PtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tW
hR4WPjNeR9jXqdC7/UpSJTnlRg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/character-entities": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.o
rg/character-entities/-/character-entities-2
.0.2.tgz",
@@ -2303,6 +2472,13 @@
         "node": ">= 18"
       }
     },
+    "node_modules/convert-source-map": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.o
rg/convert-source-map/-/convert-source-map-2
.0.0.tgz",
+      "integrity": "sha512-Kvp459HrV2FEJ1CA
si1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61
m0xktWwt+1HSYf3JZsTms3aRJg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/cookie": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.o
rg/cookie/-/cookie-1.1.1.tgz",
@@ -2618,6 +2794,16 @@
       "integrity": "sha512-mlsTRyGaPBjPedk6
Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS
85f93veegeCPkL8R4GLClnjLFw==",
       "license": "MIT"
     },
+    "node_modules/expect-type": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.o
rg/expect-type/-/expect-type-1.4.0.tgz",
+      "integrity": "sha512-KfYbmpRm0VbLjEvV
a9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFI
xlGsgBQizV9q5J9NHj4VG0n+pA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
     "node_modules/extend": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.o
rg/extend/-/extend-3.0.2.tgz",
@@ -4131,6 +4317,13 @@
         "url": "https://github.com/inikulin
/parse5?sponsor=1"
       }
     },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.o
rg/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe
+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADw
spS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/piccolore": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.o
rg/piccolore/-/piccolore-0.1.3.tgz",
@@ -4628,6 +4821,13 @@
         "node": ">=20"
       }
     },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.o
rg/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWX
ZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwP
dAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/sisteransi": {
       "version": "1.0.5",
       "resolved": "https://registry.npmjs.o
rg/sisteransi/-/sisteransi-1.0.5.tgz",
@@ -4665,6 +4865,20 @@
         "url": "https://github.com/sponsors
/wooorm"
       }
     },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.o
rg/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/
7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1
P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/std-env": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.o
rg/std-env/-/std-env-4.2.0.tgz",
+      "integrity": "sha512-oCUKSupKTHX53Eyj
DtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUG
U9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/stringify-entities": {
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.o
rg/stringify-entities/-/stringify-entities-4
.0.4.tgz",
@@ -4710,6 +4924,13 @@
       "integrity": "sha512-pkY1fj1cKHb2seWD
y0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9
CF0yXv/QZFY7sEJmj24dK+Rrqw==",
       "license": "MIT"
     },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.o
rg/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6
lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxox
QjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/tinyclip": {
       "version": "0.1.12",
       "resolved": "https://registry.npmjs.o
rg/tinyclip/-/tinyclip-0.1.12.tgz",
@@ -4744,6 +4965,16 @@
         "url": "https://github.com/sponsors
/SuperchupuDev"
       }
     },
+    "node_modules/tinyrainbow": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.o
rg/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+      "integrity": "sha512-Bf+ILmBgretUrdJx
zXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kY
DipTBcLrCVyUzAPz7QmArb0mmw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
     "node_modules/trim-lines": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.o
rg/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -5650,6 +5881,96 @@
         }
       }
     },
+    "node_modules/vitest": {
+      "version": "4.1.10",
+      "resolved": "https://registry.npmjs.o
rg/vitest/-/vitest-4.1.10.tgz",
+      "integrity": "sha512-R9jUTe5S4Qb0HCd4
TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9A
bqSk6nt0PnOx4H4WNJYZ13GUPw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/expect": "4.1.10",
+        "@vitest/mocker": "4.1.10",
+        "@vitest/pretty-format": "4.1.10",
+        "@vitest/runner": "4.1.10",
+        "@vitest/snapshot": "4.1.10",
+        "@vitest/spy": "4.1.10",
+        "@vitest/utils": "4.1.10",
+        "es-module-lexer": "^2.0.0",
+        "expect-type": "^1.3.0",
+        "magic-string": "^0.30.21",
+        "obug": "^2.1.1",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.3",
+        "std-env": "^4.0.0-rc.1",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^1.0.2",
+        "tinyglobby": "^0.2.15",
+        "tinyrainbow": "^3.1.0",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0
",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^20.0.0 || ^22.0.0 || >=24
.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/
vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@opentelemetry/api": "^1.9.0",
+        "@types/node": "^20.0.0 || ^22.0.0 
|| >=24.0.0",
+        "@vitest/browser-playwright": "4.1.
10",
+        "@vitest/browser-preview": "4.1.10"
,
+        "@vitest/browser-webdriverio": "4.1
.10",
+        "@vitest/coverage-istanbul": "4.1.1
0",
+        "@vitest/coverage-v8": "4.1.10",
+        "@vitest/ui": "4.1.10",
+        "happy-dom": "*",
+        "jsdom": "*",
+        "vite": "^6.0.0 || ^7.0.0 || ^8.0.0
"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@opentelemetry/api": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser-playwright": {
+          "optional": true
+        },
+        "@vitest/browser-preview": {
+          "optional": true
+        },
+        "@vitest/browser-webdriverio": {
+          "optional": true
+        },
+        "@vitest/coverage-istanbul": {
+          "optional": true
+        },
+        "@vitest/coverage-v8": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        },
+        "vite": {
+          "optional": false
+        }
+      }
+    },
     "node_modules/web-namespaces": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.o
rg/web-namespaces/-/web-namespaces-2.0.1.tgz
",
@@ -5669,6 +5990,23 @@
         "node": ">=4"
       }
     },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.o
rg/why-is-node-running/-/why-is-node-running
-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqn
yh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7Xvlmtbw
uTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/xxhash-wasm": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.o
rg/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz",
 

diff --git a/ooknet-design/package.json b/ooknet-design/package.json
index e845ae1..78b118d 100644
--- a/ooknet-design/package.json
+++ b/ooknet-design/package.json
@@ -9,7 +9,8 @@
     "dev": "astro dev",
     "build": "astro build",
     "preview": "astro preview",
-    "astro": "astro"
+    "astro": "astro",
+    "test": "vitest run"
   },
   "dependencies": {
     "@fontsource/cascadia-mono": "^5.2.3",
@@ -18,6 +19,7 @@
     "fuse.js": "^7.3.0"
   },
   "devDependencies": {
-    "sass": "^1.99.0"
+    "sass": "^1.99.0",
+    "vitest": "^4.1.10"
   }
 }

diff --git a/ooknet-design/package.json b/oo
knet-design/package.json
index e845ae1..78b118d 100644
--- a/ooknet-design/package.json
+++ b/ooknet-design/package.json
@@ -9,7 +9,8 @@
     "dev": "astro dev",
     "build": "astro build",
     "preview": "astro preview",
-    "astro": "astro"
+    "astro": "astro",
+    "test": "vitest run"
   },
   "dependencies": {
     "@fontsource/cascadia-mono": "^5.2.3",
@@ -18,6 +19,7 @@
     "fuse.js": "^7.3.0"
   },
   "devDependencies": {
-    "sass": "^1.99.0"
+    "sass": "^1.99.0",
+    "vitest": "^4.1.10"
   }
 }
 

diff --git a/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts b/ooknet-desi
gn/src/components/ArticleMeta/ArticleMeta.ts
index fe2667d..8f60dc9 100644
--- a/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts
+++ b/ooknet-design/src/components/ArticleMeta/ArticleMeta.ts
@@ -7,16 +7,28 @@ export interface Props {
   path: string;
 }
 
+// The path can exceed the narrow frame; char-wrap it under the label.
+function filedUnder(path: string, width: number): string[] {
+  const label = "[FILED UNDER]: ";
+  const avail = Math.max(1, width - len(label));
+  const cps = [...`/${path}`];
+  const out = [label + cps.slice(0, avail).join("")];
+  for (let i = avail; i < cps.length; i += avail) {
+    out.push(" ".repeat(len(label)) + cps.slice(i, i + avail).join(""));
+  }
+  return out;
+}
+
 export function buildMeta(width: number, fm: NoteFrontmatter, path: string): stri
ng {
   // Below ~60 cells the JSON-style META block can't fit (the tags array
   // alone is ~40 chars), so narrow frames drop it.
   const compact = width < 60;
 
   const lines: string[] = [
-    `[FILED UNDER]: /${path}`,
+    ...filedUnder(path, width),
     rule(width),
     "",
-    kv("AUTHOR",    fm.authors.join(", "), 12, width),
+    kv("AUTHOR",    fm.authors.join(", ") || "UNATTRIBUTED", 12, width),
     kv("PUBLISHED", fm.published,           12, width),
     kv("STATUS",    fm.status,              12, width),
   ];

diff --git a/ooknet-design/src/components/Ar
ticleMeta/ArticleMeta.ts b/ooknet-design/src
/components/ArticleMeta/ArticleMeta.ts
index fe2667d..8f60dc9 100644
--- a/ooknet-design/src/components/ArticleMe
ta/ArticleMeta.ts
+++ b/ooknet-design/src/components/ArticleMe
ta/ArticleMeta.ts
@@ -7,16 +7,28 @@ export interface Props {
   path: string;
 }
 
+// The path can exceed the narrow frame; ch
ar-wrap it under the label.
+function filedUnder(path: string, width: nu
mber): string[] {
+  const label = "[FILED UNDER]: ";
+  const avail = Math.max(1, width - len(lab
el));
+  const cps = [...`/${path}`];
+  const out = [label + cps.slice(0, avail).
join("")];
+  for (let i = avail; i < cps.length; i += 
avail) {
+    out.push(" ".repeat(len(label)) + cps.s
lice(i, i + avail).join(""));
+  }
+  return out;
+}
+
 export function buildMeta(width: number, fm
: NoteFrontmatter, path: string): string {
   // Below ~60 cells the JSON-style META bl
ock can't fit (the tags array
   // alone is ~40 chars), so narrow frames 
drop it.
   const compact = width < 60;
 
   const lines: string[] = [
-    `[FILED UNDER]: /${path}`,
+    ...filedUnder(path, width),
     rule(width),
     "",
-    kv("AUTHOR",    fm.authors.join(", "), 
12, width),
+    kv("AUTHOR",    fm.authors.join(", ") |
| "UNATTRIBUTED", 12, width),
     kv("PUBLISHED", fm.published,          
 12, width),
     kv("STATUS",    fm.status,             
 12, width),
   ];
 

diff --git a/ooknet-design/src/components/ArticleTags/ArticleTags.test.ts b/ooknet
-design/src/components/ArticleTags/ArticleTags.test.ts
new file mode 100644
index 0000000..aa37711
--- /dev/null
+++ b/ooknet-design/src/components/ArticleTags/ArticleTags.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from "vitest";
+import { layoutTags, TAGS_LABEL_W, TAG_GAP } from "./ArticleTags";
+import { badgeWidth } from "../Badge/Badge";
+
+describe("layoutTags", () => {
+  it("keeps tags on one row when they fit", () => {
+    expect(layoutTags(["A", "B"], 48)).toEqual([["A", "B"]]);
+  });
+
+  it("wraps to a new row when the frame width is exceeded", () => {
+    const rows = layoutTags(["DESIGN-SYSTEM", "REFERENCE", "TYPOGRAPHY"], 48);
+    expect(rows.length).toBeGreaterThan(1);
+    for (const row of rows) {
+      const used = row.reduce(
+        (s, t, i) => s + badgeWidth(t) + (i > 0 ? TAG_GAP : 0),
+        0,
+      );
+      expect(used).toBeLessThanOrEqual(48 - TAGS_LABEL_W);
+    }
+  });
+
+  it("emits an overlong single tag on its own row rather than dropping it", () =>
 {
+    const huge = "X".repeat(60);
+    expect(layoutTags([huge, "OK"], 48)).toEqual([[huge], ["OK"]]);
+  });
+
+  it("returns no rows for no tags", () => {
+    expect(layoutTags([], 48)).toEqual([]);
+  });
+});

diff --git a/ooknet-design/src/components/Ar
ticleTags/ArticleTags.test.ts b/ooknet-desig
n/src/components/ArticleTags/ArticleTags.tes
t.ts
new file mode 100644
index 0000000..aa37711
--- /dev/null
+++ b/ooknet-design/src/components/ArticleTa
gs/ArticleTags.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from "vites
t";
+import { layoutTags, TAGS_LABEL_W, TAG_GAP 
} from "./ArticleTags";
+import { badgeWidth } from "../Badge/Badge"
;
+
+describe("layoutTags", () => {
+  it("keeps tags on one row when they fit",
 () => {
+    expect(layoutTags(["A", "B"], 48)).toEq
ual([["A", "B"]]);
+  });
+
+  it("wraps to a new row when the frame wid
th is exceeded", () => {
+    const rows = layoutTags(["DESIGN-SYSTEM
", "REFERENCE", "TYPOGRAPHY"], 48);
+    expect(rows.length).toBeGreaterThan(1);
+    for (const row of rows) {
+      const used = row.reduce(
+        (s, t, i) => s + badgeWidth(t) + (i
 > 0 ? TAG_GAP : 0),
+        0,
+      );
+      expect(used).toBeLessThanOrEqual(48 -
 TAGS_LABEL_W);
+    }
+  });
+
+  it("emits an overlong single tag on its o
wn row rather than dropping it", () => {
+    const huge = "X".repeat(60);
+    expect(layoutTags([huge, "OK"], 48)).to
Equal([[huge], ["OK"]]);
+  });
+
+  it("returns no rows for no tags", () => {
+    expect(layoutTags([], 48)).toEqual([]);
+  });
+});
 

diff --git a/ooknet-design/src/components/Frame/Frame.astro b/ooknet-design/src/co
mponents/Frame/Frame.astro
index 5e70b40..a9ae51b 100644
--- a/ooknet-design/src/components/Frame/Frame.astro
+++ b/ooknet-design/src/components/Frame/Frame.astro
@@ -1,7 +1,10 @@
 ---
 // Frame - renders the same ASCII builder at both desktop (FRAME_W) and
 // mobile (MOBILE_FRAME_W) widths, then CSS toggles which one is visible.
+// Every frame passes through checkGrid, so an overflowing line or an
+// off-grid glyph fails the build instead of bending a border.
 import Pre from "../Pre/Pre.astro";
+import { checkGrid } from "../../lib/ascii";
 import { FRAME_W, MOBILE_FRAME_W } from "../../lib/config";
 
 interface Props {
@@ -10,6 +13,7 @@ interface Props {
 }
 
 const { build, class: cls = "" } = Astro.props as Props;
+const ctx = `Frame${cls ? `(${cls})` : ""} on ${Astro.url.pathname}`;
 ---
-<Pre class={`frame-wide ${cls}`.trim()}>{build(FRAME_W)}</Pre>
-<Pre class={`frame-narrow ${cls}`.trim()}>{build(MOBILE_FRAME_W)}</Pre>
+<Pre class={`frame-wide ${cls}`.trim()}>{checkGrid(build(FRAME_W), FRAME_W, ctx)}
</Pre>
+<Pre class={`frame-narrow ${cls}`.trim()}>{checkGrid(build(MOBILE_FRAME_W), MOBIL
E_FRAME_W, ctx)}</Pre>

diff --git a/ooknet-design/src/components/Fr
ame/Frame.astro b/ooknet-design/src/componen
ts/Frame/Frame.astro
index 5e70b40..a9ae51b 100644
--- a/ooknet-design/src/components/Frame/Fra
me.astro
+++ b/ooknet-design/src/components/Frame/Fra
me.astro
@@ -1,7 +1,10 @@
 ---
 // Frame - renders the same ASCII builder a
t both desktop (FRAME_W) and
 // mobile (MOBILE_FRAME_W) widths, then CSS
 toggles which one is visible.
+// Every frame passes through checkGrid, so
 an overflowing line or an
+// off-grid glyph fails the build instead o
f bending a border.
 import Pre from "../Pre/Pre.astro";
+import { checkGrid } from "../../lib/ascii"
;
 import { FRAME_W, MOBILE_FRAME_W } from "..
/../lib/config";
 
 interface Props {
@@ -10,6 +13,7 @@ interface Props {
 }
 
 const { build, class: cls = "" } = Astro.pr
ops as Props;
+const ctx = `Frame${cls ? `(${cls})` : ""} 
on ${Astro.url.pathname}`;
 ---
-<Pre class={`frame-wide ${cls}`.trim()}>{bu
ild(FRAME_W)}</Pre>
-<Pre class={`frame-narrow ${cls}`.trim()}>{
build(MOBILE_FRAME_W)}</Pre>
+<Pre class={`frame-wide ${cls}`.trim()}>{ch
eckGrid(build(FRAME_W), FRAME_W, ctx)}</Pre>
+<Pre class={`frame-narrow ${cls}`.trim()}>{
checkGrid(build(MOBILE_FRAME_W), MOBILE_FRAM
E_W, ctx)}</Pre>
 

diff --git a/ooknet-design/src/content.config.ts b/ooknet-design/src/content.confi
g.ts
index f928ef0..ce664a6 100644
--- a/ooknet-design/src/content.config.ts
+++ b/ooknet-design/src/content.config.ts
@@ -1,23 +1,29 @@
 import { defineCollection, z } from "astro:content";
 import { glob } from "astro/loaders";
 
+// Only title, note, and published are required to author a note; the
+// rest defaults so a bare draft can't crash the content sync.
 const notes = defineCollection({
   loader: glob({ pattern: "**/*.md", base: "./src/content/notes" }),
   schema: z.object({
     title: z.string(),
     note: z.string(),                       // e.g. "N217"
+    published: z.string(),                  // ISO date
     volume: z.string().optional(),
     date: z.string().optional(),
-    authors: z.array(z.string()),
-    published: z.string(),                  // ISO date
-    status: z.string(),
-    meta: z.object({
-      source: z.string(),
-      category: z.string(),
-      tags: z.array(z.string()),
-      version: z.string(),
-    }),
-    filed: z.string(),
+    authors: z.array(z.string()).default([]),
+    status: z.string().default("DRAFT"),
+    meta: z
+      .object({
+        source: z.string().default("ooknet"),
+        category: z.string().default("uncategorized"),
+        tags: z.array(z.string()).default([]),
+        version: z.string().default("v0.1"),
+      })
+      // zod v4 uses defaults as-is without re-parsing, so the object
+      // default must be spelled out in full.
+      .default({ source: "ooknet", category: "uncategorized", tags: [], version: 
"v0.1" }),
+    filed: z.string().default("/UNFILED"),
     page: z.number().optional(),
     seeAlso: z.array(z.string()).optional(),
     draft: z.boolean().default(false),

diff --git a/ooknet-design/src/content.confi
g.ts b/ooknet-design/src/content.config.ts
index f928ef0..ce664a6 100644
--- a/ooknet-design/src/content.config.ts
+++ b/ooknet-design/src/content.config.ts
@@ -1,23 +1,29 @@
 import { defineCollection, z } from "astro:
content";
 import { glob } from "astro/loaders";
 
+// Only title, note, and published are requ
ired to author a note; the
+// rest defaults so a bare draft can't cras
h the content sync.
 const notes = defineCollection({
   loader: glob({ pattern: "**/*.md", base: 
"./src/content/notes" }),
   schema: z.object({
     title: z.string(),
     note: z.string(),                      
 // e.g. "N217"
+    published: z.string(),                 
 // ISO date
     volume: z.string().optional(),
     date: z.string().optional(),
-    authors: z.array(z.string()),
-    published: z.string(),                 
 // ISO date
-    status: z.string(),
-    meta: z.object({
-      source: z.string(),
-      category: z.string(),
-      tags: z.array(z.string()),
-      version: z.string(),
-    }),
-    filed: z.string(),
+    authors: z.array(z.string()).default([]
),
+    status: z.string().default("DRAFT"),
+    meta: z
+      .object({
+        source: z.string().default("ooknet"
),
+        category: z.string().default("uncat
egorized"),
+        tags: z.array(z.string()).default([
]),
+        version: z.string().default("v0.1")
,
+      })
+      // zod v4 uses defaults as-is without
 re-parsing, so the object
+      // default must be spelled out in ful
l.
+      .default({ source: "ooknet", category
: "uncategorized", tags: [], version: "v0.1"
 }),
+    filed: z.string().default("/UNFILED"),
     page: z.number().optional(),
     seeAlso: z.array(z.string()).optional()
,
     draft: z.boolean().default(false),
 

diff --git a/ooknet-design/src/lib/ascii.test.ts b/ooknet-design/src/lib/ascii.tes
t.ts
new file mode 100644
index 0000000..87c58fd
--- /dev/null
+++ b/ooknet-design/src/lib/ascii.test.ts
@@ -0,0 +1,103 @@
+import { describe, expect, it } from "vitest";
+import { G, box, center, checkGrid, kv, len, pad, row2, row3, rule, ruleD, wrap }
 from "./ascii";
+
+describe("len", () => {
+  it("counts codepoints, not UTF-16 units", () => {
+    expect(len("abc")).toBe(3);
+    expect(len("?")).toBe(1);
+    expect(len("")).toBe(0);
+    expect(len(null)).toBe(0);
+    expect(len(undefined)).toBe(0);
+  });
+});
+
+describe("pad", () => {
+  it("pads end by default", () => {
+    expect(pad("ab", 5)).toBe("ab   ");
+  });
+  it("pads start", () => {
+    expect(pad("ab", 5, "start")).toBe("   ab");
+  });
+  it("centers with the extra cell on the right", () => {
+    expect(pad("ab", 5, "center")).toBe(" ab  ");
+  });
+  it("truncates overlong input at codepoint boundaries", () => {
+    expect(pad("abcdef", 3)).toBe("abc");
+    expect(pad("???", 2)).toBe("??");
+  });
+});
+
+describe("rules", () => {
+  it("rule and ruleD repeat to exact width", () => {
+    expect(rule(4)).toBe("────");
+    expect(ruleD(3)).toBe("═══");
+  });
+});
+
+describe("box", () => {
+  it("emits every line at exactly the box width", () => {
+    const b = box({ width: 12, lines: ["hi", "a longer li"], title: "T" });
+    for (const line of b.split("\n")) expect(len(line)).toBe(12);
+  });
+  it("draws corners and title separator", () => {
+    const lines = box({ width: 8, lines: ["x"], title: "T" }).split("\n");
+    expect(lines[0]).toBe(G.TL + "──────" + G.TR);
+    expect(lines[2]).toBe(G.LJ + "──────" + G.RJ);
+    expect(lines.at(-1)).toBe(G.BL + "──────" + G.BR);
+  });
+  it("centers content when asked", () => {
+    const lines = box({ width: 8, lines: ["ab"], align: "center" }).split("\n");
+    expect(lines[1]).toBe("│  ab  │");
+  });
+});
+
+describe("rows", () => {
+  it("kv pads key and value to the total width", () => {
+    expect(kv("K", "V", 4, 10)).toBe("K   V     ");
+  });
+  it("row2 fills the gap to exact width", () => {
+    const r = row2("LEFT", "RIGHT", 20);
+    expect(len(r)).toBe(20);
+    expect(r.startsWith("LEFT")).toBe(true);
+    expect(r.endsWith("RIGHT")).toBe(true);
+  });
+  it("row3 places left, center, right in exact width", () => {
+    const r = row3("L", "CC", "R", 12);
+    expect(len(r)).toBe(12);
+    expect(r[0]).toBe("L");
+    expect(r[11]).toBe("R");
+    expect(r).toContain("CC");
+  });
+  it("center returns exact width", () => {
+    expect(center("ab", 6)).toBe("  ab  ");
+  });
+});
+
+describe("wrap", () => {
+  it("wraps greedily at word boundaries", () => {
+    expect(wrap("aa bb cc", 5)).toBe("aa bb\ncc");
+  });
+  it("leaves a too-long word on its own line rather than splitting it", () => {
+    expect(wrap("abcdefgh xy", 4)).toBe("abcdefgh\nxy");
+  });
+  it("returns single line when everything fits", () => {
+    expect(wrap("aa bb", 10)).toBe("aa bb");
+  });
+});
+
+describe("checkGrid", () => {
+  it("accepts frame glyphs, extras, latin-1, and ASCII", () => {
+    const ok = "│ ok caf? - ? - ↑↓ ╌ ═ ┼ │";
+    expect(checkGrid(ok, 40, "t")).toBe(ok);
+  });
+  it("throws on lines exceeding the width", () => {
+    expect(() => checkGrid("abcdef", 5, "t")).toThrow(/6 cells, max 5/);
+  });
+  it("throws on off-grid characters with the codepoint named", () => {
+    expect(() => checkGrid("ok ?", 10, "t")).toThrow(/U\+1F44D/);
+    expect(() => checkGrid("??", 10, "t")).toThrow(/off-grid/);
+  });
+  it("rejects glyphs the font subset lacks, naming the context", () => {
+    expect(() => checkGrid("a ? b", 10, "my-frame")).toThrow(/my-frame/);
+  });
+});

diff --git a/ooknet-design/src/lib/ascii.tes
t.ts b/ooknet-design/src/lib/ascii.test.ts
new file mode 100644
index 0000000..87c58fd
--- /dev/null
+++ b/ooknet-design/src/lib/ascii.test.ts
@@ -0,0 +1,103 @@
+import { describe, expect, it } from "vites
t";
+import { G, box, center, checkGrid, kv, len
, pad, row2, row3, rule, ruleD, wrap } from 
"./ascii";
+
+describe("len", () => {
+  it("counts codepoints, not UTF-16 units",
 () => {
+    expect(len("abc")).toBe(3);
+    expect(len("?")).toBe(1);
+    expect(len("")).toBe(0);
+    expect(len(null)).toBe(0);
+    expect(len(undefined)).toBe(0);
+  });
+});
+
+describe("pad", () => {
+  it("pads end by default", () => {
+    expect(pad("ab", 5)).toBe("ab   ");
+  });
+  it("pads start", () => {
+    expect(pad("ab", 5, "start")).toBe("   
ab");
+  });
+  it("centers with the extra cell on the ri
ght", () => {
+    expect(pad("ab", 5, "center")).toBe(" a
b  ");
+  });
+  it("truncates overlong input at codepoint
 boundaries", () => {
+    expect(pad("abcdef", 3)).toBe("abc");
+    expect(pad("???", 2)).toBe("??");
+  });
+});
+
+describe("rules", () => {
+  it("rule and ruleD repeat to exact width"
, () => {
+    expect(rule(4)).toBe("────");
+    expect(ruleD(3)).toBe("═══");
+  });
+});
+
+describe("box", () => {
+  it("emits every line at exactly the box w
idth", () => {
+    const b = box({ width: 12, lines: ["hi"
, "a longer li"], title: "T" });
+    for (const line of b.split("\n")) expec
t(len(line)).toBe(12);
+  });
+  it("draws corners and title separator", (
) => {
+    const lines = box({ width: 8, lines: ["
x"], title: "T" }).split("\n");
+    expect(lines[0]).toBe(G.TL + "──────" +
 G.TR);
+    expect(lines[2]).toBe(G.LJ + "──────" +
 G.RJ);
+    expect(lines.at(-1)).toBe(G.BL + "─────
─" + G.BR);
+  });
+  it("centers content when asked", () => {
+    const lines = box({ width: 8, lines: ["
ab"], align: "center" }).split("\n");
+    expect(lines[1]).toBe("│  ab  │");
+  });
+});
+
+describe("rows", () => {
+  it("kv pads key and value to the total wi
dth", () => {
+    expect(kv("K", "V", 4, 10)).toBe("K   V
     ");
+  });
+  it("row2 fills the gap to exact width", (
) => {
+    const r = row2("LEFT", "RIGHT", 20);
+    expect(len(r)).toBe(20);
+    expect(r.startsWith("LEFT")).toBe(true)
;
+    expect(r.endsWith("RIGHT")).toBe(true);
+  });
+  it("row3 places left, center, right in ex
act width", () => {
+    const r = row3("L", "CC", "R", 12);
+    expect(len(r)).toBe(12);
+    expect(r[0]).toBe("L");
+    expect(r[11]).toBe("R");
+    expect(r).toContain("CC");
+  });
+  it("center returns exact width", () => {
+    expect(center("ab", 6)).toBe("  ab  ");
+  });
+});
+
+describe("wrap", () => {
+  it("wraps greedily at word boundaries", (
) => {
+    expect(wrap("aa bb cc", 5)).toBe("aa bb
\ncc");
+  });
+  it("leaves a too-long word on its own lin
e rather than splitting it", () => {
+    expect(wrap("abcdefgh xy", 4)).toBe("ab
cdefgh\nxy");
+  });
+  it("returns single line when everything f
its", () => {
+    expect(wrap("aa bb", 10)).toBe("aa bb")
;
+  });
+});
+
+describe("checkGrid", () => {
+  it("accepts frame glyphs, extras, latin-1
, and ASCII", () => {
+    const ok = "│ ok caf? - ? - ↑↓ ╌ ═ ┼ │"
;
+    expect(checkGrid(ok, 40, "t")).toBe(ok)
;
+  });
+  it("throws on lines exceeding the width",
 () => {
+    expect(() => checkGrid("abcdef", 5, "t"
)).toThrow(/6 cells, max 5/);
+  });
+  it("throws on off-grid characters with th
e codepoint named", () => {
+    expect(() => checkGrid("ok ?", 10, "t")
).toThrow(/U\+1F44D/);
+    expect(() => checkGrid("??", 10, "t")).
toThrow(/off-grid/);
+  });
+  it("rejects glyphs the font subset lacks,
 naming the context", () => {
+    expect(() => checkGrid("a ? b", 10, "my
-frame")).toThrow(/my-frame/);
+  });
+});
 

diff --git a/ooknet-design/src/lib/ascii.ts b/ooknet-design/src/lib/ascii.ts
index 466add5..afdd52d 100644
--- a/ooknet-design/src/lib/ascii.ts
+++ b/ooknet-design/src/lib/ascii.ts
@@ -11,6 +11,30 @@ export const G = {
 // pairs otherwise.
 export const len = (s: unknown): number => [...String(s ?? "")].length;
 
+// Codepoints known to render exactly one cell wide in the frame font
+// (fontsource Cascadia Mono, latin subset): printable ASCII + tab,
+// Latin-1 supplement, box drawing, and the measured extras - - - ...  ↑ ↓.
+// Anything else - emoji, CJK, glyphs the subset lacks (?, ?, ?) - bends
+// a frame border. Measure a new glyph against "0" in the browser before
+// adding it here.
+const ON_GRID = /^[\t\x20-\x7E\u00A0-\u00FF\u2013\u2014\u2022\u2026\u2122\u2191\u
2193\u2500-\u257F]$/u;
+
+/** Throws unless every line of `text` fits `maxW` cells and uses only
+ *  on-grid glyphs. Returns `text` so call sites can wrap in place. */
+export function checkGrid(text: string, maxW: number, ctx: string): string {
+  for (const line of text.split("\n")) {
+    if (len(line) > maxW) {
+      throw new Error(`[ascii] ${ctx}: line is ${len(line)} cells, max ${maxW}: "
${[...line].slice(0, 40).join("")}..."`);
+    }
+    const bad = [...line].find((c) => !ON_GRID.test(c));
+    if (bad !== undefined) {
+      const cp = bad.codePointAt(0)!.toString(16).toUpperCase().padStart(4, "0");
+      throw new Error(`[ascii] ${ctx}: off-grid character "${bad}" (U+${cp}). If 
it measures one cell wide in the frame font, add it to ON_GRID in ascii.ts.`);
+    }
+  }
+  return text;
+}
+
 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 466add5..afdd52d 100644
--- a/ooknet-design/src/lib/ascii.ts
+++ b/ooknet-design/src/lib/ascii.ts
@@ -11,6 +11,30 @@ export const G = {
 // pairs otherwise.
 export const len = (s: unknown): number => 
[...String(s ?? "")].length;
 
+// Codepoints known to render exactly one c
ell wide in the frame font
+// (fontsource Cascadia Mono, latin subset)
: printable ASCII + tab,
+// Latin-1 supplement, box drawing, and the
 measured extras - - - ...  ↑ ↓.
+// Anything else - emoji, CJK, glyphs the s
ubset lacks (?, ?, ?) - bends
+// a frame border. Measure a new glyph agai
nst "0" in the browser before
+// adding it here.
+const ON_GRID = /^[\t\x20-\x7E\u00A0-\u00FF
\u2013\u2014\u2022\u2026\u2122\u2191\u2193\u
2500-\u257F]$/u;
+
+/** Throws unless every line of `text` fits
 `maxW` cells and uses only
+ *  on-grid glyphs. Returns `text` so call 
sites can wrap in place. */
+export function checkGrid(text: string, max
W: number, ctx: string): string {
+  for (const line of text.split("\n")) {
+    if (len(line) > maxW) {
+      throw new Error(`[ascii] ${ctx}: line
 is ${len(line)} cells, max ${maxW}: "${[...
line].slice(0, 40).join("")}..."`);
+    }
+    const bad = [...line].find((c) => !ON_G
RID.test(c));
+    if (bad !== undefined) {
+      const cp = bad.codePointAt(0)!.toStri
ng(16).toUpperCase().padStart(4, "0");
+      throw new Error(`[ascii] ${ctx}: off-
grid character "${bad}" (U+${cp}). If it mea
sures one cell wide in the frame font, add i
t to ON_GRID in ascii.ts.`);
+    }
+  }
+  return text;
+}
+
 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/notes.test.ts b/ooknet-design/src/lib/notes.tes
t.ts
new file mode 100644
index 0000000..cf0bcea
--- /dev/null
+++ b/ooknet-design/src/lib/notes.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from "vitest";
+import { byNoteNum, noteNum, published } from "./notes";
+
+describe("noteNum", () => {
+  it("extracts the number from a note id", () => {
+    expect(noteNum("N217")).toBe(217);
+    expect(noteNum("N0")).toBe(0);
+    expect(noteNum("VOLUME 04 / NOTE 218")).toBe(4218);
+  });
+  it("falls back to 0 with no digits", () => {
+    expect(noteNum("DRAFT")).toBe(0);
+  });
+});
+
+describe("byNoteNum", () => {
+  it("sorts numerically, not lexicographically", () => {
+    const notes = ["N9", "N217", "N0", "N1000"].map((n) => ({ data: { note: n } }
));
+    expect(notes.sort(byNoteNum).map((n) => n.data.note)).toEqual(["N0", "N9", "N
217", "N1000"]);
+  });
+});
+
+describe("published", () => {
+  it("filters drafts", () => {
+    expect(published({ data: { draft: true } })).toBe(false);
+    expect(published({ data: { draft: false } })).toBe(true);
+    expect(published({ data: {} })).toBe(true);
+  });
+});

diff --git a/ooknet-design/src/lib/notes.tes
t.ts b/ooknet-design/src/lib/notes.test.ts
new file mode 100644
index 0000000..cf0bcea
--- /dev/null
+++ b/ooknet-design/src/lib/notes.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from "vites
t";
+import { byNoteNum, noteNum, published } fr
om "./notes";
+
+describe("noteNum", () => {
+  it("extracts the number from a note id", 
() => {
+    expect(noteNum("N217")).toBe(217);
+    expect(noteNum("N0")).toBe(0);
+    expect(noteNum("VOLUME 04 / NOTE 218"))
.toBe(4218);
+  });
+  it("falls back to 0 with no digits", () =
> {
+    expect(noteNum("DRAFT")).toBe(0);
+  });
+});
+
+describe("byNoteNum", () => {
+  it("sorts numerically, not lexicographica
lly", () => {
+    const notes = ["N9", "N217", "N0", "N10
00"].map((n) => ({ data: { note: n } }));
+    expect(notes.sort(byNoteNum).map((n) =>
 n.data.note)).toEqual(["N0", "N9", "N217", 
"N1000"]);
+  });
+});
+
+describe("published", () => {
+  it("filters drafts", () => {
+    expect(published({ data: { draft: true 
} })).toBe(false);
+    expect(published({ data: { draft: false
 } })).toBe(true);
+    expect(published({ data: {} })).toBe(tr
ue);
+  });
+});
 

diff --git a/ooknet-design/src/lib/notes.ts b/ooknet-design/src/lib/notes.ts
new file mode 100644
index 0000000..8cb12cb
--- /dev/null
+++ b/ooknet-design/src/lib/notes.ts
@@ -0,0 +1,13 @@
+// Pure note helpers. astro:content stays in the pages so this module
+// is unit-testable.
+
+export const published = ({ data }: { data: { draft?: boolean } }) => !data.draft
;
+
+export const noteNum = (note: string): number =>
+  parseInt(note.replace(/\D/g, ""), 10) || 0;
+
+type HasNote = { data: { note: string } };
+
+/** Ascending by note number: N0, N1, ..., N217. */
+export const byNoteNum = (a: HasNote, b: HasNote): number =>
+  noteNum(a.data.note) - noteNum(b.data.note);

diff --git a/ooknet-design/src/lib/notes.ts 
b/ooknet-design/src/lib/notes.ts
new file mode 100644
index 0000000..8cb12cb
--- /dev/null
+++ b/ooknet-design/src/lib/notes.ts
@@ -0,0 +1,13 @@
+// Pure note helpers. astro:content stays i
n the pages so this module
+// is unit-testable.
+
+export const published = ({ data }: { data:
 { draft?: boolean } }) => !data.draft;
+
+export const noteNum = (note: string): numb
er =>
+  parseInt(note.replace(/\D/g, ""), 10) || 
0;
+
+type HasNote = { data: { note: string } };
+
+/** Ascending by note number: N0, N1, ..., 
N217. */
+export const byNoteNum = (a: HasNote, b: Ha
sNote): number =>
+  noteNum(a.data.note) - noteNum(b.data.not
e);
 

diff --git a/ooknet-design/src/lib/rehype-ascii.test.ts b/ooknet-design/src/lib/re
hype-ascii.test.ts
new file mode 100644
index 0000000..b06a99e
--- /dev/null
+++ b/ooknet-design/src/lib/rehype-ascii.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from "vitest";
+import { buildAsciiTable, type Cell } from "./rehype-ascii";
+import { len } from "./ascii";
+
+const cell = (text: string, align: Cell["align"] = "end"): Cell => ({ text, align
 });
+
+const HEADER = [cell("NAME"), cell("VALUE")];
+
+describe("buildAsciiTable", () => {
+  it("emits every line at the same width, within the target", () => {
+    const t = buildAsciiTable([HEADER, [cell("a"), cell("b")]], 1, 40);
+    const widths = new Set(t.split("\n").map(len));
+    expect(widths.size).toBe(1);
+    expect([...widths][0]).toBeLessThanOrEqual(40);
+  });
+
+  it("keeps single-line tables compact (no row separators)", () => {
+    const t = buildAsciiTable([HEADER, [cell("a"), cell("b")], [cell("c"), cell("
d")]], 1, 40);
+    expect(t).not.toContain("╌");
+    // one solid separator under the header only
+    expect(t.split("\n").filter((l) => l.startsWith("├")).length).toBe(1);
+  });
+
+  it("adds dashed separators between body rows once any cell wraps", () => {
+    const long = "a description long enough that it must wrap onto several lines"
;
+    const t = buildAsciiTable(
+      [HEADER, [cell("frame"), cell(long)], [cell("select"), cell("short")]],
+      1,
+      30,
+    );
+    expect(t).toContain("╌");
+    const dashed = t.split("\n").filter((l) => l.includes("╌"));
+    expect(dashed.length).toBe(1); // between the two body rows
+  });
+
+  it("right-aligns cells marked align start", () => {
+    const t = buildAsciiTable([HEADER, [cell("x"), cell("42", "start")]], 1, 40);
+    const row = t.split("\n").find((l) => l.includes("42"))!;
+    expect(row).toMatch(/ 42 │$/);
+  });
+
+  it("shrinks the widest column until the table fits", () => {
+    const wide = "word ".repeat(30).trim();
+    const t = buildAsciiTable([HEADER, [cell("k"), cell(wide)]], 1, 48);
+    for (const line of t.split("\n")) expect(len(line)).toBeLessThanOrEqual(48);
+  });
+
+  it("pads ragged rows to the full column count", () => {
+    const t = buildAsciiTable([HEADER, [cell("only-one")]], 1, 40);
+    const widths = new Set(t.split("\n").map(len));
+    expect(widths.size).toBe(1);
+  });
+});

diff --git a/ooknet-design/src/lib/rehype-as
cii.test.ts b/ooknet-design/src/lib/rehype-a
scii.test.ts
new file mode 100644
index 0000000..b06a99e
--- /dev/null
+++ b/ooknet-design/src/lib/rehype-ascii.tes
t.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from "vites
t";
+import { buildAsciiTable, type Cell } from 
"./rehype-ascii";
+import { len } from "./ascii";
+
+const cell = (text: string, align: Cell["al
ign"] = "end"): Cell => ({ text, align });
+
+const HEADER = [cell("NAME"), cell("VALUE")
];
+
+describe("buildAsciiTable", () => {
+  it("emits every line at the same width, w
ithin the target", () => {
+    const t = buildAsciiTable([HEADER, [cel
l("a"), cell("b")]], 1, 40);
+    const widths = new Set(t.split("\n").ma
p(len));
+    expect(widths.size).toBe(1);
+    expect([...widths][0]).toBeLessThanOrEq
ual(40);
+  });
+
+  it("keeps single-line tables compact (no 
row separators)", () => {
+    const t = buildAsciiTable([HEADER, [cel
l("a"), cell("b")], [cell("c"), cell("d")]],
 1, 40);
+    expect(t).not.toContain("╌");
+    // one solid separator under the header
 only
+    expect(t.split("\n").filter((l) => l.st
artsWith("├")).length).toBe(1);
+  });
+
+  it("adds dashed separators between body r
ows once any cell wraps", () => {
+    const long = "a description long enough
 that it must wrap onto several lines";
+    const t = buildAsciiTable(
+      [HEADER, [cell("frame"), cell(long)],
 [cell("select"), cell("short")]],
+      1,
+      30,
+    );
+    expect(t).toContain("╌");
+    const dashed = t.split("\n").filter((l)
 => l.includes("╌"));
+    expect(dashed.length).toBe(1); // betwe
en the two body rows
+  });
+
+  it("right-aligns cells marked align start
", () => {
+    const t = buildAsciiTable([HEADER, [cel
l("x"), cell("42", "start")]], 1, 40);
+    const row = t.split("\n").find((l) => l
.includes("42"))!;
+    expect(row).toMatch(/ 42 │$/);
+  });
+
+  it("shrinks the widest column until the t
able fits", () => {
+    const wide = "word ".repeat(30).trim();
+    const t = buildAsciiTable([HEADER, [cel
l("k"), cell(wide)]], 1, 48);
+    for (const line of t.split("\n")) expec
t(len(line)).toBeLessThanOrEqual(48);
+  });
+
+  it("pads ragged rows to the full column c
ount", () => {
+    const t = buildAsciiTable([HEADER, [cel
l("only-one")]], 1, 40);
+    const widths = new Set(t.split("\n").ma
p(len));
+    expect(widths.size).toBe(1);
+  });
+});
 

diff --git a/ooknet-design/src/lib/rehype-ascii.ts b/ooknet-design/src/lib/rehype-
ascii.ts
index c9a4a9c..2a07ace 100644
--- a/ooknet-design/src/lib/rehype-ascii.ts
+++ b/ooknet-design/src/lib/rehype-ascii.ts
@@ -4,7 +4,7 @@
 // gets a head-rule. Everything emits both the desktop and mobile
 // variants in the same tree; CSS toggles visibility per viewport.
 import type { Root, Element, ElementContent, RootContent, Properties } from "hast
";
-import { G, len, pad, wrap, type PadSide } from "./ascii";
+import { G, checkGrid, len, pad, wrap, type PadSide } from "./ascii";
 import { FRAME_W, MOBILE_FRAME_W } from "./config";
 
 const FN_RE = /\b([a-zA-Z_]\w*)(?=\s*\()/g;
@@ -79,6 +79,7 @@ function buildBoxChildren(text: string, lang: string, frameW: nu
mber): ElementCo
   ];
 
   for (const line of lines) {
+    checkGrid(line, inner, "code block");
     const padLen = Math.max(0, inner - len(line));
     children.push(frame(`${G.V} `));
     let last = 0;
@@ -120,7 +121,7 @@ function makeCodePre(text: string, lang: string, frameW: numbe
r, extraClass: str
 
 // --------------------------------------------------------------- table
 
-type Cell = { text: string; align: PadSide };
+export type Cell = { text: string; align: PadSide };
 
 function tableCells(tbl: Element): { rows: Cell[][]; headerRows: number } {
   const rows: Cell[][] = [];
@@ -144,7 +145,7 @@ function tableCells(tbl: Element): { rows: Cell[][]; headerRow
s: number } {
   return { rows, headerRows };
 }
 
-function buildAsciiTable(rows: Cell[][], headerRows: number, width: number): stri
ng {
+export function buildAsciiTable(rows: Cell[][], headerRows: number, width: number
): string {
   const nCols = Math.max(...rows.map((r) => r.length));
   const norm = rows.map((r) => {
     const out = [...r];
@@ -185,7 +186,7 @@ function buildAsciiTable(rows: Cell[][], headerRows: number, w
idth: number): str
     if (ri === headerRows - 1) out.push(edge(G.LJ, G.X, G.RJ));
   });
   out.push(edge(G.BL, G.BJ, G.BR));
-  return out.join("\n");
+  return checkGrid(out.join("\n"), width, "table");
 }
 
 function makeTablePre(text: string, extraClass: string): Element {
@@ -225,7 +226,8 @@ function ruleSpan(frameW: number, variant: "frame-wide" | "fra
me-narrow", cls =
 }
 
 function captionSpan(text: string, frameW: number, variant: string): Element {
-  return blockSpan("fig-caption", variant, [{ type: "text", value: wrap(text, fra
meW) }]);
+  const wrapped = checkGrid(wrap(text, frameW), frameW, "figure caption");
+  return blockSpan("fig-caption", variant, [{ type: "text", value: wrapped }]);
 }
 
 export function rehypeAscii() {

diff --git a/ooknet-design/src/lib/rehype-as
cii.ts b/ooknet-design/src/lib/rehype-ascii.
ts
index c9a4a9c..2a07ace 100644
--- a/ooknet-design/src/lib/rehype-ascii.ts
+++ b/ooknet-design/src/lib/rehype-ascii.ts
@@ -4,7 +4,7 @@
 // gets a head-rule. Everything emits both 
the desktop and mobile
 // variants in the same tree; CSS toggles v
isibility per viewport.
 import type { Root, Element, ElementContent
, RootContent, Properties } from "hast";
-import { G, len, pad, wrap, type PadSide } 
from "./ascii";
+import { G, checkGrid, len, pad, wrap, type
 PadSide } from "./ascii";
 import { FRAME_W, MOBILE_FRAME_W } from "./
config";
 
 const FN_RE = /\b([a-zA-Z_]\w*)(?=\s*\()/g;
@@ -79,6 +79,7 @@ function buildBoxChildren(
text: string, lang: string, frameW: number):
 ElementCo
   ];
 
   for (const line of lines) {
+    checkGrid(line, inner, "code block");
     const padLen = Math.max(0, inner - len(
line));
     children.push(frame(`${G.V} `));
     let last = 0;
@@ -120,7 +121,7 @@ function makeCodePre(tex
t: string, lang: string, frameW: number, ext
raClass: str
 
 // ----------------------------------------
----------------------- table
 
-type Cell = { text: string; align: PadSide 
};
+export type Cell = { text: string; align: P
adSide };
 
 function tableCells(tbl: Element): { rows: 
Cell[][]; headerRows: number } {
   const rows: Cell[][] = [];
@@ -144,7 +145,7 @@ function tableCells(tbl:
 Element): { rows: Cell[][]; headerRows: num
ber } {
   return { rows, headerRows };
 }
 
-function buildAsciiTable(rows: Cell[][], he
aderRows: number, width: number): string {
+export function buildAsciiTable(rows: Cell[
][], headerRows: number, width: number): str
ing {
   const nCols = Math.max(...rows.map((r) =>
 r.length));
   const norm = rows.map((r) => {
     const out = [...r];
@@ -185,7 +186,7 @@ function buildAsciiTable
(rows: Cell[][], headerRows: number, width: 
number): str
     if (ri === headerRows - 1) out.push(edg
e(G.LJ, G.X, G.RJ));
   });
   out.push(edge(G.BL, G.BJ, G.BR));
-  return out.join("\n");
+  return checkGrid(out.join("\n"), width, "
table");
 }
 
 function makeTablePre(text: string, extraCl
ass: string): Element {
@@ -225,7 +226,8 @@ function ruleSpan(frameW
: number, variant: "frame-wide" | "frame-nar
row", cls =
 }
 
 function captionSpan(text: string, frameW: 
number, variant: string): Element {
-  return blockSpan("fig-caption", variant, 
[{ type: "text", value: wrap(text, frameW) }
]);
+  const wrapped = checkGrid(wrap(text, fram
eW), frameW, "figure caption");
+  return blockSpan("fig-caption", variant, 
[{ type: "text", value: wrapped }]);
 }
 
 export function rehypeAscii() {
 

diff --git a/ooknet-design/src/lib/types.ts b/ooknet-design/src/lib/types.ts
index 21db1c1..7ca6683 100644
--- a/ooknet-design/src/lib/types.ts
+++ b/ooknet-design/src/lib/types.ts
@@ -19,4 +19,5 @@ export interface NoteFrontmatter {
   filed: string;           // e.g. "/INFO/GRAPH/ENTROPY/BOUND"
   page?: number;
   seeAlso?: string[];
+  draft?: boolean;
 }

diff --git a/ooknet-design/src/lib/types.ts 
b/ooknet-design/src/lib/types.ts
index 21db1c1..7ca6683 100644
--- a/ooknet-design/src/lib/types.ts
+++ b/ooknet-design/src/lib/types.ts
@@ -19,4 +19,5 @@ export interface NoteFront
matter {
   filed: string;           // e.g. "/INFO/G
RAPH/ENTROPY/BOUND"
   page?: number;
   seeAlso?: string[];
+  draft?: boolean;
 }
 

diff --git a/ooknet-design/src/pages/index.astro b/ooknet-design/src/pages/index.a
stro
index e2ba75c..be49b27 100644
--- a/ooknet-design/src/pages/index.astro
+++ b/ooknet-design/src/pages/index.astro
@@ -4,10 +4,9 @@ import ArticleLayout from "../layouts/ArticleLayout/ArticleLayout
.astro";
 import TopHeader from "../components/TopHeader/TopHeader.astro";
 import Frame from "../components/Frame/Frame.astro";
 import { rule, ruleD, kv, row2 } from "../lib/ascii";
+import { published, byNoteNum } from "../lib/notes";
 
-const notes = await getCollection("notes", ({ data }) => !data.draft);
-const noteNum = (note: string) => parseInt(note.replace(/\D/g, ""), 10) || 0;
-notes.sort((a, b) => noteNum(b.data.note) - noteNum(a.data.note));
+const notes = (await getCollection("notes", published)).sort(byNoteNum).reverse()
;
 
 const buildIndexHeader = (w: number) => [
   row2("INDEX OF NOTES", `${notes.length} entries`, w),

diff --git a/ooknet-design/src/pages/index.a
stro b/ooknet-design/src/pages/index.astro
index e2ba75c..be49b27 100644
--- a/ooknet-design/src/pages/index.astro
+++ b/ooknet-design/src/pages/index.astro
@@ -4,10 +4,9 @@ import ArticleLayout from "
../layouts/ArticleLayout/ArticleLayout.astro
";
 import TopHeader from "../components/TopHea
der/TopHeader.astro";
 import Frame from "../components/Frame/Fram
e.astro";
 import { rule, ruleD, kv, row2 } from "../l
ib/ascii";
+import { published, byNoteNum } from "../li
b/notes";
 
-const notes = await getCollection("notes", 
({ data }) => !data.draft);
-const noteNum = (note: string) => parseInt(
note.replace(/\D/g, ""), 10) || 0;
-notes.sort((a, b) => noteNum(b.data.note) -
 noteNum(a.data.note));
+const notes = (await getCollection("notes",
 published)).sort(byNoteNum).reverse();
 
 const buildIndexHeader = (w: number) => [
   row2("INDEX OF NOTES", `${notes.length} e
ntries`, w),
 

diff --git a/ooknet-design/src/pages/notes/[slug].astro b/ooknet-design/src/pages/
notes/[slug].astro
index 343b055..0530758 100644
--- a/ooknet-design/src/pages/notes/[slug].astro
+++ b/ooknet-design/src/pages/notes/[slug].astro
@@ -12,11 +12,10 @@ import ArticleFooter from "../../components/ArticleFooter/Arti
cleFooter.astro";
 import Prose from "../../components/Prose/Prose.astro";
 import Pre from "../../components/Pre/Pre.astro";
 import type { PageLink } from "../../components/ArticlePagination/ArticlePaginati
on";
+import { published, byNoteNum } from "../../lib/notes";
 
 export async function getStaticPaths() {
-  const notes = await getCollection("notes", ({ data }) => !data.draft);
-  const num = (s: string) => parseInt(s.replace(/\D/g, ""), 10) || 0;
-  notes.sort((a, b) => num(a.data.note) - num(b.data.note));
+  const notes = (await getCollection("notes", published)).sort(byNoteNum);
   const link = (n?: (typeof notes)[number]): PageLink | null =>
     n ? { id: n.id, note: n.data.note } : null;
   return notes.map((n, i) => ({

diff --git a/ooknet-design/src/pages/notes/[
slug].astro b/ooknet-design/src/pages/notes/
[slug].astro
index 343b055..0530758 100644
--- a/ooknet-design/src/pages/notes/[slug].a
stro
+++ b/ooknet-design/src/pages/notes/[slug].a
stro
@@ -12,11 +12,10 @@ import ArticleFooter fro
m "../../components/ArticleFooter/ArticleFoo
ter.astro";
 import Prose from "../../components/Prose/P
rose.astro";
 import Pre from "../../components/Pre/Pre.a
stro";
 import type { PageLink } from "../../compon
ents/ArticlePagination/ArticlePagination";
+import { published, byNoteNum } from "../..
/lib/notes";
 
 export async function getStaticPaths() {
-  const notes = await getCollection("notes"
, ({ data }) => !data.draft);
-  const num = (s: string) => parseInt(s.rep
lace(/\D/g, ""), 10) || 0;
-  notes.sort((a, b) => num(a.data.note) - n
um(b.data.note));
+  const notes = (await getCollection("notes
", published)).sort(byNoteNum);
   const link = (n?: (typeof notes)[number])
: PageLink | null =>
     n ? { id: n.id, note: n.data.note } : n
ull;
   return notes.map((n, i) => ({
 

diff --git a/ooknet-design/src/pages/search.json.ts b/ooknet-design/src/pages/sear
ch.json.ts
index 66cdaf3..5805975 100644
--- a/ooknet-design/src/pages/search.json.ts
+++ b/ooknet-design/src/pages/search.json.ts
@@ -1,9 +1,10 @@
 // Static index consumed by the masthead fuzzy search.
 import type { APIRoute } from "astro";
 import { getCollection } from "astro:content";
+import { published, byNoteNum } from "../lib/notes";
 
 export const GET: APIRoute = async () => {
-  const notes = await getCollection("notes", ({ data }) => !data.draft);
+  const notes = (await getCollection("notes", published)).sort(byNoteNum).reverse
();
   const data = notes.map((n) => ({
     id: n.id,
     note: n.data.note,

diff --git a/ooknet-design/src/pages/search.
json.ts b/ooknet-design/src/pages/search.jso
n.ts
index 66cdaf3..5805975 100644
--- a/ooknet-design/src/pages/search.json.ts
+++ b/ooknet-design/src/pages/search.json.ts
@@ -1,9 +1,10 @@
 // Static index consumed by the masthead fu
zzy search.
 import type { APIRoute } from "astro";
 import { getCollection } from "astro:conten
t";
+import { published, byNoteNum } from "../li
b/notes";
 
 export const GET: APIRoute = async () => {
-  const notes = await getCollection("notes"
, ({ data }) => !data.draft);
+  const notes = (await getCollection("notes
", published)).sort(byNoteNum).reverse();
   const data = notes.map((n) => ({
     id: n.id,
     note: n.data.note,
 
[ BACK TO LOG ]
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET