From 64c13c5dc2dcdf93b8129c06efd55db2f60293db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:12:05 +0000 Subject: [PATCH 01/34] chore(deps): bump ajv from 6.12.6 to 6.14.0 Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.6 to 6.14.0. - [Release notes](https://github.com/ajv-validator/ajv/releases) - [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.6...v6.14.0) --- updated-dependencies: - dependency-name: ajv dependency-version: 6.14.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f6955f..7db6f33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "typescript": "5.2.2" }, "engines": { - "node": ">=18.0" + "node": "22.12.0" } }, "node_modules/@algolia/abtesting": { @@ -9752,9 +9752,9 @@ } }, "node_modules/file-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -14328,9 +14328,9 @@ } }, "node_modules/null-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -19297,9 +19297,9 @@ } }, "node_modules/url-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", From d07697a74d2ba3ca07ca14e411fdee57a7312997 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Feb 2026 07:12:09 +0000 Subject: [PATCH 02/34] chore(deps): bump lodash-es and mermaid Bumps [lodash-es](https://github.com/lodash/lodash) and [mermaid](https://github.com/mermaid-js/mermaid). These dependencies needed to be updated together. Updates `lodash-es` from 4.17.21 to 4.17.23 - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23) Updates `mermaid` from 11.12.2 to 11.12.3 - [Release notes](https://github.com/mermaid-js/mermaid/releases) - [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.12.2...mermaid@11.12.3) --- updated-dependencies: - dependency-name: lodash-es dependency-version: 4.17.23 dependency-type: indirect - dependency-name: mermaid dependency-version: 11.12.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 117 ++++++++++++++++++++-------------------------- 1 file changed, 50 insertions(+), 67 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f6955f..a6a2535 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "typescript": "5.2.2" }, "engines": { - "node": ">=18.0" + "node": "22.12.0" } }, "node_modules/@algolia/abtesting": { @@ -2010,54 +2010,42 @@ "license": "MIT" }, "node_modules/@chevrotain/cst-dts-gen": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", - "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.1.1.tgz", + "integrity": "sha512-fRHyv6/f542qQqiRGalrfJl/evD39mAvbJLCekPazhiextEatq1Jx1K/i9gSd5NNO0ds03ek0Cbo/4uVKmOBcw==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/gast": "11.0.3", - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/gast": "11.1.1", + "@chevrotain/types": "11.1.1", + "lodash-es": "4.17.23" } }, - "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/@chevrotain/gast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", - "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.1.1.tgz", + "integrity": "sha512-Ko/5vPEYy1vn5CbCjjvnSO4U7GgxyGm+dfUZZJIWTlQFkXkyym0jFYrWEU10hyCjrA7rQtiHtBr0EaZqvHFZvg==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/types": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/types": "11.1.1", + "lodash-es": "4.17.23" } }, - "node_modules/@chevrotain/gast/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/@chevrotain/regexp-to-ast": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", - "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.1.1.tgz", + "integrity": "sha512-ctRw1OKSXkOrR8VTvOxrQ5USEc4sNrfwXHa1NuTcR7wre4YbjPcKw+82C2uylg/TEwFRgwLmbhlln4qkmDyteg==", "license": "Apache-2.0" }, "node_modules/@chevrotain/types": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", - "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.1.tgz", + "integrity": "sha512-wb2ToxG8LkgPYnKe9FH8oGn3TMCBdnwiuNC5l5y+CtlaVRbCytU0kbVsk6CGrqTL4ZN4ksJa0TXOYbxpbthtqw==", "license": "Apache-2.0" }, "node_modules/@chevrotain/utils": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", - "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.1.1.tgz", + "integrity": "sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==", "license": "Apache-2.0" }, "node_modules/@colors/colors": { @@ -4779,12 +4767,12 @@ } }, "node_modules/@mermaid-js/parser": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", - "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.0.0.tgz", + "integrity": "sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==", "license": "MIT", "dependencies": { - "langium": "3.3.1" + "langium": "^4.0.0" } }, "node_modules/@noble/hashes": { @@ -7140,17 +7128,17 @@ } }, "node_modules/chevrotain": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", - "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.1.1.tgz", + "integrity": "sha512-f0yv5CPKaFxfsPTBzX7vGuim4oIC1/gcS7LUGdBSwl2dU6+FON6LVUksdOo1qJjoUvXNn45urgh8C+0a24pACQ==", "license": "Apache-2.0", "dependencies": { - "@chevrotain/cst-dts-gen": "11.0.3", - "@chevrotain/gast": "11.0.3", - "@chevrotain/regexp-to-ast": "11.0.3", - "@chevrotain/types": "11.0.3", - "@chevrotain/utils": "11.0.3", - "lodash-es": "4.17.21" + "@chevrotain/cst-dts-gen": "11.1.1", + "@chevrotain/gast": "11.1.1", + "@chevrotain/regexp-to-ast": "11.1.1", + "@chevrotain/types": "11.1.1", + "@chevrotain/utils": "11.1.1", + "lodash-es": "4.17.23" } }, "node_modules/chevrotain-allstar": { @@ -7165,12 +7153,6 @@ "chevrotain": "^11.0.0" } }, - "node_modules/chevrotain/node_modules/lodash-es": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "license": "MIT" - }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -11496,19 +11478,20 @@ } }, "node_modules/langium": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", - "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.1.tgz", + "integrity": "sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==", "license": "MIT", "dependencies": { - "chevrotain": "~11.0.3", - "chevrotain-allstar": "~0.3.0", + "chevrotain": "~11.1.1", + "chevrotain-allstar": "~0.3.1", "vscode-languageserver": "~9.0.1", "vscode-languageserver-textdocument": "~1.0.11", - "vscode-uri": "~3.0.8" + "vscode-uri": "~3.1.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.10.0", + "npm": ">=10.2.3" } }, "node_modules/latest-version": { @@ -12207,14 +12190,14 @@ } }, "node_modules/mermaid": { - "version": "11.12.2", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.2.tgz", - "integrity": "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==", + "version": "11.12.3", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.12.3.tgz", + "integrity": "sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==", "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", - "@mermaid-js/parser": "^0.6.3", + "@mermaid-js/parser": "^1.0.0", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", @@ -12226,7 +12209,7 @@ "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", - "lodash-es": "^4.17.21", + "lodash-es": "^4.17.23", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", @@ -19510,9 +19493,9 @@ "license": "MIT" }, "node_modules/vscode-uri": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", - "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "license": "MIT" }, "node_modules/watchpack": { From eb26045b5254c49040663b37d2d5cbfb7ad16091 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:17:23 +0000 Subject: [PATCH 03/34] chore(deps): bump svgo from 3.3.2 to 3.3.3 Bumps [svgo](https://github.com/svg/svgo) from 3.3.2 to 3.3.3. - [Release notes](https://github.com/svg/svgo/releases) - [Commits](https://github.com/svg/svgo/compare/v3.3.2...v3.3.3) --- updated-dependencies: - dependency-name: svgo dependency-version: 3.3.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f6955f..c48de16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "typescript": "5.2.2" }, "engines": { - "node": ">=18.0" + "node": "22.12.0" } }, "node_modules/@algolia/abtesting": { @@ -5348,15 +5348,6 @@ "node": ">=14.16" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -17607,9 +17598,9 @@ "license": "MIT" }, "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -18489,18 +18480,18 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.0.0", + "sax": "^1.5.0" }, "bin": { "svgo": "bin/svgo" From 5571d14949494b799f3dbff8e4b5c7a2c01da873 Mon Sep 17 00:00:00 2001 From: Nick Lange Date: Sun, 29 Mar 2026 03:32:43 -0400 Subject: [PATCH 04/34] Chore: Restore missing Jules learnings and fix dependency issues --- .jules/bolt.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.jules/bolt.md b/.jules/bolt.md index 72293cf..83d5acd 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -9,3 +9,15 @@ This journal documents critical performance learnings for the 5L Labs project. ## 2026-02-22 - Static Image Formats **Learning:** The Docusaurus React setup efficiently handles direct WebP imports in `src/pages/index.js`, dropping the hero banner LCP image payload by ~85% (233KB -> 35KB) without needing additional Webpack loader configurations. **Action:** Default to `.webp` formats for large static UI elements (like logos or hero images) rather than `.png`. + +## 2025-05-24 - Client-side Markdown Rendering for Static Previews +**Learning:** Using client-side markdown parsers (like react-markdown) for static content previews significantly increases bundle size unnecessarily. Processing markdown to plain text or HTML at build time is a much more efficient strategy for static site generators. +**Action:** Always check if content transformation can be moved to the build step before importing heavy runtime libraries. + +## 2025-10-26 - Implicit Dependency Upgrades with Bun +**Learning:** `bun install` may implicitly upgrade major versions of dependencies (e.g., React 18 to 19) if `package.json` ranges allow it and the lockfile isn't respected or is regenerated. This can cause massive, out-of-scope diffs. +**Action:** Always verify `bun.lock` diffs after installation. Revert lockfile changes if they are not intended. + +## 2025-05-24 - Hero Image Optimization & CLS +**Learning:** Large unoptimized images in the hero section are a primary cause of slow LCP and CLS. Providing explicit `width` and `height` attributes to the `img` tag, even if overridden by CSS, allows the browser to reserve the correct aspect ratio space immediately. +**Action:** Always optimize hero images (compress/resize) and define explicit dimensions to prevent layout shifts. From 31099adcab86c0e8c34e4494ca42ef8202cd83b2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 5 Apr 2026 04:46:59 +0000 Subject: [PATCH 05/34] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Add?= =?UTF-8?q?=20Cross-Origin-Opener-Policy=20and=20preload=20to=20HSTS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds Cross-Origin-Opener-Policy (same-origin) to mitigate cross-origin window interaction attacks (e.g., reverse tabnabbing and spectre). Enhances Strict-Transport-Security (HSTS) with the preload directive to bolster protocol downgrade attack protection. Co-authored-by: NickJLange <1529105+NickJLange@users.noreply.github.com> --- render.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/render.yaml b/render.yaml index 451a005..c0cbfcb 100644 --- a/render.yaml +++ b/render.yaml @@ -25,7 +25,10 @@ services: value: "camera=(), microphone=(), geolocation=(), payment=()" - path: /* name: Strict-Transport-Security - value: max-age=31536000; includeSubDomains + value: max-age=31536000; includeSubDomains; preload + - path: /* + name: Cross-Origin-Opener-Policy + value: same-origin - path: /* name: Content-Security-Policy value: "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://www.google-analytics.com; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https://www.google-analytics.com https://analytics.google.com https://www.googletagmanager.com https://api.iconify.design https://api.simplesvg.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';" From 229d9c6f4e9e18ea50ea6e9f3be60900d51440e5 Mon Sep 17 00:00:00 2001 From: Nick Lange Date: Tue, 21 Apr 2026 00:47:41 -0400 Subject: [PATCH 06/34] Feat: Add homepage redesign preview page with 4 wireframe directions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the 5L Labs Homepage design handoff as a live preview at /homepage-preview — all 4 directions (Manifesto, Journal, Terminal, Schematic) in a pannable/zoomable canvas with a tweaks panel for density, accent color, and annotation visibility. Co-Authored-By: Claude Sonnet 4.6 --- .../HomepageRedesign/DesignCanvas.jsx | 191 ++++++++ src/components/HomepageRedesign/Journal.jsx | 138 ++++++ src/components/HomepageRedesign/Manifesto.jsx | 106 +++++ .../HomepageRedesign/PreviewApp.jsx | 173 ++++++++ src/components/HomepageRedesign/Schematic.jsx | 142 ++++++ src/components/HomepageRedesign/Terminal.jsx | 129 ++++++ src/components/HomepageRedesign/shared.jsx | 94 ++++ src/components/HomepageRedesign/sketch.css | 407 ++++++++++++++++++ src/pages/homepage-preview.js | 30 ++ 9 files changed, 1410 insertions(+) create mode 100644 src/components/HomepageRedesign/DesignCanvas.jsx create mode 100644 src/components/HomepageRedesign/Journal.jsx create mode 100644 src/components/HomepageRedesign/Manifesto.jsx create mode 100644 src/components/HomepageRedesign/PreviewApp.jsx create mode 100644 src/components/HomepageRedesign/Schematic.jsx create mode 100644 src/components/HomepageRedesign/Terminal.jsx create mode 100644 src/components/HomepageRedesign/shared.jsx create mode 100644 src/components/HomepageRedesign/sketch.css create mode 100644 src/pages/homepage-preview.js diff --git a/src/components/HomepageRedesign/DesignCanvas.jsx b/src/components/HomepageRedesign/DesignCanvas.jsx new file mode 100644 index 0000000..2345a3d --- /dev/null +++ b/src/components/HomepageRedesign/DesignCanvas.jsx @@ -0,0 +1,191 @@ +import React, { useRef, useCallback, useEffect } from 'react'; + +const DC = { + bg: '#f0eee9', + grid: 'rgba(0,0,0,0.06)', + label: 'rgba(60,50,40,0.7)', + title: 'rgba(40,30,20,0.85)', + subtitle: 'rgba(60,50,40,0.6)', + font: '-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif', +}; + +export function DesignCanvas({ children, minScale = 0.1, maxScale = 8, style = {} }) { + const vpRef = useRef(null); + const worldRef = useRef(null); + const tf = useRef({ x: 0, y: 0, scale: 1 }); + + const apply = useCallback(() => { + const { x, y, scale } = tf.current; + const el = worldRef.current; + if (el) el.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`; + }, []); + + useEffect(() => { + const vp = vpRef.current; + if (!vp) return; + + const zoomAt = (cx, cy, factor) => { + const r = vp.getBoundingClientRect(); + const px = cx - r.left, py = cy - r.top; + const t = tf.current; + const next = Math.min(maxScale, Math.max(minScale, t.scale * factor)); + const k = next / t.scale; + t.x = px - (px - t.x) * k; + t.y = py - (py - t.y) * k; + t.scale = next; + apply(); + }; + + const isMouseWheel = (e) => + e.deltaMode !== 0 || + (e.deltaX === 0 && Number.isInteger(e.deltaY) && Math.abs(e.deltaY) >= 40); + + let isGesturing = false; + const onWheel = (e) => { + e.preventDefault(); + if (isGesturing) return; + if (e.ctrlKey) { + zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.01)); + } else if (isMouseWheel(e)) { + zoomAt(e.clientX, e.clientY, Math.exp(-Math.sign(e.deltaY) * 0.18)); + } else { + tf.current.x -= e.deltaX; + tf.current.y -= e.deltaY; + apply(); + } + }; + + let gsBase = 1; + const onGestureStart = (e) => { e.preventDefault(); isGesturing = true; gsBase = tf.current.scale; }; + const onGestureChange = (e) => { + e.preventDefault(); + zoomAt(e.clientX, e.clientY, (gsBase * e.scale) / tf.current.scale); + }; + const onGestureEnd = (e) => { e.preventDefault(); isGesturing = false; }; + + let drag = null; + const onPointerDown = (e) => { + const onBg = e.target === vp || e.target === worldRef.current; + if (!(e.button === 1 || (e.button === 0 && onBg))) return; + e.preventDefault(); + vp.setPointerCapture(e.pointerId); + drag = { id: e.pointerId, lx: e.clientX, ly: e.clientY }; + vp.style.cursor = 'grabbing'; + }; + const onPointerMove = (e) => { + if (!drag || e.pointerId !== drag.id) return; + tf.current.x += e.clientX - drag.lx; + tf.current.y += e.clientY - drag.ly; + drag.lx = e.clientX; drag.ly = e.clientY; + apply(); + }; + const onPointerUp = (e) => { + if (!drag || e.pointerId !== drag.id) return; + vp.releasePointerCapture(e.pointerId); + drag = null; + vp.style.cursor = ''; + }; + + vp.addEventListener('wheel', onWheel, { passive: false }); + vp.addEventListener('gesturestart', onGestureStart, { passive: false }); + vp.addEventListener('gesturechange', onGestureChange, { passive: false }); + vp.addEventListener('gestureend', onGestureEnd, { passive: false }); + vp.addEventListener('pointerdown', onPointerDown); + vp.addEventListener('pointermove', onPointerMove); + vp.addEventListener('pointerup', onPointerUp); + vp.addEventListener('pointercancel', onPointerUp); + + return () => { + vp.removeEventListener('wheel', onWheel); + vp.removeEventListener('gesturestart', onGestureStart); + vp.removeEventListener('gesturechange', onGestureChange); + vp.removeEventListener('gestureend', onGestureEnd); + vp.removeEventListener('pointerdown', onPointerDown); + vp.removeEventListener('pointermove', onPointerMove); + vp.removeEventListener('pointerup', onPointerUp); + vp.removeEventListener('pointercancel', onPointerUp); + }; + }, [apply, minScale, maxScale]); + + const gridSvg = `url("data:image/svg+xml,%3Csvg width='120' height='120' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M120 0H0v120' fill='none' stroke='${encodeURIComponent(DC.grid)}' stroke-width='1'/%3E%3C/svg%3E")`; + + return ( +
+
+ {children} +
+
+ ); +} + +export function DCSection({ title, subtitle, children, gap = 48 }) { + return ( +
+
+
{title}
+ {subtitle && ( +
{subtitle}
+ )} +
+
+ {children} +
+
+ ); +} + +export function DCArtboard({ label, children, width, height, style = {} }) { + return ( +
+ {label && ( +
{label}
+ )} +
+ {children} +
+
+ ); +} diff --git a/src/components/HomepageRedesign/Journal.jsx b/src/components/HomepageRedesign/Journal.jsx new file mode 100644 index 0000000..8ab025a --- /dev/null +++ b/src/components/HomepageRedesign/Journal.jsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { Nav, Footer, Note, PH } from './shared'; + +const posts = [ + ['Feb 2026', 'Frontier Research', 'Learning about learning?', 'Transformers, the Platonic Representation Hypothesis, and curiosity-driven learning.', true], + ['Jan 2026', 'Applied AI', 'Benchmarking private embeddings against OpenAI at 1/40th the cost', 'Open Embeddings v0.4 beats ada-002 on MTEB subsets. Here\'s how.', false], + ['Dec 2025', 'Self-Hosted IoT', 'Flashing Tasmota on a $4 Zigbee hub: a 2026 guide', 'The cheapest path to a cloudless smart home.', false], + ['Nov 2025', 'Frontier Research', 'Differential privacy without the accuracy cliff', 'Notes from six months of chasing an ε under 1.', false], + ['Oct 2025', 'Applied AI', 'What recruiter-ranking data actually measures', 'Our methodology, our biases, our priors.', false], +]; + +export default function Journal() { + return ( +
+
+
+
5l-labs.com
+
+
+
+
+ ); +} diff --git a/src/components/HomepageRedesign/Manifesto.jsx b/src/components/HomepageRedesign/Manifesto.jsx new file mode 100644 index 0000000..af29545 --- /dev/null +++ b/src/components/HomepageRedesign/Manifesto.jsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { Nav, Footer, Note, PH, Lines } from './shared'; + +export default function Manifesto() { + return ( +
+
+
+
5l-labs.com
+
+
+
+
+ ); +} diff --git a/src/components/HomepageRedesign/PreviewApp.jsx b/src/components/HomepageRedesign/PreviewApp.jsx new file mode 100644 index 0000000..dea9e37 --- /dev/null +++ b/src/components/HomepageRedesign/PreviewApp.jsx @@ -0,0 +1,173 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { DesignCanvas, DCSection, DCArtboard } from './DesignCanvas'; +import Manifesto from './Manifesto'; +import Journal from './Journal'; +import Terminal from './Terminal'; +import Schematic from './Schematic'; +import './sketch.css'; + +const LS_VIEW = '5l-wireframe-view'; + +const TWEAK_DEFAULTS = { + density: 'roomy', + accent: '#1e5f8a', + notes: 'show', +}; + +const ACCENT_SWATCHES = ['#c84a1f', '#1e5f8a', '#2a7d4f', '#1a1a1a']; + +function Toolbar({ view, onSetView }) { + const buttons = [ + { id: 'canvas', label: 'All · canvas' }, + { id: '1', label: '1 · Manifesto' }, + { id: '2', label: '2 · Journal' }, + { id: '3', label: '3 · Terminal' }, + { id: '4', label: '4 · Schematic' }, + ]; + return ( +
+ VIEW + {buttons.map((b, i) => ( + + {i === 1 &&
} + + + ))} +
+ ); +} + +function TweaksPanel({ tweaks, onTweak }) { + return ( +
+
TWEAKS
+ +
+ +
+ {['tight', 'roomy'].map(v => ( + + ))} +
+
+ +
+ +
+ {ACCENT_SWATCHES.map(c => ( +
+
+ +
+ +
+ {['show', 'hide'].map(v => ( + + ))} +
+
+
+ ); +} + +function CanvasView() { + return ( + + + + + + + + + + + + + + + + + ); +} + +function SingleView({ which }) { + const components = { '1': Manifesto, '2': Journal, '3': Terminal, '4': Schematic }; + const Comp = components[which]; + return ( +
+
+ +
+
+ ); +} + +export default function PreviewApp() { + const rootRef = useRef(null); + + const [view, setView] = useState(() => { + try { return localStorage.getItem(LS_VIEW) || 'canvas'; } catch { return 'canvas'; } + }); + + const [tweaks, setTweaks] = useState(TWEAK_DEFAULTS); + + const handleSetView = (v) => { + try { localStorage.setItem(LS_VIEW, v); } catch {} + setView(v); + }; + + const handleTweak = (key, value) => { + setTweaks(prev => ({ ...prev, [key]: value })); + }; + + // Apply tweaks via CSS custom properties and class toggles on the root element + useEffect(() => { + const el = rootRef.current; + if (!el) return; + el.style.setProperty('--accent', tweaks.accent); + el.style.setProperty('--wf-accent', tweaks.accent); + el.classList.toggle('roomy', tweaks.density === 'roomy'); + el.classList.toggle('notes-hidden', tweaks.notes === 'hide'); + }, [tweaks]); + + const rootClasses = [ + 'wf-preview-root', + tweaks.density === 'roomy' ? 'roomy' : '', + tweaks.notes === 'hide' ? 'notes-hidden' : '', + ].filter(Boolean).join(' '); + + return ( +
+ + + {view === 'canvas' ? : } + + {view === 'canvas' && ( +
+ 5L Labs — Homepage wireframes +
4 rough directions · low-fi, b&w + rust accent.
+
Click a tab above to zoom into one.
+
+ )} + + +
+ ); +} diff --git a/src/components/HomepageRedesign/Schematic.jsx b/src/components/HomepageRedesign/Schematic.jsx new file mode 100644 index 0000000..f25352a --- /dev/null +++ b/src/components/HomepageRedesign/Schematic.jsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { Nav, Footer, Note } from './shared'; + +export default function Schematic() { + return ( +
+
+
+
5l-labs.com
+
+
+
+
+ ); +} diff --git a/src/components/HomepageRedesign/Terminal.jsx b/src/components/HomepageRedesign/Terminal.jsx new file mode 100644 index 0000000..de06bf9 --- /dev/null +++ b/src/components/HomepageRedesign/Terminal.jsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { Nav, Footer, Note } from './shared'; + +const entries = [ + ['2026-02-23', 'writing', 'frontier', 'learning about learning?', '14 min'], + ['2026-02-10', 'release', 'applied-ai', 'open-embeddings v0.4.0', 'changelog'], + ['2026-01-28', 'writing', 'applied-ai', 'benchmarking private embeddings vs ada-002', '8 min'], + ['2026-01-14', 'project', 'iot', 'overlord-network-kill-switch v1.2', 'hardware'], + ['2025-12-30', 'writing', 'iot', 'flashing tasmota on a $4 zigbee hub', '6 min'], + ['2025-12-10', 'release', 'applied-ai', 'recruiter-rankings preview open', 'preview'], + ['2025-11-22', 'writing', 'frontier', 'differential privacy without the accuracy cliff', '11 min'], +]; + +export default function Terminal() { + return ( +
+
+
+
5l-labs.com
+
+
+
+
+ ); +} diff --git a/src/components/HomepageRedesign/shared.jsx b/src/components/HomepageRedesign/shared.jsx new file mode 100644 index 0000000..7bc287a --- /dev/null +++ b/src/components/HomepageRedesign/shared.jsx @@ -0,0 +1,94 @@ +import React from 'react'; + +export function Note({ top, left, right, bottom, children, arrow }) { + return ( +
+ {children} + {arrow && ( + + + + + )} +
+ ); +} + +export function PH({ w, h, label, style }) { + return
{label}
; +} + +export function Lines({ rows = 3, widths }) { + const ws = widths || ['w-90', 'w-80', 'w-60']; + return ( +
+ {Array.from({ length: rows }).map((_, i) => ( +
+ ))} +
+ ); +} + +export function Nav({ active, compact }) { + const links = compact + ? ['Research', 'Products', 'Consulting', 'GitHub'] + : ['Research', 'Products', 'Writing', 'Consulting', 'GitHub']; + return ( +
+
+ 5L + 5L Labs +
+
+ {links.map(l => ( + {l} + ))} +
+
+ ); +} + +export function Footer() { + return ( +
+
+
5L Labs
+
+ Commercial privacy-first.
+ Advancing technology for humans and bots. +
+
+
+
Research
+
    +
  • Private AI/ML
  • +
  • Private IoT
  • +
  • Frontier
  • +
+
+
+
Projects
+
    +
  • Open Embeddings
  • +
  • Recruiter Rankings
  • +
  • Kill Switch
  • +
+
+
+
Contact
+
    +
  • inquiries@5l-labs.com
  • +
  • GitHub
  • +
  • LinkedIn
  • +
+
+
+ ); +} diff --git a/src/components/HomepageRedesign/sketch.css b/src/components/HomepageRedesign/sketch.css new file mode 100644 index 0000000..4dc1305 --- /dev/null +++ b/src/components/HomepageRedesign/sketch.css @@ -0,0 +1,407 @@ +/* + * Sketchy wireframe look — handwritten but readable, b&w + one accent + * Variables scoped to .wf-preview-root to avoid conflicts with site globals. + */ + +@import url('https://fonts.googleapis.com/css2?family=Architects+Daughter&family=Kalam:wght@400;700&family=Caveat:wght@500;700&family=JetBrains+Mono:wght@400;500&display=swap'); + +.wf-preview-root { + --wf-ink: #1a1a1a; + --wf-ink-2: #333; + --wf-ink-3: #666; + --wf-ink-4: #999; + --wf-paper: #fbfaf6; + --wf-paper-2: #f3f1ea; + --wf-accent: #c84a1f; + --wf-accent-soft: #fde3d6; + --wf-yellow: #ffec8a; + --wf-hand: 'Kalam', 'Caveat', cursive; + --wf-hand-tight: 'Architects Daughter', 'Kalam', cursive; + --wf-mono: 'JetBrains Mono', ui-monospace, monospace; + + /* Aliases used in inline styles */ + --ink: var(--wf-ink); + --ink-2: var(--wf-ink-2); + --ink-3: var(--wf-ink-3); + --ink-4: var(--wf-ink-4); + --paper: var(--wf-paper); + --paper-2: var(--wf-paper-2); + --accent: var(--wf-accent); + --accent-soft: var(--wf-accent-soft); + --yellow: var(--wf-yellow); + --hand: var(--wf-hand); + --hand-tight: var(--wf-hand-tight); + --mono: var(--wf-mono); + + margin: 0; + padding: 0; + background: #f0eee9; + font-family: 'Kalam', cursive; + box-sizing: border-box; + height: 100%; + width: 100%; +} + +/* ── Wireframe base ── */ +.wf-preview-root .wf { + font-family: var(--wf-hand-tight); + color: var(--wf-ink); + background: var(--wf-paper); + line-height: 1.35; + box-sizing: border-box; +} +.wf-preview-root .wf *, +.wf-preview-root .wf *::before, +.wf-preview-root .wf *::after { + box-sizing: border-box; +} + +/* Boxes */ +.wf-preview-root .wf .box { + border: 1.5px solid var(--wf-ink); + border-radius: 2px; + position: relative; +} +.wf-preview-root .wf .box-dashed { border-style: dashed; } +.wf-preview-root .wf .box-soft { border-color: var(--wf-ink-3); } +.wf-preview-root .wf .box-accent { border-color: var(--wf-accent); } + +/* Sketchy wavy underline */ +.wf-preview-root .wf .u-sketch { + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: repeat-x; + background-position: 0 100%; + background-size: 120px 6px; + padding-bottom: 6px; +} + +/* Text utilities */ +.wf-preview-root .wf .hand { font-family: var(--wf-hand); } +.wf-preview-root .wf .hand-tight { font-family: var(--wf-hand-tight); } +.wf-preview-root .wf .mono { font-family: var(--wf-mono); } +.wf-preview-root .wf .muted { color: var(--wf-ink-3); } +.wf-preview-root .wf .fade { color: var(--wf-ink-4); } +.wf-preview-root .wf .accent { color: var(--wf-accent); } +.wf-preview-root .wf .hi { + background: var(--wf-yellow); + padding: 0 4px; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; +} + +/* Placeholder image */ +.wf-preview-root .wf .ph { + background: + repeating-linear-gradient(-45deg, + transparent 0, transparent 10px, + rgba(0,0,0,0.07) 10px, rgba(0,0,0,0.07) 11px); + border: 1.5px solid var(--wf-ink-3); + display: flex; align-items: center; justify-content: center; + color: var(--wf-ink-3); + font-family: var(--wf-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.08em; + text-align: center; + padding: 8px; +} + +/* Sketchy button */ +.wf-preview-root .wf .btn { + display: inline-flex; align-items: center; gap: 8px; + border: 1.5px solid var(--wf-ink); + padding: 10px 18px; + font-family: var(--wf-hand-tight); + font-size: 15px; + background: var(--wf-paper); + color: var(--wf-ink); + box-shadow: 3px 3px 0 var(--wf-ink); + text-decoration: none; + cursor: pointer; +} +.wf-preview-root .wf .btn-accent { + background: var(--wf-accent); + color: white; + border-color: var(--wf-ink); +} +.wf-preview-root .wf .btn-ghost { + box-shadow: none; + background: transparent; + border-style: dashed; +} + +/* Chip */ +.wf-preview-root .wf .chip { + display: inline-block; + border: 1.2px solid var(--wf-ink-2); + padding: 2px 8px; + font-size: 11px; + font-family: var(--wf-mono); + border-radius: 999px; + background: var(--wf-paper); +} +.wf-preview-root .wf .chip-accent { + border-color: var(--wf-accent); + color: var(--wf-accent); +} + +.wf-preview-root .wf .arrow { + font-family: var(--wf-hand); + color: var(--wf-accent); +} + +/* Annotation callout */ +.wf-preview-root .wf .note { + position: absolute; + font-family: var(--wf-hand); + color: var(--wf-accent); + font-size: 14px; + line-height: 1.2; + max-width: 180px; + pointer-events: none; +} +.wf-preview-root .wf .note svg { + position: absolute; + overflow: visible; +} + +/* Density: roomy mode */ +.wf-preview-root.roomy .wf .page { + padding: 48px 64px !important; + line-height: 1.5; +} +.wf-preview-root.roomy .wf h1 { margin-bottom: 24px !important; } +.wf-preview-root.roomy .wf-nav { margin-bottom: 40px !important; } + +/* Notes hidden */ +.wf-preview-root.notes-hidden .wf .note { display: none !important; } + +/* Fake text lines */ +.wf-preview-root .wf .line { + height: 7px; + background: var(--wf-ink-4); + border-radius: 2px; + opacity: 0.35; + margin: 6px 0; +} +.wf-preview-root .wf .line.w-90 { width: 90%; } +.wf-preview-root .wf .line.w-80 { width: 80%; } +.wf-preview-root .wf .line.w-70 { width: 70%; } +.wf-preview-root .wf .line.w-60 { width: 60%; } +.wf-preview-root .wf .line.w-50 { width: 50%; } +.wf-preview-root .wf .line.w-40 { width: 40%; } + +/* Scribbled divider */ +.wf-preview-root .wf hr.scribble { + border: none; + height: 4px; + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: repeat-x; + background-size: 200px 4px; + margin: 16px 0; +} + +/* Fake browser artboard */ +.wf-preview-root .wf-browser { + background: var(--wf-paper); + width: 100%; height: 100%; + display: flex; flex-direction: column; + overflow: hidden; +} +.wf-preview-root .wf-browser .chrome { + display: flex; align-items: center; gap: 10px; + padding: 10px 14px; + border-bottom: 1.5px solid var(--wf-ink); + background: var(--wf-paper-2); + font-family: var(--wf-mono); + font-size: 11px; + color: var(--wf-ink-3); + flex-shrink: 0; +} +.wf-preview-root .wf-browser .chrome .dot { + width: 10px; height: 10px; + border-radius: 50%; + border: 1.2px solid var(--wf-ink-3); +} +.wf-preview-root .wf-browser .chrome .url { + flex: 1; + border: 1.2px solid var(--wf-ink-3); + border-radius: 12px; + padding: 2px 10px; + background: var(--wf-paper); +} +.wf-preview-root .wf-browser .page { + flex: 1; + overflow: auto; + padding: 32px 40px; +} + +/* Nav */ +.wf-preview-root .wf-nav { + display: flex; align-items: center; justify-content: space-between; + padding-bottom: 18px; + border-bottom: 1.2px dashed var(--wf-ink-3); + margin-bottom: 28px; +} +.wf-preview-root .wf-nav .logo { + font-weight: 700; font-size: 18px; + display: flex; align-items: center; gap: 8px; +} +.wf-preview-root .wf-nav .logo-mark { + width: 28px; height: 28px; + border: 1.5px solid var(--wf-ink); + display: flex; align-items: center; justify-content: center; + font-family: var(--wf-mono); font-size: 13px; font-weight: 700; + background: var(--wf-paper); +} +.wf-preview-root .wf-nav .links { + display: flex; gap: 22px; + font-size: 14px; +} +.wf-preview-root .wf-nav .links span { cursor: pointer; } + +/* Footer */ +.wf-preview-root .wf-foot { + margin-top: 48px; padding-top: 20px; + border-top: 1.2px dashed var(--wf-ink-3); + display: grid; grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 24px; + font-size: 13px; +} +.wf-preview-root .wf-foot h5 { + font-size: 11px; font-family: var(--wf-mono); + text-transform: uppercase; letter-spacing: 0.08em; + color: var(--wf-ink-3); margin: 0 0 10px; font-weight: 500; +} +.wf-preview-root .wf-foot ul { list-style: none; padding: 0; margin: 0; } +.wf-preview-root .wf-foot li { margin-bottom: 6px; } + +/* ── Preview chrome (toolbar, badge, tweaks) ── */ +.wf-preview-root .preview-toolbar { + position: fixed; top: 16px; left: 50%; transform: translateX(-50%); + z-index: 1000; + display: flex; gap: 6px; + background: #fbfaf6; + border: 1.5px solid #1a1a1a; + box-shadow: 3px 3px 0 #1a1a1a; + padding: 6px; + font-family: 'Architects Daughter', cursive; +} +.wf-preview-root .preview-toolbar button { + font-family: 'Architects Daughter', cursive; + font-size: 14px; + background: transparent; + border: none; + padding: 8px 14px; + cursor: pointer; + color: #333; +} +.wf-preview-root .preview-toolbar button:hover { background: rgba(0,0,0,0.05); } +.wf-preview-root .preview-toolbar button.active { + background: #1a1a1a; + color: #fbfaf6; +} +.wf-preview-root .preview-toolbar .sep { + width: 1px; background: rgba(0,0,0,0.15); margin: 4px 2px; +} +.wf-preview-root .preview-toolbar .mono-label { + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + color: #666; + align-self: center; + padding: 0 8px; + letter-spacing: 0.1em; +} + +/* Single view */ +.wf-preview-root .single-stage { + min-height: 100vh; + padding: 90px 40px 40px; + display: flex; justify-content: center; +} +.wf-preview-root .single-stage .frame { + width: 1280px; + max-width: 100%; + height: calc(100vh - 130px); + min-height: 800px; + box-shadow: 0 2px 6px rgba(0,0,0,0.1), 0 10px 40px rgba(0,0,0,0.12); + border: 1.5px solid #1a1a1a; + overflow: hidden; +} + +/* Title badge */ +.wf-preview-root .title-badge { + position: fixed; bottom: 20px; left: 20px; + z-index: 1000; + background: #fef4a8; + border: 1.5px solid #1a1a1a; + box-shadow: 3px 3px 0 #1a1a1a; + padding: 10px 14px; + font-family: 'Kalam', cursive; + font-size: 13px; + max-width: 280px; + line-height: 1.3; +} +.wf-preview-root .title-badge b { + font-size: 15px; display: block; margin-bottom: 2px; +} +.wf-preview-root .title-badge .muted-label { + color: #666; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + letter-spacing: 0.1em; +} + +/* Tweaks panel */ +.wf-preview-root .tweaks-panel { + position: fixed; bottom: 20px; right: 20px; + z-index: 1000; + background: #fbfaf6; + border: 1.5px solid #1a1a1a; + box-shadow: 3px 3px 0 #1a1a1a; + padding: 14px 16px; + font-family: 'Architects Daughter', cursive; + font-size: 13px; + width: 220px; +} +.wf-preview-root .tweaks-panel h5 { + margin: 0 0 10px; + font-family: 'JetBrains Mono', monospace; + font-size: 10px; + letter-spacing: 0.14em; + color: #666; + font-weight: 500; +} +.wf-preview-root .tweaks-panel .tweak-row { margin-bottom: 12px; } +.wf-preview-root .tweaks-panel label { + display: block; margin-bottom: 4px; font-size: 11px; color: #666; +} +.wf-preview-root .tweaks-panel .seg { + display: flex; + border: 1.2px solid #1a1a1a; +} +.wf-preview-root .tweaks-panel .seg button { + flex: 1; + font-family: 'Architects Daughter', cursive; + font-size: 12px; + background: transparent; + border: none; + padding: 6px; + cursor: pointer; +} +.wf-preview-root .tweaks-panel .seg button.on { + background: #1a1a1a; + color: #fbfaf6; +} +.wf-preview-root .tweaks-panel .swatches { + display: flex; gap: 6px; +} +.wf-preview-root .tweaks-panel .swatches button { + width: 28px; height: 28px; padding: 0; + border: 1.2px solid #1a1a1a; + cursor: pointer; +} +.wf-preview-root .tweaks-panel .swatches button.on { + outline: 2px solid #1a1a1a; + outline-offset: 2px; +} diff --git a/src/pages/homepage-preview.js b/src/pages/homepage-preview.js new file mode 100644 index 0000000..c0d1d6e --- /dev/null +++ b/src/pages/homepage-preview.js @@ -0,0 +1,30 @@ +import React from 'react'; +import Head from '@docusaurus/Head'; +import BrowserOnly from '@docusaurus/BrowserOnly'; + +export default function HomepagePreview() { + return ( + <> + + 5L Labs — Homepage Preview + + + + Loading preview…
}> + {() => { + const PreviewApp = require('../components/HomepageRedesign/PreviewApp').default; + return ; + }} + + + ); +} From 0a640e2c9121f0b3698ab2d9ee072f93e8a43aac Mon Sep 17 00:00:00 2001 From: Nick Lange Date: Tue, 21 Apr 2026 01:30:41 -0400 Subject: [PATCH 07/34] Feat: Replace homepage with Terminal direction Monospace index layout: dark terminal banner, sortable content table pulling from real blog/release data, projects and consulting panels. Real links wired to existing routes and external properties. Co-Authored-By: Claude Sonnet 4.6 --- src/pages/index.js | 203 ++++++++++++++++++++++++++++++------- src/pages/index.module.css | 192 ++++++++++++++++++++++++++++++++--- 2 files changed, 344 insertions(+), 51 deletions(-) diff --git a/src/pages/index.js b/src/pages/index.js index 2d1b33f..5024781 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,47 +1,176 @@ -import React from "react"; -import clsx from "clsx"; -import Link from "@docusaurus/Link"; -import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; -import Layout from "@theme/Layout"; -import HomepageContent from "../components/HomepageContent"; -import styles from "./index.module.css"; +import React from 'react'; +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Layout from '@theme/Layout'; +import homepageConfig from '../config/homepage'; +import latestPost from '../generated/latest-post.json'; +import styles from './index.module.css'; -import Logo from "@site/static/img/5L_Labs_Logo.webp"; - -function HomepageHeader() { - const { siteConfig } = useDocusaurusContext(); - return ( -
-
-
- {/* Explicit width/height to prevent CLS and ensure proper aspect ratio */} - 5L Labs Logo -
-

{siteConfig.title}

-

{siteConfig.tagline}

-
-
-
-
- ); -} +const INDEX_ENTRIES = [ + { + date: new Date(latestPost.date).toISOString().slice(0, 10), + type: 'writing', + area: 'frontier', + title: latestPost.title, + meta: '14 min', + url: latestPost.url, + }, + { + date: '2026-02-10', + type: 'release', + area: 'applied-ai', + title: 'open-embeddings v0.4.0', + meta: 'changelog', + url: 'https://www.open-embeddings.org', + }, + { + date: '2026-01-28', + type: 'writing', + area: 'applied-ai', + title: 'benchmarking private embeddings vs ada-002', + meta: '8 min', + url: '/applied-ai-engineering', + }, + { + date: '2026-01-14', + type: 'project', + area: 'iot', + title: 'overlord-network-kill-switch v1.2', + meta: 'hardware', + url: 'https://github.com/5L-Labs/overlord-network-kill-switch', + }, + { + date: '2025-12-30', + type: 'writing', + area: 'iot', + title: 'flashing tasmota on a $4 zigbee hub', + meta: '6 min', + url: '/self-hosted-iot', + }, + { + date: '2025-12-10', + type: 'release', + area: 'applied-ai', + title: 'recruiter-rankings preview open', + meta: 'preview', + url: 'https://www.recruiter-rankings.com', + }, + { + date: '2025-11-22', + type: 'writing', + area: 'frontier', + title: 'differential privacy without the accuracy cliff', + meta: '11 min', + url: '/frontier-research', + }, +]; export default function Home() { const { siteConfig } = useDocusaurusContext(); return ( - -
- +
+ + {/* Terminal banner */} +
+
$ whoami
+
5L Labs — commercial privacy-first research, est. 2023 (NYC)
+
$ cat mission.txt
+
{homepageConfig.missionStatement}
+
$ ls areas/
+
+ private-ai/{' '} + private-iot/{' '} + frontier/{' '} + applied-ai/ +
+
+ $ ./hire-us --consulting{' '} + +
+
+ + {/* Index */} +
+
+ INDEX OF / + · + SORT BY: + DATE ↓ + AREA + TYPE +
+ + + + + + + + + + + + + {INDEX_ENTRIES.map((entry, i) => ( + + + + + + + + ))} + +
DATETYPEAREATITLE
{entry.date} + + {entry.type} + + {entry.area} + {entry.title} + {entry.meta}
+
— load more —
+
+ + {/* Projects + Consulting */} +
+ +
+
~/projects
+ {homepageConfig.products.map((p) => ( +
+
+ + {p.title.toLowerCase().replace(/\s*\(.*?\)/, '')} + + +
+
{p.description}
+
+ ))} +
+ +
+
~/consulting
+
+ We take on a small number of engagements each quarter. +
+
+ Private ML systems, on-device inference, and IoT architecture audits. +
+
+ CURRENT AVAILABILITY:{' '} + Q3 2026 +
+ + ./inquire → + +
+ +
); diff --git a/src/pages/index.module.css b/src/pages/index.module.css index 9f71a5d..ee2f0b6 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -1,23 +1,187 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ +.page { + max-width: 1100px; + margin: 0 auto; + padding: 48px 40px 80px; + font-family: var(--ifm-font-family-monospace); + font-size: 13px; +} -.heroBanner { - padding: 4rem 0; - text-align: center; +/* ── Terminal banner ── */ +.terminal { + padding: 20px 24px; + background: #101010; + color: #e8e6df; + font-family: var(--ifm-font-family-monospace); + font-size: 13px; + line-height: 1.6; + border-radius: 4px; + margin-bottom: 40px; position: relative; - overflow: hidden; +} +.terminalPrompt { color: #7bd27b; } +.terminalDir { color: #f0a060; } +.terminalCursor { color: var(--ifm-color-primary); } + +/* ── Index section ── */ +.indexSection { margin-bottom: 36px; } + +.indexHeader { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 14px; + color: var(--ifm-color-content-secondary); + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; } -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } +.indexTable { + width: 100%; + border-collapse: collapse; + font-family: var(--ifm-font-family-monospace); + font-size: 13px; +} +.indexTable thead tr { + border-bottom: 1.5px solid var(--ifm-color-content); + text-align: left; + color: var(--ifm-color-content-secondary); +} +.indexTable thead th { + padding: 8px 4px; + font-weight: 500; + font-size: 11px; + letter-spacing: 0.08em; +} +.indexTable thead th:last-child { text-align: right; } + +.indexTable tbody tr { + border-bottom: 1px dashed var(--ifm-color-emphasis-300); +} +.indexTable tbody tr:hover { + background: var(--ifm-color-emphasis-100); +} +.indexTable tbody td { + padding: 10px 4px; + color: var(--ifm-color-content-secondary); + vertical-align: middle; +} +.indexTable tbody td.titleCell { + font-family: var(--ifm-heading-font-family, var(--ifm-font-family-base)); + font-size: 15px; + color: var(--ifm-color-content); +} +.indexTable tbody td.titleCell a { + color: var(--ifm-color-content); + text-decoration: none; +} +.indexTable tbody td.titleCell a:hover { + color: var(--ifm-color-primary); +} +.indexTable tbody td:last-child { text-align: right; } + +.loadMore { + text-align: center; + margin-top: 14px; + color: var(--ifm-color-content-secondary); + font-size: 12px; } -.buttons { +/* ── Chips ── */ +.chip { + display: inline-block; + border: 1.2px solid var(--ifm-color-emphasis-400); + padding: 2px 8px; + font-size: 11px; + font-family: var(--ifm-font-family-monospace); + border-radius: 999px; + background: transparent; + color: var(--ifm-color-content-secondary); + cursor: pointer; +} +.chipActive { + border-color: var(--ifm-color-primary); + color: var(--ifm-color-primary); +} + +/* ── Two-column lower section ── */ +.twoCol { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 28px; +} + +.box { + border: 1.5px solid var(--ifm-color-emphasis-300); + border-radius: 4px; + padding: 20px; +} + +.boxLabel { + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--ifm-color-primary); + margin-bottom: 12px; +} + +.projectRow { display: flex; + justify-content: space-between; + font-size: 14px; + padding: 10px 0; + border-bottom: 1px dashed var(--ifm-color-emphasis-300); +} +.projectRow:last-of-type { border-bottom: none; } +.projectName { color: var(--ifm-color-content); text-decoration: none; } +.projectName:hover { color: var(--ifm-color-primary); } +.projectMeta { color: var(--ifm-color-content-secondary); } +.projectDesc { font-size: 12px; color: var(--ifm-color-content-secondary); } + +.consultTitle { + font-family: var(--ifm-heading-font-family, var(--ifm-font-family-base)); + font-size: 18px; + line-height: 1.2; + margin-bottom: 10px; + color: var(--ifm-color-content); +} +.consultDesc { + font-size: 13px; + color: var(--ifm-color-content-secondary); + margin-bottom: 14px; +} +.availability { + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ifm-color-content-secondary); + margin-bottom: 12px; +} +.availabilityVal { color: var(--ifm-color-primary); } + +.btnAccent { + display: inline-flex; align-items: center; - justify-content: center; + gap: 6px; + background: var(--ifm-color-primary); + color: #fff; + border: none; + padding: 9px 18px; + font-family: var(--ifm-font-family-monospace); + font-size: 13px; + border-radius: 3px; + text-decoration: none; + cursor: pointer; + box-shadow: 3px 3px 0 rgba(0,0,0,0.15); +} +.btnAccent:hover { + color: #fff; + opacity: 0.9; + text-decoration: none; +} + +@media (max-width: 768px) { + .page { padding: 32px 20px 60px; } + .twoCol { grid-template-columns: 1fr; } + .indexTable { font-size: 12px; } } From e96aabe29c07abb85d4a362463aae0201b0bffac Mon Sep 17 00:00:00 2001 From: Nick Lange Date: Wed, 22 Apr 2026 00:14:02 -0400 Subject: [PATCH 08/34] Feat: Add archive page and wire homepage index to real post data - generate-latest-post.js now emits all-posts.json (19 real entries) - Homepage pulls top 7 from all-posts.json; "load more" links to /archive - /archive shows full table with live area filter chips - Removed all placeholder/wireframe content from the index Co-Authored-By: Claude Sonnet 4.6 --- scripts/generate-latest-post.js | 142 ++++++++++++++------------ src/generated/all-posts.json | 173 ++++++++++++++++++++++++++++++++ src/pages/archive.js | 90 +++++++++++++++++ src/pages/index.js | 83 ++++----------- 4 files changed, 357 insertions(+), 131 deletions(-) create mode 100644 src/generated/all-posts.json create mode 100644 src/pages/archive.js diff --git a/scripts/generate-latest-post.js b/scripts/generate-latest-post.js index 4b8f66a..f76d1f5 100644 --- a/scripts/generate-latest-post.js +++ b/scripts/generate-latest-post.js @@ -6,102 +6,112 @@ const BLOG_DIRS = [ 'blog-self-hosted-iot', 'blog-applied-home-ml-iot', 'blog-applied-ai-engineering', - 'blog-frontier-research' + 'blog-frontier-research', ]; -const OUTPUT_FILE = path.join(__dirname, '../src/generated/latest-post.json'); - -const TRUNCATE_RE = /[\s\S]*$/; -const HTML_TAGS_RE = /<[^>]*>/g; -const IMAGES_RE = /!\[(.*?)\]\(.*?\)/g; -const LINKS_RE = /\[(.*?)\]\(.*?\)/g; -const HEADINGS_RE = /^#+\s+/gm; +const AREA_LABELS = { + 'blog-self-hosted-iot': 'self-hosted-iot', + 'blog-applied-home-ml-iot': 'home-ml-iot', + 'blog-applied-ai-engineering': 'applied-ai', + 'blog-frontier-research': 'frontier', + 'blog-misc': 'misc', +}; + +const LATEST_OUTPUT = path.join(__dirname, '../src/generated/latest-post.json'); +const ALL_OUTPUT = path.join(__dirname, '../src/generated/all-posts.json'); + +const TRUNCATE_RE = /[\s\S]*$/; +const HTML_TAGS_RE = /<[^>]*>/g; +const IMAGES_RE = /!\[(.*?)\]\(.*?\)/g; +const LINKS_RE = /\[(.*?)\]\(.*?\)/g; +const HEADINGS_RE = /^#+\s+/gm; const BLOCKQUOTES_RE = /^>\s+/gm; const CODE_BLOCKS_RE = /```[\s\S]*?```/g; const INLINE_CODE_RE = /`([^`]+)`/g; const BOLD_ITALIC_RE = /[*_]{1,3}(.*?)[*_]{1,3}/g; -const HR_RE = /^-{3,}$/gm; -const NEWLINES_RE = /\n+/g; +const HR_RE = /^-{3,}$/gm; +const NEWLINES_RE = /\n+/g; function stripMarkdown(markdown) { if (!markdown) return ''; return markdown - .replace(TRUNCATE_RE, '') // Remove everything after truncate - .replace(HTML_TAGS_RE, '') // Remove HTML tags - .replace(IMAGES_RE, '') // Remove images - .replace(LINKS_RE, '$1') // Remove links but keep text - .replace(HEADINGS_RE, '') // Remove headings - .replace(BLOCKQUOTES_RE, '') // Remove blockquotes - .replace(CODE_BLOCKS_RE, '') // Remove code blocks - .replace(INLINE_CODE_RE, '$1') // Remove inline code - .replace(BOLD_ITALIC_RE, '$1') // Remove bold/italic - .replace(HR_RE, '') // Remove hr - .replace(NEWLINES_RE, ' ') // Collapse newlines + .replace(TRUNCATE_RE, '') + .replace(HTML_TAGS_RE, '') + .replace(IMAGES_RE, '') + .replace(LINKS_RE, '$1') + .replace(HEADINGS_RE, '') + .replace(BLOCKQUOTES_RE, '') + .replace(CODE_BLOCKS_RE, '') + .replace(INLINE_CODE_RE, '$1') + .replace(BOLD_ITALIC_RE, '$1') + .replace(HR_RE, '') + .replace(NEWLINES_RE, ' ') .trim(); } -function getLatestPost() { - let latestPost = null; +function getAllPosts() { const DATE_REGEX = /^(\d{4})-(\d{2})-(\d{2})/; + const posts = []; BLOG_DIRS.forEach(dir => { const dirPath = path.join(__dirname, '..', dir); if (!fs.existsSync(dirPath)) return; - const files = fs.readdirSync(dirPath); - files.forEach(file => { + const routeBase = dir.replace('blog-', ''); + const area = AREA_LABELS[dir]; + + fs.readdirSync(dirPath).forEach(file => { if (!file.endsWith('.md') && !file.endsWith('.mdx')) return; - // Extract date from filename (YYYY-MM-DD-...) const match = file.match(DATE_REGEX); if (!match) return; - const [_, yearStr, monthStr, dayStr] = match; const date = new Date(`${yearStr}-${monthStr}-${dayStr}`); - if (!latestPost || date > latestPost.date) { - const content = fs.readFileSync(path.join(dirPath, file), 'utf-8'); - const { data, content: markdownContent } = matter(content); - - let postContent = ''; - if (data.description) { - postContent = data.description; - } else { - postContent = stripMarkdown(markdownContent); - } - - const truncated = postContent.length > 550 ? postContent.substring(0, 550) + '...' : postContent; - - const slug = data.slug || file.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.(md|mdx)$/, ''); - - const routeBasePath = dir.replace('blog-', ''); - - let url; - if (data.slug) { - url = `/${routeBasePath}/${data.slug}`; - } else { - url = `/${routeBasePath}/${yearStr}/${monthStr}/${dayStr}/${slug}`; - } - - latestPost = { - date: date, - title: data.title || slug, - content: truncated, - url: url - }; - } + const raw = fs.readFileSync(path.join(dirPath, file), 'utf-8'); + const { data, content: markdownContent } = matter(raw); + + const slug = data.slug || + file.replace(/^\d{4}-\d{2}-\d{2}-/, '').replace(/\.(md|mdx)$/, ''); + + const url = data.slug + ? `/${routeBase}/${data.slug}` + : `/${routeBase}/${yearStr}/${monthStr}/${dayStr}/${slug}`; + + let excerpt = data.description || stripMarkdown(markdownContent); + if (excerpt.length > 550) excerpt = excerpt.substring(0, 550) + '...'; + + posts.push({ + date: date.toISOString(), + dateLabel: `${yearStr}-${monthStr}-${dayStr}`, + title: data.title || slug, + area, + type: 'writing', + url, + excerpt, + }); }); }); - return latestPost; + return posts.sort((a, b) => new Date(b.date) - new Date(a.date)); } -const latestPost = getLatestPost(); - -if (latestPost) { - fs.writeFileSync(OUTPUT_FILE, JSON.stringify(latestPost, null, 2)); - console.log(`Latest post generated: ${latestPost.title}`); +const allPosts = getAllPosts(); + +// all-posts.json — used by homepage and archive page +fs.writeFileSync(ALL_OUTPUT, JSON.stringify(allPosts, null, 2)); +console.log(`All posts generated: ${allPosts.length} entries`); + +// latest-post.json — backwards compat for anything else that imports it +if (allPosts.length) { + const latest = allPosts[0]; + fs.writeFileSync(LATEST_OUTPUT, JSON.stringify({ + date: latest.date, + title: latest.title, + content: latest.excerpt, + url: latest.url, + }, null, 2)); + console.log(`Latest post generated: ${latest.title}`); } else { - console.log('No blog posts found.'); - fs.writeFileSync(OUTPUT_FILE, JSON.stringify({}, null, 2)); + fs.writeFileSync(LATEST_OUTPUT, JSON.stringify({}, null, 2)); } diff --git a/src/generated/all-posts.json b/src/generated/all-posts.json new file mode 100644 index 0000000..20352b1 --- /dev/null +++ b/src/generated/all-posts.json @@ -0,0 +1,173 @@ +[ + { + "date": "2026-02-23T00:00:00.000Z", + "dateLabel": "2026-02-23", + "title": "Learning about learning?", + "area": "frontier", + "type": "writing", + "url": "/frontier-research/learning-again", + "excerpt": "Exploring the limits of Transformer architectures, the Platonic Representation Hypothesis, and the role of curiosity-driven learning in the next generation of AI." + }, + { + "date": "2026-02-18T00:00:00.000Z", + "dateLabel": "2026-02-18", + "title": "GarageCam Lives! ExecuTorch on RPi 5", + "area": "home-ml-iot", + "type": "writing", + "url": "/applied-home-ml-iot/executorch-bootstrap", + "excerpt": "A deep dive into deploying a private ML pipeline for GarageCam using ExecuTorch on Raspberry Pi 5, featuring real-time inference and MQTT integration." + }, + { + "date": "2025-12-10T00:00:00.000Z", + "dateLabel": "2025-12-10", + "title": "Time Decay of Information", + "area": "frontier", + "type": "writing", + "url": "/frontier-research/information-aging-out", + "excerpt": "Exploring the \"Time Decay\" of information in AI models—how embeddings and weights handle the aging of explicit, implicit, and undated knowledge." + }, + { + "date": "2025-12-01T00:00:00.000Z", + "dateLabel": "2025-12-01", + "title": "NetworkManager and VPN Tunneling", + "area": "self-hosted-iot", + "type": "writing", + "url": "/self-hosted-iot/network-manager-forced-updates", + "excerpt": "A technical guide to configuring NetworkManager, static IPs, and WireGuard VPN tunneling on Debian Bookworm/Trixie for private home networking." + }, + { + "date": "2025-11-16T00:00:00.000Z", + "dateLabel": "2025-11-16", + "title": "Thoughts on Jarvis 2.0", + "area": "home-ml-iot", + "type": "writing", + "url": "/applied-home-ml-iot/jarvis2.0", + "excerpt": "Conceptualizing \"Jarvis 2.0\"—a private, multi-modal AI assistant for the home, utilizing semantic routing and local speech recognition." + }, + { + "date": "2025-10-19T00:00:00.000Z", + "dateLabel": "2025-10-19", + "title": "Overlord - Home Network Kill Switch", + "area": "self-hosted-iot", + "type": "writing", + "url": "/self-hosted-iot/updated-network-controls", + "excerpt": "Updates on the Overlord Network Kill Switch project, supporting Pi-hole v6 and Ubiquiti 9.4.19 for simple, one-tap parental controls in HomeKit." + }, + { + "date": "2025-09-15T00:00:00.000Z", + "dateLabel": "2025-09-15", + "title": "Continued Adventures in CheapML for IoT", + "area": "home-ml-iot", + "type": "writing", + "url": "/applied-home-ml-iot/continued-iot-ml-on-the-cheap", + "excerpt": "A progress update on the GarageCam project, focusing on VLM labeling (Qwen), model conversion (PyTorch to TFLite), and the challenges of running ML on an RPi 4." + }, + { + "date": "2025-06-08T00:00:00.000Z", + "dateLabel": "2025-06-08", + "title": "Adventures in CheapML - GarageCam", + "area": "home-ml-iot", + "type": "writing", + "url": "/applied-home-ml-iot/garage-cam-on-the-cheap", + "excerpt": "A DIY approach to garage door and car presence detection using a Wyze Cam v3, Thingino, and a custom Python script for cheap ML at the edge." + }, + { + "date": "2025-05-17T00:00:00.000Z", + "dateLabel": "2025-05-17", + "title": "Almost Bricking a v3 Wyze Cam (and back again...)", + "area": "self-hosted-iot", + "type": "writing", + "url": "/self-hosted-iot/getting-off-of-wyze-camv3", + "excerpt": "A personal account of flashing the custom Thingino firmware onto a Wyze Cam v3 to achieve local-only control and escape the cloud." + }, + { + "date": "2025-05-03T00:00:00.000Z", + "dateLabel": "2025-05-03", + "title": "Wyzely Saying Goodbye to Wyze!", + "area": "self-hosted-iot", + "type": "writing", + "url": "/self-hosted-iot/getting-off-of-wyze", + "excerpt": "Transitioning away from Wyze's cloud-dependent cameras to a local-only setup using Scrypted, ONVIF, and HomeKit for better privacy and control." + }, + { + "date": "2025-04-19T00:00:00.000Z", + "dateLabel": "2025-04-19", + "title": "Private Agents - Pim Particles (Embeddings)", + "area": "applied-ai", + "type": "writing", + "url": "/applied-ai-engineering/embeddings-pim-particles", + "excerpt": "Exploring the role of embeddings (Pim Particles) in private agent architectures and how federated learning fits into the local-first AI model." + }, + { + "date": "2025-04-12T00:00:00.000Z", + "dateLabel": "2025-04-12", + "title": "Interoperability and Mini Minds", + "area": "home-ml-iot", + "type": "writing", + "url": "/applied-home-ml-iot/interoperability-and-monetization", + "excerpt": "Exploring how multiple small language models (Mini Minds) can collaborate in a local IoT ecosystem, and the potential monetization models for private AI." + }, + { + "date": "2025-04-05T00:00:00.000Z", + "dateLabel": "2025-04-05", + "title": "Private Agents - Pim Particles", + "area": "home-ml-iot", + "type": "writing", + "url": "/applied-home-ml-iot/pim_particles", + "excerpt": "Exploring model shrinkage techniques like LoRA and quantization to run private agents on low-power hardware like Raspberry Pi 4." + }, + { + "date": "2025-03-25T00:00:00.000Z", + "dateLabel": "2025-03-25", + "title": "NVIDIA GTC Recap", + "area": "applied-ai", + "type": "writing", + "url": "/applied-ai-engineering/nvdia-gtc-recap", + "excerpt": "A recap of NVIDIA GTC 2025 focusing on private agency, home-based ML, and the technical ingredients for secure, local AI systems." + }, + { + "date": "2025-03-16T00:00:00.000Z", + "dateLabel": "2025-03-16", + "title": "Off to Nvidia GTC", + "area": "applied-ai", + "type": "writing", + "url": "/applied-ai-engineering/nvdia-gtc", + "excerpt": "Exploring the privacy landscape at NVIDIA GTC 2025, with a focus on Distributed Training and Private LLMs." + }, + { + "date": "2025-02-22T00:00:00.000Z", + "dateLabel": "2025-02-22", + "title": "ai.engineer summit nyc", + "area": "applied-ai", + "type": "writing", + "url": "/applied-ai-engineering/ai-dot-engineer-summit-nyc", + "excerpt": "Key takeaways from the AI Engineering Summit in NYC, focusing on private agents, federated systems, and the Model Context Protocol (MCP)." + }, + { + "date": "2025-02-16T00:00:00.000Z", + "dateLabel": "2025-02-16", + "title": "Entirely Private Machine Learning Home Setup", + "area": "home-ml-iot", + "type": "writing", + "url": "/applied-home-ml-iot/offline-llms", + "excerpt": "A deep dive into setting up a private, local machine learning environment using Podman, Langfuse, Milvus, and more on a Mac M2 Pro." + }, + { + "date": "2025-01-01T00:00:00.000Z", + "dateLabel": "2025-01-01", + "title": "My Private Home", + "area": "self-hosted-iot", + "type": "writing", + "url": "/self-hosted-iot/what-is-my-private-home", + "excerpt": "Defining the architecture of a private home network across multiple locations, focusing on Ubiquity hardware, traffic routing, and privacy-first IoT." + }, + { + "date": "2024-09-30T00:00:00.000Z", + "dateLabel": "2024-09-30", + "title": "School Laptop - When You Don't Have Control", + "area": "self-hosted-iot", + "type": "writing", + "url": "/self-hosted-iot/school-chromebook-bypassing-content-filters", + "excerpt": "Tackling the challenges of managing school-issued Chromebooks that bypass home network controls using TLS 1.2 fallback and enterprise-managed proxies." + } +] \ No newline at end of file diff --git a/src/pages/archive.js b/src/pages/archive.js new file mode 100644 index 0000000..51d3d21 --- /dev/null +++ b/src/pages/archive.js @@ -0,0 +1,90 @@ +import React, { useState } from 'react'; +import Link from '@docusaurus/Link'; +import Layout from '@theme/Layout'; +import allPosts from '../generated/all-posts.json'; +import styles from './index.module.css'; + +const AREAS = ['all', ...Array.from(new Set(allPosts.map(p => p.area))).sort()]; + +export default function Archive() { + const [area, setArea] = useState('all'); + + const entries = area === 'all' ? allPosts : allPosts.filter(p => p.area === area); + + return ( + +
+ +
+
$ ls -lt writing/
+
+ {allPosts.length} entries across{' '} + frontier/{' '} + applied-ai/{' '} + home-ml-iot/{' '} + self-hosted-iot/ +
+
+ +
+
+ INDEX OF /writing + · + FILTER: + {AREAS.map(a => ( + + ))} +
+ + + + + + + + + + + + + + + + + + + + {entries.map((entry, i) => ( + + + + + + + + ))} + +
DATETYPEAREATITLE
{entry.dateLabel} + {entry.type} + {entry.area} + {entry.title} + read
+ +
+ {entries.length} {area === 'all' ? 'total' : area} entr{entries.length === 1 ? 'y' : 'ies'} +
+
+ +
+
+ ); +} diff --git a/src/pages/index.js b/src/pages/index.js index 5024781..4b2b292 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -3,67 +3,11 @@ import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; import homepageConfig from '../config/homepage'; -import latestPost from '../generated/latest-post.json'; +import allPosts from '../generated/all-posts.json'; import styles from './index.module.css'; -const INDEX_ENTRIES = [ - { - date: new Date(latestPost.date).toISOString().slice(0, 10), - type: 'writing', - area: 'frontier', - title: latestPost.title, - meta: '14 min', - url: latestPost.url, - }, - { - date: '2026-02-10', - type: 'release', - area: 'applied-ai', - title: 'open-embeddings v0.4.0', - meta: 'changelog', - url: 'https://www.open-embeddings.org', - }, - { - date: '2026-01-28', - type: 'writing', - area: 'applied-ai', - title: 'benchmarking private embeddings vs ada-002', - meta: '8 min', - url: '/applied-ai-engineering', - }, - { - date: '2026-01-14', - type: 'project', - area: 'iot', - title: 'overlord-network-kill-switch v1.2', - meta: 'hardware', - url: 'https://github.com/5L-Labs/overlord-network-kill-switch', - }, - { - date: '2025-12-30', - type: 'writing', - area: 'iot', - title: 'flashing tasmota on a $4 zigbee hub', - meta: '6 min', - url: '/self-hosted-iot', - }, - { - date: '2025-12-10', - type: 'release', - area: 'applied-ai', - title: 'recruiter-rankings preview open', - meta: 'preview', - url: 'https://www.recruiter-rankings.com', - }, - { - date: '2025-11-22', - type: 'writing', - area: 'frontier', - title: 'differential privacy without the accuracy cliff', - meta: '11 min', - url: '/frontier-research', - }, -]; +const PREVIEW_COUNT = 7; +const INDEX_ENTRIES = allPosts.slice(0, PREVIEW_COUNT); export default function Home() { const { siteConfig } = useDocusaurusContext(); @@ -105,6 +49,13 @@ export default function Home() {
+ + + + + + + @@ -117,22 +68,24 @@ export default function Home() { {INDEX_ENTRIES.map((entry, i) => ( - + - - + ))}
DATE
{entry.date}{entry.dateLabel} - + {entry.type} {entry.area} + {entry.title} {entry.meta}read
-
— load more —
+
+ — {allPosts.length - PREVIEW_COUNT} more entries → view full archive — +
{/* Projects + Consulting */} @@ -141,7 +94,7 @@ export default function Home() {
~/projects
{homepageConfig.products.map((p) => ( -
+
{p.title.toLowerCase().replace(/\s*\(.*?\)/, '')} From 856c2ed193093fd984e6d3bd5aa06f58e6a00f16 Mon Sep 17 00:00:00 2001 From: Nick Lange Date: Sat, 25 Apr 2026 23:29:31 -0400 Subject: [PATCH 09/34] =?UTF-8?q?Fix:=20Address=20all=20code=20review=20is?= =?UTF-8?q?sues=20=E2=80=94=20remove=20hardcoding=20and=20shortcuts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - homepage.js: add consulting block (availability, blurb, services, inquireUrl) and areas array as single source of truth for both UI and generate script - index.js: drive terminal areas and consulting panel from config; fix redundant filter computation; use entry.url as React key; fix mobile column hiding (apply .hideOnMobile to th/td, not col); add empty state; make ↗ column a real link - archive.js: read location.hash on mount to honour deep-links from homepage filter; same key/empty-state/link fixes as index.js - generate-latest-post.js: support MDX truncate marker; add LIST_ITEMS_RE before BOLD_ITALIC_RE to prevent list-bullet corruption; read type from frontmatter; remove dead blog-misc AREA_LABELS entry with comment - .gitignore: add explicit src/generated/ paths for generated JSON files Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 4 + 5L Labs Homepage-handoff.zip | Bin 0 -> 21340 bytes docusaurus.config.js | 2 +- openspec/tooling/claude/settings.local.json | 19 +++ scripts/generate-latest-post.js | 22 +-- src/config/homepage.js | 13 ++ src/pages/archive.js | 45 ++++-- src/pages/index.js | 80 +++++++---- src/pages/index.module.css | 151 +++++++++++--------- 9 files changed, 214 insertions(+), 122 deletions(-) create mode 100644 5L Labs Homepage-handoff.zip create mode 100644 openspec/tooling/claude/settings.local.json diff --git a/.gitignore b/.gitignore index 5da53e8..d4f0b86 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,7 @@ embedding_generation.log __pycache__/ server.log \n# OpenSpec Agent Configs\n.agent/\n.claude/\n.gemini/\n.kilocode/\n + +# Generated at build time — do not commit +src/generated/all-posts.json +src/generated/latest-post.json diff --git a/5L Labs Homepage-handoff.zip b/5L Labs Homepage-handoff.zip new file mode 100644 index 0000000000000000000000000000000000000000..85fe3a101d9a9d74ee6252b496c3e85807240e9f GIT binary patch literal 21340 zcmb@tbBt$E_w`w}ZQI{2+qP}nw$bG-+qSFAwr$&X)pYXCJb8b~%#+Dv?)m@b-jl4g z*XQhg6r@4H(14)+t!eTk9iad1!2yB>Vzr^SF*I_fx3IT0buctHWmFOs6pQL8c6KpDSQ@E>9Rt+lR`^XZOAk zeuVxY8&YU6Mg#UjW5xUap6cv-TFJahjca@N_%QA9j{DfC#J%LVb$|VN_G{IzeilXo z3lv^}V`$!VIke5&%X{o&L7mbs4`WMApgh}kTF5390fu+0+Z5;CJ5(XMYRRAM(0C00xd zl1tF})O$P`$1uUub~C6Oy^W)RipSKIcu2WA!wE~5uj|yrHuDUs# z6$NwJ2I3*+GwPVYQ^qyhqibR3C}ZBg67pt3Xe+zZ`2`S;%1&IJI-SZP&7f*(bWE~J zFsKqsM>NWhOqPp-vrwL&_a3?KS6CS_KRW$V!X3AG%{*4U&n)381cmN1bxuuwVo9h< z(75WL*8`=5<+ih8Riu zLK%IX045|^HXWX?p6WgSKHCSqcbSv5!ssG76UT{JjWxM)D7jbulE=-hP4gxvg47Lt$%Cf5*lGOD>8ac#nO zVuYxzh%_n!P_oW~|IJIP(@<)PaMgPm7Ss1+7tVHQH3`|K&f!=EmTXkTp?9xW+5y8! zdFB6b|B$sPX5akxbu6Vp*U!BJdn24&XX5$I^6o*(yM~@c;HFReJVAkgj3I!46#gHT%9VA&1yf1&5bK1>#J`pY8%H&MF68qP8+XpEXa=@ zCT6edVsH!_i;v1ilL#dth~Sw^naJJz)=m;U1Q)U(&TyLtvbDl3q1)`mCkvUe=OL@>n_@oECK0dY8CMwfgYO~$efx7&ClA(J?>jzrg%C#{kM z5A*}*G{lw|-lyKXU9|`Dcx&x|01_AmOHL{VO`Bs46OO0D6?mc3MkF3i$LqB`Ss<2> zQvw+CV5h|a5?))*a@_cA;aPL}buz-*2n1Re0Hnm|nlfylX+v!U{aGfU3N}wBTYt}@ zC#NsL4v6o8UAh)*;Dd%(Ld10oz7N5y)kVlmDuJ_1tiM{7DXClLqBhi~au|wC%A32p zBQbd*$_52s+Q@!;k|n6OH0cnX5^@CC{lo*j+<)~0l3jz9QubrLPS*bRi;fxk0AUxF zi`YSGRt*zmXJx6ZwP%I65Ub?jwb&!olRLFlo(-QU&tEn@5!O1d;}B?_B?!!uuftco zmH!=SH!8ld>f{MbH_dOf1JXrTe6dAz?1|lhQQcla115eaDk8bKjGm>79RV;E%gKai`VPy2k zcIGa;;2E744~>FRFeR8Du1kzQilvN_@fUgClgt%?z7W4$o@))~{f!?|`SMFhf@`A< zX2D+qS$k6cR&lS1=;r!rF|g{}CT zTJf6PmpeG9|=?PQ(!poNSrLRG=H zVX1hdsjx8&LWk~)kZ`PB9K%1XE1ssm(tC)bEdf4*Bki7c?97lk1vU<12Y=2>dX^X= z#eGjTOUhs_(~qxG$+xJDGGu(X;XFCoD$eXmlFout7|%c)5s73x(qa?KbrmflHt$nl zvzIl-pQHc9a=%nX(YPT!P|uuPZk^J?^ zk)Wi_6Wd4mDeX0N6%B>0vU+A!M*&@3EOISBPH_}zE!P>w$hPXyDEHKh+dSkn#siel zI6%QvkqXqP#mT%0e!-tol1-=Yq!&{N1X|_cESay&qNuVHlq-5T7F^2pnrC8p`cP9Q zSeuAw3~e-t9+@ndA4@u*BoSB|aN;$qmHlg2;Y0IxoE)XS-`~Hl*Jg*}|B8rX%e)E@ zDSpNuX&Lb3%mn-HjB+iXDSW6%vKpa0o68Whz$2x<+6#1&Rhvw4CdgBCt`zo*60Bkf zjEB+WxaRj-U+Sd{>@TwnH`uQEx=uZ^7$)ao*!+A3D}}qA@_!pEOxw+Ap|X`_`D|_q z8FHql#{?PN&aoJH??qR0@ehqkWA}`Jp6VFPtnO#t$Yj&WXwr#Hm5w&*kxR!2Z+}Ou zR$$n%BWwGv?ni{3%5>)w^Vz5;c;~h;bc&%!yyE4Y>y5fn#w0*-Op!qmL}yEk%fR{g zCz!Q5Ga5wBECn%koNPy~JYh;&Ky)IdVl~n!j^62!JJL8#mv2%zkB)in#4ybGbf25A z{l2KxXOKJGSlIn~I6L{h^7A$n*IL`d>-|1(<;r#=jtTn{UA*(faWk^Ap9y<+cz%Xx zKkdDO0G0*a$C16@av7ggoEbtN;#8>k#<8^(PwxF**+*)lk%cVMPGrBJPI)pmV?XWb zMp0rvM$6;r`6c4_{du-Q_T}02_v_B{CR5d)reRxt!encw)9*>u-7(=4p3R3-HRnE* z%u>QAWj|Hlql;2+WaU6lXu;b>p6g_1F24Koi)*s`>+Ai6vQ+E8FU*8h_; zR$h;{XWN8vwX$(RJ=*`{(2Kfp`=XWK|Ld{xWoruX7jt|+7Jq0!|z$@-;`};`<=ioWuli!r}%?x* z)(CzOX?I4x(~Iwr1e+r?34*dCF+7IX#W{Y0@~7lzCy4(CY$J(NcvUD5hLWXTwWd~v zy`|c$vY;hBFwJIF2(}Aw zzLjXdVqfPdbR$6XZ@mkQS~w&`JBgsTU@2TPn?A!xA5h}#DFyroqEem$=Lu0cn7P6U zb4(pM?7e(W>9}mUF76dC`7<4HBklS?b9)9nXF2R0*N*Jzc=*==17wy=DR9^uG$ublM|@ zBcvUxyD0bWM9E3a|O_UxGv5SBx14wF6yX z3^;#N-}@T8i1k~XxJ9@+vc@4Swm#?GHquo2aEzUM_tL^fD+MfxPMf)|<76M)-o9WN zcO}Rw=FCQeLaz;uscMZ>2y^Xn!l%|1o0i4_ORIN)&x%s@q1wlIf%2_J8`ar%h=~G( z30sX8w^@%3AdYD?dG^~1wD&9c~N zaOc$B7gmio4m*%fj|V~vqVL~mHXy(9?Y~V|qLLfiflXK7;v0H;pXXCpJtTsQMTU9; zH!JC3^z!15tmyLxX?5gm54iYW4UJ1b>oE8PEhSErECD-x^myz}G%Zy)HdD6kV*b)Svx!)hsR1xq^mVF?_T|=h65NK_G?B4$gPs#Oh z@chX?^2LXKTaj|suwA?4wE-8Dy(xzYIq~F0 zn4|3MSjI%yJrM^b2i0b~?2^64h+#wJ zQQ2w@c}tw-pif|Xz6zf6roFjqT=Aii_maxT#=8-E8vj#jMzTbxq?~WF+j*HwX;ybl z^kZdBXySl!nHPcm8Xm&(fcGN|_MU!?KgWrwWCR#0BNJ+yrI1nu1y;#$$wk@s#Z*#3 zIDeA{XfwiWj?&dR7>$Lx1&BxSGTq;iSb+b7Q9uoUQ-P>w>(J< za~moxvL4w!Af!1`X!aT@AJ}}xGj(bi(V#oaB(P@>h9oR3e>KY@FNydDamnn?@Vf?- zFO*@wiu5sN(zv*4J+rVh{f3Jy_5Uh2eBa{U%0`Yfb-}=2>*6-b^?c^);!(9Bb&DHB z`k_c)!fUzGv|fGvT`rLlGHilVs7#^tBsa}&UroC6srCHx4?6!Bf@TRZ8WSJ@0rAoP zHw2~mZwP8)>TGFlM{jIs=Vs{4VCC%bAN)Mjy0P2fO8Lnt<@jS+TP>DyM0RDCeFtur zy`62q{-Vd?JckhsEF~@eixN#TzKMQ?)q(#Cls-_9wqVw z5pgD$5Rn`oKHMQR)HSzTH=5L66&ljj)k*6Sp$_%lqi3A?LT9icO_;?p_MSxBM z>tR$0)5IQ_9RQ%>08e=v{EJyo#j4gHd0hv0w?8ko>QWvgV%fq4N=G0{NnBh!a(BIV z35}wT6C~}(`1A|#8~JX>dk?wIa(GtxE&B0b_UrdPW?&)Um#`YD{@~sN2T$vx|AUDz z{6K~eLAbf2ht(M746iPu8GcGim=y?hQ~q&rD|Dl?(;Ut>Y|1c7&L3wdDLi7-O$7Le zBsfT~YIqFdPLrgat@y*`tskm3=)G4A=fK;GCr79p`?@@0i~c8p!U9c3o^?F}%~s#{ z5&NBef9rSnd|8w=Fy<(SY-m+DGZAB$#M?bcVi*l29m)L3IR}fQnsyy5s)(edli3%~ z@~I3%rWTige;ZZ1l(H$A7*xcQ4(juM^8;K8pO=d@^o_bwmqFSf-A0bBh~~6tmVb-A zi_$*V2dvAb7z8MlMnOL1qlMfOvD3jC7p^B zjMt5jgXY2F_|5u5XbO96Py?F%1yANYt`y1> z5_G>2!W05MI7bx{l=So^3dP_{KW=M?%Te_(+nQ7h2#$0&Z<>9@Y2?#q!q8#kq0QHL z=g4dysS9+1bA+;f(bl#eW(u-(@LZJ8tkY@*FAb^3mb`xd0;1UAR<_=TPa>|Om8}bf zPNS2;kRoPJrROtYvaGuPH4FDX3L=2PnJNNL)eE-WdSCxzqHJf1-T!B5{VN8P-~MN8 z_-hN|rh42K%Ks}DPkyU}Xg}Y|Jh3K$pcZ0JYsoOT5RmIk9JXcI@ zX_jwQ?Y+z~4)okjur9~##8>I%8wqVF%M!T-9evYU2&scPWw}>b@xnUdpc09SCt()Z zu$3^R7zYXg5mSj3Xz#;Am(U`qd{bMTv<-e}Sx$-&f{EP}@aIx5=*{1W6=JfsEH$^L z!kAPTQwSb4KpPpDv&0j@ILv-(uGmsju~(Tkn*}$tNu%B;imHnhC61W9241eyLdNvD z1x~)DvnMTMm#Gje3|w0uaxNK^Dh!e->$+mILWf$KKC{n4rJNPl##T*Za$?@p5dSv( z6^O;>S0(Wk^^8Fgd%NX+yHJppA63@qxx9#Xc5qp(yuL$x&@ zr+BoV2N(@++8BBa1)(D^gl1)+qLfpfjqqS;ILB+~gwOBxQBUShY2Yu@1)Cv?-Jh!& zZ4HnyZa&oAe8;-PR&xbp%Z)HBqrV6s3xit(qma=^V98#0FN?XGvM;C!RJRxY_C6+X zTl;dVgIFghKi(!bQ|UKQDm~bk5XQT}80o+lhqMH0`G`#p=qDH?+DM+V-; zq}P$UdR~<2ff{NFh@rDFty&gcEEWA;XPPatP#3C<$|G{;=5d;^qcO4G zu*~4B2_$tjYv;>=H6=ZB49gGH=^b--J>)AcEW-HhaEY;r) zRiw0=_)glX+0;;O@Q22R`r{IXRi4*^NAj;Wxbf3m4o?9CS$E1QTDl z;%*g3OJsC6q(AAa;2xH}^M=oL)eWyBu@OZsyF?o6FJ14xXx>@PI3-^+HT6B@svHwi zG&Oq$d{QnE%WwSEyL+@oSN)bBisF`~hkFC57ow}DH?uy39mHs$f2{ljNeH8`4Dg*O#YGyAx!ornnWmQ zA^uzfUb()TzwVpgS&wsc5?$CR7EOGC9eN+32AIgeCLSm-KKyAg*js2U*E_B&C{LI7 zDqLuTbS`?^RAD#mYs+~&9v;1OXw#}LSkfd@;p2%pShYIi<@J7gw4sSi7v?(Kik35u z_`ze$b`itQd~DlY1+`6T8<4yigYYC#`h0qNYT^TbsVzxhWm{eL30^PQo|QK^@$z~; zU*+E|f7tRolBd>Ht5HppWxLj#Zo!enntP{I+C5zD>BD;UJ7_E5R>-z-3y1Fc1U!m4 zc4oZ@-DIBo9p#*T_QbmRD6Sc<<5y|;Q?`u&y>4-g& z6w`3yRHT)b;bGS={Nwgw!m6u6<;tlqJL-+)@6T@R^+ow)>-pR;YF#fcXKWn;+ny(O zk);mdAGZjcFo*fh)L_&!#HRWa?F3dxP+NJp>tvH#`dZ%?E<>%iyq-G(C|t;p>Qq?H z`mVD#(Xabm0xGsdH=Uf z#^&2~$2dEp_GXKx%Xafyr?oR`a!MO(5y4I?Ifp_AQ087EMWyXLWwaDL$%Kp1M-CDP zgJS#V@~ur9FIs_FRFc)$s25yUwBeewCdxLf0jw2NA!iSp#;KH<0=pL^@MD?g(G6ny zxjeQ`(q<||nvjsnJAG|sDA&{bJ8sWkw$(NhsLJcp*~$sX*%N~i5VjS&lc@U+z+iRA zdRu$P(tEoW@22J?jYk>VoiM(bDiSD>~mU+xMAK-+B1{=60zslzrD!cB#jn z_gkWL`2Ib|mlqX8Z6ntin7VR=8etdN$`2w;NIy6ZuF*$3dS=t4+TyEp?SK>GTn>*@ zblDC|-)c3DxyqjBPbSy7RDU^LOzsX1T>!0{)tJl59=_d%Ec!7yU~Hj2?)%8N8(@{M z#~X0wH|VP>Kj^CX@_crekr8R~PG~LQL9aw&%bqdm*G52?{{jEMaEQF8 zn79TG2q>NEzu^$$f5RbnOD9t^Cqr9PXGSY~S0_6|oBzQ?Rhn0J8>36#H2K*6g;ypIC|E`&Hr0gOdskYDlyETd-6xP3%gK{)~B;3AEH^ zRAY$}rbm}Jf<}o011CPbC!|kYo%txBvf$wmKJO1Z2CLquuHowJw{Eusx82^}Dz(Cl zMOqS48sIE^XI3J`^Q<0>?ZRi?Oi<(I@fSQ8G^bvGN9fU`S9kEJZ?=;P|f&U_iHh?P7#D zb_a{)0nYT`D`@0!io^+mHg?-237Zm>-J>jZM@K&y=A@EsQ6Vx46;Vt3_@r76IK4>9 zkudEP7~@lP=v-o=s^CzF#G_>BQRkYhNiIP?^sUD#0uyljYAHZFDBTdNvdUtB5W5{6 zT@u9okWukqhGZ8Z77z-)uwfjS(KLm^dm_>8E6};!CGDD6XJ^>r7^DXzuNg+!O)Xd- zvf*dq^1pVVVBsB#Hz+E{w2KuG>{NHw;Pq)ZO~{7l*w?slaGUC4&!7!uUZhFGNbg1_ zvXc=}@Y~3Wk+XwPl(+o!uc-JR5lH5KxDl#Y!`Se18Ed;S2}Kro644MKP+&49#KOe_ zm(S2|wDkH;N!zyG@zbx$_qQw$CkMt~5kR%fV z3gqLVYK*~2f`}-_hhf5hK2d)W6oA!6SR$RWme}TY$3;R!22PfVX(oqg--L;v5(Q2} z88b`7m&p_~d{2;(l^G+$Pz*Q1M92imM+znXa$9mtTTiK*&Gl?$D%-l(e*A$ukx6Kak);#Ph-F;qnq#1o~E^gSKy2P#;U`rRdV zc#E1Qa$?q|36@#~N=#o6XIBN<>RxuHCjz+g9-|G-`NES{Wh{ahY>MP(xukRPH0ap6 zedH;IFVnH>#IlJ55fpLqGLUA zv$Bw~VP1v8sKiNZgN=!(`z)YPN#ZJ1#|h<^#Ti1EN!I+&HJim~eL+^r1%wXM?w5W~ z4X2cdYLf@SO8ko13mf|P*`Qz88@rz924$?{;Yna`rJGu)sMdXikgco^nYTCoeP4MH z^!5|OT!WiLR%-X83iKdbmpc^mA@nz~Prq|Tnac$`24O%mJ>6(tNGf864d5~+QI4nVoRA!5zbdDaLr4`v0&a?RmL$r2XDB_X~I?%p6*xS)s z$Kh@(XpOE--|5D8taZrUEmaR|2I6*R7>37}VX>d)uk%oco#d=i1_IusgL*Hu3UF|2P~LA4}N{#h<|N6;6HU>xfc?2M@wp5Bm7z1R0p5!089V5yg=@O zJDX4H7*z9C4HRG^Ug5&(VU>$Yu}u#0iPM)y@)1EOuhRt|`@j`Lt+4%Qn41Z*j4K-! z$dm&mAr2G>Sx{{p7+OqWTbPuSUHBy$Au>Er4DS*R4t7FA?!%mAbgqLp`zv>4>NjTI zuXL#jrVyE>JXo2uk{z<&yC2-=AknOc0%T z*B~jfiqT2}5@Q;PgBjw|LbfJM`AK~of~J|F`00#QfSBYNTlG51DS3HxtKW-aTF-N-`*baS>$ghybkjo{+~sL)C*GZ5#L{bg zQzlyo^HzsBzGrRA474r6) z8RKy_gLl1d0Yl-L8nvu{AY^n>JD|TGV_L|HLGs(EX3V2rrgz?jiq;4>O>ej_W|-m3 zvjf=>)X2`aWfin+5buLNVS;&jhq5LMJc?aju0_h5I8f#t?qg>wdc87KOp?705P`*3 z*w4~{f(>{On|tYqdfzX%NdX|A{MmJ+gBPvEk55E1)r7W8D%RDE{yP3_SzPME`(z55 zlbvB{f>ayRk1JlT9iAuHzpPmXdaD>;j07_kfCg>X!Ncn3uhPOJh@9dz#Z?(EYZ>S) ziKO21vZ~QTWe`7Jysm+!J>J`09p8I!{2 zm)sK^S#0^d)3HxgQYXJowXZ>1t#4HXv=k0DdnuKywU|eM8=*3T36tPh2;ANewec-e zG1rdMkVu#^2{Oldd3>BeGE+<{UDR6}T|y_w-pIUFnOA>gy5-g8qa`;) zrfH#8qlpUbiU~!-4{T`uxfW5Pa02ESme160VCLfdSligY`kd&m)XEuRjkJBNPG6lK zP1{pG=o04#(rKTbXNTDtb5NvP>FP7jS6ObYayi;#n5u49DgaUyzND7EdgXP(*vcHk zXx;5~9cEwqZ>zT3YS^i*n8jhaalSBZ4%;-6M#rt7<7K&uw&WmJ)R%}f8i``4MgiIl zxo{P5qoBlBIQ5@Y1462pkn~-aW{qMY=0Z!`k+O~76+siQnU&VWGHV)f$+#ygJvp6x zT6nV5h(k>+n_fcZ%|AZAk30XFpyJ{_X_bNo0-D7AZ+Q*y|1z)H8roT!nL4}J|Bu8r zsj+3ZF^=5*q6~*YZ$KE2Mm-i=s6mqyQnNMQKwCb-0p(Or7QWwq$S`xQ)IbAus!Eu5 z+VR*O=af^VA+?NH&cTl<8Uq9~wzZaS-~DDbDhA5;D>C0KC>XNm>r3BZg>YN%<6!Jd zvJjXL^A~C;NY^NGQV9<8;XD3e;*>>OoiRmnms56r=&cg$Wi(S+WEz4%ZLPLClpMdt3~`HI)*@`TF`h6_8%U)|bSV(zrA0kLgHo2>CTSge^Mpq!Qded}649`Yktgtk z*`m3RoDO{!)zPHKY2|}lC!qvgRuZ-iE;k3owe*Fvz&K$pP#i0^0S4@RA)0{@fkO8N z-3FUaF!t}$?#afnenYjr(&6)2`C?3Jw^ZrTP$LR_d?FNOD+FSULzZ8fS3s5>Z_I5D zd&e_iYL(gQuC2^{;3lEN5to-EK^K4zT(@3pt@wgvsu523m1W%#Ya=nb(Xy;x-+C7G z4G12o$%R0lcY}c?8#c+r2w&o_M}Chd_XUWEg&VmasDxtQt2c3co4t8Iemir%brZOe ztxvbRXa`rUQyPCyfzlN{tu~;K=CJF>#p6axBCmUKSDLLpmKF8%wBmoovS$|8JUn+2 zv|e%9VZR0@vqIrJz!S{G(alRjMyw#9Hni46O)?h5n+ocHH#ojZkQYdhrop~}sneAD z@asa8c4!)Lp~uV1@$g2gMzP1-dkgWyx# zp0(2YXetLf=R9GBB0vrbDn@>^r*Fg6C^p!Nf-Vc4PotiNUWzA4-lWokfN9VFf|K6W zi)*V}LOo|9=1pVHjt~N!j_xM@#Rp!(p3emZn4USFxJS$F5 zs#5DZuD#>fzo2>EZ?_3I-}yhD?kn0_pNvbO@ zOan0|8Zjj;br(N1{kYn|UPW}`Gz8daAZ368;?nU8z+sj7M@LX zS_(!ZOH}~0U|A28VZmBSv+9uo@0_F~5M-ESI6j&Mn%T0X69A#(#JSaf)n%Rczp_%v zD>TN1`tUv44G()BEu$@MaES+8PpGU{s-RF)gcG=#(2z4o3weh3r&Fk>-=c-1# zfDz08ren%v_vI6aAYVrOxin$n=lvR_pT!W#oyptSm|W}VNo167J7w@BUvUVzG@}WU zh2$K5c5Aj)=hTz)knXOybGzvT?Wm15y&%n;I?vn9UV)Hq*(G?q^ z$8Yy^Yk7BS|E}xT{rDQw7ylkn!Uq#qhEqhgvi9v`5f}O z-NuPK-ILOFl+{nJBU}$!r`k(zC$y43u&+Yl&h|zpFmkeavI>Fr?u`}H#7&e zV`r(lzeP@7wKCL?Zg%VZbUKcmpsR$x8dpIq)W#}raAS>hl|&w{`y)ovq3Sb`v$lL`Ud-kcI}9#Q)ry6<6VXgv5K*zhtVUD zCxt5VT!ArN(gcW@fKviskcE;8YDV;ogH+y$6(U*c|M9PcZ?cAfCs6mUBo6iP$UE;W z6$k?eFFa%)GKknnqTIQ+cVUwGD}-MJYDHR)KajgO5Q-`d=j9DB5FWSmcA|( zDq=-DQ7qj>*T2UUNevcQ>-ytj5&-y^OnURw3i|cb&!bk@P%Q7>7Yyz(jJubQ?=V6| zf`c6WxkQ?xpxqXmn2Hou$c8u92ox%@*#7iMD1R8**fw|gSLs1Bp%@GjY2Zc&@JRcX z0>1D&cH1yS%FxCIWqgN6bcx)yEcZh+a7(ywQ-HQ}{8b@007bkV?*^kDpI$(OH8nj1 zhkzqea~vUa_!6cv_~d&S^=Ld+vZ&u1F($H&Bn`ZfWjDcc6NKOQ|--sIH;|O+ewc6 z)5weVfCp`a17_B@6$>tWz7tz4;aP{^BV(Mzsy@Pp0lk1LfEh6uVWpLNI!6Zpq{kYL zJ3MPmdJjV2BG}?))MO7ZX;;VC{sd|uF{n3b7PuZeB_gPXFs0B@ab$4*E-c~z+odYW z*uc}6CH2A@lX;E^OGux{NyXoBE=eSi{3Cg>+O%j9hI*hUi@r7e5ANL5|;B+-R{kH5Q zkTh=b^W%bq5XFs5SLhw2lg|&Kxl1giNqf+mdFKef4_^p#0;2}e%zM>p$8+pQWfCW&fg~c~)FBn; zBTLFv-z#rgpxL>t<~8X0dlxSH`#ylz>NGMpY?m!J?ABbRmfVyl&^_ok57-E`V3{Xm zk^kEk^gmDAU9WjLJ784Rh{ZXGLg(%IJpP&*2%;EmjxDu7IM7{Qnzh;{)jQ647}tXx zJv-D`u6NhbxRP_U%=WTgx(}luPENEc5oi=bs&wGKc2@61I+9xwsttY^RI0VL?*0^O zd2+h6YH1H%qlas(#5PO2r>eh)&roLu;EBQJDMev35tPF4eLXrqnI&vcWn;9(!tVra z)c0}P1fIC|Hghj&9~Wi@DsR&{tED*igL~BY-RI37ukCV8rE1a*?HymfKcdxa?^vYk z>UDAACw;DLB}K%1AJP!|huyDF*=%JZjype?uk6We0J5xt&=4P1d-! z3(!ixsU^J?=ZfqJNjG`_z5{zwS3qz_`glnH*QTKjQ?jpPi6spS^fM8Eurc!wB!e(B=j!Wh-|vBqohU zkYW8N-=PNS0R*KZoI3{&_qD}rLtc5+*&jdDoLE`nopa~MUwquFM-{^4+QhEXwEBDF zX&U&fGM*f1K@^0P7`T)QlV%YJAeAG9u!6PG!zgyLFru#`z*l0NMWhbRvOAI_6?(tc z|L>olxF(o|%*`^Mw%r1GxoNeh+f@?H+4EPIA!aOlCXFPuFogy+?bfHPrOnIY%nfiA zGPz`J&q{@HJ7eMXv|Bygo~M0Hg`U86D<)#K9oH;V7`YMICrr0*@LO{JYI($wp->|> z^xv+3$n$^cl>GD2bPUD>@l>JwX+3Xu)|GBZOSY7b4C5X1yDhq%^uKbL*Nx7{@^j@$Pvyz+5$4v&Z$Oe~fyNESHx$Ltq`aW*09tf9!-SyQ*tSdG(hY z_IwP+JuhfD$@0_4FIqIINWco+7R1mMu*8!-G#EvsAdmZ`R9&xG#WMW8mVI~uv_qiN z#^a!HEsLI@DTBzmF2t4EnkDD1!GAakVB|%TMac*YvSM>d;*BrhvFuOY5d__%DT=qFv2ZQqCfya}b2P03A6b>eX zmotZmI5W{S{&MuxQb}0@{&cTlu))p&p@aJb1P6a~(c^!c`#iwybhmeVJ$xUU-L}7_ zbTpD@;O0x9;fra=w!~2R%hwnkI{cN(j07i36_Lhk6d415>!XS36w@z}vD{Yj>%id= z9TUrl1&k+9zpKV{X_+4DH&O-lMN?0C5fLZ%MN5IDL`2X~ydIxMuYK$|)4uK*aQD2~ z_uWW|rDC!$0@=PgcKzI>@YOK?@q9hO6KFGL+ZrXO?rchj#D6cnVUCDk#n?Nt0>aW9RC z@tk`XYhslD@d6$3=j7|*@bmR^^n1uQ+;1glYuVT)$czb_Lc2b#M)G3UEj?=gz8DE+ ze7Sxxi-t5()Bhm`eI)oU+neb6`4{v4*9>6)5JgPPzeAKA;{O)<{_8cg|E=leY+>kR zYVto4U#Hqu>;@ZZ*S`}KFrUC^`4lz@w@fek;tdwdAg;)IZ6ZuT1%*WkBiRH7NljOD zVBZKn!`z}gKO4(NTpN2YQ^znKXEP6u1EPS;EGiiyGcn<&g=QYG=qfo>V&mqRsd>6qO3h@YFf0iLw%^ezCn z=u!v4a1r%lrlqxhNLb^+3{1&=DU1m&aKPDNJaoGtza3QCA&a8IbHSOjVCrP5_f+KO zl{0tyjFuX{qMYTrT7#xIJ?RpyX{&lF&{*SNd(V)MXu_#6#D03ZbJ=0wyO*4&`!Sq8AUll=frS^r#47pvIu;;B=UzG`$Y<`dILe*^)n z15FHXdGzs;>n|uLqC{;52ZJchJb5k8gFz%9fJ$ge8k|2x<{8~u5IKpVocmRz+^Rx1=V|;qZK=X4V1hUi_5!U zb0|n5s(%vlAv|`}_~*<(lQ}TyQb>GEiEJ9IW#bNXj#DuTFSajo8c;uVMa@(1Ta5-? zBC{K!dfThwMD`=fM#@zwaMNhG_Oi_=+OZd6#NZhOW5jUwT+}mV-5NF?ya_62bg1Y4 zr`5zw+n~dTtX2(q0;9;jqaCvQ=0(6wPUEoTfYrfEv;_*n-~NQ{d`=&t!^K*mp<&@O zas_;L!Eoo@Yvg3+cCH*_CufaXhd$@22h;pza=-AjwhHZ2WvstCuyE9dz;@GYds^xR z6LHmti)llvX(3Uzq&T}B3gMG*Q_A1umcOiNC(tj0y;M4L$5-~oc*EGYfOR578;~$M z9{m1_INsw(MY=Eb0X?xljG23dokxsN<84 zY>@I0=O{1#ppl76c2j_Bfz{g(=uIDSpZF|(0{d+lCa28Iq69hz(!eD>;D3JP{sf`a zSD}kgPZC+8K}G*cQs4g97=#1L5YgYz(e}#2SnHfV`sTd;<=;jBkLEioKB_I<~gE>cF|5 z9iW81VH_h8;gok}Vc(pj(dZ}O(2%)lSYRSv_>4Jsam?J3TugfoC*7+d9&`9!#?Aqv z`K8+}Bhl!kWUk%2LhyXI{jo9gEIAZp#YEK0qVEh3P|aEd)( zzE-XdU4NxFw>!3nh1nNk3=l-h8}nX2eN&g}a9|s9GpFUbs6%aAbW{y;td3F{t}@oM zl4a4WnE~^^CVq-(APe`4hK1(d2CIo$-!p+fL>z#f=14C)$kfB{joHLt2^J9sxhHCAp`T~tnkIC7#oWT6+ zB@H7jLGo%jOQP-3K?G7OQBc)>E&GL;L^2x(YSJ$;4*^yUtNr z`jnlvD83y$R&HWClu`YWFS#Ck@Z4x=bORg|q=^e6oxTxVLfiT8hf^{J0QFM^G{0O~ z$EZNB{dsYQf_(+=M+d8eG63E&v?av-fEs|O9+bm8adCVCi|_60|tReCtv~yNN<8P0i_v3FCp}j5D@|C zReBAGiWEhJpoAg_q6kQlUP7-@jCAR}gHlB~_&jq)^!D82{s8;;;+p@=zSdrA<7&RS zlGvdq%#1G;z>i!x*`RDnU;w*LW87hcm6KUu$ZazEcl zWWobmJC=)3V--@D7e%}K<9e%9An{`uRE&6lUAsHBFJp_D_04(M^iD2ju+O*l_E_H< zs{igA_3xC~NRHLpige)B8)^c!gI5n-KrhN>BpXO5WweP~$pvMu8MLGlOm>v=;pf@l zL}?F7spD0E^g-#5&q-{zq4UI%T3y0KJhYFc=!g5S`+bq1Tnc-}8wm&mX3e=U1(cW8 zpJ}iR1RG+7q}1--An7UT2D?Ynm@CDDI{ARc;-^<_gx_t%stCp@eig87|5B>sn*P+g z(t`Bff>)3(#>U*2wlc0VHYn+2O!caM)u{UY6g_=0_&YWjcB{CWEckeyjHeY%c-D< z=l}4<62aaX>?+>k`5hg`c%cB~@VNbur7H}FQniS= z;@h3=f*ckxAK#8VirYbQ!G|RZL36Xpu3ZPF%^x4yHCH|AxX>*xD9LN=;$4-3OHJ)3 zxBp7kbQfH|yW;5{1jfOX@|Pv6KarC;*=sk&62NsB=BC&k^unnGd>sj7_zG+}f4!+2 z@=YpV{t)n*$?9FTEBhJt{5lN>0%{VT;rzKcmF4!zM}aFDRTJ09Tu`+Xm{q#sipS$t z14`ELm`x#Wuen@mlu8mCs<(1+-M_fZt-LI#i`BfUrvfKC>*V)wNOMrf0)yGq0pS>G zW^-CPsS8!K&<1dyqG_6BA>le&I2nGlI%|b9-{)LIpU?1Q8>KRy4%<1X&AlA~!rV0f zyeNeH2ohKs5r6HgD#F3~eH-eqwKY1^biJ^*0$H9&l!5W$LJq8Z4>)@#jEXqsHOpT%{nRlk*(mL7Eu_XQf|0_<0_AZrVL_`*6 z|7||F_?JsxPiuE4yWd@Uou=k4)6Lt#yV)|>7xGCjAF8(+Y+v&8^tbA^StTR^Z z5i{vBbg~VdlsrP1$Qn=fgFz;zTJ`a-h@HnEi8wnHKicbovUS~*_1x(E;d|LFxxGmB zC@BWk;f=M};IQ7NF6qR9dE*KE?6&dQTE#6nuw}9v(}&SesOw}F^!-aMGgmQX*#XG3 z)pa0$qrnGc6mfaN=Y%1@FiFJ2A_|f_khyYm-DW9kR)!5Qe1jSW1i3Sp#KiPP=7&Sl zg~8eTn-sz^$4g%gcNql|q6I!jpqtKN> zmjOUqPv2lRy{#U|q;?GbkX*ZJQZt2(+psN}67Ip3V&C+9!Jpz;KgQ|P%8KwJn1Wxv z+pWMvNwDnuQzqfm-4LxDIWN?7I>XeUv0?$bfb8l>XdExa^fcSU0`sw}+4^I2CL+?X zsx1Aqw>J8$4_OSJ_(tPp_q2DX*BSSa&MT|f*g7~+3k?3&@IcKwpa&# zj{kVg-0r8uJx5rma*8UUF6k_hOI23iGrf5>n50On2jf(}9JIXg1(vyd3A032kw0mO8$TFsc0{V~L2AHhXi*f! zvP%?bs5hphu$>b;XI^VUAypCY8`v$(E<*lr_G_FaG?*4=0L+85({MsN#BH&T3&nOX zXuPj>bYNkndJ`3ICtkIeP()sfYx+b@r6+W}Zaeb5&|3o@fO6eJP%}9XqA-%Gt_9?W zJh}o+js+%;L-?&le7)o1)j>Co7K7co)x84GcL`4&H>r{jX!kbc!xf5GXLI212CUVL zHCHvuFllFS)Q+R6_(jOjU=fa#h(39EV@0)(Wr;G>h~gTXRfhtCr;lQRGO2i1c6K^4 z=55I=yVbpDtx9o%(d80d2e|2tO_NsH87))Md!P+s+g*C6)s$5b#EZS_7KOgGj`ot4 zM->-!PjdABi)QF{NMoy%##9|PL7x0_<-YX73dCM|@fot}l)-X5E4alzdh2dnvyj%7 z`Bkz9(T%n3^HK28oi=CtOQ}z)YrZkd?J>q0?C;kel^jH{*HlCBiTVB`^{dTu-*QIk zgN-oz^L>L0@CY<6R%GJSNbMSX`Kgh%jl;RAL(&e_{Mt&N_o4lg?VEV4M8ZNY8TZDw zBR*;vl$RfUUoQIvbh>7n$M=%PTBZK8q9TW#ngklX+`+NC01C70*>n7liU9Vz5C!J- zFXi|1*l0bTqbF>HRIe>6Ia2A5y(!sqi(bpFS(k&@1)V+9(NR{qzPOShj`BU^l0YfX zs@hzxU_}KY4NI?e)pr3i#=d8$$JAuc>h$i~*>K4% zq?21>e*`&pmU!;6s$hBBj3oV&h_54}ot0@;uLQT6eZ$KxJkxvqCAJ1x$IGMkiD>#L;RU(H{<4Ql0wBb^GDbr{{uiX4NmtI`w-K()Zlh^R^ zbJo4vZ>oBYk|N6yGZ*~t_+PbU4ng(+>Zw7{=e`9z14fpE1hpue`e2zSt1Z*6-@MgGjl74i)FwX}%Ku$Qv8*|Lu+LWwBs;v~YYp{>Su;NOwK+WV1 znG2Q3t8}WthRqQafam=Zsqb#L;?*7?ACsms!XSB3R*hBS#jL&Kjd%5g9TygCSt{Cv zg%-b+V_6phf(vSvH%HxTX3*kz+5qCwgz$O|4#8?wx~T|~uxR?#n5(bJHo~RqN%T3h zRlG7fnhC&tPxyT)*r+=hkU$8X(dN=M*)>a^=m;Yl&#%PphQ;vtjid^`FufeiQ>K#V zuVkQZ<;g{mC|;oVT((M#ad`MTGfP&+=! z0Ie1ijsf7qX2>BDk%M^WRgh&GJzri^%{QljAqptXYpX3MkuqjJGaEADjL~P}y6EiC z2tIdZv>L>fCjK7VY+~##HP*NEEi)qVeE>5uxIeA;_4wE*5*a&j=sllkJdd`pK*d<> z?x+gxT0L7Ix$gI%SxhU8*Hh?F>aN;nKLBA1d{lnp^!@SgFV=e}Klhd$mC!Qt5EM^2 z<>rgR6%@_by1?+pT*ISG@t47<{lf)C;{T2@bS^Go*U5>9p8ubBjhF;P^z)M6$;)6r zF8Tdhe{t7OSM%3~Cv)CEl-!@yew+pWN5j86FZMg+WP0L9Wc+7o)Bg+Pe+S5a2b{F~ z{Rmfb^BIPL3dlK$|#95[\s\S]*$/; +// Supports both (.md) and {/* truncate */} (.mdx) +const TRUNCATE_RE = /(?:|\{\/\* truncate \*\/\})[\s\S]*$/; const HTML_TAGS_RE = /<[^>]*>/g; const IMAGES_RE = /!\[(.*?)\]\(.*?\)/g; const LINKS_RE = /\[(.*?)\]\(.*?\)/g; @@ -28,6 +32,7 @@ const HEADINGS_RE = /^#+\s+/gm; const BLOCKQUOTES_RE = /^>\s+/gm; const CODE_BLOCKS_RE = /```[\s\S]*?```/g; const INLINE_CODE_RE = /`([^`]+)`/g; +const LIST_ITEMS_RE = /^[\*\-\+]\s+/gm; // must run before BOLD_ITALIC_RE const BOLD_ITALIC_RE = /[*_]{1,3}(.*?)[*_]{1,3}/g; const HR_RE = /^-{3,}$/gm; const NEWLINES_RE = /\n+/g; @@ -43,6 +48,7 @@ function stripMarkdown(markdown) { .replace(BLOCKQUOTES_RE, '') .replace(CODE_BLOCKS_RE, '') .replace(INLINE_CODE_RE, '$1') + .replace(LIST_ITEMS_RE, '') .replace(BOLD_ITALIC_RE, '$1') .replace(HR_RE, '') .replace(NEWLINES_RE, ' ') @@ -82,11 +88,11 @@ function getAllPosts() { if (excerpt.length > 550) excerpt = excerpt.substring(0, 550) + '...'; posts.push({ - date: date.toISOString(), + date: date.toISOString(), dateLabel: `${yearStr}-${monthStr}-${dayStr}`, - title: data.title || slug, + title: data.title || slug, area, - type: 'writing', + type: data.type || 'writing', url, excerpt, }); @@ -98,11 +104,9 @@ function getAllPosts() { const allPosts = getAllPosts(); -// all-posts.json — used by homepage and archive page fs.writeFileSync(ALL_OUTPUT, JSON.stringify(allPosts, null, 2)); console.log(`All posts generated: ${allPosts.length} entries`); -// latest-post.json — backwards compat for anything else that imports it if (allPosts.length) { const latest = allPosts[0]; fs.writeFileSync(LATEST_OUTPUT, JSON.stringify({ diff --git a/src/config/homepage.js b/src/config/homepage.js index 3bd9572..3635436 100644 --- a/src/config/homepage.js +++ b/src/config/homepage.js @@ -1,10 +1,22 @@ const homepageConfig = { missionStatement: "Advancing Technology for Humans and Bots with Privacy in mind.", + contactInfo: { email: "inquiries@5l-labs.com", linkedin: "https://www.linkedin.com/company/5l-labs/", twitter: "https://twitter.com/5l_labs", }, + + // Matches AREA_LABELS in scripts/generate-latest-post.js — single source of truth + areas: ['self-hosted-iot', 'home-ml-iot', 'applied-ai', 'frontier'], + + consulting: { + availability: 'Q3 2026', + blurb: 'We take on a small number of engagements each quarter.', + services: 'Private ML systems, on-device inference, and IoT architecture audits.', + inquireUrl: '/docs', + }, + researchAreas: [ { title: "Private AI/ML", @@ -17,6 +29,7 @@ const homepageConfig = { link: "/self-hosted-iot", }, ], + products: [ { title: "Recruiter Rankings (Preview)", diff --git a/src/pages/archive.js b/src/pages/archive.js index 51d3d21..a629cb0 100644 --- a/src/pages/archive.js +++ b/src/pages/archive.js @@ -1,14 +1,25 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import Link from '@docusaurus/Link'; import Layout from '@theme/Layout'; +import { useLocation } from '@docusaurus/router'; +import homepageConfig from '../config/homepage'; import allPosts from '../generated/all-posts.json'; import styles from './index.module.css'; -const AREAS = ['all', ...Array.from(new Set(allPosts.map(p => p.area))).sort()]; +const AREAS = ['all', ...homepageConfig.areas]; export default function Archive() { + const location = useLocation(); const [area, setArea] = useState('all'); + // Honour deep-links from homepage "load more" (e.g. /archive#home-ml-iot) + useEffect(() => { + const hash = location.hash.replace('#', ''); + if (hash && AREAS.includes(hash)) { + setArea(hash); + } + }, [location.hash]); + const entries = area === 'all' ? allPosts : allPosts.filter(p => p.area === area); return ( @@ -22,10 +33,12 @@ export default function Archive() {
$ ls -lt writing/
{allPosts.length} entries across{' '} - frontier/{' '} - applied-ai/{' '} - home-ml-iot/{' '} - self-hosted-iot/ + {homepageConfig.areas.map((a, i) => ( + + {a}/ + {i < homepageConfig.areas.length - 1 ? ' ' : ''} + + ))}
@@ -55,25 +68,31 @@ export default function Archive() { - DATE + DATE TYPE - AREA + AREA TITLE ↗ - {entries.map((entry, i) => ( - - {entry.dateLabel} + {entries.length === 0 ? ( + + + No entries found. + + + ) : entries.map((entry) => ( + + {entry.dateLabel} {entry.type} - {entry.area} + {entry.area} {entry.title} - read + ↗ ))} diff --git a/src/pages/index.js b/src/pages/index.js index 4b2b292..d81e0bb 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import Link from '@docusaurus/Link'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import Layout from '@theme/Layout'; @@ -7,10 +7,18 @@ import allPosts from '../generated/all-posts.json'; import styles from './index.module.css'; const PREVIEW_COUNT = 7; -const INDEX_ENTRIES = allPosts.slice(0, PREVIEW_COUNT); +const AREAS = ['all', ...homepageConfig.areas]; export default function Home() { const { siteConfig } = useDocusaurusContext(); + const [area, setArea] = useState('all'); + + const filtered = area === 'all' ? allPosts : allPosts.filter(p => p.area === area); + const entries = filtered.slice(0, PREVIEW_COUNT); + const remaining = filtered.length - PREVIEW_COUNT; + + const { consulting, areas } = homepageConfig; + return ( {homepageConfig.missionStatement}
$ ls areas/
- private-ai/{' '} - private-iot/{' '} - frontier/{' '} - applied-ai/ + {areas.map(a => ( + {a}/ + ))}
$ ./hire-us --consulting{' '} @@ -42,10 +49,16 @@ export default function Home() {
INDEX OF / · - SORT BY: - DATE ↓ - AREA - TYPE + FILTER: + {AREAS.map(a => ( + + ))}
@@ -58,34 +71,43 @@ export default function Home() { - + - + - {INDEX_ENTRIES.map((entry, i) => ( - - + {entries.length === 0 ? ( + + + + ) : entries.map((entry) => ( + + - + - + ))}
DATEDATE TYPEAREAAREA TITLE
{entry.dateLabel}
+ No entries found. +
{entry.dateLabel} - - {entry.type} - + {entry.type} {entry.area}{entry.area} {entry.title} read
-
- — {allPosts.length - PREVIEW_COUNT} more entries → view full archive — -
+ + {remaining > 0 && ( +
+ + — {remaining} more → full archive — + +
+ )} {/* Projects + Consulting */} @@ -108,17 +130,13 @@ export default function Home() {
~/consulting
-
- We take on a small number of engagements each quarter. -
-
- Private ML systems, on-device inference, and IoT architecture audits. -
+
{consulting.blurb}
+
{consulting.services}
CURRENT AVAILABILITY:{' '} - Q3 2026 + {consulting.availability}
- + ./inquire →
diff --git a/src/pages/index.module.css b/src/pages/index.module.css index ee2f0b6..26131f1 100644 --- a/src/pages/index.module.css +++ b/src/pages/index.module.css @@ -2,117 +2,133 @@ max-width: 1100px; margin: 0 auto; padding: 48px 40px 80px; - font-family: var(--ifm-font-family-monospace); + font-family: var(--font-mono); font-size: 13px; + line-height: 1.55; } /* ── Terminal banner ── */ .terminal { padding: 20px 24px; - background: #101010; + background: #0d1117; color: #e8e6df; - font-family: var(--ifm-font-family-monospace); + font-family: var(--font-mono); font-size: 13px; line-height: 1.6; + border: 1px solid #30363d; border-radius: 4px; margin-bottom: 40px; - position: relative; } .terminalPrompt { color: #7bd27b; } .terminalDir { color: #f0a060; } -.terminalCursor { color: var(--ifm-color-primary); } +.terminalCursor { color: #7bd27b; } -/* ── Index section ── */ +/* Light mode: terminal needs to stand out from white bg */ +[data-theme='light'] .terminal { + background: #1a1a1a; + border-color: #333; +} + +/* ── Index ── */ .indexSection { margin-bottom: 36px; } .indexHeader { display: flex; align-items: center; - gap: 12px; - margin-bottom: 14px; - color: var(--ifm-color-content-secondary); + gap: 10px; + margin-bottom: 12px; + color: var(--text-muted); font-size: 11px; letter-spacing: 0.14em; - text-transform: uppercase; } .indexTable { width: 100%; border-collapse: collapse; - font-family: var(--ifm-font-family-monospace); + font-family: var(--font-mono); font-size: 13px; + table-layout: fixed; } + +/* Column widths */ +.indexTable col.colDate { width: 108px; } +.indexTable col.colType { width: 96px; } +.indexTable col.colArea { width: 108px; } +.indexTable col.colTitle { width: auto; } +.indexTable col.colMeta { width: 80px; } + .indexTable thead tr { - border-bottom: 1.5px solid var(--ifm-color-content); + border-bottom: 1.5px solid var(--text-main); text-align: left; - color: var(--ifm-color-content-secondary); } .indexTable thead th { - padding: 8px 4px; + padding: 8px 6px; font-weight: 500; font-size: 11px; letter-spacing: 0.08em; + color: var(--text-muted); + white-space: nowrap; } .indexTable thead th:last-child { text-align: right; } .indexTable tbody tr { - border-bottom: 1px dashed var(--ifm-color-emphasis-300); -} -.indexTable tbody tr:hover { - background: var(--ifm-color-emphasis-100); + border-bottom: 1px dashed color-mix(in srgb, var(--text-muted) 30%, transparent); } +.indexTable tbody tr:hover { background: color-mix(in srgb, var(--text-muted) 8%, transparent); } + .indexTable tbody td { - padding: 10px 4px; - color: var(--ifm-color-content-secondary); + padding: 10px 6px; + color: var(--text-muted); vertical-align: middle; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } -.indexTable tbody td.titleCell { - font-family: var(--ifm-heading-font-family, var(--ifm-font-family-base)); - font-size: 15px; - color: var(--ifm-color-content); +.indexTable tbody td.colTitle { + color: var(--text-main); + white-space: normal; } -.indexTable tbody td.titleCell a { - color: var(--ifm-color-content); +.indexTable tbody td.colTitle a { + color: var(--text-main); text-decoration: none; } -.indexTable tbody td.titleCell a:hover { - color: var(--ifm-color-primary); -} +.indexTable tbody td.colTitle a:hover { color: var(--brand-primary); } .indexTable tbody td:last-child { text-align: right; } .loadMore { text-align: center; margin-top: 14px; - color: var(--ifm-color-content-secondary); + color: var(--text-muted); font-size: 12px; } /* ── Chips ── */ .chip { display: inline-block; - border: 1.2px solid var(--ifm-color-emphasis-400); - padding: 2px 8px; + border: 1px solid color-mix(in srgb, var(--text-muted) 50%, transparent); + padding: 1px 7px; font-size: 11px; - font-family: var(--ifm-font-family-monospace); + font-family: var(--font-mono); border-radius: 999px; background: transparent; - color: var(--ifm-color-content-secondary); + color: var(--text-muted); cursor: pointer; + white-space: nowrap; } .chipActive { - border-color: var(--ifm-color-primary); - color: var(--ifm-color-primary); + border-color: var(--brand-primary); + color: var(--brand-primary); } /* ── Two-column lower section ── */ .twoCol { display: grid; grid-template-columns: 1fr 1fr; - gap: 28px; + gap: 24px; } .box { - border: 1.5px solid var(--ifm-color-emphasis-300); + border: 1px solid color-mix(in srgb, var(--text-muted) 35%, transparent); border-radius: 4px; padding: 20px; } @@ -120,68 +136,67 @@ .boxLabel { font-size: 11px; letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--ifm-color-primary); - margin-bottom: 12px; + color: var(--brand-primary); + margin-bottom: 14px; +} + +.projectGroup { + padding: 10px 0; + border-bottom: 1px dashed color-mix(in srgb, var(--text-muted) 30%, transparent); } +.projectGroup:last-child { border-bottom: none; padding-bottom: 0; } .projectRow { display: flex; justify-content: space-between; + align-items: baseline; font-size: 14px; - padding: 10px 0; - border-bottom: 1px dashed var(--ifm-color-emphasis-300); + margin-bottom: 3px; } -.projectRow:last-of-type { border-bottom: none; } -.projectName { color: var(--ifm-color-content); text-decoration: none; } -.projectName:hover { color: var(--ifm-color-primary); } -.projectMeta { color: var(--ifm-color-content-secondary); } -.projectDesc { font-size: 12px; color: var(--ifm-color-content-secondary); } +.projectName { color: var(--text-main); text-decoration: none; } +.projectName:hover { color: var(--brand-primary); } +.projectMeta { color: var(--text-muted); font-size: 12px; } +.projectDesc { font-size: 12px; color: var(--text-muted); } .consultTitle { - font-family: var(--ifm-heading-font-family, var(--ifm-font-family-base)); - font-size: 18px; - line-height: 1.2; + font-family: var(--font-mono); + font-size: 15px; + line-height: 1.4; margin-bottom: 10px; - color: var(--ifm-color-content); + color: var(--text-main); } .consultDesc { font-size: 13px; - color: var(--ifm-color-content-secondary); + color: var(--text-muted); margin-bottom: 14px; } .availability { font-size: 11px; letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--ifm-color-content-secondary); - margin-bottom: 12px; + color: var(--text-muted); + margin-bottom: 14px; } -.availabilityVal { color: var(--ifm-color-primary); } +.availabilityVal { color: var(--brand-primary); } .btnAccent { display: inline-flex; align-items: center; - gap: 6px; - background: var(--ifm-color-primary); + background: var(--brand-primary); color: #fff; border: none; - padding: 9px 18px; - font-family: var(--ifm-font-family-monospace); + padding: 8px 16px; + font-family: var(--font-mono); font-size: 13px; border-radius: 3px; text-decoration: none; cursor: pointer; - box-shadow: 3px 3px 0 rgba(0,0,0,0.15); -} -.btnAccent:hover { - color: #fff; - opacity: 0.9; - text-decoration: none; + box-shadow: 3px 3px 0 rgba(0,0,0,0.2); } +.btnAccent:hover { color: #fff; opacity: 0.9; text-decoration: none; } @media (max-width: 768px) { .page { padding: 32px 20px 60px; } .twoCol { grid-template-columns: 1fr; } - .indexTable { font-size: 12px; } + .indexTable { font-size: 12px; table-layout: auto; } + .hideOnMobile { display: none; } } From 6a55201cfe7a97dceb424496cd03662e03c168b4 Mon Sep 17 00:00:00 2001 From: Nick Lange Date: Sat, 25 Apr 2026 23:36:08 -0400 Subject: [PATCH 10/34] Feat: Add inquiry form with SMS opt-in consent for Twilio verification - New /inquiry page with name, email, phone, project description fields - SMS opt-in checkbox with required legal language (STOP/HELP/rates) - Links to /privacy-policy; form falls back to mailto if no Formspree ID - Formspree-ready: set FORMSPREE_ID env var to wire up submissions - consulting.inquireUrl updated from /docs to /inquiry - Restore privacy-policy.md from upstream/main (was missing from branch) Co-Authored-By: Claude Sonnet 4.6 --- src/config/homepage.js | 2 +- src/pages/inquiry.js | 186 +++++++++++++++++++++++++++++++++++ src/pages/inquiry.module.css | 161 ++++++++++++++++++++++++++++++ src/pages/privacy-policy.md | 53 ++++++++++ 4 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 src/pages/inquiry.js create mode 100644 src/pages/inquiry.module.css create mode 100644 src/pages/privacy-policy.md diff --git a/src/config/homepage.js b/src/config/homepage.js index 3635436..09068b5 100644 --- a/src/config/homepage.js +++ b/src/config/homepage.js @@ -14,7 +14,7 @@ const homepageConfig = { availability: 'Q3 2026', blurb: 'We take on a small number of engagements each quarter.', services: 'Private ML systems, on-device inference, and IoT architecture audits.', - inquireUrl: '/docs', + inquireUrl: '/inquiry', }, researchAreas: [ diff --git a/src/pages/inquiry.js b/src/pages/inquiry.js new file mode 100644 index 0000000..ad475fc --- /dev/null +++ b/src/pages/inquiry.js @@ -0,0 +1,186 @@ +import React, { useState } from 'react'; +import Layout from '@theme/Layout'; +import Link from '@docusaurus/Link'; +import homepageConfig from '../config/homepage'; +import styles from './inquiry.module.css'; + +// To wire up form submission, create a free account at https://formspree.io, +// create a form, and replace this with your form ID. +const FORMSPREE_ID = process.env.FORMSPREE_ID || null; + +const INITIAL = { name: '', email: '', phone: '', message: '', smsConsent: false }; + +export default function Inquiry() { + const [fields, setFields] = useState(INITIAL); + const [status, setStatus] = useState('idle'); // idle | submitting | success | error + + const set = (key) => (e) => { + const value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; + setFields(prev => ({ ...prev, [key]: value })); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setStatus('submitting'); + + if (FORMSPREE_ID) { + try { + const res = await fetch(`https://formspree.io/f/${FORMSPREE_ID}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: JSON.stringify({ + name: fields.name, + email: fields.email, + phone: fields.phone || '(not provided)', + message: fields.message, + sms_consent: fields.smsConsent ? 'Yes — consented to SMS' : 'No', + }), + }); + setStatus(res.ok ? 'success' : 'error'); + } catch { + setStatus('error'); + } + } else { + // Fallback: open email client with pre-filled body + const body = encodeURIComponent( + `Name: ${fields.name}\nEmail: ${fields.email}\nPhone: ${fields.phone || 'N/A'}\n\n${fields.message}\n\nSMS consent: ${fields.smsConsent ? 'Yes' : 'No'}` + ); + window.location.href = `mailto:${homepageConfig.contactInfo.email}?subject=Consulting%20Inquiry&body=${body}`; + setStatus('success'); + } + }; + + if (status === 'success') { + return ( + +
+
+
+

Got it.

+

We'll be in touch at {fields.email}.

+

We take on 1–2 engagements per quarter — expect a response within a few business days.

+ ← back to home +
+
+
+ ); + } + + return ( + +
+
+
~/consulting/inquire
+

Start an inquiry

+

+ We take 1–2 engagements per quarter. Current availability:{' '} + {homepageConfig.consulting.availability}. +

+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +