From f3414fa31dba63020df436e3ec834eda13da3990 Mon Sep 17 00:00:00 2001 From: Carson Seese Date: Tue, 1 Sep 2026 15:36:31 -0400 Subject: [PATCH 01/20] misc cleanups --- .oxlintrc.json | 2 +- README.md | 33 +- knip.jsonc | 8 +- mise.lock | 76 ++--- mise.toml | 6 +- package.json | 19 +- pnpm-lock.yaml | 737 +++++++++++++++++++++----------------------- pnpm-workspace.yaml | 5 +- tsconfig.json | 3 +- 9 files changed, 417 insertions(+), 472 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index da07d38..2e43414 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -46,7 +46,7 @@ "patterns": [ { "group": ["legacy/**", "**/legacy/**"], - "message": "legacy/ is reference only \u2014 never import from it (plan/00-overview.md)" + "message": "legacy/ is reference only, never import from it (plan/00-overview.md)" } ] } diff --git a/README.md b/README.md index dd733fe..ab77021 100644 --- a/README.md +++ b/README.md @@ -2,35 +2,10 @@ ## Project Setup -1. Install NodeJS. Check `.nvmrc` for the version used by this project. -2. Run `npm install` in the project root to install the necessary dependencies. -3. If using VSCode, install the reccomended extensions. +1. Install [Mise](https://mise.jdx.dev/) +2. Run `mise install` ## Development -1. Run `npm run dev` to start NextJS dev server. -2. Browse to `http://localhost:3000` to view the site. - -### Adding a new page - -> [!NOTE] -> Learn more about NextJS App Router [here](https://nextjs.org/docs/app). - -1. Go to `src/app` and create a new directory for the page. - - For example, if you want to create a page at `scstem.org/hello-world`, you'd create a folder named `hello-world` in the `src/app` directory. -2. Add `page.tsx` to your new directory with the following code (suggested: Replace `PageName` with the name of your page): - - Note: Anything returned from your page's default function is wrapped by `src/app/layout.tsx`. That file adds things like the navigation bar, footer, etc. - -```ts - -export const metadata: Metadata = { - title: "My New Page", - description: - "This is my new page", - // ... other metadata fields -}; - -export default function PageName(): ReactNode { - return
Hello World!
; -} -``` +1. Run `mise dev` to start Astro dev server. +2. Browse to `http://localhost:4321` to view the site. diff --git a/knip.jsonc b/knip.jsonc index f77a5b0..33cefe3 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -3,13 +3,12 @@ "entry": ["src/pages/**", "functions/**"], "project": ["**/*.{ts,tsx,js,mjs,cjs,astro}"], "ignore": ["legacy/**", "tools/lint/anti-slop/**"], + // fontconfig's cache tool, called by the one-time OG font setup. Present on any machine // with fontconfig, which is the only kind that can render the cards at all. "ignoreBinaries": ["fc-cache"], - // Referenced only from CSS — `tailwindcss` via `@import`, three of the four faces via `url()` - // in src/styles/fonts.css — and knip does not follow imports inside compiled extensions. Inter - // is there for a different reason: fonts.css points at the trimmed copies in src/styles/fonts/, - // and the package is what `pnpm assets:fonts` regenerates them from. + + // Referenced only from CSS "ignoreDependencies": [ "tailwindcss", "@fontsource-variable/inter", @@ -17,6 +16,7 @@ "@fontsource-variable/source-code-pro", "@fontsource/architects-daughter" ], + "rules": { "files": "error", "dependencies": "error", diff --git a/mise.lock b/mise.lock index 0d2ee5d..a8dfbd5 100644 --- a/mise.lock +++ b/mise.lock @@ -1,75 +1,81 @@ # @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html [[tools.node]] -version = "26.7.0" +version = "26.8.0" backend = "core:node" [tools.node."platforms.linux-arm64"] -checksum = "sha256:925aa6157dd37542d0d7f2e28b7bf61e7b39284411210b0498bc3788db4aef68" -url = "https://nodejs.org/dist/v26.7.0/node-v26.7.0-linux-arm64.tar.gz" +checksum = "sha256:c358e4c9bd4cd1a4521fa2ae140c88637a3679fc3e99cd1aa6bca1ec5131f13f" +url = "https://nodejs.org/dist/v26.8.0/node-v26.8.0-linux-arm64.tar.gz" [tools.node."platforms.linux-arm64-musl"] -checksum = "sha256:e650560ab1ecec0eb5845b43b87d6024c87b4e143c3e044cbb607cc135dbaadb" -url = "https://unofficial-builds.nodejs.org/download/release/v26.7.0/node-v26.7.0-linux-arm64-musl.tar.gz" +checksum = "sha256:ec56d752fe301436a04cef093cb55d467dc7bbc2c1f2ef6aff6f23467d058fea" +url = "https://unofficial-builds.nodejs.org/download/release/v26.8.0/node-v26.8.0-linux-arm64-musl.tar.gz" [tools.node."platforms.linux-x64"] -checksum = "sha256:bd6b6c31e377bad9ad579bed72e5bc11f4c879ac9452ad51d30e646ea3d828df" -url = "https://nodejs.org/dist/v26.7.0/node-v26.7.0-linux-x64.tar.gz" +checksum = "blake3:a95bb9d887076f94f0d774921647fd9d7ccd3ef6a182ff12ac8c97a81b158cb5" +url = "https://nodejs.org/dist/v26.8.0/node-v26.8.0-linux-x64.tar.gz" [tools.node."platforms.linux-x64-musl"] -checksum = "sha256:84fc4e29e5f86022a40bac50a28a1b9275dd1f32eebbf4db499e2573ff822124" -url = "https://unofficial-builds.nodejs.org/download/release/v26.7.0/node-v26.7.0-linux-x64-musl.tar.gz" +checksum = "sha256:b796985564be6b033f3a7c49f278e791055f2d6697a5d3df6af95d3dab3e8acd" +url = "https://unofficial-builds.nodejs.org/download/release/v26.8.0/node-v26.8.0-linux-x64-musl.tar.gz" [tools.node."platforms.macos-arm64"] -checksum = "sha256:7ee659a7768e641bbfd5360940660b8e8fd0052f77488f365562bac522fc15d4" -url = "https://nodejs.org/dist/v26.7.0/node-v26.7.0-darwin-arm64.tar.gz" +checksum = "sha256:d6516f067a433cd979b27dfdb0bad88ce97f47be33b713d04c8361b1502f19c6" +url = "https://nodejs.org/dist/v26.8.0/node-v26.8.0-darwin-arm64.tar.gz" [tools.node."platforms.macos-x64"] -checksum = "sha256:f279d1ed28ce57f7788bf23435d2ad7fdd7438904ad5c4d8a1081a7cde3d4b96" -url = "https://nodejs.org/dist/v26.7.0/node-v26.7.0-darwin-x64.tar.gz" +checksum = "sha256:58e0866e3b4355c1e3140fdc0c1b1b2a55616f4e5a37fda12123e98114a05138" +url = "https://nodejs.org/dist/v26.8.0/node-v26.8.0-darwin-x64.tar.gz" [tools.node."platforms.windows-x64"] -checksum = "sha256:d3bd72755141ed32bbcd841228ee81897c8a98d50dfa7dae2179399a0a7c90f8" -url = "https://nodejs.org/dist/v26.7.0/node-v26.7.0-win-x64.zip" +checksum = "sha256:a27c620080cd4da2bd0458fd7dbe1c40baef67cc28c0f0bf21a229d867af7c00" +url = "https://nodejs.org/dist/v26.8.0/node-v26.8.0-win-x64.zip" [[tools."npm:@puppeteer/browsers"]] version = "3.2.1" backend = "npm:@puppeteer/browsers" [[tools."npm:chrome-devtools-mcp"]] -version = "1.7.0" +version = "1.8.0" backend = "npm:chrome-devtools-mcp" [[tools.pnpm]] -version = "11.22.0" +version = "11.24.0" backend = "aqua:pnpm/pnpm" [tools.pnpm."platforms.linux-arm64"] -checksum = "sha256:f1426231f365bdfd46c15fa3d1211c3936ee2c4e557afd304f6c66dbf1b2a8bf" -url = "https://github.com/pnpm/pnpm/releases/download/v11.22.0/pnpm-linux-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/515895951" +checksum = "sha256:52f9cd410ef3bbb1cdd79c399b27772fa6d929bc3cf7c80ff3923a81e1c93be3" +url = "https://github.com/pnpm/pnpm/releases/download/v11.24.0/pnpm-linux-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/527747053" +provenance = "github-attestations" [tools.pnpm."platforms.linux-arm64-musl"] -checksum = "sha256:6e53557024be48e59ab8760f9117c0e5c0e0a37ab420f71f302d86216970d28f" -url = "https://github.com/pnpm/pnpm/releases/download/v11.22.0/pnpm-linux-arm64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/515895950" +checksum = "sha256:950160e3d1d039e0bd1f234da5a307fdc9c1d6b9c030683adfcb85f607c7d500" +url = "https://github.com/pnpm/pnpm/releases/download/v11.24.0/pnpm-linux-arm64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/527747056" +provenance = "github-attestations" [tools.pnpm."platforms.linux-x64"] -checksum = "sha256:4c592fa410eb23b69691a9efb9bf21c87c15b3e9d88c6ec8acdd354a0eb8de71" -url = "https://github.com/pnpm/pnpm/releases/download/v11.22.0/pnpm-linux-x64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/515895948" +checksum = "sha256:0a9b76dedb5fe8cc5e7f3a9fd70854181cfb08a487ebaf0c9cfb044dc860936c" +url = "https://github.com/pnpm/pnpm/releases/download/v11.24.0/pnpm-linux-x64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/527747059" +provenance = "github-attestations" [tools.pnpm."platforms.linux-x64-musl"] -checksum = "sha256:45425b06e747cbcaff4940d7b4a55e694645f15f9339dbf7f2601cfb21400545" -url = "https://github.com/pnpm/pnpm/releases/download/v11.22.0/pnpm-linux-x64-musl.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/515895945" +checksum = "sha256:2fce00e10729d0c1f3836d499b908697fd3cd0832332651fbcee79c0a7442dcb" +url = "https://github.com/pnpm/pnpm/releases/download/v11.24.0/pnpm-linux-x64-musl.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/527747057" +provenance = "github-attestations" [tools.pnpm."platforms.macos-arm64"] -checksum = "sha256:2000dcc8f0718852c2806ba4dca1edaedf18a4a39264474d5a1c8fcee250adfd" -url = "https://github.com/pnpm/pnpm/releases/download/v11.22.0/pnpm-darwin-arm64.tar.gz" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/515895947" +checksum = "sha256:c4df428b6c348b3037ba851359215065ce070d711dc96a9750052fdbfbe240d3" +url = "https://github.com/pnpm/pnpm/releases/download/v11.24.0/pnpm-darwin-arm64.tar.gz" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/527747061" +provenance = "github-attestations" [tools.pnpm."platforms.windows-x64"] -checksum = "sha256:1de83ad5100acfd2adb5c8bc6f8a428cee9ff4e365deff57c22bfc0cccaa4ddb" -url = "https://github.com/pnpm/pnpm/releases/download/v11.22.0/pnpm-win32-x64.zip" -url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/515895946" +checksum = "sha256:c5ca642230551342a9fffafe508c181f346e2c79fcae2b1cae4c37ceb423481a" +url = "https://github.com/pnpm/pnpm/releases/download/v11.24.0/pnpm-win32-x64.zip" +url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/527747055" +provenance = "github-attestations" diff --git a/mise.toml b/mise.toml index 34b9246..47fdf66 100644 --- a/mise.toml +++ b/mise.toml @@ -4,11 +4,11 @@ minimum_release_age = "7d" npm.package_manager = "pnpm" [tools] -node = "26.7.0" -pnpm = "11.22.0" +node = "26.8.0" +pnpm = "11.24.0" "npm:@puppeteer/browsers" = "3.2.1" -"npm:chrome-devtools-mcp" = "1.7.0" +"npm:chrome-devtools-mcp" = "1.8.0" [env] _.path = ["{{config_root}}/node_modules/.bin"] diff --git a/package.json b/package.json index f7b6bc0..2e44aad 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@fontsource-variable/source-code-pro": "5.3.0", "@fontsource/architects-daughter": "5.3.0", "@tailwindcss/vite": "4.3.3", - "astro": "7.2.4", + "astro": "7.2.6", "class-variance-authority": "0.7.1", "cnfast": "0.1.0", "sharp": "0.35.3", @@ -37,28 +37,25 @@ }, "devDependencies": { "@astrojs/check": "0.9.10", - "@cloudflare/workers-types": "5.20260819.1", - "@oxlint/plugins": "1.79.0", + "@cloudflare/workers-types": "5.20260825.1", + "@oxlint/plugins": "1.80.0", "@tabler/icons": "3.46.0", - "@types/node": "26.2.0", + "@types/node": "26.3.0", "@typescript/native-preview": "7.0.0-dev.20260707.2", "astro-eslint-parser": "3.1.0", - "eslint": "10.8.1", + "eslint": "10.9.1", "eslint-plugin-astro": "3.1.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-perfectionist": "5.10.1", "jiti": "2.7.0", "knip": "6.32.2", - "oxfmt": "0.64.0", - "oxlint": "1.79.0", + "oxfmt": "0.65.0", + "oxlint": "1.80.0", "oxlint-tsgolint": "7.0.2001", "prettier": "3.9.6", "prettier-plugin-astro": "0.14.1", "prettier-plugin-tailwindcss": "0.8.1", "typescript": "6.0.3", - "typescript-eslint": "8.67.0" - }, - "engines": { - "node": ">=26" + "typescript-eslint": "8.68.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73ea71a..ec886a9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,10 +25,10 @@ importers: version: 5.3.0 '@tailwindcss/vite': specifier: 4.3.3 - version: 4.3.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.3.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) astro: - specifier: 7.2.4 - version: 7.2.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0) + specifier: 7.2.6 + version: 7.2.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@types/node@26.3.0)(jiti@2.7.0)(yaml@2.9.0) class-variance-authority: specifier: 0.7.1 version: 0.7.1 @@ -37,7 +37,7 @@ importers: version: 0.1.0 sharp: specifier: 0.35.3 - version: 0.35.3(@types/node@26.2.0) + version: 0.35.3(@types/node@26.3.0) tailwindcss: specifier: 4.3.3 version: 4.3.3 @@ -46,17 +46,17 @@ importers: specifier: 0.9.10 version: 0.9.10(prettier-plugin-astro@0.14.1)(prettier@3.9.6)(typescript@6.0.3) '@cloudflare/workers-types': - specifier: 5.20260819.1 - version: 5.20260819.1 + specifier: 5.20260825.1 + version: 5.20260825.1 '@oxlint/plugins': - specifier: 1.79.0 - version: 1.79.0 + specifier: 1.80.0 + version: 1.80.0 '@tabler/icons': specifier: 3.46.0 version: 3.46.0 '@types/node': - specifier: 26.2.0 - version: 26.2.0 + specifier: 26.3.0 + version: 26.3.0 '@typescript/native-preview': specifier: 7.0.0-dev.20260707.2 version: 7.0.0-dev.20260707.2 @@ -64,17 +64,17 @@ importers: specifier: 3.1.0 version: 3.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(supports-color@7.2.0) eslint: - specifier: 10.8.1 - version: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + specifier: 10.9.1 + version: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) eslint-plugin-astro: specifier: 3.1.0 - version: 3.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript-eslint@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)) + version: 3.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript-eslint@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)) eslint-plugin-jsx-a11y: specifier: 6.10.2 - version: 6.10.2(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + version: 6.10.2(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)) eslint-plugin-perfectionist: specifier: 5.10.1 - version: 5.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + version: 5.10.1(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) jiti: specifier: 2.7.0 version: 2.7.0 @@ -82,11 +82,11 @@ importers: specifier: 6.32.2 version: 6.32.2 oxfmt: - specifier: 0.64.0 - version: 0.64.0 + specifier: 0.65.0 + version: 0.65.0 oxlint: - specifier: 1.79.0 - version: 1.79.0(oxlint-tsgolint@7.0.2001) + specifier: 1.80.0 + version: 1.80.0(oxlint-tsgolint@7.0.2001) oxlint-tsgolint: specifier: 7.0.2001 version: 7.0.2001 @@ -103,8 +103,8 @@ importers: specifier: 6.0.3 version: 6.0.3 typescript-eslint: - specifier: 8.67.0 - version: 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + specifier: 8.68.0 + version: 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) packages: @@ -114,37 +114,18 @@ packages: peerDependencies: typescript: ^5.0.0 || ^6.0.0 - '@astrojs/compiler-binding-darwin-arm64@0.3.2': - resolution: {integrity: sha512-MM8tn8CSimcfytaOla4b6acN8mKWiL/rlAA1fpT3/Wl7dNGSE4y8FjTN/zJVNnb63CsLWG5zZwCt01TXtDKh9g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@astrojs/compiler-binding-darwin-arm64@0.4.0': resolution: {integrity: sha512-ZVUwHundaQyFNjE6uoa0usaC0WOCitDCLS/4mdb4rOiJXwVUuKJBMxI5WMzXLWmamsXtK/Z//ifLXvV5Yeh4Hw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@astrojs/compiler-binding-darwin-x64@0.3.2': - resolution: {integrity: sha512-2lXOlzf8xb7jLomRsf/aswh61/NnGusynB2OwFkK6k4pmOtpfXMYnG0PLfXrEvxXYj69NdCnmUYXtHDd+JOOag==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@astrojs/compiler-binding-darwin-x64@0.4.0': resolution: {integrity: sha512-FI6G8AY8u6fR1SI/QRR5yGMwtvZwP34CDmZpZ5HwJGa50UM1VISTLhqkhV4a476pmgd25X1Aur2dqw6hUnrlKA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@astrojs/compiler-binding-linux-arm64-gnu@0.3.2': - resolution: {integrity: sha512-BmU3kWj7qnLrd4vzm49zFEPJ5oFnn1tCT4Vt9hZbqdU5Cmb8GZl7fn6VFsnNfe7B18a2gIFtVzbLINtYl5kBjQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@astrojs/compiler-binding-linux-arm64-gnu@0.4.0': resolution: {integrity: sha512-lB9gLFJK7m82EnjaU8nlRBEfcwGNeHidW3sSjODTUjMNaoewVuUz9fwwdY5M4jiSXIqWLH3yl6TX8FTDKA74Sw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -152,13 +133,6 @@ packages: os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-arm64-musl@0.3.2': - resolution: {integrity: sha512-f0heT9ZZEseSu5bHCeb80eL2DH07ArE6U9xi1WT/PEusNjzPmEr3GJsjG1tRLo5VYUUYX7h3ScaqGmGrMOVGmw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - '@astrojs/compiler-binding-linux-arm64-musl@0.4.0': resolution: {integrity: sha512-HPbvWqbxFxyaoQJhLxCaSjtYBx9KBo7JGVzEFZCmMl968a2PsSH0UfiODYgYPXofTOIsIH2aoCcrHXML0IA3ig==} engines: {node: ^20.19.0 || >=22.12.0} @@ -166,13 +140,6 @@ packages: os: [linux] libc: [musl] - '@astrojs/compiler-binding-linux-x64-gnu@0.3.2': - resolution: {integrity: sha512-M8fOUt0itRpqiGyoEA/ij184s8O+hqbCz3+YozRusOOM3osgGljpDThhbKAJjqh82wOo6FioQ4w8PBvU1XMD5Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - '@astrojs/compiler-binding-linux-x64-gnu@0.4.0': resolution: {integrity: sha512-tQKolMxoJ/+0AmLWm1PmJ/i+z3i10ZU1bNuVjEDulCf48azEMtUNjTZgHJ5MPtpYRNc7dlETr8QujUfduzoC7Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -180,13 +147,6 @@ packages: os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-x64-musl@0.3.2': - resolution: {integrity: sha512-/Kebk8sO6HnLeSd691JkaAPfN7CqR9/KEXmWvyNPkaKNGmj8rTZ/lf2uXtnPu92Lan84UrKdIKVPy1fSo2encQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - '@astrojs/compiler-binding-linux-x64-musl@0.4.0': resolution: {integrity: sha512-5v5YymudsxMHp3NBLCS8BUlu5CRqeLtWD9cKS/4nIhIEHCbpz9okmVV6I0HWqmBAPhWYcDa3vw/vltYPrOQCTA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -194,52 +154,27 @@ packages: os: [linux] libc: [musl] - '@astrojs/compiler-binding-wasm32-wasi@0.3.2': - resolution: {integrity: sha512-pUA6xbcOSB7DhfzIArB8BCAkFfAIqriiR7zl5zOStd6oU2G0kIKj+GUdGnyXyhfiv881Hffyk5tC0mR18sDjDw==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - '@astrojs/compiler-binding-wasm32-wasi@0.4.0': resolution: {integrity: sha512-m/phuH3x3PREvv1OnkM44NoPh4MatUadix1fB1u5SvMLCyDTUZykDJbKnWf1cjnYmHdlB8HcjTjl6JrCqAIXcw==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@astrojs/compiler-binding-win32-arm64-msvc@0.3.2': - resolution: {integrity: sha512-ESruf+6Qkl1trHUFxI6GSf6t52j8yN2kCNSzMWdzt7V/T09tFHrYzrVaJQohb2C9bJUH76pNvX6Zb51+xCQc9Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@astrojs/compiler-binding-win32-arm64-msvc@0.4.0': resolution: {integrity: sha512-B9zYf3okEY83kM8gydlpH2BHP00w4ifxPqlYlWrgTwuD6wnkrJDCwBlgy1q31cERjCJRXN1lrE2VmkLvFjv/6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': - resolution: {integrity: sha512-wzzVrEbOwbsLWOdEbocskjMRx2aZPxJ7ZbmL+jnpBamFwmigm+2M/wzuM6JWncocgYwLic1csSpalBh96kQKXA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@astrojs/compiler-binding-win32-x64-msvc@0.4.0': resolution: {integrity: sha512-zB0Nrv0dGc0zZWPGDRmmETTPhDRqyZjAjk+gWMlVrJX5U89obpB3VUUE1ZiHxOCN5LQojeLK6O8L/dnoHolvNQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@astrojs/compiler-binding@0.3.2': - resolution: {integrity: sha512-8w/9CWmYrAJJ8N0SY3O43ws2BgxoW6u3QsD8u2mE140lMYAlwh+tlNoUeSBq22wVheFuiBbR212l6ixZ2IIgCQ==} - engines: {node: ^20.19.0 || >=22.12.0} - '@astrojs/compiler-binding@0.4.0': resolution: {integrity: sha512-x2RjDUuWfwLNtc3mjAdSRInwqh/rqbLar9cm/5FOMbHvmYZB7yfKewzSclAxWjIZsypJDXv1lhaP2WG+P8TK3g==} engines: {node: ^20.19.0 || >=22.12.0} - '@astrojs/compiler-rs@0.3.2': - resolution: {integrity: sha512-xlx/T7JovIKduu4ucbTQUxQ5+Q8wxkHxhLjnZk3VlJbhbQ9RLvvuDk1p2YYFYFQ5y14dVm3FGGO4isQXa4F+Tg==} - engines: {node: '>=22.12.0'} - '@astrojs/compiler-rs@0.4.0': resolution: {integrity: sha512-koVikeon1kreEy+/JzLQRy3vzHHQVOjycs4degg4vFufKApZOwMZvSSAEztYNhmcQVfNVsVZZI4cEge3cexAbQ==} engines: {node: '>=22.12.0'} @@ -262,8 +197,8 @@ packages: prettier-plugin-astro: optional: true - '@astrojs/markdown-satteri@0.3.7': - resolution: {integrity: sha512-NHcHbrKW/opbZnTZQ5nH293BdcK2VV0tjuzI88CLnvy2njiEJVwUQ5KFnYd4NoWgHJCY/I1CyjGWCR4Vv3khXQ==} + '@astrojs/markdown-satteri@0.3.8': + resolution: {integrity: sha512-n8ItpFTCmlDsVR5+rwDmehSf+jFCYLWmZiisZNGuF7xILqYhsVeBfcha4qgS2Seq3fAc9Tm58ZVrvqFQ6RQgRQ==} '@astrojs/prism@4.0.2': resolution: {integrity: sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==} @@ -357,8 +292,8 @@ packages: resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} engines: {node: '>= 20.12.0'} - '@cloudflare/workers-types@5.20260819.1': - resolution: {integrity: sha512-nUoWrl+16WfocHgXAARAvpQ7dLMF4tSmTvvgLLg5clYhP2j8CYerZ2KC8agnlWHqOAKp45B3F+LcsjNZrZyiRA==} + '@cloudflare/workers-types@5.20260825.1': + resolution: {integrity: sha512-/XZntbK+BlJWC5jxkaDNhnDLr2Bf2627sZ6VMrxqKrnc84pc6gNu5NdAyaTkZn1LzQVFIa3SL7V/7veLF59P7w==} '@emmetio/abbreviation@2.3.3': resolution: {integrity: sha512-mgv58UrU3rh4YgbE/TzgLQwJ3pFsHHhCLqY20aJq+9comytTXUDNGG/SMtSeMJdkpxgXSXunBGLD8Boka3JyVA==} @@ -1036,124 +971,124 @@ packages: cpu: [x64] os: [win32] - '@oxfmt/binding-android-arm-eabi@0.64.0': - resolution: {integrity: sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==} + '@oxfmt/binding-android-arm-eabi@0.65.0': + resolution: {integrity: sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.64.0': - resolution: {integrity: sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==} + '@oxfmt/binding-android-arm64@0.65.0': + resolution: {integrity: sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.64.0': - resolution: {integrity: sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==} + '@oxfmt/binding-darwin-arm64@0.65.0': + resolution: {integrity: sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.64.0': - resolution: {integrity: sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==} + '@oxfmt/binding-darwin-x64@0.65.0': + resolution: {integrity: sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.64.0': - resolution: {integrity: sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==} + '@oxfmt/binding-freebsd-x64@0.65.0': + resolution: {integrity: sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.64.0': - resolution: {integrity: sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==} + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': + resolution: {integrity: sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.64.0': - resolution: {integrity: sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==} + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': + resolution: {integrity: sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.64.0': - resolution: {integrity: sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==} + '@oxfmt/binding-linux-arm64-gnu@0.65.0': + resolution: {integrity: sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.64.0': - resolution: {integrity: sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==} + '@oxfmt/binding-linux-arm64-musl@0.65.0': + resolution: {integrity: sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.64.0': - resolution: {integrity: sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==} + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': + resolution: {integrity: sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.64.0': - resolution: {integrity: sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==} + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': + resolution: {integrity: sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.64.0': - resolution: {integrity: sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==} + '@oxfmt/binding-linux-riscv64-musl@0.65.0': + resolution: {integrity: sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.64.0': - resolution: {integrity: sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==} + '@oxfmt/binding-linux-s390x-gnu@0.65.0': + resolution: {integrity: sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.64.0': - resolution: {integrity: sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==} + '@oxfmt/binding-linux-x64-gnu@0.65.0': + resolution: {integrity: sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.64.0': - resolution: {integrity: sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==} + '@oxfmt/binding-linux-x64-musl@0.65.0': + resolution: {integrity: sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.64.0': - resolution: {integrity: sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==} + '@oxfmt/binding-openharmony-arm64@0.65.0': + resolution: {integrity: sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.64.0': - resolution: {integrity: sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==} + '@oxfmt/binding-win32-arm64-msvc@0.65.0': + resolution: {integrity: sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.64.0': - resolution: {integrity: sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==} + '@oxfmt/binding-win32-ia32-msvc@0.65.0': + resolution: {integrity: sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.64.0': - resolution: {integrity: sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==} + '@oxfmt/binding-win32-x64-msvc@0.65.0': + resolution: {integrity: sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -1188,130 +1123,130 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.79.0': - resolution: {integrity: sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==} + '@oxlint/binding-android-arm-eabi@1.80.0': + resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.79.0': - resolution: {integrity: sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==} + '@oxlint/binding-android-arm64@1.80.0': + resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.79.0': - resolution: {integrity: sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==} + '@oxlint/binding-darwin-arm64@1.80.0': + resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.79.0': - resolution: {integrity: sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==} + '@oxlint/binding-darwin-x64@1.80.0': + resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.79.0': - resolution: {integrity: sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==} + '@oxlint/binding-freebsd-x64@1.80.0': + resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.79.0': - resolution: {integrity: sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==} + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.79.0': - resolution: {integrity: sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==} + '@oxlint/binding-linux-arm-musleabihf@1.80.0': + resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.79.0': - resolution: {integrity: sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==} + '@oxlint/binding-linux-arm64-gnu@1.80.0': + resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.79.0': - resolution: {integrity: sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==} + '@oxlint/binding-linux-arm64-musl@1.80.0': + resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.79.0': - resolution: {integrity: sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==} + '@oxlint/binding-linux-ppc64-gnu@1.80.0': + resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.79.0': - resolution: {integrity: sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==} + '@oxlint/binding-linux-riscv64-gnu@1.80.0': + resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.79.0': - resolution: {integrity: sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==} + '@oxlint/binding-linux-riscv64-musl@1.80.0': + resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.79.0': - resolution: {integrity: sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==} + '@oxlint/binding-linux-s390x-gnu@1.80.0': + resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.79.0': - resolution: {integrity: sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==} + '@oxlint/binding-linux-x64-gnu@1.80.0': + resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.79.0': - resolution: {integrity: sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==} + '@oxlint/binding-linux-x64-musl@1.80.0': + resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.79.0': - resolution: {integrity: sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==} + '@oxlint/binding-openharmony-arm64@1.80.0': + resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.79.0': - resolution: {integrity: sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==} + '@oxlint/binding-win32-arm64-msvc@1.80.0': + resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.79.0': - resolution: {integrity: sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==} + '@oxlint/binding-win32-ia32-msvc@1.80.0': + resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.79.0': - resolution: {integrity: sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==} + '@oxlint/binding-win32-x64-msvc@1.80.0': + resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/plugins@1.79.0': - resolution: {integrity: sha512-S0uyoxakDINJ4DPgqxGlEEvrdSMeQb7Z2lKVjxoY2gwsbZbfg2Xr8Klfeo5ZeraHmmdBCELFUHkSe6KEmBpMvg==} + '@oxlint/plugins@1.80.0': + resolution: {integrity: sha512-QRgH1XqQEYNHa4f1vvPQ5fAdNdncHGIUG1ZWLlGIZHky3qwCEeAKYitZNbZMtaXtAQAAFFTOwqUfzESvimqZNA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} '@rolldown/binding-android-arm-eabi@1.2.5': @@ -1568,8 +1503,8 @@ packages: '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} - '@types/node@26.2.0': - resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + '@types/node@26.3.0': + resolution: {integrity: sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw==} '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -1577,16 +1512,16 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.67.0': - resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==} + '@typescript-eslint/eslint-plugin@8.68.0': + resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.67.0 + '@typescript-eslint/parser': ^8.68.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.67.0': - resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} + '@typescript-eslint/parser@8.68.0': + resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1598,18 +1533,34 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/project-service@8.68.0': + resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/scope-manager@8.67.0': resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/scope-manager@8.68.0': + resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/tsconfig-utils@8.67.0': resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.67.0': - resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} + '@typescript-eslint/tsconfig-utils@8.68.0': + resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.68.0': + resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1619,12 +1570,22 @@ packages: resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/types@8.68.0': + resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/typescript-estree@8.67.0': resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/typescript-estree@8.68.0': + resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.67.0': resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1632,10 +1593,21 @@ packages: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/utils@8.68.0': + resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + '@typescript-eslint/visitor-keys@8.67.0': resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript-eslint/visitor-keys@8.68.0': + resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2': resolution: {integrity: sha512-wny2pgKjGbiZtnOIHVa3tXC1UfDqxNEFzyPGmiqybedG8hipG2Nfp0l5UxbaKCjkLacUpH/W5bP2hBOMVhCOzg==} engines: {node: '>=16.20.0'} @@ -1813,8 +1785,8 @@ packages: resolution: {integrity: sha512-lS5qlx401Q1ZUhQ44XQnM4uT2qI6hA0H+vstrd841xGVxunFOs0ut3DWJGYHK1l7mxRxQdDi1j53pfE7/AyGlA==} engines: {node: ^22.22.3 || ^24.16.0 || >=26.3.0} - astro@7.2.4: - resolution: {integrity: sha512-+cuLsBns2wwUHI9a10xZMbjrF91m7+QNwqTVeljTx0B8Lf+8h0LgVGjdVIL2FALDKD2I565lczeeS3BFC+KdZg==} + astro@7.2.6: + resolution: {integrity: sha512-qzT4tAhgHYX/6/MUGslAkF0PVkM4WNsMw2A1GrbfA9cXp68hmdqF3f7MuF5rRFvwQbjowqjOf90ao9whfgV5mw==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: @@ -2029,8 +2001,8 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} dom-serializer@2.0.0: @@ -2159,8 +2131,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.8.1: - resolution: {integrity: sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==} + eslint@10.9.1: + resolution: {integrity: sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -2860,8 +2832,8 @@ packages: oxc-resolver@11.24.2: resolution: {integrity: sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw==} - oxfmt@0.64.0: - resolution: {integrity: sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==} + oxfmt@0.65.0: + resolution: {integrity: sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2877,8 +2849,8 @@ packages: resolution: {integrity: sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==} hasBin: true - oxlint@1.79.0: - resolution: {integrity: sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==} + oxlint@1.80.0: + resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3301,8 +3273,8 @@ packages: typescript-auto-import-cache@0.3.6: resolution: {integrity: sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ==} - typescript-eslint@8.67.0: - resolution: {integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==} + typescript-eslint@8.68.0: + resolution: {integrity: sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -3683,50 +3655,24 @@ snapshots: - prettier - prettier-plugin-astro - '@astrojs/compiler-binding-darwin-arm64@0.3.2': - optional: true - '@astrojs/compiler-binding-darwin-arm64@0.4.0': optional: true - '@astrojs/compiler-binding-darwin-x64@0.3.2': - optional: true - '@astrojs/compiler-binding-darwin-x64@0.4.0': optional: true - '@astrojs/compiler-binding-linux-arm64-gnu@0.3.2': - optional: true - '@astrojs/compiler-binding-linux-arm64-gnu@0.4.0': optional: true - '@astrojs/compiler-binding-linux-arm64-musl@0.3.2': - optional: true - '@astrojs/compiler-binding-linux-arm64-musl@0.4.0': optional: true - '@astrojs/compiler-binding-linux-x64-gnu@0.3.2': - optional: true - '@astrojs/compiler-binding-linux-x64-gnu@0.4.0': optional: true - '@astrojs/compiler-binding-linux-x64-musl@0.3.2': - optional: true - '@astrojs/compiler-binding-linux-x64-musl@0.4.0': optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)': - dependencies: - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.4.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)': dependencies: '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3) @@ -3735,33 +3681,12 @@ snapshots: - '@emnapi/runtime' optional: true - '@astrojs/compiler-binding-win32-arm64-msvc@0.3.2': - optional: true - '@astrojs/compiler-binding-win32-arm64-msvc@0.4.0': optional: true - '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': - optional: true - '@astrojs/compiler-binding-win32-x64-msvc@0.4.0': optional: true - '@astrojs/compiler-binding@0.3.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)': - optionalDependencies: - '@astrojs/compiler-binding-darwin-arm64': 0.3.2 - '@astrojs/compiler-binding-darwin-x64': 0.3.2 - '@astrojs/compiler-binding-linux-arm64-gnu': 0.3.2 - '@astrojs/compiler-binding-linux-arm64-musl': 0.3.2 - '@astrojs/compiler-binding-linux-x64-gnu': 0.3.2 - '@astrojs/compiler-binding-linux-x64-musl': 0.3.2 - '@astrojs/compiler-binding-wasm32-wasi': 0.3.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3) - '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.2 - '@astrojs/compiler-binding-win32-x64-msvc': 0.3.2 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@astrojs/compiler-binding@0.4.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)': optionalDependencies: '@astrojs/compiler-binding-darwin-arm64': 0.4.0 @@ -3777,13 +3702,6 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@astrojs/compiler-rs@0.3.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)': - dependencies: - '@astrojs/compiler-binding': 0.3.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - '@astrojs/compiler-rs@0.4.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)': dependencies: '@astrojs/compiler-binding': 0.4.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3) @@ -3830,7 +3748,7 @@ snapshots: transitivePeerDependencies: - typescript - '@astrojs/markdown-satteri@0.3.7': + '@astrojs/markdown-satteri@0.3.8': dependencies: '@astrojs/internal-helpers': 0.10.4 '@astrojs/prism': 4.0.2 @@ -3918,7 +3836,7 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@cloudflare/workers-types@5.20260819.1': {} + '@cloudflare/workers-types@5.20260825.1': {} '@emmetio/abbreviation@2.3.3': dependencies: @@ -4053,9 +3971,9 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))': dependencies: - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -4377,61 +4295,61 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.24.2': optional: true - '@oxfmt/binding-android-arm-eabi@0.64.0': + '@oxfmt/binding-android-arm-eabi@0.65.0': optional: true - '@oxfmt/binding-android-arm64@0.64.0': + '@oxfmt/binding-android-arm64@0.65.0': optional: true - '@oxfmt/binding-darwin-arm64@0.64.0': + '@oxfmt/binding-darwin-arm64@0.65.0': optional: true - '@oxfmt/binding-darwin-x64@0.64.0': + '@oxfmt/binding-darwin-x64@0.65.0': optional: true - '@oxfmt/binding-freebsd-x64@0.64.0': + '@oxfmt/binding-freebsd-x64@0.65.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.64.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.64.0': + '@oxfmt/binding-linux-arm-musleabihf@0.65.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.64.0': + '@oxfmt/binding-linux-arm64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.64.0': + '@oxfmt/binding-linux-arm64-musl@0.65.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.64.0': + '@oxfmt/binding-linux-ppc64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.64.0': + '@oxfmt/binding-linux-riscv64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.64.0': + '@oxfmt/binding-linux-riscv64-musl@0.65.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.64.0': + '@oxfmt/binding-linux-s390x-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.64.0': + '@oxfmt/binding-linux-x64-gnu@0.65.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.64.0': + '@oxfmt/binding-linux-x64-musl@0.65.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.64.0': + '@oxfmt/binding-openharmony-arm64@0.65.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.64.0': + '@oxfmt/binding-win32-arm64-msvc@0.65.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.64.0': + '@oxfmt/binding-win32-ia32-msvc@0.65.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.64.0': + '@oxfmt/binding-win32-x64-msvc@0.65.0': optional: true '@oxlint-tsgolint/darwin-arm64@7.0.2001': @@ -4452,64 +4370,64 @@ snapshots: '@oxlint-tsgolint/win32-x64@7.0.2001': optional: true - '@oxlint/binding-android-arm-eabi@1.79.0': + '@oxlint/binding-android-arm-eabi@1.80.0': optional: true - '@oxlint/binding-android-arm64@1.79.0': + '@oxlint/binding-android-arm64@1.80.0': optional: true - '@oxlint/binding-darwin-arm64@1.79.0': + '@oxlint/binding-darwin-arm64@1.80.0': optional: true - '@oxlint/binding-darwin-x64@1.79.0': + '@oxlint/binding-darwin-x64@1.80.0': optional: true - '@oxlint/binding-freebsd-x64@1.79.0': + '@oxlint/binding-freebsd-x64@1.80.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.79.0': + '@oxlint/binding-linux-arm-gnueabihf@1.80.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.79.0': + '@oxlint/binding-linux-arm-musleabihf@1.80.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.79.0': + '@oxlint/binding-linux-arm64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.79.0': + '@oxlint/binding-linux-arm64-musl@1.80.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.79.0': + '@oxlint/binding-linux-ppc64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.79.0': + '@oxlint/binding-linux-riscv64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.79.0': + '@oxlint/binding-linux-riscv64-musl@1.80.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.79.0': + '@oxlint/binding-linux-s390x-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.79.0': + '@oxlint/binding-linux-x64-gnu@1.80.0': optional: true - '@oxlint/binding-linux-x64-musl@1.79.0': + '@oxlint/binding-linux-x64-musl@1.80.0': optional: true - '@oxlint/binding-openharmony-arm64@1.79.0': + '@oxlint/binding-openharmony-arm64@1.80.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.79.0': + '@oxlint/binding-win32-arm64-msvc@1.80.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.79.0': + '@oxlint/binding-win32-ia32-msvc@1.80.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.79.0': + '@oxlint/binding-win32-x64-msvc@1.80.0': optional: true - '@oxlint/plugins@1.79.0': {} + '@oxlint/plugins@1.80.0': {} '@rolldown/binding-android-arm-eabi@1.2.5': optional: true @@ -4661,12 +4579,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) '@tybys/wasm-util@0.10.3': dependencies: @@ -4699,25 +4617,25 @@ snapshots: dependencies: undici-types: 7.18.2 - '@types/node@26.2.0': + '@types/node@26.3.0': dependencies: undici-types: 8.3.0 '@types/sax@1.2.7': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.3.0 '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.67.0 - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/type-utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.68.0 + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -4725,14 +4643,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.68.0 debug: 4.4.3(supports-color@7.2.0) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4746,22 +4664,40 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.68.0(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3) + '@typescript-eslint/types': 8.68.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@8.67.0': dependencies: '@typescript-eslint/types': 8.67.0 '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/scope-manager@8.68.0': + dependencies: + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 + '@typescript-eslint/tsconfig-utils@8.67.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.68.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) debug: 4.4.3(supports-color@7.2.0) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -4769,6 +4705,8 @@ snapshots: '@typescript-eslint/types@8.67.0': {} + '@typescript-eslint/types@8.68.0': {} + '@typescript-eslint/typescript-estree@8.67.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@typescript-eslint/project-service': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) @@ -4784,13 +4722,39 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.68.0(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.68.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@6.0.3) + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 + debug: 4.4.3(supports-color@7.2.0) + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.67.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)) '@typescript-eslint/scope-manager': 8.67.0 '@typescript-eslint/types': 8.67.0 '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -4800,6 +4764,11 @@ snapshots: '@typescript-eslint/types': 8.67.0 eslint-visitor-keys: 5.0.1 + '@typescript-eslint/visitor-keys@8.68.0': + dependencies: + '@typescript-eslint/types': 8.68.0 + eslint-visitor-keys: 5.0.1 + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260707.2': optional: true @@ -4998,11 +4967,11 @@ snapshots: - '@emnapi/runtime' - supports-color - astro@7.2.4(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(jiti@2.7.0)(yaml@2.9.0): + astro@7.2.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@types/node@26.3.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: - '@astrojs/compiler-rs': 0.3.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3) + '@astrojs/compiler-rs': 0.4.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3) '@astrojs/internal-helpers': 0.10.4 - '@astrojs/markdown-satteri': 0.3.7 + '@astrojs/markdown-satteri': 0.3.8 '@astrojs/telemetry': 3.3.3 '@capsizecss/unpack': 4.0.1 '@clack/prompts': 1.7.0 @@ -5015,7 +4984,7 @@ snapshots: common-ancestor-path: 2.0.0 cookie: 2.0.1 devalue: 5.9.1 - diff: 8.0.4 + diff: 9.0.0 dset: 3.1.4 es-module-lexer: 2.3.2 esbuild: 0.28.2 @@ -5048,13 +5017,13 @@ snapshots: ultrahtml: 1.7.0 unifont: 0.7.5 unstorage: 1.17.5 - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 zod: 4.4.3 optionalDependencies: - sharp: 0.35.3(@types/node@26.2.0) + sharp: 0.35.3(@types/node@26.3.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -5278,7 +5247,7 @@ snapshots: dependencies: dequal: 2.0.3 - diff@8.0.4: {} + diff@9.0.0: {} dom-serializer@2.0.0: dependencies: @@ -5449,27 +5418,27 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-astro@3.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript-eslint@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)): + eslint-plugin-astro@3.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint-plugin-jsx-a11y@6.10.2(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)))(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript-eslint@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)) '@jridgewell/sourcemap-codec': 1.5.5 '@typescript-eslint/types': 8.67.0 astro-eslint-parser: 3.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.3)(supports-color@7.2.0) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) espree: 11.2.0 globals: 17.11.0 postcss: 8.5.26 postcss-selector-parser: 7.1.5 optionalDependencies: - '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint-plugin-jsx-a11y: 6.10.2(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) - typescript-eslint: 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint-plugin-jsx-a11y: 6.10.2(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)) + typescript-eslint: 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)): + eslint-plugin-jsx-a11y@6.10.2(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 @@ -5479,7 +5448,7 @@ snapshots: axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) hasown: 2.0.4 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -5488,10 +5457,10 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-perfectionist@5.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + eslint-plugin-perfectionist@5.10.1(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.67.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) natural-orderby: 5.0.0 transitivePeerDependencies: - supports-color @@ -5508,9 +5477,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0): + eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5(supports-color@7.2.0) '@eslint/config-helpers': 0.7.0 @@ -6249,29 +6218,29 @@ snapshots: '@oxc-resolver/binding-win32-arm64-msvc': 11.24.2 '@oxc-resolver/binding-win32-x64-msvc': 11.24.2 - oxfmt@0.64.0: + oxfmt@0.65.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.64.0 - '@oxfmt/binding-android-arm64': 0.64.0 - '@oxfmt/binding-darwin-arm64': 0.64.0 - '@oxfmt/binding-darwin-x64': 0.64.0 - '@oxfmt/binding-freebsd-x64': 0.64.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.64.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.64.0 - '@oxfmt/binding-linux-arm64-gnu': 0.64.0 - '@oxfmt/binding-linux-arm64-musl': 0.64.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.64.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.64.0 - '@oxfmt/binding-linux-riscv64-musl': 0.64.0 - '@oxfmt/binding-linux-s390x-gnu': 0.64.0 - '@oxfmt/binding-linux-x64-gnu': 0.64.0 - '@oxfmt/binding-linux-x64-musl': 0.64.0 - '@oxfmt/binding-openharmony-arm64': 0.64.0 - '@oxfmt/binding-win32-arm64-msvc': 0.64.0 - '@oxfmt/binding-win32-ia32-msvc': 0.64.0 - '@oxfmt/binding-win32-x64-msvc': 0.64.0 + '@oxfmt/binding-android-arm-eabi': 0.65.0 + '@oxfmt/binding-android-arm64': 0.65.0 + '@oxfmt/binding-darwin-arm64': 0.65.0 + '@oxfmt/binding-darwin-x64': 0.65.0 + '@oxfmt/binding-freebsd-x64': 0.65.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.65.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.65.0 + '@oxfmt/binding-linux-arm64-gnu': 0.65.0 + '@oxfmt/binding-linux-arm64-musl': 0.65.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.65.0 + '@oxfmt/binding-linux-riscv64-musl': 0.65.0 + '@oxfmt/binding-linux-s390x-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-gnu': 0.65.0 + '@oxfmt/binding-linux-x64-musl': 0.65.0 + '@oxfmt/binding-openharmony-arm64': 0.65.0 + '@oxfmt/binding-win32-arm64-msvc': 0.65.0 + '@oxfmt/binding-win32-ia32-msvc': 0.65.0 + '@oxfmt/binding-win32-x64-msvc': 0.65.0 oxlint-tsgolint@7.0.2001: optionalDependencies: @@ -6282,27 +6251,27 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 7.0.2001 '@oxlint-tsgolint/win32-x64': 7.0.2001 - oxlint@1.79.0(oxlint-tsgolint@7.0.2001): + oxlint@1.80.0(oxlint-tsgolint@7.0.2001): optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.79.0 - '@oxlint/binding-android-arm64': 1.79.0 - '@oxlint/binding-darwin-arm64': 1.79.0 - '@oxlint/binding-darwin-x64': 1.79.0 - '@oxlint/binding-freebsd-x64': 1.79.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.79.0 - '@oxlint/binding-linux-arm-musleabihf': 1.79.0 - '@oxlint/binding-linux-arm64-gnu': 1.79.0 - '@oxlint/binding-linux-arm64-musl': 1.79.0 - '@oxlint/binding-linux-ppc64-gnu': 1.79.0 - '@oxlint/binding-linux-riscv64-gnu': 1.79.0 - '@oxlint/binding-linux-riscv64-musl': 1.79.0 - '@oxlint/binding-linux-s390x-gnu': 1.79.0 - '@oxlint/binding-linux-x64-gnu': 1.79.0 - '@oxlint/binding-linux-x64-musl': 1.79.0 - '@oxlint/binding-openharmony-arm64': 1.79.0 - '@oxlint/binding-win32-arm64-msvc': 1.79.0 - '@oxlint/binding-win32-ia32-msvc': 1.79.0 - '@oxlint/binding-win32-x64-msvc': 1.79.0 + '@oxlint/binding-android-arm-eabi': 1.80.0 + '@oxlint/binding-android-arm64': 1.80.0 + '@oxlint/binding-darwin-arm64': 1.80.0 + '@oxlint/binding-darwin-x64': 1.80.0 + '@oxlint/binding-freebsd-x64': 1.80.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.80.0 + '@oxlint/binding-linux-arm-musleabihf': 1.80.0 + '@oxlint/binding-linux-arm64-gnu': 1.80.0 + '@oxlint/binding-linux-arm64-musl': 1.80.0 + '@oxlint/binding-linux-ppc64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-gnu': 1.80.0 + '@oxlint/binding-linux-riscv64-musl': 1.80.0 + '@oxlint/binding-linux-s390x-gnu': 1.80.0 + '@oxlint/binding-linux-x64-gnu': 1.80.0 + '@oxlint/binding-linux-x64-musl': 1.80.0 + '@oxlint/binding-openharmony-arm64': 1.80.0 + '@oxlint/binding-win32-arm64-msvc': 1.80.0 + '@oxlint/binding-win32-ia32-msvc': 1.80.0 + '@oxlint/binding-win32-x64-msvc': 1.80.0 oxlint-tsgolint: 7.0.2001 p-limit@3.1.0: @@ -6516,7 +6485,7 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.2 - sharp@0.35.3(@types/node@26.2.0): + sharp@0.35.3(@types/node@26.3.0): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 @@ -6547,7 +6516,7 @@ snapshots: '@img/sharp-win32-arm64': 0.35.3 '@img/sharp-win32-ia32': 0.35.3 '@img/sharp-win32-x64': 0.35.3 - '@types/node': 26.2.0 + '@types/node': 26.3.0 shebang-command@2.0.0: dependencies: @@ -6757,13 +6726,13 @@ snapshots: dependencies: semver: 7.8.5 - typescript-eslint@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + typescript-eslint@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.68.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.68.0(eslint@10.9.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.9.1(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -6857,7 +6826,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0): + vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -6865,15 +6834,15 @@ snapshots: rolldown: 1.2.5 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.2.0 + '@types/node': 26.3.0 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 yaml: 2.9.0 - vitefu@1.1.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)): optionalDependencies: - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@26.3.0)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) volar-service-css@0.0.71(@volar/language-service@2.4.28(typescript@6.0.3)): dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6c96b42..f7e9006 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,9 +5,8 @@ allowBuilds: esbuild: true # eslint-plugin-jsx-a11y 6.10.2 is its latest release and its peer range stops at -# eslint ^9, but the rules load and fire under eslint 10 (a seeded alt-text and -# click-events violation is reported). Without this the unmet peer is a warning -# on every install. +# eslint ^9, but the rules load and fire under eslint 10. Without this the unmet +# peer is a warning on every install. peerDependencyRules: allowedVersions: eslint-plugin-jsx-a11y>eslint: "10" diff --git a/tsconfig.json b/tsconfig.json index acf699e..2e445be 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,5 @@ "@/*": ["./src/*"] } }, - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist", "legacy", "functions", "node_modules", "tools/lint/anti-slop"] + "include": [".astro/types.d.ts", "src/**/*"] } From 7dda942340587447eb3c323feafb3981b2e1f069 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 20:01:49 +0000 Subject: [PATCH 02/20] overhaul: one toolchain, TypeScript tools, leaner CI and docs Drop oxlint, oxfmt, the vendored nkzw and anti-slop rule sets, and tsgo. ESLint (strictTypeChecked + stylisticTypeChecked, astro jsx-a11y-strict) now lints every .ts/.js/.astro file and Prettier formats everything, Markdown included. functions/ and tools/ typecheck with tsc. ADR 0012 records the decision and supersedes 0001. tools/**/*.mjs become TypeScript run directly by Node; a tools/tsconfig enforces erasable syntax. Shared walk() lives in tools/lib/fs.ts. CI collapses to one workflow with a shared setup action and three parallel jobs: check (every step reports, ESLint findings annotate the PR), build (metadata + link check, dist artifact), lighthouse (scores and failed assertions in the job summary). mise.toml keeps only the toolchain pins and the Chrome tasks; .vscode points at ESLint/Prettier. Astro idioms: JSX comments -> HTML comments, htmlFor alias renamed, fragment removed, time zone and EIN read from site data. Duplicated markup extracted into VideoLink and SocialLinks; PostalAddress built once in jsonld.ts; dead functions/api/test.ts removed. Comments and docs lose phase/decision-ID narration and stale paths. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018hkX8tZ5Z745EsLeoxz3R5 --- .claude/hooks/format-lint.sh | 87 +- .claude/skills/shadcn-astro/SKILL.md | 2 +- .editorconfig | 2 +- .github/actions/setup/action.yml | 24 + .github/eslint-matcher.json | 22 + .github/workflows/ci.yml | 105 ++- .github/workflows/lighthouse.yml | 53 -- .oxfmtrc.json | 24 - .oxlintrc.json | 74 -- .prettierignore | 20 +- .prettierrc.json | 11 +- .vscode/extensions.json | 6 +- .vscode/settings.json | 50 +- AGENTS.md | 34 +- DESIGN.md | 154 ++-- README.md | 15 +- astro.config.ts | 15 +- docs/adr/0001-toolchain-split.md | 12 +- docs/adr/0005-webp-only-image-variants.md | 18 +- docs/adr/0008-no-font-preloads.md | 12 +- docs/adr/0010-og-cards.md | 4 +- docs/adr/0011-inter-weight-axis.md | 14 +- docs/adr/0012-single-toolchain.md | 37 + docs/analytics.md | 61 +- docs/content.md | 42 +- docs/tooling.md | 270 ++---- eslint.config.ts | 95 +- functions/api/calendar/[name].ts | 12 +- functions/api/form/submit.ts | 1 + functions/api/test.ts | 3 - functions/ics.ts | 41 +- functions/tsconfig.json | 5 +- functions/types.ts | 2 +- knip.jsonc | 29 +- mise.toml | 47 +- package.json | 32 +- plan/00-overview.md | 90 +- plan/01-foundation.md | 6 +- plan/02-design-system.md | 1 + plan/03-primitives.md | 28 +- plan/04-content-model.md | 5 +- plan/05-app-shell.md | 7 +- plan/06-homepage.md | 9 +- plan/07-pages.md | 20 +- plan/08-events.md | 4 +- plan/09-assets-performance.md | 4 +- plan/10-seo.md | 2 +- plan/11-cutover.md | 2 +- plan/12-content-strategy.md | 10 +- plan/todo.md | 7 +- pnpm-lock.yaml | 823 ++---------------- src/components/Analytics.astro | 2 +- src/components/Seo.astro | 5 +- src/components/ui/ContactForm.astro | 13 +- src/components/ui/Countdown.astro | 4 +- src/components/ui/CtaBanner.astro | 2 +- src/components/ui/FaqList.astro | 2 +- src/components/ui/FeatureCard.astro | 6 +- src/components/ui/Footer.astro | 35 +- src/components/ui/Hero.astro | 14 +- src/components/ui/HeroVideo.astro | 2 +- src/components/ui/Navbar.astro | 26 +- src/components/ui/PhotoTimeline.astro | 8 +- src/components/ui/Section.astro | 6 +- src/components/ui/SocialLinks.astro | 44 + src/components/ui/SponsorCard.astro | 11 +- src/components/ui/SponsorStrip.astro | 2 +- src/components/ui/StatBand.astro | 13 +- src/components/ui/VideoLink.astro | 52 ++ .../ui/primitives/AccordionItem.astro | 6 +- src/components/ui/primitives/Button.astro | 3 +- .../ui/primitives/Button.variants.ts | 2 +- src/components/ui/primitives/Callout.astro | 4 +- src/components/ui/primitives/CardTitle.astro | 4 +- src/components/ui/primitives/Carousel.astro | 10 +- src/components/ui/primitives/Dialog.astro | 8 +- src/components/ui/primitives/Label.astro | 4 +- src/components/ui/primitives/Pattern.astro | 13 +- src/components/ui/primitives/README.md | 13 +- .../ui/primitives/RegistrationMarks.astro | 2 +- src/components/ui/primitives/Skeleton.astro | 2 +- src/components/ui/primitives/TitleBlock.astro | 4 +- src/content.config.ts | 23 +- src/data/events.ts | 4 +- src/data/site.ts | 31 +- src/layouts/BaseLayout.astro | 4 +- src/layouts/EventLayout.astro | 23 +- src/layouts/ProgramLayout.astro | 5 +- src/lib/analytics.ts | 8 +- src/lib/cn.ts | 23 +- src/lib/contrast.ts | 2 +- src/lib/event-date.ts | 4 +- src/lib/events.ts | 4 +- src/lib/hand.ts | 4 +- src/lib/images.ts | 15 +- src/lib/jsonld.ts | 37 +- src/lib/tokens.ts | 4 +- src/pages/404.astro | 4 +- src/pages/about.astro | 14 +- src/pages/calendar/[name].astro | 19 +- src/pages/contact.astro | 31 +- src/pages/donate.astro | 21 +- src/pages/get-involved.astro | 6 +- src/pages/index.astro | 37 +- src/pages/llms.txt.ts | 2 +- src/pages/openhouse.astro | 2 +- src/pages/programs/fll/index.astro | 10 +- src/pages/programs/frc/index.astro | 41 +- src/pages/programs/frc/kickoff.astro | 2 +- src/pages/programs/frc/robots.astro | 20 +- src/pages/programs/index.astro | 6 +- src/pages/sponsors.astro | 15 +- src/pages/styleguide.astro | 141 ++- src/styles/global.css | 13 +- .../{font-subset.mjs => font-subset.ts} | 16 +- tools/assets/{og-cards.mjs => og-cards.ts} | 47 +- tools/assets/{og-fonts.mjs => og-fonts.ts} | 6 +- ...timize-sources.mjs => optimize-sources.ts} | 43 +- ...t-size-group.mjs => cn-font-size-group.ts} | 20 +- tools/checks/content-references.mjs | 104 --- tools/checks/content-references.ts | 122 +++ .../{verify-meta.mjs => verify-meta.ts} | 89 +- tools/ci/lighthouse-summary.ts | 64 ++ tools/lib/fs.ts | 17 + tools/lint/anti-slop/LICENSE | 21 - tools/lint/anti-slop/VENDOR.md | 11 - tools/lint/anti-slop/index.ts | 41 - .../rules/no-chained-type-assertions.ts | 77 -- .../no-conditional-empty-object-spread.ts | 49 -- .../rules/no-known-value-widening.ts | 247 ------ .../lint/anti-slop/rules/no-module-mocking.ts | 91 -- .../anti-slop/rules/no-object-parameters.ts | 126 --- .../lint/anti-slop/rules/no-reflect-apply.ts | 28 - tools/lint/anti-slop/rules/no-reflect-get.ts | 28 - .../lint/anti-slop/rules/no-runtime-typeof.ts | 67 -- .../rules/no-shape-in-symbol-names.ts | 39 - .../anti-slop/rules/no-unknown-parameters.ts | 83 -- .../anti-slop/rules/no-unknown-returns.ts | 115 --- .../rules/no-unknown-type-aliases.ts | 70 -- .../rules/no-unsafe-dictionary-type.ts | 134 --- .../anti-slop/rules/no-widen-then-assert.ts | 366 -------- ...quire-safety-comment-for-type-assertion.ts | 62 -- .../lint/anti-slop/shared/dictionary-types.ts | 502 ----------- .../shared/lexical-type-parameters.ts | 61 -- tools/lint/anti-slop/shared/reflect-method.ts | 35 - tools/lint/nkzw/LICENSE | 21 - tools/lint/nkzw/VENDOR.md | 51 -- tools/lint/nkzw/oxlintrc.json | 230 ----- tools/tsconfig.json | 19 + tsconfig.json | 9 +- 150 files changed, 1465 insertions(+), 4996 deletions(-) create mode 100644 .github/actions/setup/action.yml create mode 100644 .github/eslint-matcher.json delete mode 100644 .github/workflows/lighthouse.yml delete mode 100644 .oxfmtrc.json delete mode 100644 .oxlintrc.json create mode 100644 docs/adr/0012-single-toolchain.md delete mode 100644 functions/api/test.ts create mode 100644 src/components/ui/SocialLinks.astro create mode 100644 src/components/ui/VideoLink.astro rename tools/assets/{font-subset.mjs => font-subset.ts} (79%) rename tools/assets/{og-cards.mjs => og-cards.ts} (77%) rename tools/assets/{og-fonts.mjs => og-fonts.ts} (89%) rename tools/assets/{optimize-sources.mjs => optimize-sources.ts} (70%) rename tools/checks/{cn-font-size-group.mjs => cn-font-size-group.ts} (82%) delete mode 100644 tools/checks/content-references.mjs create mode 100644 tools/checks/content-references.ts rename tools/checks/{verify-meta.mjs => verify-meta.ts} (66%) create mode 100644 tools/ci/lighthouse-summary.ts create mode 100644 tools/lib/fs.ts delete mode 100644 tools/lint/anti-slop/LICENSE delete mode 100644 tools/lint/anti-slop/VENDOR.md delete mode 100644 tools/lint/anti-slop/index.ts delete mode 100644 tools/lint/anti-slop/rules/no-chained-type-assertions.ts delete mode 100644 tools/lint/anti-slop/rules/no-conditional-empty-object-spread.ts delete mode 100644 tools/lint/anti-slop/rules/no-known-value-widening.ts delete mode 100644 tools/lint/anti-slop/rules/no-module-mocking.ts delete mode 100644 tools/lint/anti-slop/rules/no-object-parameters.ts delete mode 100644 tools/lint/anti-slop/rules/no-reflect-apply.ts delete mode 100644 tools/lint/anti-slop/rules/no-reflect-get.ts delete mode 100644 tools/lint/anti-slop/rules/no-runtime-typeof.ts delete mode 100644 tools/lint/anti-slop/rules/no-shape-in-symbol-names.ts delete mode 100644 tools/lint/anti-slop/rules/no-unknown-parameters.ts delete mode 100644 tools/lint/anti-slop/rules/no-unknown-returns.ts delete mode 100644 tools/lint/anti-slop/rules/no-unknown-type-aliases.ts delete mode 100644 tools/lint/anti-slop/rules/no-unsafe-dictionary-type.ts delete mode 100644 tools/lint/anti-slop/rules/no-widen-then-assert.ts delete mode 100644 tools/lint/anti-slop/rules/require-safety-comment-for-type-assertion.ts delete mode 100644 tools/lint/anti-slop/shared/dictionary-types.ts delete mode 100644 tools/lint/anti-slop/shared/lexical-type-parameters.ts delete mode 100644 tools/lint/anti-slop/shared/reflect-method.ts delete mode 100644 tools/lint/nkzw/LICENSE delete mode 100644 tools/lint/nkzw/VENDOR.md delete mode 100644 tools/lint/nkzw/oxlintrc.json create mode 100644 tools/tsconfig.json diff --git a/.claude/hooks/format-lint.sh b/.claude/hooks/format-lint.sh index c56b623..82fcdfc 100755 --- a/.claude/hooks/format-lint.sh +++ b/.claude/hooks/format-lint.sh @@ -1,89 +1,28 @@ #!/usr/bin/env bash -# Formats and lints one edited file with whichever toolchain owns its extension -# (docs/tooling.md). Lint failures exit 2 so the findings go back to the agent. +# Formats and lints the file Claude Code just edited. Lint failures exit 2 so the +# findings go back to the agent; a missing toolchain exits 0 so a fresh clone is not +# blocked before `pnpm install`. set -uo pipefail repo_root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}" [[ -n "$repo_root" ]] || exit 0 -payload="$(cat)" -file="$(printf '%s' "$payload" | jq -r '.tool_response.filePath // .tool_input.file_path // empty')" +file="$(jq -r '.tool_response.filePath // .tool_input.file_path // empty')" [[ -n "$file" ]] || exit 0 -[[ -f "$file" ]] || exit 0 - -# Absolute, and inside this repo. -case "$file" in - /*) ;; - *) file="$repo_root/$file" ;; -esac -case "$file" in - "$repo_root"/*) ;; - *) exit 0 ;; -esac - -rel="${file#"$repo_root"/}" -case "$rel" in - legacy/* | dist/* | .astro/* | node_modules/* | plan/* | tools/lint/anti-slop/* | pnpm-lock.yaml) - exit 0 - ;; -esac +[[ "$file" = /* ]] || file="$repo_root/$file" +[[ -f "$file" && "$file" = "$repo_root"/* ]] || exit 0 cd "$repo_root" || exit 0 +bin="$repo_root/node_modules/.bin" +[[ -x "$bin/prettier" ]] || exit 0 -# Use the installed binaries directly so the hook works without pnpm on PATH. -# Status 127 means the tool itself is missing, which callers must not report as -# lint findings: on a fresh clone, before `pnpm install`, every edit would -# otherwise be rejected with an empty message. -readonly TOOL_MISSING=127 - -run() { - local tool="$1"; shift - local bin="$repo_root/node_modules/.bin/$tool" - if [[ -x "$bin" ]]; then - "$bin" "$@" 2>&1 - return - fi - command -v pnpm >/dev/null || return "$TOOL_MISSING" - [[ -d "$repo_root/node_modules" ]] || return "$TOOL_MISSING" - pnpm exec "$tool" "$@" 2>&1 -} +"$bin/prettier" --write --ignore-unknown --log-level warn "$file" -# Runs a linter and exits 2 with its findings. A missing toolchain exits 0 so the -# edit is allowed through un-linted rather than blocked with nothing to act on. -lint() { - local findings status - findings="$(run "$@")" - status=$? - case "$status" in - 0) return 0 ;; - "$TOOL_MISSING") exit 0 ;; - *) +case "${file##*.}" in + ts | mts | js | mjs | astro) + findings="$("$bin/eslint" --max-warnings 0 "$file" 2>&1)" || { printf '%s\n' "$findings" >&2 exit 2 - ;; - esac -} - -case "${file##*.}" in - ts | tsx | js | jsx | mjs | cjs | json | jsonc | json5) - # Without --ignore-path, oxfmt also reads .prettierignore, which excludes - # every extension oxfmt owns. - run oxfmt --ignore-path .gitignore "$file" >/dev/null - lint oxlint --type-aware "$file" - ;; - # oxfmt's directory scan formats these too, so `pnpm check` fails on an unformatted one — - # but oxlint has no rules for them, so they are formatted and not linted. Passing one to - # oxlint is not a no-op: it reports "No files found to lint" and exits non-zero. - css | yaml | yml | toml) - run oxfmt --ignore-path .gitignore "$file" >/dev/null - ;; - astro) - run prettier --write "$file" >/dev/null - lint eslint --max-warnings 0 "$file" - ;; - md) - run prettier --write "$file" >/dev/null + } ;; esac - -exit 0 diff --git a/.claude/skills/shadcn-astro/SKILL.md b/.claude/skills/shadcn-astro/SKILL.md index 05ac6cd..3d5fb57 100644 --- a/.claude/skills/shadcn-astro/SKILL.md +++ b/.claude/skills/shadcn-astro/SKILL.md @@ -5,7 +5,7 @@ description: Port a shadcn/ui component to an Astro primitive in src/components/ # Porting shadcn components to Astro primitives -This site ships **zero client framework runtime** (D3). shadcn is React + Radix, so a port is a +This site ships **zero client framework runtime**. shadcn is React + Radix, so a port is a re-implementation, not a copy — but the _design API_ comes across almost verbatim, and should. ## When to use this diff --git a/.editorconfig b/.editorconfig index 879a99a..f76ad3d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -# For what neither oxfmt nor Prettier covers (yaml, toml, shell, gitignore-likes). +# For what Prettier does not format (toml, shell, gitignore-likes). root = true [*] diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..ba5b003 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,24 @@ +name: Setup +description: Install the mise-pinned toolchain and the pnpm dependencies, with both cached. +runs: + using: composite + steps: + - uses: jdx/mise-action@v4 + with: + version: 2026.8.14 + cache: true + + # mise-action caches tool binaries, not pnpm's content-addressable store. + - name: Locate pnpm store + shell: bash + run: echo "PNPM_STORE=$(pnpm store path --silent)" >>"$GITHUB_ENV" + + - uses: actions/cache@v6 + with: + path: ${{ env.PNPM_STORE }} + key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: pnpm-store-${{ runner.os }}- + + - name: Install dependencies + shell: bash + run: pnpm install --frozen-lockfile --prefer-offline diff --git a/.github/eslint-matcher.json b/.github/eslint-matcher.json new file mode 100644 index 0000000..eba0bf6 --- /dev/null +++ b/.github/eslint-matcher.json @@ -0,0 +1,22 @@ +{ + "problemMatcher": [ + { + "owner": "eslint-stylish", + "pattern": [ + { + "regexp": "^([^\\s].*)$", + "file": 1 + }, + { + "regexp": "^\\s+(\\d+):(\\d+)\\s+(error|warning|info)\\s+(.*)\\s\\s+(.*)$", + "line": 1, + "column": 2, + "severity": 3, + "message": 4, + "code": 5, + "loop": true + } + ] + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cef64b0..8d9bcee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,6 +2,8 @@ name: CI on: pull_request: + push: + branches: [main, staging] permissions: contents: read @@ -12,50 +14,52 @@ concurrency: jobs: check: - name: Check + Build + name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 + - uses: ./.github/actions/setup - - name: Install mise tools - uses: jdx/mise-action@v4 - with: - version: 2026.8.14 - install: true - cache: true - - # mise-action's cache covers mise-managed tool binaries (node, pnpm), not - # pnpm's content-addressable store, so without this every run re-downloads - # the whole tree. - - name: Locate pnpm store - run: echo "PNPM_STORE=$(pnpm store path --silent)" >>"$GITHUB_ENV" - - - name: Cache pnpm store - uses: actions/cache@v6 - with: - path: ${{ env.PNPM_STORE }} - key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: pnpm-store-${{ runner.os }}- - - - name: Install dependencies - run: pnpm install --frozen-lockfile --prefer-offline + # Every step runs even after an earlier one fails, so one PR run reports every kind of + # problem. The ESLint matcher turns findings into inline annotations on the PR diff. + - name: Typecheck + run: pnpm typecheck + - name: Lint + if: ${{ !cancelled() }} + run: | + echo "::add-matcher::.github/eslint-matcher.json" + pnpm lint + - name: Format + if: ${{ !cancelled() }} + run: pnpm fmt:check + - name: Unused code and dependencies + if: ${{ !cancelled() }} + run: pnpm knip + - name: Design tokens + if: ${{ !cancelled() }} + run: pnpm check:tokens + - name: Content references + if: ${{ !cancelled() }} + run: pnpm check:content - - name: Check (typecheck + lint + format + knip) - run: pnpm check + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/setup - name: Build run: pnpm build - # Needs dist/, so it cannot live in `pnpm check`: uniqueness of titles and descriptions, - # and whether an og:image resolves, are only answerable across the whole built site. - - name: Verify page metadata - run: pnpm run check:meta + # Title and description uniqueness, and whether every og:image resolves, are only + # answerable across the whole built site. + - name: Page metadata + run: pnpm check:meta - # --root-dir is required for the root-relative hrefs Astro emits (/_astro/...); - # without it lychee cannot resolve them in local files and errors on every page. - # llms.txt is in scope because its links are hand-written paths in src/data/events.ts, - # which nothing else would catch drifting from the routes they name. - - name: Link check + # --root-dir resolves the root-relative hrefs Astro emits. llms.txt is in scope because its + # links are hand-written paths that nothing else checks against the routes they name. + - name: Links uses: lycheeverse/lychee-action@v2 with: args: >- @@ -63,3 +67,36 @@ jobs: --root-dir ${{ github.workspace }}/dist 'dist/**/*.html' 'dist/llms.txt' fail: true + + - uses: actions/upload-artifact@v5 + with: + name: dist + path: dist/ + retention-days: 1 + + lighthouse: + name: Lighthouse + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/setup + - uses: actions/download-artifact@v6 + with: + name: dist + path: dist/ + + # lhci is a CI-only tool run through dlx rather than a devDependency: it pulls Lighthouse + # and Puppeteer, which nobody needs to develop a page (docs/adr/0007-lighthouse-ci-gate.md). + - name: Lighthouse CI + run: pnpm dlx @lhci/cli@0.15.1 autorun + + - name: Summarize + if: ${{ !cancelled() }} + run: node tools/ci/lighthouse-summary.ts >>"$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v5 + if: ${{ !cancelled() }} + with: + name: lighthouse-reports + path: .lighthouseci/reports/ diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml deleted file mode 100644 index 848efdc..0000000 --- a/.github/workflows/lighthouse.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Lighthouse - -on: - pull_request: - -permissions: - contents: read - -concurrency: - group: lighthouse-${{ github.ref }} - cancel-in-progress: true - -jobs: - budgets: - name: Budgets - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: Install mise tools - uses: jdx/mise-action@v4 - with: - version: 2026.8.14 - install: true - cache: true - - - name: Locate pnpm store - run: echo "PNPM_STORE=$(pnpm store path --silent)" >>"$GITHUB_ENV" - - - name: Cache pnpm store - uses: actions/cache@v6 - with: - path: ${{ env.PNPM_STORE }} - key: pnpm-store-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: pnpm-store-${{ runner.os }}- - - - name: Install dependencies - run: pnpm install --frozen-lockfile --prefer-offline - - - name: Build - run: pnpm build - - # lhci is a CI-only tool run through dlx rather than a devDependency: it pulls Lighthouse - # and Puppeteer, which no one needs to develop a page (docs/adr/0007-lighthouse-ci-gate.md). - - name: Lighthouse CI - run: pnpm dlx @lhci/cli@0.15.1 autorun - - - name: Upload reports - if: ${{ !cancelled() }} - uses: actions/upload-artifact@v5 - with: - name: lighthouse-reports - path: .lighthouseci/reports/ diff --git a/.oxfmtrc.json b/.oxfmtrc.json deleted file mode 100644 index 4ceca93..0000000 --- a/.oxfmtrc.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "$schema": "./node_modules/oxfmt/configuration_schema.json", - "sortImports": true, - "sortPackageJson": true, - "sortTailwindcss": true, - "trailingComma": "all", - "overrides": [ - { - "files": ["**/*.json", "**/*.jsonc", "**/*.json5"], - "options": { - "trailingComma": "none" - } - } - ], - "ignorePatterns": [ - "legacy/**", - "dist/**", - ".astro/**", - ".claude/**", - "tools/lint/anti-slop/**", - "**/*.md", - "**/*.astro" - ] -} diff --git a/.oxlintrc.json b/.oxlintrc.json deleted file mode 100644 index 2e43414..0000000 --- a/.oxlintrc.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "extends": ["./tools/lint/nkzw/oxlintrc.json"], - "env": { - "browser": true, - "builtin": true, - "es2024": true, - "node": true - }, - "plugins": ["typescript", "unicorn", "oxc", "import"], - "options": { - "typeAware": true, - "typeCheck": true - }, - "categories": { - "correctness": "error", - "suspicious": "error", - "perf": "error", - "style": "off" - }, - "jsPlugins": [ - { - "name": "anti-slop", - "specifier": "./tools/lint/anti-slop/index.ts" - } - ], - "rules": { - "anti-slop/no-chained-type-assertions": "error", - "anti-slop/no-conditional-empty-object-spread": "error", - "anti-slop/no-known-value-widening": "error", - "anti-slop/no-module-mocking": "error", - "anti-slop/no-object-parameters": "error", - "anti-slop/no-reflect-apply": "error", - "anti-slop/no-reflect-get": "error", - "anti-slop/no-runtime-typeof": "error", - "anti-slop/no-shape-in-symbol-names": "error", - "anti-slop/no-unknown-parameters": "error", - "anti-slop/no-unknown-returns": "error", - "anti-slop/no-unknown-type-aliases": "error", - "anti-slop/no-unsafe-dictionary-type": "error", - "anti-slop/no-widen-then-assert": "error", - "anti-slop/require-safety-comment-for-type-assertion": "error", - "no-restricted-imports": [ - "error", - { - "patterns": [ - { - "group": ["legacy/**", "**/legacy/**"], - "message": "legacy/ is reference only, never import from it (plan/00-overview.md)" - } - ] - } - ], - "perfectionist/sort-objects": "off" - }, - "overrides": [ - { - "files": ["functions/**", "tools/**"], - "rules": { - "no-console": "off" - } - } - ], - "ignorePatterns": [ - "**/*.astro", - "legacy/**", - "dist/**", - ".astro/**", - "tools/lint/anti-slop/**", - ".claude/**", - "plan/**", - "node_modules/**" - ] -} diff --git a/.prettierignore b/.prettierignore index 77c609c..748b6c0 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,14 +1,6 @@ -# Prettier is invoked against an explicit `**/*.{astro,md}` glob (see package.json), -# because a blanket `*` ignore prunes directories and cannot be undone per-file. -# oxfmt owns every other extension — see docs/tooling.md. -legacy/** -dist/** -.astro/** -node_modules/** -tools/lint/anti-slop/** - -# Hand-formatted markdown: tables, checkboxes, and line breaks here are deliberate. -plan/** -docs/adr/** -DESIGN.md -.browser/** +legacy/ +dist/ +.astro/ +.browser/ +.lighthouseci/ +pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json index dc82700..1bd475f 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,13 +1,6 @@ { "printWidth": 100, - "trailingComma": "all", "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"], - "overrides": [ - { - "files": "*.astro", - "options": { - "parser": "astro" - } - } - ] + "tailwindStylesheet": "./src/styles/global.css", + "tailwindFunctions": ["cn", "cva"] } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 1bba082..97c27d7 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,11 +1,9 @@ { "recommendations": [ "astro-build.astro-vscode", - "oxc.oxc-vscode", + "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "bradlc.vscode-tailwindcss", - "hverlin.mise-vscode", - "dbaeumer.vscode-eslint", - "typescriptteam.native-preview" + "hverlin.mise-vscode" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 5af3ec6..90c501d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,57 +1,19 @@ { "editor.formatOnSave": true, - "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { - "source.fixAll.oxc": "explicit", - "source.organizeImports.oxc": "explicit" + "source.fixAll.eslint": "explicit" }, - "[typescript]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[json]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[jsonc]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[css]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[yaml]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[toml]": { - "editor.defaultFormatter": "oxc.oxc-vscode" - }, - "[astro]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "eslint.validate": ["astro"], - "eslint.useFlatConfig": true, + "eslint.validate": ["javascript", "typescript", "astro"], "files.associations": { "*.css": "tailwindcss" }, "tailwindCSS.classFunctions": ["cn", "cva"], - "tailwindCSS.experimental.classRegex": [["cn\\(([^)]*)\\)", "[\"'`]([^\"'`]*)[\"'`]"]], "search.exclude": { "legacy/**": true, "dist/**": true, - "pnpm-lock.yaml": true, - "node_modules/**": true + "pnpm-lock.yaml": true }, - "js/ts.experimental.useTsgo": true, - "js/ts.tsdk.path": "node_modules/typescript/lib", - "js/ts.preferences.importModuleSpecifier": "non-relative" + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.preferences.importModuleSpecifier": "non-relative" } diff --git a/AGENTS.md b/AGENTS.md index 07e562e..8536bc3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,27 +8,26 @@ Pages. **Zero client-side framework runtime** — `.astro` components and plain ## Commands -| Command | What it does | -| ------------------------------ | -------------------------------------------------------------------------- | -| `mise install && pnpm install` | Set up (mise pins node + pnpm; see `docs/tooling.md`) | -| `pnpm dev` | Dev server | -| `pnpm check` | Typecheck + lint + format check + knip. **Must pass before every commit.** | -| `pnpm build` | Static build to `dist/` | -| `pnpm fmt` / `pnpm lint:fix` | Write formatting / autofix lint | +| Command | What it does | +| ------------------------------ | ---------------------------------------------------------------------------------------- | +| `mise install && pnpm install` | Set up (mise pins node + pnpm; see `docs/tooling.md`) | +| `pnpm dev` | Dev server | +| `pnpm check` | Typecheck + lint + format check + knip + repo checks. **Must pass before every commit.** | +| `pnpm build` | Static build to `dist/` | +| `pnpm fmt` / `pnpm lint:fix` | Write formatting / autofix lint | -`mise run ` forwards to the same `package.json` scripts, which are the single source of truth. +## Toolchain -## Toolchain ownership +ESLint (typed `strictTypeChecked` + `stylisticTypeChecked`, `eslint-plugin-astro` with +`jsx-a11y-strict`) lints every `.ts`, `.js`, and `.astro` file. Prettier formats everything. +TypeScript is checked by `astro check` (`src/`, config files) and `tsc` (`functions/`, `tools/`). +oxlint/oxfmt return when they support `.astro` (`docs/adr/0012-single-toolchain.md`). -| Extensions | Linter | Formatter | -| -------------------------------- | ---------------------------------------------- | --------- | -| `.ts .js .mjs .cjs .json .jsonc` | oxlint (type-aware, vendored nkzw + anti-slop) | oxfmt | -| `.css` | — (oxlint has no CSS rules) | oxfmt | -| `.astro` | ESLint (typed, jsx-a11y-strict) | Prettier | -| `.md` | — | Prettier | +The Claude Code hook in `.claude/hooks/format-lint.sh` formats and lints every file you edit and +feeds lint failures back to you. -The Claude Code hook in `.claude/hooks/format-lint.sh` runs the right pair on every file you -edit, and feeds lint failures back to you. Do not reach for the other toolchain by hand. +Repo-specific checks and asset pipelines are TypeScript scripts under `tools/`, run directly by +Node (`node tools/checks/verify-meta.ts`); every one has a `package.json` script. ## Architecture @@ -39,6 +38,7 @@ edit, and feeds lint failures back to you. Do not reach for the other toolchain - `src/content/` — markdown content collections (sponsors, events, faq, news, robots, photos). - `src/data/site.ts` — org facts, external URLs, calendar and analytics IDs. No hardcoded constants. - `functions/` — Cloudflare Pages Functions (form submit, calendar proxy). Own tsconfig. +- `tools/` — repo checks, asset pipelines, and CI helpers. Own tsconfig. - `legacy/` — the old Next.js site. **Reference only; never import from it.** ## Rules diff --git a/DESIGN.md b/DESIGN.md index bcda696..4d977c1 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -2,23 +2,24 @@ Source of truth for the visual design of scstem.org. Every UI decision an implementer makes should be answerable from this document; if it isn't, propose an addition here first (PR + owner review), then build. -The system is a **brand-faithful refresh** of the 2024–2026 site, grounded in the official **Brand Guidelines v1** (colors, fonts, logo, and naming rules below are normative from that document) and settled through an owner design review in 2026-08. The organizing metaphor is the **build document**, in three layers: the page is a machined metal sheet (pockets cut *into* it), blueprint linework and spec labels are scribed *onto* it, and an **engineer's hand markup** — highlighter swipes, chalk circles and underlines, sketched arrows — sits over the top. The machine provides structure; the hand provides warmth; data speaks in a monospaced spec-sheet voice. The official t-shirt art (wireframe gear-bulb with dimension callouts, the circled-word tagline, the PROJECT/ORGANIZATION/URL title block) is a normative reference. The balance rule: the sheet stays technical, not themed — the bar is *great, not just good*: fewer competing treatments, stronger hierarchy, deliberate everything. +The system is a **brand-faithful refresh** of the 2024–2026 site, grounded in the official **Brand Guidelines v1** (colors, fonts, logo, and naming rules below are normative from that document) and settled through an owner design review in 2026-08. The organizing metaphor is the **build document**, in three layers: the page is a machined metal sheet (pockets cut _into_ it), blueprint linework and spec labels are scribed _onto_ it, and an **engineer's hand markup** — highlighter swipes, chalk circles and underlines, sketched arrows — sits over the top. The machine provides structure; the hand provides warmth; data speaks in a monospaced spec-sheet voice. The official t-shirt art (wireframe gear-bulb with dimension callouts, the circled-word tagline, the PROJECT/ORGANIZATION/URL title block) is a normative reference. The balance rule: the sheet stays technical, not themed — the bar is _great, not just good_: fewer competing treatments, stronger hierarchy, deliberate everything. ## 1. Brand essence, voice, and naming South Central STEM Collective (SC2) is a 501(c)(3) making hands-on STEM — FIRST robotics first among it — accessible to students aged 9–18 in and around Franklin County, PA. The site speaks to three audiences at once: **students** (this looks fun and serious), **parents** (this is safe, organized, worth our time), **sponsors** (this is a credible investment in the community). Voice: **energetic, concrete, community-proud.** -- This, not that: *workshop*, not *startup*. *Confident*, not *hype*. *Specific* ("12+ years, 9 competition robots"), not *vague* ("empowering the future"). *Warm*, not *corporate*. + +- This, not that: _workshop_, not _startup_. _Confident_, not _hype_. _Specific_ ("12+ years, 9 competition robots"), not _vague_ ("empowering the future"). _Warm_, not _corporate_. - Sentence case everywhere — headings, buttons, nav. Title Case only for proper nouns. No ALL CAPS except eyebrow labels and SCP spec labels (§3). - First person plural ("we build", "join us"); address the reader as "you". - Numbers beat adjectives. Real names, real seasons, real awards (with permission). -**Name usage (from Brand Guidelines):** full name "South Central STEM Collective" with capitalized *STEM*; may break to two lines after "Central". Short name "SC2" (capitalized SC) — **never in headings**, and never on a page where the full name isn't present elsewhere. "SCSTEM" only for domains/handles, never in prose. +**Name usage (from Brand Guidelines):** full name "South Central STEM Collective" with capitalized _STEM_; may break to two lines after "Central". Short name "SC2" (capitalized SC) — **never in headings**, and never on a page where the full name isn't present elsewhere. "SCSTEM" only for domains/handles, never in prose. ## 2. Color -Dark-only for now (D15). All component color comes from **semantic tokens** — raw hex values and Tailwind palette classes are banned in components (the logo mark's fixed colors are brand-asset colors, exempt like any other image). Tokens live in `src/styles/global.css` `@theme`; a future light theme is a new token block, not a refactor. +Dark-only for now. All component color comes from **semantic tokens** — raw hex values and Tailwind palette classes are banned in components (the logo mark's fixed colors are brand-asset colors, exempt like any other image). Tokens live in `src/styles/global.css` `@theme`; a future light theme is a new token block, not a refactor. ### Brand palette (normative, Brand Guidelines v1) @@ -26,26 +27,26 @@ Safety Yellow `#FACC15` · Science Blue `#3B82F6` · Foundation Gray `#4B5563` ### Surfaces — the recessed system -The page is the raised material; cards are **pockets machined into it**. (This is the *opposite* of the default elevated-lighter-card dark UI — deliberately.) +The page is the raised material; cards are **pockets machined into it**. (This is the _opposite_ of the default elevated-lighter-card dark UI — deliberately.) -| Token | Value | Use | -|---|---|---| -| `background` | `#262626` | Page ground (brand Background Black) | -| `card` | `#171717` | Pockets: cards, panels, form fields, footer band (brand Black) | -| `section-tint` | `#212121` | Full-width alternate section bands | -| `border` | `#3A3A3A` | Hairlines, card borders, dividers | +| Token | Value | Use | +| -------------- | --------- | -------------------------------------------------------------- | +| `background` | `#262626` | Page ground (brand Background Black) | +| `card` | `#171717` | Pockets: cards, panels, form fields, footer band (brand Black) | +| `section-tint` | `#212121` | Full-width alternate section bands | +| `border` | `#3A3A3A` | Hairlines, card borders, dividers | **Pocket anatomy (V2, "machined pocket")** — the standard card treatment: `card` fill, 1px `#383838` border, `radius-lg`, and inset edge physics: `box-shadow: inset 0 2px 8px rgb(0 0 0 / 0.55), inset 0 -1px 0 rgb(255 255 255 / 0.05)`. -**Feature pocket (V2+V3, "drawing pocket")**: the same, plus the engineering grid (§2 motifs) rendered *inside* the pocket at ~5% opacity — reserved for feature moments (program cards, CTA panels, stat bands) on ≥ md screens; dense card grids and mobile stay plain V2. +**Feature pocket (V2+V3, "drawing pocket")**: the same, plus the engineering grid (§2 motifs) rendered _inside_ the pocket at ~5% opacity — reserved for feature moments (program cards, CTA panels, stat bands) on ≥ md screens; dense card grids and mobile stay plain V2. **Hover (interactive pockets)**: pockets don't float — border warms to 40%-alpha `primary`, floor lifts `#171717 → #1A1A1A`, no translate, no glow, no shadow change. ### Text -| Token | Value | Contrast on `#262626` | Use | -|---|---|---|---| -| `foreground` | `#FAFAFA` | ≈14.5:1 (AAA) | Headings, nav, emphasis (brand White) | -| `body` | `#D4D4D4` | 10.2:1 (AAA) | **All reading copy** | -| `muted` | `#A3A3A3` | ≈6.0:1 (AA) | Captions, meta, labels only — never paragraphs | +| Token | Value | Contrast on `#262626` | Use | +| ------------ | --------- | --------------------- | ---------------------------------------------- | +| `foreground` | `#FAFAFA` | ≈14.5:1 (AAA) | Headings, nav, emphasis (brand White) | +| `body` | `#D4D4D4` | 10.2:1 (AAA) | **All reading copy** | +| `muted` | `#A3A3A3` | ≈6.0:1 (AA) | Captions, meta, labels only — never paragraphs | Rule: **AAA (≥7:1) for anything longer than a caption.** `muted` is the floor; nothing text-bearing goes dimmer. @@ -54,30 +55,31 @@ Rule: **AAA (≥7:1) for anything longer than a caption.** `muted` is the floor; All ratios in this section are measured against `background` `#262626` and verified at build time on `/styleguide`; the build fails if one drops below its floor. -Every brand accent is a **token pair**: the brand hex for *fills* (buttons, bands, chips, large graphics — with a near-black label), and a brightened variant for *text/icons/focus on dark* (the brand hexes other than yellow fail AA as dark-bg text). No exceptions, no third variants. +Every brand accent is a **token pair**: the brand hex for _fills_ (buttons, bands, chips, large graphics — with a near-black label), and a brightened variant for _text/icons/focus on dark_ (the brand hexes other than yellow fail AA as dark-bg text). No exceptions, no third variants. -| Accent | Fill (brand hex / label color) | Text-on-dark (≈ contrast) | Role | -|---|---|---|---| -| Safety Yellow | `#FACC15` / `#171717` | `#FACC15` (9.8:1 — bright enough to be both) | **Action**: CTAs, links, focus ring, key-word emphasis. The default `primary`. | -| Science Blue | `#3B82F6` / `#171717` | `#60A5FA` (5.9:1) | **Informational**: info callouts, calendar/event chips, data UI. Also the **designated primary of a future light theme** (yellow is illegible on white) — do not repurpose. | -| Hazard Green | `#16A34A` / `#08240F` | `#3ECF6E` (7.4:1) | FRC/Biohazard theme accent pair | -| Danger Orange | `#F97316` / `#241102` | `#FB923C` (6.6:1) | FLL theme accent pair | -| Destructive | `#DB262F` / `#FAFAFA` (4.6:1) | `#FCA5A5` (7.9:1) | **Errors only**: form validation, destructive confirmations. Not a brand accent and never decorative — it appears when something is wrong and nowhere else. | +| Accent | Fill (brand hex / label color) | Text-on-dark (≈ contrast) | Role | +| ------------- | ------------------------------ | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Safety Yellow | `#FACC15` / `#171717` | `#FACC15` (9.8:1 — bright enough to be both) | **Action**: CTAs, links, focus ring, key-word emphasis. The default `primary`. | +| Science Blue | `#3B82F6` / `#171717` | `#60A5FA` (5.9:1) | **Informational**: info callouts, calendar/event chips, data UI. Also the **designated primary of a future light theme** (yellow is illegible on white) — do not repurpose. | +| Hazard Green | `#16A34A` / `#08240F` | `#3ECF6E` (7.4:1) | FRC/Biohazard theme accent pair | +| Danger Orange | `#F97316` / `#241102` | `#FB923C` (6.6:1) | FLL theme accent pair | +| Destructive | `#DB262F` / `#FAFAFA` (4.6:1) | `#FCA5A5` (7.9:1) | **Errors only**: form validation, destructive confirmations. Not a brand accent and never decorative — it appears when something is wrong and nowhere else. | -### Program themes (D16) +### Program themes `data-theme="frc"` / `data-theme="fll"` on the page root remap **only** `primary`, `primary-bright`, `primary-foreground`, and `ring` to the program's accent pair. Everything else is identical — program pages are the same site wearing team colors. Adding a program = one token block. Rules: -- **One action accent per view** (the theme's `primary`). Blue may appear alongside it only in its informational role. The legacy mix (yellow CTA + blue links + orange button on one page) stays dead. + +- **One action accent per view** (the theme's `primary`). Blue may appear alongside it only in its informational role. - Long body text is never accent-colored, never pure white — `body` token only. - Foundation Gray `#4B5563` lives in the logo and imagery; it is not a UI token. ### Signature motifs -1. **Key-word emphasis**: display headings may emphasize exactly one phrase — either `primary`-colored text or a highlighter swipe (hand markup §13), never both, never more than one phrase. Link-style underlines never appear in headings. +1. **Key-word emphasis**: display headings may emphasize exactly one phrase — either `primary`-colored text or a highlighter swipe (§2.13), never both, never more than one phrase. Link-style underlines never appear in headings. 2. **Accent hairline**: heroes end with a 2px `primary` rule, full-bleed. Card titles may carry a 32px × 2px `primary` rule beneath. -3. **Engineering grid** (replaces the legacy circuit-board texture): fine graph-paper grid (~24–28px cell, 1px `#FAFAFA` strokes) at 4–7% opacity, on heroes and section breaks and inside V2+V3 feature-pocket floors — never behind body copy, and **never on the page ground**: a page-level grid was tried and rejected in review (it competes with the pocket grid floors and dulls the recess effect). The ground stays smooth; its life comes from the atmosphere devices below, so the grid stays special where it appears. Optional **dimension-line ticks** (`primary` at ≤50%, SCP annotation) as rare garnish. +3. **Engineering grid** (replaces the legacy circuit-board texture): fine graph-paper grid (~24–28px cell, 1px `#FAFAFA` strokes) at 4–7% opacity, on heroes and section breaks and inside V2+V3 feature-pocket floors — never behind body copy, and **never on the page ground**, where it competes with the pocket grid floors and dulls the recess effect. The ground stays smooth; its life comes from the atmosphere devices below. Optional **dimension-line ticks** (`primary` at ≤50%, SCP annotation) as rare garnish. 4. **Framed media**: photo collages/feature media get a 2px `primary` border + `radius-lg` — the "team picture frame". ### Atmosphere layer @@ -86,12 +88,12 @@ Large unmodulated `background` fields read sterile. Between the hero and the foo 5. **Ambient pools**: one radial gradient anchored to a section's top or its heading — `primary` at 3–6% alpha (or `#FAFAFA` at 2–4% for neutral sections), fading to transparent by ~70%. A small lightness modulation of the ground; text contrast is unaffected. Max one per section. 6. **Ghost section numerals**: oversized Source Code Pro 600 numerals (`01`, `02`, …) at 5–8% alpha `foreground`, placed behind section headings — the device from the Brand Guidelines' own section pages. Decorative (`aria-hidden`), numbering only top-level page sections, 3–4 per page max. -7. **Ruler dividers**: a full-container tick-mark strip (baseline + graduated ticks, `foreground` at ≤18%) as the *strong* section divider; plain 1px hairlines remain the quiet default. +7. **Ruler dividers**: a full-container tick-mark strip (baseline + graduated ticks, `foreground` at ≤18%) as the _strong_ section divider; plain 1px hairlines remain the quiet default. 8. **Registration marks**: small corner brackets (`primary` at ≤35%, 1.5px stroke, ~20px) on the corners of one feature pocket per view — the drafting-sheet crop-mark garnish. Never on standard cards. ### Scribed register (from the t-shirt art) -The blueprint devices printed *onto* the sheet. Each appears at most once per page unless noted: +The blueprint devices printed _onto_ the sheet. Each appears at most once per page unless noted: 9. **Title block**: the engineering-drawing identity strip — bordered compartments, each an SCP uppercase label (`PROJECT:` / `ORGANIZATION:` / `URL:` …) over an Inter (or SCP for URLs/codes) value, 1px `border` dividers. Its home is the footer bottom (the drawing sheet's corner), horizontal strip ≥ md, stacked on mobile; contact/event pages may use the boxed stack as an info card. 10. **Scribed lineart**: the wireframe gear-bulb (and sibling blueprint drawings) as large, faint decorative art — stroke-only, `foreground` or `primary`, 4–6% opacity, on `card` bands (footer, feature panels), never behind body copy. Obtain the real vector from the merch/brand source files into `src/assets/brand/` (the mockups use a drawn approximation). @@ -99,16 +101,16 @@ The blueprint devices printed *onto* the sheet. Each appears at most once per pa ### Hand markup register (the human layer) -The engineer's markup drawn *over* the sheet — this is what keeps the machined language from feeling sterile. **The machined and scribed layers stay perfectly geometric everywhere; hand markup never earns more real estate than it has now — minor touches, not a style.** All strokes are deliberately imperfect (hand-drawn SVG paths: slight curve, open ends, small rotation), 2–3px, `stroke-linecap: round`. **Pill-shaped UI is banned** (reviewed and rejected): a circled word is a chalk oval, never a `border-radius: 999px` box. +The engineer's markup drawn _over_ the sheet — this is what keeps the machined language from feeling sterile. **The machined and scribed layers stay perfectly geometric everywhere; hand markup never earns more real estate than it has now — minor touches, not a style.** All strokes are deliberately imperfect (hand-drawn SVG paths: slight curve, open ends, small rotation), 2–3px, `stroke-linecap: round`. **Pill-shaped UI is banned**: a circled word is a chalk oval, never a `border-radius: 999px` box. **Variation rule**: every markup device ships as a set of **at least 3 distinct SVG path variants** (implemented as primitives, e.g. `ChalkOval variant={1|2|3}`), further varied per-instance by small rotation/flip. Two adjacent instances never share a variant — identical "hand-drawn" marks read as a stamp and break the illusion. Swipes vary by rotation (±0.5–2°), inset, and alpha within their range. 12. **Chalk ovals**: key words circled with a hand-drawn open ellipse — `foreground` white (chalk) in hero/photo contexts, `primary` (grease pencil) on the ground. The "Real ⬭Skills⬭. Real ⬭Robots⬭. Real ⬭Fun⬭." treatment; the tagline itself is sanctioned brand copy for heroes/CTAs. Tagline/display contexts only, one run per view. -13. **Highlighter swipes**: a skewed translucent `primary` rectangle (25–35% alpha — **25% is the default**, the only value in the range that keeps white text at AAA: 7.6:1 on the ground, 9.2:1 on `card`; ±0.5–2° rotation, 2–3px radius) behind white key words — the marker-highlight alternative to `primary`-colored text. A heading uses colored text *or* a swipe, never both; verify the white-on-swipe contrast on `/styleguide`. -14. **Chalk underlines**: hand-drawn, slightly curved underline strokes (`primary`). Two uses: beneath a heading, and **inline within a sentence** under a short key phrase (≤3 words — e.g. "building the ~future of STEM~") as the sanctioned in-prose emphasis. Distinct from the machined 32px card rule, which stays perfectly straight — machined vs. hand is a deliberate contrast, never blended. +13. **Highlighter swipes**: a skewed translucent `primary` rectangle (25–35% alpha — **25% is the default**, the only value in the range that keeps white text at AAA: 7.6:1 on the ground, 9.2:1 on `card`; ±0.5–2° rotation, 2–3px radius) behind white key words — the marker-highlight alternative to `primary`-colored text. A heading uses colored text _or_ a swipe, never both; verify the white-on-swipe contrast on `/styleguide`. +14. **Chalk underlines**: hand-drawn, slightly curved underline strokes (`primary`). Two uses: beneath a heading, and **inline within a sentence** under a short key phrase (≤3 words — e.g. "building the ~~future of STEM~~") as the sanctioned in-prose emphasis. Distinct from the machined 32px card rule, which stays perfectly straight — machined vs. hand is a deliberate contrast, never blended. 15. **Sketch arrows**: one hand-drawn curved arrow per page. **An arrow's target is always handwritten annotation text (§3), never a regular-font element** — a sketched arrow pointing at typeset UI breaks the fiction. The annotation may itself be a link (e.g. the handwritten "Become a sponsor"). -Motion note (§6 applies): hand-markup strokes may *draw on* as their entrance (stroke-dashoffset, once, ~500ms, reduced-motion disables) — the one sanctioned decorative animation, because it enacts the metaphor. +Motion note (§6 applies): hand-markup strokes may _draw on_ as their entrance (stroke-dashoffset, once, ~500ms, reduced-motion disables) — the one sanctioned decorative animation, because it enacts the metaphor. **Register budget**: across all machined, scribed, and hand-markup devices (grid, ticks, ruler, numerals, pools, marks, title block, lineart, callouts, ovals, swipes, underlines, arrows), a viewport shows **at most 4 distinct devices**. If a new one enters a view, another leaves. @@ -118,27 +120,27 @@ Restraint rule: these are atmosphere, not decoration — if a device is noticeab Per Brand Guidelines: Orbitron for page headings/titles (avoid very long or small lines), Inter for body/subheadings, Source Code Pro for monospaced/stylistic elements. -| Role | Font | Weights | Where | -|---|---|---|---| -| Display/headings | **Orbitron** (variable) | 500–700 | h1–h3 and eyebrow labels only. Never below h3 size, never italic, never long lines (≤ ~40 chars/line) — ration the display voice. | -| UI & body | **Inter** (variable) | 400/500/600 | Everything else: body, h4–h6, nav, buttons, forms, captions | -| Data voice | **Source Code Pro** | 400/600 | **Stats and numerals, countdowns, dates, spec labels** (ages chips, tier badges, "REF" annotations). The spec-sheet register of the build-document metaphor. | -| Annotation hand | **Architects Daughter** | 400 | Hand-markup annotations only (≤5 words): sketch-arrow targets, margin notes, the handwritten link they point at. Never UI chrome, body copy, headings, or labels. | +| Role | Font | Weights | Where | +| ---------------- | ----------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Display/headings | **Orbitron** (variable) | 500–700 | h1–h3 and eyebrow labels only. Never below h3 size, never italic, never long lines (≤ ~40 chars/line) — ration the display voice. | +| UI & body | **Inter** (variable) | 400/500/600 | Everything else: body, h4–h6, nav, buttons, forms, captions | +| Data voice | **Source Code Pro** | 400/600 | **Stats and numerals, countdowns, dates, spec labels** (ages chips, tier badges, "REF" annotations). The spec-sheet register of the build-document metaphor. | +| Annotation hand | **Architects Daughter** | 400 | Hand-markup annotations only (≤5 words): sketch-arrow targets, margin notes, the handwritten link they point at. Never UI chrome, body copy, headings, or labels. | Fluid scale (clamp between 360px and 1440px viewports), defined as tokens: -| Token | Size (min → max) | Line height | Font | -|---|---|---|---| -| `display` | 2.5rem → 4.25rem | 1.05 | Orbitron 700 | -| `h1` | 2rem → 3rem | 1.1 | Orbitron 700 | -| `h2` | 1.5rem → 2.25rem | 1.15 | Orbitron 600 | -| `h3` | 1.25rem → 1.5rem | 1.25 | Orbitron 600 | -| `h4` | 1.125rem → 1.25rem | 1.35 | Inter 600 | -| `stat` | 1.75rem → 2.25rem | 1.1 | Source Code Pro 600, `primary` | -| `body-lg` | 1.0625rem → 1.1875rem | 1.65 | Inter 400 | -| `body` | 1rem → 1.0625rem | 1.65 | Inter 400 | -| `small` | 0.875rem | 1.5 | Inter 400/500 | -| `label` | 0.6875–0.75rem | 1.4 | Source Code Pro 600, +0.05em tracking, uppercase (spec labels/chips) | +| Token | Size (min → max) | Line height | Font | +| --------- | --------------------- | ----------- | -------------------------------------------------------------------- | +| `display` | 2.5rem → 4.25rem | 1.05 | Orbitron 700 | +| `h1` | 2rem → 3rem | 1.1 | Orbitron 700 | +| `h2` | 1.5rem → 2.25rem | 1.15 | Orbitron 600 | +| `h3` | 1.25rem → 1.5rem | 1.25 | Orbitron 600 | +| `h4` | 1.125rem → 1.25rem | 1.35 | Inter 600 | +| `stat` | 1.75rem → 2.25rem | 1.1 | Source Code Pro 600, `primary` | +| `body-lg` | 1.0625rem → 1.1875rem | 1.65 | Inter 400 | +| `body` | 1rem → 1.0625rem | 1.65 | Inter 400 | +| `small` | 0.875rem | 1.5 | Inter 400/500 | +| `label` | 0.6875–0.75rem | 1.4 | Source Code Pro 600, +0.05em tracking, uppercase (spec labels/chips) | - **Inter ships with its weight axis trimmed to 400–700** and cannot render heavier: the axis this table does not use was 12 KB of critical-path font (`docs/adr/0011-inter-weight-axis.md`). Widening the range is a `pnpm assets:fonts` change, not just a utility class. - Eyebrow labels: Orbitron 500, 12px, uppercase, `+0.08em` tracking, `primary` or `muted` — Orbitron's one all-caps use; SCP `label` is the other sanctioned caps. @@ -153,21 +155,21 @@ Fluid scale (clamp between 360px and 1440px viewports), defined as tokens: - **Spacing**: 4px base scale. Component-internal 8–24px; between-component 24–48px. - **Section rhythm**: `py-16` (mobile) / `py-24` (≥ md), consistent on every section. - **Radius — one end-mill**: three values, unified to the machining story. `radius-sm` **4px** (chips, spec labels, badges — stamped plates), `radius-md` **8px** (all controls: buttons, inputs, icon tiles), `radius-lg` **12px** (every cut feature: pockets, panels, framed media — one tool radius for everything machined). Nothing else; `16px` and pill radii are retired. -- **Elevation**: there is none — depth goes *down*, not up (§2 pocket anatomy). No drop shadows anywhere; the inset pocket shadows are the only shadows in the system. +- **Elevation**: there is none — depth goes _down_, not up (§2 pocket anatomy). No drop shadows anywhere; the inset pocket shadows are the only shadows in the system. ## 5. Layout & navigation - Container: `max-w-6xl` (72rem) + `px-4`/`px-6`. Heroes and accent rules full-bleed; content aligned to container. - Grids: 1-col → 2-col (≥ md) → 3-col (≥ lg). **No orphan rows**: plan the math (5 cards = intentional 2+3). - Breakpoints: Tailwind defaults + `3xl` = 120rem. Design mobile-first at 360px. -- **Header (sticky)**: sticky on all viewports, condensing slightly after scroll (pure CSS); `background`/95 with blur fallback, bottom hairline. Desktop: the **full-width color lockup** (`logo-color-full.svg`, ~40px) — the wordmark *is* the identity — then About / Programs ▾ / Sponsors / Donate + primary "Get involved" button. Mobile: the **square mark alone** (brand rules forbid subbing "SC2"; the full name must appear in page content — hero/footer satisfy this). +- **Header (sticky)**: sticky on all viewports, condensing slightly after scroll (pure CSS); `background`/95 with blur fallback, bottom hairline. Desktop: the **full-width color lockup** (`logo-color-full.svg`, ~40px) — the wordmark _is_ the identity — then About / Programs ▾ / Sponsors / Donate + primary "Get involved" button. Mobile: the **square mark alone** (brand rules forbid subbing "SC2"; the full name must appear in page content — hero/footer satisfy this). - **Programs**: desktop hover/focus dropdown (FLL, FRC, Robots, Calendar) whose click/tap target is a real **`/programs` hub page** — zero-JS fallback and the mobile path. Never hover-only. - **Mobile menu**: full-height sheet; ≥48px rows (About, Programs, Sponsors, Donate, Calendar); "Get involved" and "Donate" as large buttons pinned at the bottom; Esc/backdrop closes; `aria-expanded` wired. Nothing is more than two taps away. -- The about page: single flowing column (prose measure) with photo groupings as interleaved timeline sections — the legacy 3-column photo rails are retired. +- The about page: single flowing column (prose measure) with photo groupings as interleaved timeline sections. ## 6. Motion -CSS-only (D3, D20). Motion confirms — it never decorates. +CSS-only. Motion confirms — it never decorates. - Durations: 150ms (hover/focus), 250ms (menus, accordions), 500ms (scroll-in entrances). `ease-out` entrances, `ease-in-out` toggles. - Only `opacity` and `transform` animate. @@ -182,7 +184,7 @@ CSS-only (D3, D20). Motion confirms — it never decorates. - Photo treatment: `radius-lg` framed in sections; full-bleed only in heroes. Consistent warm/neutral grading. - Every image: honest `alt`; decorative pattern/grid SVGs `aria-hidden` with `alt=""`. - OG images (1200×630): photo + scrim + Orbitron title + lockup; one template, per-section variants. -- **Logo usage** (Brand Guidelines): color lockup on light *and* dark; dark/light monochrome variants per background; never recolor, never set the name in another font as a substitute for the lockup where the lockup fits. +- **Logo usage** (Brand Guidelines): color lockup on light _and_ dark; dark/light monochrome variants per background; never recolor, never set the name in another font as a substitute for the lockup where the lockup fits. ## 8. Components tone @@ -206,24 +208,24 @@ CSS-only (D3, D20). Motion confirms — it never decorates. ## 10. Do / Don't -| Don't | Do | -|---|---| -| Elevated lighter-than-page cards (the stock AI dark-UI look) | Pockets: darker cards machined *into* the page (§2) | -| Bare, unmodulated `#262626` voids between sections | One atmosphere device per boundary: ambient pool, ghost numeral, ruler divider, or tint band (§2) | -| Circuit-board wallpaper | Engineering grid + dimension ticks, ≤7%, heroes/section breaks only | -| Yellow words, yellow underlines, and blue links competing in one viewport | One action accent per view; blue only in its informational role | -| Orange button on the green FRC page | The page theme's primary pair | -| Hero copy on a busy photo behind a thin scrim on mobile | Text on solid ground below the photo (§7) | -| Body copy in `#A3A3A3` or dimmer | `body #D4D4D4` minimum; muted is captions-only | -| Orbitron paragraphs, tiny Orbitron labels, Orbitron stats | Orbitron = h1–h3 + eyebrows; **SCP owns numbers and spec labels** | -| "SC2" in a heading; "Scstem" in prose | Full name (capitalized STEM); SC2 only in prose with the full name present | -| Faking the logo: gear SVG + name in Inter | Real lockup assets: full-width on desktop chrome, square mark on mobile | -| Filled dark-on-dark tier pills | SCP outline chips per §8 | -| Pill-shaped UI (`border-radius: 999px` capsules) | Radius tokens only; circled words are hand-drawn chalk ovals (§2.12) | -| 5 cards centered as 3+2 with a floating orphan | Grid math planned: intentional 2+3 | -| Drop shadows, glows, hover-lift on pockets | Border warms + floor lifts one step; depth only goes down | -| `alt="image"` / missing alt | Descriptive alt or explicit `alt=""` | -| Generic hero copy ("Empowering the future…") | Specific, local, human | +| Don't | Do | +| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Elevated lighter-than-page cards (the stock AI dark-UI look) | Pockets: darker cards machined _into_ the page (§2) | +| Bare, unmodulated `#262626` voids between sections | One atmosphere device per boundary: ambient pool, ghost numeral, ruler divider, or tint band (§2) | +| Circuit-board wallpaper | Engineering grid + dimension ticks, ≤7%, heroes/section breaks only | +| Yellow words, yellow underlines, and blue links competing in one viewport | One action accent per view; blue only in its informational role | +| Orange button on the green FRC page | The page theme's primary pair | +| Hero copy on a busy photo behind a thin scrim on mobile | Text on solid ground below the photo (§7) | +| Body copy in `#A3A3A3` or dimmer | `body #D4D4D4` minimum; muted is captions-only | +| Orbitron paragraphs, tiny Orbitron labels, Orbitron stats | Orbitron = h1–h3 + eyebrows; **SCP owns numbers and spec labels** | +| "SC2" in a heading; "Scstem" in prose | Full name (capitalized STEM); SC2 only in prose with the full name present | +| Faking the logo: gear SVG + name in Inter | Real lockup assets: full-width on desktop chrome, square mark on mobile | +| Filled dark-on-dark tier pills | SCP outline chips per §8 | +| Pill-shaped UI (`border-radius: 999px` capsules) | Radius tokens only; circled words are hand-drawn chalk ovals (§2.12) | +| 5 cards centered as 3+2 with a floating orphan | Grid math planned: intentional 2+3 | +| Drop shadows, glows, hover-lift on pockets | Border warms + floor lifts one step; depth only goes down | +| `alt="image"` / missing alt | Descriptive alt or explicit `alt=""` | +| Generic hero copy ("Empowering the future…") | Specific, local, human | ## 11. Change process diff --git a/README.md b/README.md index ab77021..4e60799 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # scstem.org -## Project Setup +The website of the South Central STEM Collective. Astro, deployed by Cloudflare Pages. -1. Install [Mise](https://mise.jdx.dev/) -2. Run `mise install` +```sh +mise install # pinned node + pnpm +pnpm install +pnpm dev # http://localhost:4321 +pnpm check # everything CI checks, minus the build +``` -## Development - -1. Run `mise dev` to start Astro dev server. -2. Browse to `http://localhost:4321` to view the site. +`docs/tooling.md` covers the toolchain; `AGENTS.md` the rules for working in the codebase. diff --git a/astro.config.ts b/astro.config.ts index e0ce731..c806954 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -1,8 +1,7 @@ -import { readFileSync } from "node:fs"; - import sitemap from "@astrojs/sitemap"; import tailwindcss from "@tailwindcss/vite"; import { defineConfig, envField } from "astro/config"; +import { readFileSync } from "node:fs"; // Loaded through jiti, and this module imports nothing from `astro:*`, so the config can read the // same origin everything else derives canonical and OG URLs from. @@ -12,14 +11,12 @@ const outDir = "./dist"; /** * A URL that is both in the sitemap and `noindex` is a "Submitted URL marked 'noindex'" error in - * Search Console, so the two have to agree — and the page itself is the only honest source of - * which it is. `/styleguide` sets `noindex` through the `Seo` prop; an event retired with - * `hidden: true` (D17) becomes a redirect page Astro emits with the same tag. Reading the emitted - * HTML covers both, and anything else that grows a `noindex`, without a second list to maintain. + * Search Console, so the two have to agree, and the emitted page is the only source of which it + * is: `/styleguide` sets `noindex` through the `Seo` prop, and a retired event becomes a redirect + * page Astro emits with the same tag. Reading the HTML covers both without a second list. * * Safe to read here: `@astrojs/sitemap` filters in `astro:build:done`, after every page is on - * disk. `astro:content` is *not* reachable from the config, which is why the events collection - * cannot be consulted directly. + * disk. `astro:content` is not reachable from the config, so the collection cannot be consulted. */ const isIndexable = (page: string): boolean => { const { pathname } = new URL(page); @@ -47,7 +44,7 @@ export default defineConfig({ default: "1x00000000000000000000AA", }), /** - * Cloudflare Web Analytics' beacon token (D21). Public by design — it ships in the page — + * Cloudflare Web Analytics' beacon token. Public by design — it ships in the page — * and empty by default, which is how a preview or a fresh clone runs with no beacon at all. * The production value is set in the Pages dashboard, beside the Turnstile keys; until it * is, GA4 is the only analytics the site has (`docs/analytics.md`). diff --git a/docs/adr/0001-toolchain-split.md b/docs/adr/0001-toolchain-split.md index c45dcba..6191523 100644 --- a/docs/adr/0001-toolchain-split.md +++ b/docs/adr/0001-toolchain-split.md @@ -1,6 +1,6 @@ # 0001 — Split the lint/format toolchain between oxc and ESLint/Prettier -- **Status:** accepted +- **Status:** superseded by [0012](0012-single-toolchain.md) - **Date:** 2026-08-28 - **Decision reference:** D12, D24 (`plan/00-overview.md`) @@ -14,11 +14,11 @@ a typed linter for their frontmatter and an accessibility ruleset for their mark Split ownership by extension, with no overlap: -| Extensions | Linter | Formatter | -| --- | --- | --- | -| `.ts .js .mjs .cjs .json .jsonc .css` | oxlint (`typeAware`, `typeCheck`, vendored anti-slop) | oxfmt | -| `.astro` | ESLint (`strictTypeChecked` + `eslint-plugin-astro` `jsx-a11y-strict`) | Prettier | -| `.md` | — | Prettier | +| Extensions | Linter | Formatter | +| ------------------------------------- | ---------------------------------------------------------------------- | --------- | +| `.ts .js .mjs .cjs .json .jsonc .css` | oxlint (`typeAware`, `typeCheck`, vendored anti-slop) | oxfmt | +| `.astro` | ESLint (`strictTypeChecked` + `eslint-plugin-astro` `jsx-a11y-strict`) | Prettier | +| `.md` | — | Prettier | Import sorting is enabled on both halves — oxfmt's `sortImports` and `eslint-plugin-perfectionist`'s `sort-imports` for `.astro` frontmatter — so the whole repo is diff --git a/docs/adr/0005-webp-only-image-variants.md b/docs/adr/0005-webp-only-image-variants.md index 290d26e..070b22e 100644 --- a/docs/adr/0005-webp-only-image-variants.md +++ b/docs/adr/0005-webp-only-image-variants.md @@ -7,7 +7,7 @@ `plan/09-assets-performance.md` §2 asks every `` call site to emit "AVIF+WebP formats". Phase 09 is also where the sources it would apply to reached their final shape: every raster in -`src/assets/` is a WebP master, re-encoded by `tools/assets/optimize-sources.mjs` at quality 80 +`src/assets/` is a WebP master, re-encoded by `tools/assets/optimize-sources.ts` at quality 80 and capped at 2560px. Two measurements taken against `src/assets/sc2/competition-1.webp` (2048×1365, 283 KB) decided @@ -16,13 +16,13 @@ how those call sites are configured. **AVIF costs 40× the encode time for a saving WebP already matches.** Sharp, encoding a 1920px variant on the build machine: -| format | quality | effort | size | time | -| ------ | ------- | ------ | ---- | ---- | -| WebP | 80 | — | 268 KB | 0.37 s | -| WebP | 70 | — | 211 KB | 0.28 s | -| AVIF | 55 | 4 (sharp default) | 173 KB | 11.4 s | -| AVIF | 55 | 2 | 188 KB | 1.8 s | -| AVIF | 55 | 0 | 205 KB | 0.36 s | +| format | quality | effort | size | time | +| ------ | ------- | ----------------- | ------ | ------ | +| WebP | 80 | — | 268 KB | 0.37 s | +| WebP | 70 | — | 211 KB | 0.28 s | +| AVIF | 55 | 4 (sharp default) | 173 KB | 11.4 s | +| AVIF | 55 | 2 | 188 KB | 1.8 s | +| AVIF | 55 | 0 | 205 KB | 0.36 s | AVIF only beats WebP at an effort level that costs eleven seconds per variant. The build emits 172 variants; at sharp's default effort that is roughly half an hour added to every build and @@ -40,7 +40,7 @@ this phase: Those are the widest steps: a re-compression of an already-lossy q80 file at q80, which adds generation loss and bytes at the same time. Separately, Astro fills the `src` attribute — the -fallback for a client that ignores `srcset` — from the source's *intrinsic* size whenever no +fallback for a client that ignores `srcset` — from the source's _intrinsic_ size whenever no `width` prop is given, so a 2560px master produced a 436 KB variant that no page ever displays. ## Decision diff --git a/docs/adr/0008-no-font-preloads.md b/docs/adr/0008-no-font-preloads.md index 30d2be4..df43009 100644 --- a/docs/adr/0008-no-font-preloads.md +++ b/docs/adr/0008-no-font-preloads.md @@ -14,12 +14,12 @@ variable (12.1 KB) — from `src/styles/fonts.ts`, to shorten the flash of fallb Phase 09's head audit measured what that costs. Lighthouse, mobile preset, simulated throttling (1.6 Mbps, 150 ms RTT), one run per configuration: -| URL | | FCP | LCP | CLS | Perf | -| --- | --- | ---: | ---: | ---: | ---: | -| `/programs/frc/robots/` | with preloads | 1054 ms | 2028 ms | 0.000 | 99 | -| `/programs/frc/robots/` | without | **766 ms** | **1366 ms** | 0.000 | 100 | -| `/sponsors/` | with preloads | 1062 ms | 1958 ms | 0.000 | 99 | -| `/sponsors/` | without | **754 ms** | **1129 ms** | 0.000 | 100 | +| URL | | FCP | LCP | CLS | Perf | +| ----------------------- | ------------- | ---------: | ----------: | ----: | ---: | +| `/programs/frc/robots/` | with preloads | 1054 ms | 2028 ms | 0.000 | 99 | +| `/programs/frc/robots/` | without | **766 ms** | **1366 ms** | 0.000 | 100 | +| `/sponsors/` | with preloads | 1062 ms | 1958 ms | 0.000 | 99 | +| `/sponsors/` | without | **754 ms** | **1129 ms** | 0.000 | 100 | A preload is a High-priority request issued from ``, ahead of the render-blocking stylesheet and well ahead of the hero `` the browser finds later in the body. On a link that diff --git a/docs/adr/0010-og-cards.md b/docs/adr/0010-og-cards.md index c5eeff2..0947e77 100644 --- a/docs/adr/0010-og-cards.md +++ b/docs/adr/0010-og-cards.md @@ -21,7 +21,7 @@ seven files that change when the photography or the section list changes, which ## Decision -`tools/assets/og-cards.mjs` renders all seven cards from one template and is **run by hand**; +`tools/assets/og-cards.ts` renders all seven cards from one template and is **run by hand**; the output is committed. `pnpm assets:og-fonts` is the one-time font step, and `pnpm assets:og` renders. @@ -53,5 +53,5 @@ the output is committed. `pnpm assets:og-fonts` is the one-time font step, and it and neither runs in CI. - A new section wants a new card: add it to the `cards` list, run `pnpm assets:og`, and pass it as `ogImage` with an `ogImageAlt` — `Seo` throws at build without the alt. -- `tools/checks/verify-meta.mjs` asserts every page's `og:image` is absolute and resolves to a +- `tools/checks/verify-meta.ts` asserts every page's `og:image` is absolute and resolves to a built file, so a card that is renamed and not rewired fails CI rather than a card debugger. diff --git a/docs/adr/0011-inter-weight-axis.md b/docs/adr/0011-inter-weight-axis.md index 926ca84..ee68020 100644 --- a/docs/adr/0011-inter-weight-axis.md +++ b/docs/adr/0011-inter-weight-axis.md @@ -18,16 +18,16 @@ render. Measured, instancing each variable face down to the range its role actually spans: -| Face | Shipped | Trimmed | Saving | Range | -| --- | ---: | ---: | ---: | --- | -| Inter latin | 47.1 KB | **35.2 KB** | 11.9 KB | 400–700 | -| Inter latin-ext | 83.1 KB | **57.9 KB** | 25.2 KB | 400–700 | -| Source Code Pro latin | 21.5 KB | 18.3 KB | 3.2 KB | 400–600 | -| Orbitron latin | 11.5 KB | 10.8 KB | 0.7 KB | 500–700 | +| Face | Shipped | Trimmed | Saving | Range | +| --------------------- | ------: | ----------: | ------: | ------- | +| Inter latin | 47.1 KB | **35.2 KB** | 11.9 KB | 400–700 | +| Inter latin-ext | 83.1 KB | **57.9 KB** | 25.2 KB | 400–700 | +| Source Code Pro latin | 21.5 KB | 18.3 KB | 3.2 KB | 400–600 | +| Orbitron latin | 11.5 KB | 10.8 KB | 0.7 KB | 500–700 | ## Decision -Instance **Inter only**, to `wght 400 700`, with `tools/assets/font-subset.mjs` +Instance **Inter only**, to `wght 400 700`, with `tools/assets/font-subset.ts` (`pnpm assets:fonts`). Output is committed to `src/styles/fonts/` and `fonts.css` points at it; the `@font-face` range is declared `400 700` to match, so the browser is told what the file can actually do. diff --git a/docs/adr/0012-single-toolchain.md b/docs/adr/0012-single-toolchain.md new file mode 100644 index 0000000..15e1c8e --- /dev/null +++ b/docs/adr/0012-single-toolchain.md @@ -0,0 +1,37 @@ +# 0012 — One toolchain: ESLint and Prettier for every file + +- **Status:** accepted +- **Date:** 2026-09-01 +- **Supersedes:** [0001](0001-toolchain-split.md) + +## Context + +ADR 0001 split lint and format ownership by extension: oxlint and oxfmt for `.ts`/`.js`/`.json`/ +`.css`, ESLint and Prettier for `.astro` and `.md`. The split bought speed on the half of the repo +oxc could read, and cost two configs, two sets of ignore rules, a routing hook, a `--ignore-path` +workaround on every oxfmt call, an explicit glob on every Prettier call, and import bans declared +twice. `.astro` files are the bulk of the site, and oxc still cannot read them. + +## Decision + +ESLint and Prettier own every file until oxlint and oxfmt support `.astro`. + +- `eslint.config.ts`: `typescript-eslint` `strictTypeChecked` + `stylisticTypeChecked` over + `**/*.{ts,mts,js,mjs,astro}` with `projectService`; `eslint-plugin-astro` `recommended` + + `jsx-a11y-strict` over `.astro`; `perfectionist/sort-imports` throughout. +- `.prettierrc.json`: `prettier-plugin-astro` and `prettier-plugin-tailwindcss` (class sorting + in markup and in `cn`/`cva` calls, resolved against `src/styles/global.css`). No per-directory + ignore list for Markdown: tables and lists are formatted like everything else. +- The vendored `tools/lint/` (nkzw's oxlint config, the anti-slop oxlint plugin) is removed with + the runtime it was written for. Its type-safety intent is carried by `strictTypeChecked`. +- `tsgo` (`@typescript/native-preview`) is dropped too: `functions/` and `tools/` typecheck with + the `tsc` the repo already ships for `astro check`. Two compilers for one small tree was not + worth a dependency. + +## Consequences + +- One linter, one formatter, one hook branch. `pnpm lint` is `eslint`, `pnpm fmt` is `prettier`. +- Lint is slower than oxlint on `.ts` files: about 15 s for the repo, type-aware. Acceptable for + a site this size. +- Reversal is the migration seam from ADR 0001, applied once oxc reads `.astro`: swap the + binaries, keep the rules. Accessibility coverage (`jsx-a11y-strict`) must survive the swap. diff --git a/docs/analytics.md b/docs/analytics.md index 133cd1c..57261ca 100644 --- a/docs/analytics.md +++ b/docs/analytics.md @@ -1,6 +1,6 @@ # Analytics -Two collectors, both cookieless-by-default, both loading after `load` (D21): +Two collectors, both cookieless-by-default, both loading after `load`: | What | Where it comes from | Status | | ------------------------ | -------------------------------------------------- | -------------------------------- | @@ -65,55 +65,28 @@ dispatches `TRACK_EVENT` with its name. Then add the row above, and mark it a ke ## Verifying it -The snippet is gated on the production hostname, so a local check has to say it is on that host. -Against a production build served by `astro preview`, with the hostname in the emitted snippet -temporarily rewritten to `localhost`, the following was observed in Chromium: - -- `dataLayer` after `load`: `["consent","default",{…}]`, `["js",Date]`, `["config","G-3TPD3DLYBR"]` - — in that order, with the consent defaults first. -- `gtag.js` injected `async` into ``. -- Clicking the header CTA pushed `["event","get_involved_click"]`; a `/donate` link pushed - `["event","donate_click"]`; an `/about` link pushed nothing. -- Dispatching `sc2:track` with `detail: "contact_submit"` pushed `["event","contact_submit"]`. -- On `localhost` without that rewrite: no `dataLayer`, no tag request, on any page. - -In production, GA4's **DebugView** is the equivalent check; `?gtm_debug=x` on a real page turns it -on for that session. +The snippet is gated on the production hostname, so a local check has to pretend to be that host: +build, rewrite the hostname in the emitted snippet to `localhost`, serve with `astro preview`, and +watch `dataLayer` after `load` — `["consent","default",{…}]`, `["js",Date]`, `["config",…]` in +that order, then `["event",…]` entries as links are clicked. In production, GA4's **DebugView** is +the equivalent check; `?gtm_debug=x` on a real page turns it on for that session. ## Owner tasks -These need account access this repository does not have. Tracked in `plan/todo.md`. +These need account access this repository does not have. - **Cloudflare Web Analytics.** Create the site in the Cloudflare dashboard, copy its beacon - token, and set `PUBLIC_CF_BEACON_TOKEN` in the Pages project's build environment (both - production and preview — the hostname gate is what keeps previews out of GA4, and CF Analytics - is per-site anyway). Until then the beacon is simply absent: the snippet skips it on an empty - token. -- **GA4 property review.** Data retention (14 months is the default; 26 is available and worth - taking), an internal-traffic filter if the workshop has a static IP, and unwanted-referral - exclusions for `paypal.com` and `docs.google.com` so a returning donor is not re-attributed to - a referral. + token, and set `PUBLIC_CF_BEACON_TOKEN` in the Pages project's build environment (production + and preview). Until then the beacon is absent: the snippet skips it on an empty token. +- **GA4 property review.** Data retention (14 months is the default; 26 is available), an + internal-traffic filter if the workshop has a static IP, and unwanted-referral exclusions for + `paypal.com` and `docs.google.com` so a returning donor is not re-attributed to a referral. - **Key events.** Mark all six events above as key events in GA4 → Admin → Events. -- **Search Console.** Verify `scstem.org`, submit `https://scstem.org/sitemap-index.xml`, and link - the property to GA4. -- **Bing Webmaster Tools.** Verify, and import from Search Console rather than re-verifying. -- **Structured data.** Run the five JSON-LD shapes (`NGO`, `WebSite`, `BreadcrumbList`, `Event`, - `FAQPage`) through the Rich Results Test and the schema.org validator once the site is on a - reachable URL. `tools/checks/verify-meta.mjs` covers parse-and-type in CI; it cannot cover - Google's own eligibility rules. ## The JS budget covers first-party script only -`plan/00-overview.md` originally budgeted "total client JS < 35 KB gzipped per page **including -analytics**". `gtag.js` is about 35 KB gzipped on its own, so that wording and D21 could not both -hold. Settled by the project owner during Phase 10: **the budget excludes analytics**, and the -overview now says so. - -What that means in practice. The Lighthouse gate measures the site without GA4 — not by omission -but by design, since the production-hostname check keeps the tag out of every local run, every -preview deploy, and CI. First-party JS is 0.7–3.1 KB per page, inline in the document, against a -35 KB budget. Production adds `gtag.js` after `load`, so it never touches LCP or TBT; it does add -to total transfer, which the 1 MB page-weight assertion has ample room for. - -If that trade ever stops being worth it, Cloudflare Web Analytics' ~5 KB cookieless beacon would -satisfy the original literal reading on its own. +The 35 KB gzipped per-page JS budget (`lighthouserc.json`) excludes analytics: `gtag.js` is about +35 KB on its own. The Lighthouse gate measures the site without GA4 because the hostname check +keeps the tag out of every local run, preview deploy, and CI run. Production adds `gtag.js` after +`load`, so it never touches LCP or TBT; it does add to total transfer, which the 1 MB page-weight +assertion has room for. diff --git a/docs/content.md b/docs/content.md index ab1e844..134d524 100644 --- a/docs/content.md +++ b/docs/content.md @@ -93,25 +93,19 @@ faq: `-05:00` in winter. That offset is what makes the date correct for someone reading in another timezone, and it feeds the event's structured data. -`description` has a length budget: `tools/checks/verify-meta.mjs` fails the build over 160 -characters, because that is roughly where Google stops printing one, and under 50 because a -one-liner tells a searcher nothing. It also has to be **unique across the site** — two pages with -the same description is the same check's other failure. Write it for someone deciding whether to -click, not as a summary of the page. - -**An event retires itself once its `end` has passed.** The first deploy after the event ends is -the one that does it: the page redirects to its parent, and the URL leaves the sitemap and -`/llms.txt` together. Nothing to remember, and nothing to clean up — dating next season's entry -forward brings the page straight back. - -`hidden: true` is still there, for retiring one _early_ or one that has no `end` at all. An entry -with no `end` never retires on its own, for the same reason the countdown never calls it passed: -nothing in the entry says when the event is over, and picking a duration would invent one. That is -why `end` is worth always setting. - -Give every event an `end`. The page counts down to `start`, says "Happening now" from then until -`end`, and "This event has passed" after it — with no `end` it never reaches the last of those, -because nothing in the file says when the event is over. +`description` has a length budget: `tools/checks/verify-meta.ts` fails CI over 160 characters, +because that is roughly where Google stops printing one, and under 50 because a one-liner tells a +searcher nothing. It also has to be **unique across the site**. Write it for someone deciding +whether to click, not as a summary of the page. + +**An event retires itself once its `end` has passed.** On the first deploy after the event ends, +the page redirects to its parent and the URL leaves the sitemap and `/llms.txt`. Dating next +season's entry forward brings the page straight back. + +**Give every event an `end`.** The page counts down to `start`, says "Happening now" from then +until `end`, and "This event has passed" after it. An entry with no `end` never reaches that last +state and never retires on its own: nothing in the file says when the event is over, and picking +a duration would invent one. `hidden: true` is for retiring an event _early_, or one with no `end`. `heroImage` is optional: without one the page uses a photo of the event's program. With one, `heroImageAlt` is required and the build fails without it (`docs/adr/0004`). @@ -125,16 +119,16 @@ Location is **omitted** for anything at the workspace: it defaults to the addres event, which also makes "off-site" visible at a glance. `faq` lists FAQ slugs to show on the page, in the order given. A slug that does not match a file -fails `pnpm check` (`tools/checks/content-references.mjs`) — Astro alone only logs it. +fails `pnpm check` (`tools/checks/content-references.ts`) — Astro alone only logs it. -### Hide an event after it passes +### Hide an event early ```md hidden: true ``` The page stops rendering and sends anyone who lands on it to its parent — `/` for the open house, -`/programs/frc` for kickoff. Flip it back next season and the page returns unchanged. +`/programs/frc` for kickoff. Flip it back and the page returns unchanged. ### Create a new event @@ -200,8 +194,8 @@ different jobs, so they are different fields. ## News posts -The `news` collection is scaffolded but has no routes yet (that is phase-2 work). Copy -`src/content/news/template.md`, and leave the template itself as `draft: true`. +The `news` collection is scaffolded but has no routes yet. Copy `src/content/news/template.md`, +and leave the template itself as `draft: true`. ## Where things live diff --git a/docs/tooling.md b/docs/tooling.md index 2cd7504..4c31b51 100644 --- a/docs/tooling.md +++ b/docs/tooling.md @@ -5,213 +5,105 @@ ```sh mise install # installs the pinned node + pnpm from mise.toml / mise.lock pnpm install -pnpm check # typecheck + lint + format check + knip +pnpm check # typecheck + lint + format check + knip + repo checks pnpm dev ``` -`mise.toml` pins node and pnpm exactly and commits checksums to `mise.lock`, so every clone and -CI run gets byte-identical tools. `mise.toml`'s `[tasks]` are thin forwarders — **`package.json` -scripts are the single source of truth**; never put a real command in a mise task. +`mise.toml` pins node and pnpm exactly and commits checksums to `mise.lock`, so every clone, CI +run, and Cloudflare Pages build gets the same tools. `package.json` scripts are the single +source of truth for commands. -### Node version - -Pinned to the node 26 line, matching what the legacy site already ran (`legacy/.nvmrc` was -v26.5.0) and what Cloudflare Pages already serves. Node 26 is the Current line today; move the -pin to 26.x LTS once that line is promoted. - -### The corepack footgun - -Corepack's `pnpm` shim can shadow the mise-pinned pnpm and silently rewrite the lockfile with a -different resolver. `which pnpm` must resolve inside the mise shims directory. If it does not: - -```sh -corepack disable pnpm -``` +Corepack's `pnpm` shim can shadow the mise-pinned pnpm and rewrite the lockfile with a different +resolver. `which pnpm` must resolve inside mise's shims directory; if not, `corepack disable pnpm`. ### Supply-chain cooldown -Two independent cooldowns, both intentional: - -- `mise.toml` → `minimum_release_age = "7d"` for node and pnpm themselves. +- `mise.toml` → `minimum_release_age = "7d"` for node and pnpm. - `pnpm-workspace.yaml` → `minimumReleaseAge: 10080` (minutes) for every npm dependency. -Consequence: **pins are the newest version that is at least a week old**, not the newest version. -`pnpm install` refuses fresher ones by design. To take a newer version early, add a -`minimumReleaseAgeExclude` entry and comment why in `pnpm-workspace.yaml`. - -Install scripts are denied by default; `allowBuilds` lists the exceptions (`sharp`, `esbuild`). - -`sharp` is a direct dependency rather than one inherited from Astro: pnpm's isolated layout keeps -Astro's copy where the bundled image service cannot resolve it, so `astro:assets` falls back to -unoptimized passthrough with one warning per image. See `docs/adr/0003-sharp-direct-dependency.md`. - -## Toolchain ownership - -| Extensions | Linter | Formatter | -| ------------------------------------- | -------------------------------------------------------- | --------- | -| `.ts .js .mjs .cjs .json .jsonc .css` | oxlint — type-aware, vendored nkzw config + anti-slop | oxfmt | -| `.astro` | ESLint — typed (`strictTypeChecked`) + `jsx-a11y-strict` | Prettier | -| `.md` | — | Prettier | - -Rationale and the migration seam for collapsing onto oxc: `docs/adr/0001-toolchain-split.md`. - -### Invocation quirks worth knowing - -- **oxfmt reads `.prettierignore` by default.** Since that file exists for Prettier, every oxfmt - invocation passes `--ignore-path .gitignore` — otherwise oxfmt would skip every file it owns. -- **Prettier is called with an explicit `**/*.{astro,md}` glob.** A blanket `*` in - `.prettierignore` prunes whole directories, and gitignore semantics cannot un-prune individual - files inside them, so the glob does the scoping and `.prettierignore` only holds real exclusions. -- **ESLint's global ignores cannot use `ignores: ["**/*", "!**/*.astro"]`** for the same reason. - Scoping comes from every config block being `files: ["**/*.astro"]`. -- **oxlint ignores `**/*.astro`** so the two linters never both own a file. -- **`eslint-plugin-jsx-a11y` is a direct devDependency.** It is an _optional_ peer of - `eslint-plugin-astro`, so without it `astroConfigs["jsx-a11y-strict"]` degrades to `{ rules: {} }` - silently — no install warning, no accessibility linting. Its published peer range stops at - eslint `^9`; `pnpm-workspace.yaml` allows eslint 10 there, and the rules do fire (check with - `eslint --print-config` on any `.astro` file — expect ~33 `jsx-a11y/*` entries, not zero). -- **The `clsx`/`tailwind-merge` and `legacy/*` import bans are declared twice** — once in - `.oxlintrc.json` and again in `eslint.config.ts`. oxlint ignores `.astro`, so the ESLint copy is - what enforces them in the file type the site is built from. Both copies must change together. -- **`functions/` has its own tsconfig** and is typechecked separately (`tsgo -p functions`, part of - `pnpm typecheck`). It uses `moduleResolution: "bundler"` because Cloudflare bundles Functions - with esbuild, which resolves the extensionless `@/*` path aliases the source already uses. It - restates the root tsconfig's strictness flags rather than extending - `astro/tsconfigs/strictest` (which pulls in DOM and Astro types the Workers runtime lacks) — - this is the only code handling untrusted request input, so it must not typecheck more loosely - than `src/`. - -### TypeScript: two compilers, on purpose - -| What | Compiler | -| ------------------------- | ----------------------------------------------------------------- | -| `src/**` and `.astro` | `astro check` → JavaScript TypeScript (`typescript`) | -| `functions/**` | `tsgo` → TypeScript 7, the Go port (`@typescript/native-preview`) | -| oxlint's type-aware rules | `tsgolint` → also Go (`oxlint-tsgolint`) | - -`functions/` runs on TypeScript 7. Measured on this tree: `tsc -p functions` 961 ms, `tsgo -p functions` -182 ms. +Pins are the newest version that is at least a week old. To take a newer one early, add a +`minimumReleaseAgeExclude` entry in `pnpm-workspace.yaml` and say why. Install scripts are denied +by default; `allowBuilds` lists the exceptions. -**`astro check` cannot move yet, and that is why `typescript` is still a dependency.** -`@astrojs/check` declares `peerDependencies: { "typescript": "^5.0.0 || ^6.0.0" }` and its language -server is built against the JavaScript compiler's Language Service API. -`@typescript/native-preview` exports only `version` and `versionMajorMinor` — there is no -`typescript.js` or `tsserver.js` in it — so nothing can substitute it there. +`sharp` is a direct dependency, not one inherited from Astro (`docs/adr/0003`). -This is not an Astro backlog item; it is upstream. From Astro's own tracking discussion -([withastro/roadmap#1321](https://github.com/withastro/roadmap/discussions/1321)), maintainer -delucis: +## Toolchain -> TypeScript 7 does not yet expose a stable programmatic API, and so tools (such as Volar) which -> embed TypeScript into their own compilers and language services can only currently rely on -> TypeScript 6.0. +| Concern | Tool | +| ----------- | ----------------------------------------------------------------------------- | +| Lint | ESLint: `strictTypeChecked` + `stylisticTypeChecked`, `astro/jsx-a11y-strict` | +| Format | Prettier, with the Astro and Tailwind plugins | +| Types | `astro check` for `src/` and config files; `tsc -p functions`, `tsc -p tools` | +| Dead code | knip | +| Repo checks | `tools/checks/*.ts` | -Astro's language server is Volar-based, so the same blocker hits Vue and Svelte. The TypeScript -team is building a replacement for the deprecated JS ("Strada") API and reportedly targets it for -**7.1**; no date is committed. **Watch that discussion** — when the API lands and `@astrojs/check` -widens its peer range, `astro check` can move and `typescript` can be dropped. +One toolchain by decision (`docs/adr/0012-single-toolchain.md`); oxlint/oxfmt return when they +read `.astro`. -`js/ts.experimental.useTsgo` in `.vscode/settings.json` routes plain `.ts` files to tsgo. That is -independent of the Astro extension, which keeps using its own TS 6 language server for `.astro`. -The tracking discussion does carry reports of import/export resolution oddities in editors with -tsgo enabled — if `.astro` intellisense misbehaves, that setting is the first thing to turn off. +- `eslint-plugin-jsx-a11y` is a direct devDependency because it is an optional peer of + `eslint-plugin-astro`: without it `jsx-a11y-strict` silently degrades to no rules. Its peer + range stops at eslint 9, so `pnpm-workspace.yaml` allows eslint 10 there. +- `jiti` is what ESLint uses to load `eslint.config.ts`. +- `functions/` and `tools/` have their own `tsconfig.json`: the Workers runtime and Node scripts + cannot use `astro/tsconfigs/strictest` (it pulls in DOM and Astro types), so each restates the + same strictness flags. +- The `legacy/**` and `clsx`/`classnames`/`tailwind-merge` import bans live in + `eslint.config.ts`'s `no-restricted-imports`. -`@typescript/native-preview` is a `7.0.0-dev.*` build; it is pinned exactly, like every other tool -here, and `pnpm-workspace.yaml`'s `minimumReleaseAge` still applies. +### `tools/` -In the editor, `js/ts.experimental.useTsgo` routes `.ts` files to tsgo while `js/ts.tsdk.path` keeps -the JavaScript compiler available for the Astro language server — the same split as the table above. +TypeScript scripts run directly by Node (type stripping, no build step), each behind a +`package.json` script: -### Lint rule sources +| Script | Does | Runs in | +| ----------------- | ----------------------------------------------------------- | --------------- | +| `check:tokens` | `cn`'s font-size group matches the `--text-*` tokens | `pnpm check` | +| `check:content` | Every content-collection `reference()` resolves | `pnpm check` | +| `check:meta` | Every built page's head: unique title/description, og:image | CI, after build | +| `assets:optimize` | Downscale and re-encode camera masters in `src/assets/` | by hand | +| `assets:fonts` | Subset Inter to the weights and scripts the site uses | by hand | +| `assets:og` | Render the OG cards in `src/assets/og/` | by hand | +| `assets:og-fonts` | Register the OG card fonts with fontconfig (one-time) | by hand | -oxlint's rule set comes from three places, layered: +Only erasable TypeScript syntax (no enums, namespaces, or parameter properties); +`tools/tsconfig.json` enforces it. -1. **`tools/lint/nkzw/oxlintrc.json`** — a vendored copy of - [`@nkzw/oxlint-config`](https://github.com/nkzw-tech/oxlint-config) (MIT), extended by path from - `.oxlintrc.json`. 146 general-purpose rules: unicorn, typescript-eslint, oxc, import-x, - perfectionist, and the core set. See its `VENDOR.md` for what was dropped (React, Relay and - test-runner rules that do not apply here) and how to update it. -2. **`tools/lint/anti-slop/`** — the vendored plugin below. -3. **`.oxlintrc.json`'s own `rules`** — the `legacy/**` import ban and the anti-slop rule list. +## Agent hook -**Every rule is an error.** `perf` was a warning and is now an error; `style` is off. A warning -nobody has to fix is a rule nobody obeys, so the choice is error or off — which is also the reason -for adopting the nkzw set, whose severities are all `error` upstream. - -**`perfectionist/sort-objects` is off**, and it is the one rule from upstream that is disabled on -its merits rather than for inapplicability. Its autofix reorders an object's keys but leaves leading -comments where they were, so on any object whose keys carry doc comments it silently produces false -documentation. Reproduced on `src/data/site.ts` in a single `--fix` pass: `shortName`'s comment -ended up labelling `description`, `titleTemplate`'s ended up on `email`, and the multi-line comment -for `calendars` ended up on `analytics`. AGENTS.md requires a comment to describe what is there, and -a rule that rewrites them to describe something else cannot be a `--fix` away from green. - -The sibling rules do not have this flaw — `sort-interfaces` and `sort-object-types` were checked -against a commented interface and a commented type literal and carried each comment with its member -— so they stay on, as do `sort-imports`, `sort-enums`, `sort-heritage-clauses` and `sort-jsx-props`. - -Two deliberate exceptions live in `.oxlintrc.json`'s `overrides`: - -- `no-console` is off under `functions/**` and `tools/**`. A Cloudflare Worker's console is its - log stream — `wrangler tail` and the dashboard read nothing else — and a CLI check script's - console is how it reports to the developer running it. Neither is the shipped-debug-logging case - the rule guards. Everywhere else it stays an error. -- Upstream's own `.ts` override is kept, which turns off the correctness rules TypeScript already - covers (`no-undef`, `no-redeclare`, …). That is the config's speed principle, not a gap. - -**Import bans use `**`, not `*`.** A single star matches one path segment, so `legacy/*` allowed -`legacy/data/config` and every deep relative path. Both linters use `["legacy/**", "**/legacy/**"]` -and both are verified against a deep relative import. - -### Vendored lint rules - -`tools/lint/anti-slop/` is a copy of [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) -(MIT), registered as an oxlint `jsPlugin`. See its `VENDOR.md` for the upstream commit and update -procedure. It is excluded from this repo's own lint, format, and typecheck — it is upstream source. - -## Agent hooks - -`.claude/hooks/format-lint.sh` runs on every `Edit`/`Write` from Claude Code: it routes the edited -file to the owning formatter, then the owning linter, and exits 2 with the findings on a lint -failure so they go back to the agent. Files in `legacy/`, `dist/`, `.astro/`, `plan/`, and the -vendored rule directory are skipped. +`.claude/hooks/format-lint.sh` runs on every `Edit`/`Write` from Claude Code: Prettier on the +file, then ESLint if it is a file ESLint reads, exiting 2 with the findings so they go back to +the agent. It exits 0 when `node_modules` is missing. ## Environment variables | Variable | Where | Purpose | | --------------------------- | --------------------- | ------------------------------------- | | `PUBLIC_TURNSTILE_SITE_KEY` | build (public) | Turnstile widget on the contact form | -| `PUBLIC_CF_BEACON_TOKEN` | build (public) | Cloudflare Web Analytics beacon (D21) | +| `PUBLIC_CF_BEACON_TOKEN` | build (public) | Cloudflare Web Analytics beacon | | `TS_SECRET_KEY` | Pages Function secret | Turnstile server-side verification | | `SLACK_FORM_POST_GENERIC` | Pages Function secret | Slack webhook for contact submissions | -`PUBLIC_CF_BEACON_TOKEN` defaults to empty, and an empty token means the beacon is simply not -injected — so a fresh clone and every preview run with GA4 alone. Setting it is an owner task in -`docs/analytics.md`. - -`PUBLIC_TURNSTILE_SITE_KEY` is declared in `astro.config.ts`'s `env.schema`, so pages import it -from `astro:env/client` rather than reaching into an untyped `import.meta.env`. It **defaults to -Cloudflare's documented always-passes test key** (`1x00000000000000000000AA`), which is why a -fresh clone and every preview deploy have a working form with no setup — and why the production -value has to be set deliberately, in the Cloudflare Pages dashboard, alongside the `TS_SECRET_KEY` -its server half checks against. A site key is public by design: it ships in the page's HTML. - -The secret half has a matching always-passes test value baked into `functions/api/form/submit.ts` -for the same reason, so the whole round trip works locally without credentials. +The public ones are declared in `astro.config.ts`'s `env.schema` and imported from +`astro:env/client`. `PUBLIC_TURNSTILE_SITE_KEY` defaults to Cloudflare's always-passes test key +and `PUBLIC_CF_BEACON_TOKEN` to empty (no beacon), so a fresh clone and every preview deploy work +with no setup; production sets both in the Pages dashboard. `functions/api/form/submit.ts` carries +the matching test secret so the form round-trips locally. ## CI -`.github/workflows/ci.yml` runs on every pull request: mise install → `pnpm install ---frozen-lockfile` → `pnpm check` → `pnpm build` → offline link check over `dist/`. All steps -block. Deploys are **not** driven by Actions — Cloudflare Pages' dashboard git integration owns -them (D14). +Cloudflare Pages builds and deploys from git. `.github/workflows/ci.yml` runs on pull requests +and pushes to `main` and `staging`, in three jobs: + +- **Check**: typecheck, lint, format, knip, and the repo checks as separate steps, all of which + run even when an earlier one fails. ESLint findings become inline annotations on the PR. +- **Build**: `pnpm build`, `check:meta`, and an offline link check over `dist/` (lychee). +- **Lighthouse**: `@lhci/cli` via `pnpm dlx` against `astro preview` serving the build artifact + (`docs/adr/0007`), three runs per URL over six page shapes. Median scores per URL and every + failed assertion go to the job summary; full reports upload as an artifact. ### Performance budgets -`.github/workflows/lighthouse.yml` is the second required check. It builds, serves `dist/` with -`astro preview`, and runs `pnpm dlx @lhci/cli@0.15.1 autorun` three times against six URLs — one -of each page shape: `/`, `/programs/frc/`, `/programs/frc/robots/`, `/sponsors/`, `/openhouse/`, -`/contact/`. Every assertion in `lighthouserc.json` is an error, so a regression blocks the merge. +`lighthouserc.json`, every assertion an error on the median of three runs: | Assertion | Budget | | ------------------------ | --------- | @@ -225,39 +117,13 @@ of each page shape: `/`, `/programs/frc/`, `/programs/frc/robots/`, `/sponsors/` | Script transfer size | < 35 KB | | Total page transfer size | < 1 MB | -Assertions aggregate on the **median** of the three runs. Lantern's LCP for this site carries one -slow run per page — a ~500 ms step in simulated FCP that appears run to run even on an idle -machine — and median-of-three absorbs exactly that. Every run on every budgeted URL is currently -under the 2000 ms LCP budget, the worst being 1953 ms; the tightest medians are `/` and -`/openhouse/` at 1807 ms. Getting there was two font changes, not the image work — see -`plan/09-assets-performance.md` for the before/after and `docs/adr/0008` and `0011` for the -reasoning. - -Mobile emulation with simulated throttling (1.6 Mbps, 150 ms RTT) — the default preset, and the -reason transfer size dominates every metric here. `astro preview` gzips, which is what makes the -measurement comparable to what Cloudflare serves; a server that did not would fail budgets -production meets. - -To run it locally, with a Chrome that Lighthouse can find: +Mobile emulation with simulated throttling, so transfer size dominates. `astro preview` gzips, +which keeps the measurement comparable to Cloudflare. Locally: ```sh pnpm build pnpm dlx @lhci/cli@0.15.1 autorun -pnpm exec astro preview stop # LHCI kills its wrapper; the preview server outlives it +pnpm exec astro preview stop # the preview server outlives lhci ``` -Reports land in `.lighthouseci/reports/` (gitignored) as HTML and JSON, and CI uploads them as an -artifact on every run. Why `pnpm dlx` rather than a devDependency or a marketplace action: -`docs/adr/0007-lighthouse-ci-gate.md`. - -The hero video on `/programs/frc/` is `preload="none"` and only arms after the `load` event, so it -does not count against that page's transfer budget during a run — see -`docs/adr/0006-hero-video-encode.md`. - -### Known environment limitation - -The sandboxed environment this repo is sometimes developed in cannot reach -`tuf-repo-cdn.sigstore.dev`, so mise's GitHub artifact-attestation check for the pnpm download -fails there. The committed configuration keeps verification **on**; only that environment relaxes -it via `MISE_AQUA_GITHUB_ATTESTATIONS=false`, and the tool checksums in `mise.lock` still apply. -Real developers and CI verify normally. +Loosening a budget is an ADR, not an edit to `lighthouserc.json`. diff --git a/eslint.config.ts b/eslint.config.ts index 466ff44..720432d 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,77 +1,62 @@ -import { parseForESLint } from "astro-eslint-parser"; -import { configs as astroConfigs } from "eslint-plugin-astro"; +import astro from "eslint-plugin-astro"; import perfectionist from "eslint-plugin-perfectionist"; import { defineConfig, globalIgnores } from "eslint/config"; import tseslint from "typescript-eslint"; export default defineConfig( - // Every block below is scoped to `**/*.astro`, so ESLint never reaches the - // extensions oxlint owns. A blanket `ignores: ["**/*"]` cannot be used here: - // it prunes directories, and unignoring files inside them does not bring them back. - globalIgnores(["legacy/**", "dist/**", ".astro/**", "node_modules/**", "public/**"]), - astroConfigs.recommended, - astroConfigs["jsx-a11y-strict"], + globalIgnores(["legacy/", "dist/", ".astro/", "public/"]), { - extends: [tseslint.configs.strictTypeChecked], - files: ["**/*.astro"], + files: ["**/*.{ts,mts,js,mjs,astro}"], + extends: [tseslint.configs.strictTypeChecked, tseslint.configs.stylisticTypeChecked], languageOptions: { - // strictTypeChecked would install the TS parser directly, which cannot read - // `.astro`. The Astro parser stays outermost and delegates frontmatter to it. - parser: { parseForESLint }, - parserOptions: { - extraFileExtensions: [".astro"], - parser: tseslint.parser, - // astro-eslint-parser does not implement projectService; it maps to `project`. - project: true, - tsconfigRootDir: import.meta.dirname, - }, + parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname }, }, plugins: { perfectionist }, rules: { - // Mirrors oxfmt's sortImports so the whole repo is sorted the same way. - "perfectionist/sort-imports": "error", - // Restated from .oxlintrc.json, which ignores `**/*.astro`. ESLint is the only linter - // that reads this extension, so without this the ban is off in the file type the site - // is built from. + "no-console": "error", "no-restricted-imports": [ "error", { + paths: [ + { name: "classnames", message: "Use `cn` from `@/lib/cn` (AGENTS.md)." }, + { name: "clsx", message: "Use `cn` from `@/lib/cn` (AGENTS.md)." }, + { name: "tailwind-merge", message: "Use `cn` from `@/lib/cn` (AGENTS.md)." }, + ], + // `**`, not `*`: a single star matches one path segment and lets deep imports through. patterns: [ - { - // `**`, not `*`: a single star matches one path segment, so `legacy/*` let - // `legacy/data/config` and every deep relative path through. - group: ["legacy/**", "**/legacy/**"], - message: "legacy/ is reference only — never import from it (plan/00-overview.md)", - }, + { group: ["legacy/**", "**/legacy/**"], message: "legacy/ is reference only." }, ], }, ], - - /** - * astro-eslint-parser does not type the JSX-like expressions in an Astro *template*, so - * every `items.map(() => )` resolves as `error` and trips this rule. It is a gap in - * the parser, not unsafety in the code: frontmatter — the part that holds real logic — is - * fully typed, and `astro check` type-checks templates properly. - * - * Sibling rules in this family (no-unsafe-assignment/-call/-member-access) may need the - * same treatment as templates grow; add them here with the same reasoning, never blanket - * off the whole family. See docs/adr/0001-toolchain-split.md. - */ + "perfectionist/sort-imports": "error", + }, + }, + { + // A Worker's console is its log stream; a CLI script's console is how it reports. + files: ["functions/**", "tools/**"], + rules: { "no-console": "off" }, + }, + { + files: ["**/*.astro"], + extends: [astro.configs.recommended, astro.configs["jsx-a11y-strict"]], + languageOptions: { + parserOptions: { + extraFileExtensions: [".astro"], + parser: tseslint.parser, + // astro-eslint-parser does not implement projectService. + project: true, + projectService: false, + }, + }, + rules: { + // The parser does not type expressions in the template, so every `items.map(() => )` + // resolves as `error`. Frontmatter is fully typed and `astro check` covers the template. "@typescript-eslint/no-unsafe-return": "off", - /** - * A `return` in Astro frontmatter — how a page short-circuits into a redirect or a 404 — - * has no enclosing function node in the parser's AST, and this rule asserts one exists: - * `Non-null Assertion Failed: Expected node to have a parent`, a crash rather than a - * finding. It cannot inspect the construct it exists to check, so it is off for `.astro`. - */ + // A `return` in frontmatter (redirect, 404) has no enclosing function in the parser's AST, + // which crashes this rule. "@typescript-eslint/no-misused-promises": "off", - /** - * A keyboard-reachable scroll container is a real pattern: an `overflow` region is not - * focusable by default, so without `tabindex="0"` its content is unreachable by keyboard - * (WCAG 2.2 SC 2.1.1). `role="region"` with an accessible name is how that container is - * named; the rule only allows `tabpanel` out of the box. Scoped to that one role — every - * other non-interactive element keeps the error. - */ + // A keyboard-reachable scroll container needs `tabindex="0"` (WCAG 2.2 SC 2.1.1) and is + // named with `role="region"`; the rule only allows `tabpanel` by default. "astro/jsx-a11y/no-noninteractive-tabindex": [ "error", { roles: ["tabpanel", "region"], tags: [] }, diff --git a/functions/api/calendar/[name].ts b/functions/api/calendar/[name].ts index 5d36b3b..bf005a1 100644 --- a/functions/api/calendar/[name].ts +++ b/functions/api/calendar/[name].ts @@ -1,12 +1,10 @@ -import { upcomingEvents } from "@/ics"; import type { CalendarEvent } from "@/types"; +import { upcomingEvents } from "@/ics"; + /** - * A branded agenda needs the calendar's events as data, not as a Google iframe (D19) — so this - * fetches the public ICS feed server-side and hands the page JSON. - * - * Doing it here rather than in the browser is what makes the feature possible at all: the feed - * sends no CORS headers, so a page cannot read it directly. + * Fetches the public ICS feed server-side and hands the page JSON. The feed sends no CORS + * headers, so a page cannot read it directly. */ /** @@ -33,7 +31,7 @@ const MAX_AGE = 900; /** What `/api/calendar/` answers with, either way. */ interface CalendarResponse { - events?: Array; + events?: CalendarEvent[]; message?: string; } diff --git a/functions/api/form/submit.ts b/functions/api/form/submit.ts index ece7d4d..7c14444 100644 --- a/functions/api/form/submit.ts +++ b/functions/api/form/submit.ts @@ -1,4 +1,5 @@ import type { GenericFormRequest } from "@/types"; + import { res, validateTurnstile } from "@/util"; export const onRequestPost: PagesFunction<{ diff --git a/functions/api/test.ts b/functions/api/test.ts deleted file mode 100644 index 8122a29..0000000 --- a/functions/api/test.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const onRequestGet: PagesFunction = async () => { - return new Response("Testing!"); -}; diff --git a/functions/ics.ts b/functions/ics.ts index 83df11f..c5fb595 100644 --- a/functions/ics.ts +++ b/functions/ics.ts @@ -1,7 +1,7 @@ import type { CalendarEvent } from "@/types"; /** - * Just enough iCalendar to render an agenda from a public Google Calendar feed (D19). Not a + * Just enough iCalendar to render an agenda from a public Google Calendar feed. Not a * general RFC 5545 implementation: it reads `VEVENT`s, resolves their times, expands the * recurrence rules Google actually emits for a team calendar, and stops there. * @@ -17,14 +17,11 @@ const MAX_OCCURRENCES = 5000; * Content lines may be folded across several physical lines, continued by a leading space or * tab (RFC 5545 §3.1). Unfolding has to happen before anything else is read. */ -const unfold = (feed: string): Array => { - const lines: Array = []; +const unfold = (feed: string): string[] => { + const lines: string[] = []; for (const raw of feed.replaceAll("\r\n", "\n").split("\n")) { - if ((raw.startsWith(" ") || raw.startsWith("\t")) && lines.length > 0) { - lines[lines.length - 1] += raw.slice(1); - } else { - lines.push(raw); - } + const previous = raw.startsWith(" ") || raw.startsWith("\t") ? lines.pop() : undefined; + lines.push(previous === undefined ? raw : previous + raw.slice(1)); } return lines; }; @@ -139,7 +136,7 @@ const parseMoment = (line: Line): Moment | undefined => { ); const allDay = hour === undefined; - const zone = line.params["TZID"]; + const zone = line.params.TZID; if (allDay || utc === "Z" || zone === undefined) { // A date, an explicit UTC stamp, and a floating time all read as written. return { allDay, at: wall, wall, zone: undefined }; @@ -168,15 +165,15 @@ const resolver = }; /** Weekday codes in `BYDAY`, indexed to match `Date#getUTCDay`. */ -const DAYS: ReadonlyArray = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"]; +const DAYS: readonly string[] = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"]; /** The weekday a `BYDAY` code names, or -1 — no assertion, so an unknown code stays a miss. */ const dayIndex = (code: string): number => DAYS.indexOf(code); interface Rule { /** `MO`, `TU`, … for weekly; `2TU`, `-1FR`, … for monthly. */ - byDay: Array; - byMonthDay: Array; + byDay: string[]; + byMonthDay: number[]; count?: number | undefined; freq: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY"; interval: number; @@ -225,8 +222,8 @@ const expand = ( start: number, windowEnd: number, toInstant: (wall: number) => number, -): Array => { - const occurrences: Array = []; +): number[] => { + const occurrences: number[] = []; const first = new Date(start); const time = { hour: first.getUTCHours(), @@ -251,7 +248,7 @@ const expand = ( */ for (let period = 0; period < MAX_OCCURRENCES; period += 1) { const cursor = new Date(start); - let dates: Array; + let dates: number[]; if (rule.freq === "DAILY") { cursor.setUTCDate(cursor.getUTCDate() + period * rule.interval); @@ -310,7 +307,7 @@ const withTime = (date: number, time: TimeOfDay): number => { }; /** The dates a monthly or yearly rule selects inside the month `cursor` starts. */ -const monthDates = (cursor: Date, rule: Rule, first: Date): Array => { +const monthDates = (cursor: Date, rule: Rule, first: Date): number[] => { const year = cursor.getUTCFullYear(); const month = cursor.getUTCMonth(); const length = new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); @@ -331,7 +328,7 @@ const monthDates = (cursor: Date, rule: Rule, first: Date): Array => { return []; } - const matches: Array = []; + const matches: number[] = []; for (let day = 1; day <= length; day += 1) { const at = Date.UTC(year, month, day); if (new Date(at).getUTCDay() === weekday) { @@ -356,7 +353,7 @@ const monthDates = (cursor: Date, rule: Rule, first: Date): Array => { interface RawEvent { description: string; end?: Moment | undefined; - excluded: Array; + excluded: number[]; location: string; /** Set on an event that overrides one occurrence of its series. */ recurrenceId?: number | undefined; @@ -367,8 +364,8 @@ interface RawEvent { uid: string; } -const parseEvents = (feed: string): Array => { - const events: Array = []; +const parseEvents = (feed: string): RawEvent[] => { + const events: RawEvent[] = []; let current: RawEvent | undefined; for (const raw of unfold(feed)) { @@ -454,7 +451,7 @@ const parseEvents = (feed: string): Array => { * @param from Start of the window, epoch milliseconds. * @param days How far ahead to look. */ -export const upcomingEvents = (feed: string, from: number, days: number): Array => { +export const upcomingEvents = (feed: string, from: number, days: number): CalendarEvent[] => { const until = from + days * 24 * 60 * 60 * 1000; const parsed = parseEvents(feed).filter((event) => event.status !== "CANCELLED"); @@ -469,7 +466,7 @@ export const upcomingEvents = (feed: string, from: number, days: number): Array< ), ); - const results: Array = []; + const results: CalendarEvent[] = []; for (const event of parsed) { const { start } = event; diff --git a/functions/tsconfig.json b/functions/tsconfig.json index d5690da..e4cffab 100644 --- a/functions/tsconfig.json +++ b/functions/tsconfig.json @@ -9,9 +9,8 @@ "@/*": ["./*"] }, "noEmit": true, - // Mirrors the root tsconfig's additions over astro/tsconfigs/strictest. The - // Workers runtime cannot use that base (it pulls in DOM and Astro types), so - // the flags are restated rather than extended. + // The Workers runtime cannot use astro/tsconfigs/strictest (DOM and Astro types), so the + // same strictness is restated. "strict": true, "exactOptionalPropertyTypes": true, "noFallthroughCasesInSwitch": true, diff --git a/functions/types.ts b/functions/types.ts index 92a07d7..61a88f6 100644 --- a/functions/types.ts +++ b/functions/types.ts @@ -21,7 +21,7 @@ export interface TurnstileVerificationResponse { export interface TurnstileResponse { challenge_ts: string; - "error-codes": Array; + "error-codes": string[]; hostname: string; success: boolean; } diff --git a/knip.jsonc b/knip.jsonc index 33cefe3..0f68ddd 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -1,32 +1,15 @@ { "$schema": "./node_modules/knip/schema.json", - "entry": ["src/pages/**", "functions/**"], - "project": ["**/*.{ts,tsx,js,mjs,cjs,astro}"], - "ignore": ["legacy/**", "tools/lint/anti-slop/**"], - - // fontconfig's cache tool, called by the one-time OG font setup. Present on any machine - // with fontconfig, which is the only kind that can render the cards at all. + // Cloudflare Pages Functions are file-routed, which knip has no plugin for. + "entry": ["functions/**/*.ts"], + "ignore": ["legacy/**"], + // fontconfig's cache tool, run by the OG font setup on machines that can render the cards. "ignoreBinaries": ["fc-cache"], - - // Referenced only from CSS + // Referenced only from CSS. "ignoreDependencies": [ - "tailwindcss", "@fontsource-variable/inter", "@fontsource-variable/orbitron", "@fontsource-variable/source-code-pro", - "@fontsource/architects-daughter" + "@fontsource/architects-daughter", ], - - "rules": { - "files": "error", - "dependencies": "error", - "devDependencies": "error", - "optionalPeerDependencies": "error", - "unlisted": "error", - "binaries": "error", - "unresolved": "error", - "exports": "error", - "types": "error", - "duplicates": "error" - } } diff --git a/mise.toml b/mise.toml index 47fdf66..d762b2a 100644 --- a/mise.toml +++ b/mise.toml @@ -14,8 +14,9 @@ pnpm = "11.24.0" _.path = ["{{config_root}}/node_modules/.bin"] ASTRO_TELEMETRY_DISABLED = "1" +# Chrome for the chrome-devtools MCP server (.mcp.json). [tasks."install:chrome"] -description = "Install Chrome for Testing" +description = "Install Chrome for Testing into .browser/" depends = ["clean:chrome"] run = """ bin=$(browsers install chrome@stable --path="$MISE_PROJECT_ROOT/.browser" | tail -n1 | awk '{print $2}') @@ -27,47 +28,3 @@ echo "Chrome linked: .browser/chrome/chrome -> $bin" [tasks."clean:chrome"] description = "Remove the installed Chrome for Testing" run = "rm -rf .browser/chrome" - -[tasks.install] -run = "pnpm install" -description = "Install dependencies" - -[tasks.dev] -run = "pnpm run dev" -description = "Start the Astro dev server" - -[tasks.build] -run = "pnpm run build" -description = "Build the static site to dist/" - -[tasks.preview] -run = "pnpm run preview" -description = "Serve the built site locally" - -[tasks.check] -run = "pnpm run check" -description = "Typecheck + lint + format check + knip" - -[tasks.typecheck] -run = "pnpm run typecheck" -description = "astro check" - -[tasks.lint] -run = "pnpm run lint" -description = "oxlint + eslint" - -[tasks."lint:fix"] -run = "pnpm run lint:fix" -description = "oxlint + eslint with autofix" - -[tasks.fmt] -run = "pnpm run fmt" -description = "oxfmt + prettier, writing changes" - -[tasks."fmt:check"] -run = "pnpm run fmt:check" -description = "oxfmt + prettier in check mode" - -[tasks.knip] -run = "pnpm run knip" -description = "Find unused files, exports, and dependencies" diff --git a/package.json b/package.json index 2e44aad..85ab791 100644 --- a/package.json +++ b/package.json @@ -7,20 +7,20 @@ "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "typecheck": "astro check && tsgo -p functions", - "lint": "oxlint --type-aware && eslint --max-warnings 0 .", - "lint:fix": "oxlint --type-aware --fix && eslint --max-warnings 0 --fix .", - "fmt": "oxfmt --ignore-path .gitignore && prettier --write \"**/*.{astro,md}\"", - "fmt:check": "oxfmt --check --ignore-path .gitignore && prettier --check \"**/*.{astro,md}\"", - "knip": "knip", "check": "pnpm run typecheck && pnpm run lint && pnpm run fmt:check && pnpm run knip && pnpm run check:tokens && pnpm run check:content", - "check:tokens": "node tools/checks/cn-font-size-group.mjs", - "check:content": "node tools/checks/content-references.mjs", - "check:meta": "node tools/checks/verify-meta.mjs", - "assets:optimize": "node tools/assets/optimize-sources.mjs", - "assets:fonts": "node tools/assets/font-subset.mjs", - "assets:og": "node tools/assets/og-cards.mjs", - "assets:og-fonts": "node tools/assets/og-fonts.mjs" + "typecheck": "astro check && tsc -p functions && tsc -p tools", + "lint": "eslint --max-warnings 0", + "lint:fix": "eslint --max-warnings 0 --fix", + "fmt": "prettier --write .", + "fmt:check": "prettier --check .", + "knip": "knip", + "check:tokens": "node tools/checks/cn-font-size-group.ts", + "check:content": "node tools/checks/content-references.ts", + "check:meta": "node tools/checks/verify-meta.ts", + "assets:optimize": "node tools/assets/optimize-sources.ts", + "assets:fonts": "node tools/assets/font-subset.ts", + "assets:og": "node tools/assets/og-cards.ts", + "assets:og-fonts": "node tools/assets/og-fonts.ts" }, "dependencies": { "@astrojs/sitemap": "3.7.3", @@ -38,20 +38,14 @@ "devDependencies": { "@astrojs/check": "0.9.10", "@cloudflare/workers-types": "5.20260825.1", - "@oxlint/plugins": "1.80.0", "@tabler/icons": "3.46.0", "@types/node": "26.3.0", - "@typescript/native-preview": "7.0.0-dev.20260707.2", - "astro-eslint-parser": "3.1.0", "eslint": "10.9.1", "eslint-plugin-astro": "3.1.0", "eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-perfectionist": "5.10.1", "jiti": "2.7.0", "knip": "6.32.2", - "oxfmt": "0.65.0", - "oxlint": "1.80.0", - "oxlint-tsgolint": "7.0.2001", "prettier": "3.9.6", "prettier-plugin-astro": "0.14.1", "prettier-plugin-tailwindcss": "0.8.1", diff --git a/plan/00-overview.md b/plan/00-overview.md index 4e20a2e..e1a6263 100644 --- a/plan/00-overview.md +++ b/plan/00-overview.md @@ -9,13 +9,13 @@ This directory is the complete implementation plan for rewriting scstem.org from - **Never merge layers individually or resequence merges by hand** — that's the point of the stack: merging a PR cascades every unmerged PR below it, bottom-up, and GitHub auto-rebases what remains. The whole stack lands into `staging` in one cascade at Phase 11. - **Review fixes on a lower layer**: commit on that layer's branch, then restack the branches above (`git rebase --update-refs` from the top of the stack, or the gh-stack tooling) and `gs push`/force-push-with-lease the rebased layers. Never let layers drift. - `staging` and `main` are untouched until Phase 11 (cutover). Cloudflare Pages gives every stack branch its own preview URL, kept noindexed by the existing `public/_headers` rules — **the topmost layer's preview is always the full-site preview** (each layer contains everything below it). -- The old site lives in **`legacy/`** during the rewrite (moved there in Phase 01). It is the *reference* for content, copy, URLs, and behavior. **Never import from `legacy/`; never copy components.** Copy *content and intent*, rebuild the implementation. +- The old site lives in **`legacy/`** during the rewrite (moved there in Phase 01). It is the _reference_ for content, copy, URLs, and behavior. **Never import from `legacy/`; never copy components.** Copy _content and intent_, rebuild the implementation. - Before every commit: `pnpm check && pnpm build` must pass. A phase is done when its acceptance criteria are all checked and CI is green. - When a phase makes a decision not covered here, record it in `docs/adr/NNNN-.md` and note it in the phase PR description. ## Objectives (from project owner) -1. Keep the brand (colors, dark look, voice) while making the design *genuinely better* — SEO + accessibility improvements, consistent brand feel, no "AI slop" aesthetic. +1. Keep the brand (colors, dark look, voice) while making the design _genuinely better_ — SEO + accessibility improvements, consistent brand feel, no "AI slop" aesthetic. 2. Content (sponsors, events, FAQ, robots, etc.) becomes markdown/data files — adding a sponsor should take 2 minutes and one small PR. 3. Astro, fully static, minimal dependencies, **zero client-side framework runtime**. 4. SEO across the board, including AI/agent SEO (llms.txt, structured data, semantic HTML). @@ -28,33 +28,33 @@ This directory is the complete implementation plan for rewriting scstem.org from ## Locked decisions (from planning interview) -| # | Decision | -|---|----------| -| D1 | Clean rewrite; `legacy/` in-tree for reference only; staging site used for verification before go-live. | -| D2 | Content via Astro Content Collections (zod-validated markdown). No CMS now; schemas kept flat/simple so a git-backed CMS (e.g. Keystatic) can be added later without restructuring. | -| D3 | Zero framework runtime. `.astro` components + vanilla ` diff --git a/src/pages/about.astro b/src/pages/about.astro index e07a832..846dd88 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -258,7 +258,7 @@ const biohazardStats = [ -
+
What’s next? That’s the story of SC2, but what about you? Whether you are a student, parent, industry professional, diff --git a/src/pages/programs/fll/index.astro b/src/pages/programs/fll/index.astro index 0e819c4..563879e 100644 --- a/src/pages/programs/fll/index.astro +++ b/src/pages/programs/fll/index.astro @@ -140,7 +140,7 @@ const latest = teamPhotos[0]; ) } -
+
Start here No experience needed, and no LEGO® collection required — the robots, the tools, and the mentors diff --git a/src/pages/programs/frc/index.astro b/src/pages/programs/frc/index.astro index 14c4e5d..a0fa4be 100644 --- a/src/pages/programs/frc/index.astro +++ b/src/pages/programs/frc/index.astro @@ -173,7 +173,7 @@ const stats = [ />
-
+
Take us to the next level Build season starts every January and runs six weeks. Students, mentors, and sponsors all have a diff --git a/src/pages/programs/frc/robots.astro b/src/pages/programs/frc/robots.astro index 0c5bdea..946f091 100644 --- a/src/pages/programs/frc/robots.astro +++ b/src/pages/programs/frc/robots.astro @@ -126,7 +126,7 @@ const robots = await Promise.all(
-
+
The next one starts in January Every robot on this page was designed, machined, wired, and programmed by students. The next one diff --git a/src/pages/programs/index.astro b/src/pages/programs/index.astro index 45f5ba2..c8ffb13 100644 --- a/src/pages/programs/index.astro +++ b/src/pages/programs/index.astro @@ -113,7 +113,7 @@ import { breadcrumbs } from "@/lib/jsonld";
-
+
Still deciding which one? Tell us the student’s age and what they are into, and we will point you at the right team. diff --git a/src/styles/global.css b/src/styles/global.css index ecb6b83..1049d21 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -94,6 +94,7 @@ --duration-micro: 150ms; /* hover, focus */ --duration-ui: 250ms; /* menus, accordions */ --duration-entrance: 500ms; /* scroll-in entrances */ + --duration-draw: 1100ms; /* hand-markup strokes drawing on */ --ease-entrance: cubic-bezier(0, 0, 0.2, 1); /* ease-out */ --ease-toggle: cubic-bezier(0.4, 0, 0.2, 1); /* ease-in-out */ @@ -112,11 +113,13 @@ mean lifts ~5 levels with a spread of ~4, so every contrast pair in DESIGN.md §2 holds. */ --texture-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.8' numOctaves='1' stitchTiles='stitch' seed='7'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='gamma' exponent='4' amplitude='1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix values='0 0 0 0 0.98 0 0 0 0 0.98 0 0 0 0 0.98 0.4 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"); - /* The link underline (DESIGN.md §2.14, §8): one hand-drawn stroke as a 200×8 tile that repeats - along the text, so it follows a link across line breaks with no markup. A data URI cannot + /* The link underline (DESIGN.md §2.14, §8): one quick pen stroke — nearly straight, a slight + bow — as a 400×8 tile that repeats along the text, so it follows a link across line breaks + with no markup. The stroke sits at the tile's bottom edge: a background cannot paint below + the content box, so this is how the line gets its distance from the text. A data URI cannot read `currentcolor`, so the stroke color is baked in and each program theme re-declares the tile in its own `primary-bright` — the one place an accent hex appears outside the token. */ - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='8' viewBox='0 0 200 8'%3E%3Cpath d='M0 5.5C22 3.8 41 6.6 62 5.2S103 3.4 124 5.4 166 6.8 200 5.5' fill='none' stroke='%23facc15' stroke-width='1.9' stroke-linecap='round'/%3E%3C/svg%3E"); + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='8' viewBox='0 0 400 8'%3E%3Cpath d='M0 7.1C110 6.3 270 6.2 400 7' fill='none' stroke='%23facc15' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E"); } /** @@ -128,7 +131,7 @@ --color-primary-bright: #3ecf6e; --color-primary-foreground: #08240f; --color-ring: #3ecf6e; - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='8' viewBox='0 0 200 8'%3E%3Cpath d='M0 5.5C22 3.8 41 6.6 62 5.2S103 3.4 124 5.4 166 6.8 200 5.5' fill='none' stroke='%233ecf6e' stroke-width='1.9' stroke-linecap='round'/%3E%3C/svg%3E"); + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='8' viewBox='0 0 400 8'%3E%3Cpath d='M0 7.1C110 6.3 270 6.2 400 7' fill='none' stroke='%233ecf6e' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E"); } [data-theme="fll"] { @@ -136,7 +139,7 @@ --color-primary-bright: #fb923c; --color-primary-foreground: #241102; --color-ring: #fb923c; - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='8' viewBox='0 0 200 8'%3E%3Cpath d='M0 5.5C22 3.8 41 6.6 62 5.2S103 3.4 124 5.4 166 6.8 200 5.5' fill='none' stroke='%23fb923c' stroke-width='1.9' stroke-linecap='round'/%3E%3C/svg%3E"); + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='8' viewBox='0 0 400 8'%3E%3Cpath d='M0 7.1C110 6.3 270 6.2 400 7' fill='none' stroke='%23fb923c' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E"); } @layer base { @@ -211,7 +214,7 @@ background-image: var(--texture-underline); background-repeat: repeat-x; background-position: 0 100%; - background-size: auto 0.42em; + background-size: auto 0.5em; transition: color var(--duration-micro) var(--ease-toggle); } @@ -430,8 +433,10 @@ font-variant-numeric: tabular-nums; } -/* Ambient pool — a small lightness modulation of the ground, anchored to a section's top. - One per section, never stacked with another atmosphere device, never behind a photo. */ +/* Ambient pool — a small lightness modulation of the ground, centred on the section's heading + and fading out well inside the section on every side: an ellipse centred on the top edge is + cut in half by it and leaves a hard line. One per section, never stacked with another + atmosphere device, never behind a photo. */ @utility ambient-pool { position: relative; isolation: isolate; @@ -443,7 +448,7 @@ z-index: -1; pointer-events: none; background: radial-gradient( - ellipse 80% 60% at 50% 0%, + ellipse 70% 55% at 50% 30%, color-mix(in srgb, var(--color-primary) 9%, transparent), transparent 70% ); @@ -456,7 +461,7 @@ @utility ambient-pool-neutral { &::before { background: radial-gradient( - ellipse 80% 60% at 50% 0%, + ellipse 70% 55% at 50% 30%, color-mix(in srgb, var(--color-foreground) 5%, transparent), transparent 70% ); @@ -638,9 +643,9 @@ } /** - * Chrome links (DESIGN.md §8): "UI links may drop underline at rest but underline on - * hover/focus". Both halves live here so a call site cannot ship one without the other. The - * underline that arrives on hover is the same hand-drawn stroke every link carries. + * Chrome links (DESIGN.md §8): nav, footer, card footers. A different layer from copy, so they + * never take the hand-drawn stroke — no underline at rest, a straight machined hairline on + * hover/focus. Both halves live here so a call site cannot ship one without the other. */ @utility ui-link { color: var(--color-foreground); @@ -649,7 +654,9 @@ &:hover, &:focus-visible { color: var(--color-primary-bright); - background-image: var(--texture-underline); + text-decoration-line: underline; + text-decoration-thickness: 1px; + text-underline-offset: 0.2em; } /* The current page in a nav (§9 requires `aria-current="page"` there). */ @@ -691,8 +698,19 @@ * Paired with `pathLength="100"` on the animated path, so the dash math is in percent and the * stroke always ends fully drawn. Hard-coded lengths silently truncate any path longer than * the guess. + * + * The entrance waits for the mark to scroll into view: `BaseLayout` marks every `.draw-on` + * `is-pending` and swaps it to `is-drawing` from an IntersectionObserver. Without JavaScript + * neither class lands and the stroke is simply drawn. */ @utility draw-on { stroke-dasharray: 100; - animation: draw-on var(--duration-entrance) var(--ease-entrance) both; + + &.is-pending { + stroke-dashoffset: 100; + } + + &.is-drawing { + animation: draw-on var(--duration-draw) var(--ease-entrance) both; + } } From 2aadd10099aba50af5ad632340038dfb9565b322 Mon Sep 17 00:00:00 2001 From: Carson Seese Date: Thu, 3 Sep 2026 09:18:18 -0400 Subject: [PATCH 16/20] design: the link underline is one gentle hand-drawn curve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stroke is a 240px tile holding a single smooth sag, ~2.3px, so a typical link shows one whole curve and a long one a slow scallop. No jitter, no wave, no ruled line: the hand is in the curve. Text anchors gain a little bottom padding so the line sits ~0.2em under the baseline; buttons and framed media reset it with pb-0. DESIGN.md §2 says the same. Co-Authored-By: Claude Fable 5.1 --- DESIGN.md | 4 +-- src/components/ui/VideoLink.astro | 2 +- .../ui/primitives/Button.variants.ts | 7 +++-- src/styles/global.css | 30 +++++++++++-------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 1633533..c350115 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -103,13 +103,13 @@ The blueprint devices printed _onto_ the sheet. Each appears at most once per pa ### Hand markup register (the human layer) -The engineer's markup drawn _over_ the sheet — this is what keeps the machined language from feeling sterile. **The machined and scribed layers stay perfectly geometric everywhere; hand markup is small and functional — a few strokes that mean something, not a style.** The one hand device that appears everywhere is the link underline (§2.14), and it earns that by carrying meaning. All strokes are deliberately imperfect (hand-drawn SVG paths: slight curve, open ends, small rotation), 2–3px, `stroke-linecap: round`. **Pill-shaped UI is banned**: a circled word is a chalk oval, never a `border-radius: 999px` box. +The engineer's markup drawn _over_ the sheet — this is what keeps the machined language from feeling sterile. **The machined and scribed layers stay perfectly geometric everywhere; hand markup is small and functional — a few strokes that mean something, not a style.** The one hand device that appears everywhere is the link underline (§2.14), and it earns that by carrying meaning. All strokes are deliberately imperfect the way a quick hand is: smooth paths with a single gentle curve, open ends, small rotation, 2–3px, `stroke-linecap: round`. Never a ruled line, never a repeating wave, and never pen jitter or displacement filters — the hand shows in the curve, not in noise. **Pill-shaped UI is banned**: a circled word is a chalk oval, never a `border-radius: 999px` box. **Variation rule**: every markup device ships as a set of **at least 3 distinct SVG path variants** (implemented as primitives, e.g. `ChalkOval variant={1|2|3}`), further varied per-instance by small rotation/flip. Two adjacent instances never share a variant — identical "hand-drawn" marks read as a stamp and break the illusion. Swipes vary by rotation (±0.5–2°), inset, and alpha within their range. 12. **Chalk ovals**: key words circled with a hand-drawn open ellipse — `foreground` white (chalk) in hero/photo contexts, `primary` (grease pencil) on the ground. The "Real ⬭Skills⬭. Real ⬭Robots⬭. Real ⬭Fun⬭." treatment; the tagline itself is sanctioned brand copy for heroes/CTAs. Tagline/display contexts only, one run per view. 13. **Highlighter swipes**: a skewed translucent `primary` rectangle (25–35% alpha — **25% is the default**, the only value in the range that keeps white text at AAA: 7.6:1 on the ground, 9.2:1 on `card`; ±0.5–2° rotation, 2–3px radius) behind white key words — the marker-highlight alternative to `primary`-colored text. A heading uses colored text _or_ a swipe, never both; verify the white-on-swipe contrast on `/styleguide`. -14. **Hand underlines — the link grammar**: hand-drawn, slightly curved underline strokes (`primary`). **An underline means a link, always.** Every text link on the site carries one (`--texture-underline`, a repeating stroke tile that follows the text across line breaks, thickening on hover); nothing that is not a link is ever underlined in copy. The grammar, in full: _underline_ = link; _swipe alone_ or _oval alone_ = emphasis; _swipe + underline_ = a **featured link**, the one link in a view the page wants noticed. Ovals never appear in body copy. The `ChalkUnderline` primitive remains for display use beneath a heading or tagline phrase — headings never contain links, so it cannot be mistaken for one. Distinct from the machined 32px card rule, which stays perfectly straight — machined vs. hand is a deliberate contrast, never blended. +14. **Hand underlines — the link grammar**: hand-drawn, slightly curved underline strokes (`primary`). **An underline means a link, always.** Every text link on the site carries one (`--texture-underline`, a repeating tile holding one smooth ~12em stroke with a gentle sag, ~2.3px, sitting ~0.2em below the baseline and following the text across line breaks; hover shades the text and never moves the stroke); nothing that is not a link is ever underlined in copy. The grammar, in full: _underline_ = link; _swipe alone_ or _oval alone_ = emphasis; _swipe + underline_ = a **featured link**, the one link in a view the page wants noticed. Ovals never appear in body copy. The `ChalkUnderline` primitive remains for display use beneath a heading or tagline phrase — headings never contain links, so it cannot be mistaken for one. Distinct from the machined 32px card rule, which stays perfectly straight — machined vs. hand is a deliberate contrast, never blended. 15. **Sketch arrows**: one hand-drawn curved arrow per page. **An arrow's target is always handwritten annotation text (§3), never a regular-font element** — a sketched arrow pointing at typeset UI breaks the fiction. The annotation may itself be a link (e.g. the handwritten "Become a sponsor"). Motion note (§6 applies): hand-markup strokes _draw on_ as their entrance (stroke-dashoffset, once, ~1100ms, reduced-motion disables) — the one sanctioned decorative animation, because it enacts the metaphor. The entrance starts when the mark scrolls into view (an IntersectionObserver in `BaseLayout`, the one script motion is allowed), not on page load, so a mark below the fold is never drawn unseen; without JavaScript the stroke is simply present. diff --git a/src/components/ui/VideoLink.astro b/src/components/ui/VideoLink.astro index b2b0e78..82c845f 100644 --- a/src/components/ui/VideoLink.astro +++ b/src/components/ui/VideoLink.astro @@ -27,7 +27,7 @@ const { href, label, poster, class: className, rel = "noopener", ...attrs } = As ` as a background - // image, and this recipe renders as an anchor whenever a Button gets an `href`. - "inline-flex items-center justify-center gap-2 rounded-md bg-none font-medium whitespace-nowrap", + // `bg-none pb-0` because the base layer draws the link underline on every `` as a + // background image over a little bottom padding, and this recipe renders as an anchor + // whenever a Button gets an `href`. + "inline-flex items-center justify-center gap-2 rounded-md bg-none pb-0 font-medium whitespace-nowrap", "transition-colors duration-(--duration-micro) ease-(--ease-toggle)", "disabled:pointer-events-none disabled:opacity-50", "aria-disabled:pointer-events-none aria-disabled:opacity-50", diff --git a/src/styles/global.css b/src/styles/global.css index 1049d21..a9f3a87 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -113,13 +113,15 @@ mean lifts ~5 levels with a spread of ~4, so every contrast pair in DESIGN.md §2 holds. */ --texture-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.8' numOctaves='1' stitchTiles='stitch' seed='7'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='gamma' exponent='4' amplitude='1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix values='0 0 0 0 0.98 0 0 0 0 0.98 0 0 0 0 0.98 0.4 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"); - /* The link underline (DESIGN.md §2.14, §8): one quick pen stroke — nearly straight, a slight - bow — as a 400×8 tile that repeats along the text, so it follows a link across line breaks - with no markup. The stroke sits at the tile's bottom edge: a background cannot paint below - the content box, so this is how the line gets its distance from the text. A data URI cannot - read `currentcolor`, so the stroke color is baked in and each program theme re-declares the - tile in its own `primary-bright` — the one place an accent hex appears outside the token. */ - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='8' viewBox='0 0 400 8'%3E%3Cpath d='M0 7.1C110 6.3 270 6.2 400 7' fill='none' stroke='%23facc15' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E"); + /* The link underline (DESIGN.md §2.14, §8): one quick hand-drawn stroke with a single gentle + sag, as a 240×16 tile (12em at the size below) that repeats along the text, so it follows a + link across line breaks with no markup and a typical link shows one whole curve. Smooth on + purpose: no jitter, no wave — the hand is in the curve. The stroke sits near the tile's + bottom edge and the anchor adds a little bottom padding, which is how the line gets its + distance from the text: a background cannot paint below the box. A data URI cannot read + `currentcolor`, so the stroke color is baked in and each program theme re-declares the tile + in its own `primary-bright` — the one place an accent hex appears outside the token. */ + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='16' viewBox='0 0 240 16'%3E%3Cpath d='M2 10.4C50 13.6 190 13.4 238 10.4' fill='none' stroke='%23facc15' stroke-width='2.3' stroke-linecap='round'/%3E%3C/svg%3E"); } /** @@ -131,7 +133,7 @@ --color-primary-bright: #3ecf6e; --color-primary-foreground: #08240f; --color-ring: #3ecf6e; - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='8' viewBox='0 0 400 8'%3E%3Cpath d='M0 7.1C110 6.3 270 6.2 400 7' fill='none' stroke='%233ecf6e' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E"); + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='16' viewBox='0 0 240 16'%3E%3Cpath d='M2 10.4C50 13.6 190 13.4 238 10.4' fill='none' stroke='%233ecf6e' stroke-width='2.3' stroke-linecap='round'/%3E%3C/svg%3E"); } [data-theme="fll"] { @@ -139,7 +141,7 @@ --color-primary-bright: #fb923c; --color-primary-foreground: #241102; --color-ring: #fb923c; - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='400' height='8' viewBox='0 0 400 8'%3E%3Cpath d='M0 7.1C110 6.3 270 6.2 400 7' fill='none' stroke='%23fb923c' stroke-width='1.7' stroke-linecap='round'/%3E%3C/svg%3E"); + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='16' viewBox='0 0 240 16'%3E%3Cpath d='M2 10.4C50 13.6 190 13.4 238 10.4' fill='none' stroke='%23fb923c' stroke-width='2.3' stroke-linecap='round'/%3E%3C/svg%3E"); } @layer base { @@ -204,7 +206,7 @@ /** * An underline means a link, always, and it is drawn by hand (DESIGN.md §2.14, §8). The * stroke is a repeating background rather than `text-decoration`, which cannot take a custom - * shape. Anchors that are not text — a button, a framed image — reset it with `bg-none`. + * shape. Anchors that are not text — a button, a framed image — reset it with `bg-none pb-0`. * Preflight sets `text-decoration: inherit` on `a`; the line is switched off here so the two * underlines never stack. */ @@ -214,7 +216,8 @@ background-image: var(--texture-underline); background-repeat: repeat-x; background-position: 0 100%; - background-size: auto 0.5em; + background-size: auto 0.8em; + padding-bottom: 0.1em; transition: color var(--duration-micro) var(--ease-toggle); } @@ -479,8 +482,8 @@ /** * The shared stroke contract for the hand-markup register (DESIGN.md §2.12–2.15): 2.5px - * round-capped `currentColor`, with `non-scaling-stroke` so the weight stays even when the SVG is stretched - * non-uniformly over a word. Applied to the ``; the paths carry only their `d` and + * round-capped `currentColor`, with `non-scaling-stroke` so the weight stays even when the SVG is + * stretched non-uniformly over a word. Applied to the ``; the paths carry only their `d` and * `pathLength`, so a change to the hand's weight is one edit rather than one per device. */ @utility hand-stroke { @@ -650,6 +653,7 @@ @utility ui-link { color: var(--color-foreground); background-image: none; + padding-bottom: 0; &:hover, &:focus-visible { From 05d26713a463b3dc8b93f0db72cbff8e06d15252 Mon Sep 17 00:00:00 2001 From: Carson Seese Date: Thu, 3 Sep 2026 09:44:16 -0400 Subject: [PATCH 17/20] design: one underline curve per line, stretched to the link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stroke SVG has no intrinsic size and stretches to the link's width (and, with box-decoration-break: clone, to each wrapped line), so a link of any length shows exactly one smooth sag and never a seam where a tile met itself. non-scaling-stroke keeps it 2.3px. Bottom padding grows to 0.16em and the curve sags away from the text, so it never touches descenders. DESIGN.md §2.14 says the same. Co-Authored-By: Claude Fable 5.1 --- DESIGN.md | 2 +- src/styles/global.css | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index c350115..3c6ccf3 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -109,7 +109,7 @@ The engineer's markup drawn _over_ the sheet — this is what keeps the machined 12. **Chalk ovals**: key words circled with a hand-drawn open ellipse — `foreground` white (chalk) in hero/photo contexts, `primary` (grease pencil) on the ground. The "Real ⬭Skills⬭. Real ⬭Robots⬭. Real ⬭Fun⬭." treatment; the tagline itself is sanctioned brand copy for heroes/CTAs. Tagline/display contexts only, one run per view. 13. **Highlighter swipes**: a skewed translucent `primary` rectangle (25–35% alpha — **25% is the default**, the only value in the range that keeps white text at AAA: 7.6:1 on the ground, 9.2:1 on `card`; ±0.5–2° rotation, 2–3px radius) behind white key words — the marker-highlight alternative to `primary`-colored text. A heading uses colored text _or_ a swipe, never both; verify the white-on-swipe contrast on `/styleguide`. -14. **Hand underlines — the link grammar**: hand-drawn, slightly curved underline strokes (`primary`). **An underline means a link, always.** Every text link on the site carries one (`--texture-underline`, a repeating tile holding one smooth ~12em stroke with a gentle sag, ~2.3px, sitting ~0.2em below the baseline and following the text across line breaks; hover shades the text and never moves the stroke); nothing that is not a link is ever underlined in copy. The grammar, in full: _underline_ = link; _swipe alone_ or _oval alone_ = emphasis; _swipe + underline_ = a **featured link**, the one link in a view the page wants noticed. Ovals never appear in body copy. The `ChalkUnderline` primitive remains for display use beneath a heading or tagline phrase — headings never contain links, so it cannot be mistaken for one. Distinct from the machined 32px card rule, which stays perfectly straight — machined vs. hand is a deliberate contrast, never blended. +14. **Hand underlines — the link grammar**: hand-drawn, slightly curved underline strokes (`primary`). **An underline means a link, always.** Every text link on the site carries one (`--texture-underline`: one smooth stroke with a gentle sag away from the text, stretched to the link's width — to each line's width when it wraps — so there is one curve per line and never a seam; ~2.3px at any width, its ends ~0.16em below the text box; hover shades the text and never moves the stroke); nothing that is not a link is ever underlined in copy. The grammar, in full: _underline_ = link; _swipe alone_ or _oval alone_ = emphasis; _swipe + underline_ = a **featured link**, the one link in a view the page wants noticed. Ovals never appear in body copy. The `ChalkUnderline` primitive remains for display use beneath a heading or tagline phrase — headings never contain links, so it cannot be mistaken for one. Distinct from the machined 32px card rule, which stays perfectly straight — machined vs. hand is a deliberate contrast, never blended. 15. **Sketch arrows**: one hand-drawn curved arrow per page. **An arrow's target is always handwritten annotation text (§3), never a regular-font element** — a sketched arrow pointing at typeset UI breaks the fiction. The annotation may itself be a link (e.g. the handwritten "Become a sponsor"). Motion note (§6 applies): hand-markup strokes _draw on_ as their entrance (stroke-dashoffset, once, ~1100ms, reduced-motion disables) — the one sanctioned decorative animation, because it enacts the metaphor. The entrance starts when the mark scrolls into view (an IntersectionObserver in `BaseLayout`, the one script motion is allowed), not on page load, so a mark below the fold is never drawn unseen; without JavaScript the stroke is simply present. diff --git a/src/styles/global.css b/src/styles/global.css index a9f3a87..9df7cc9 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -114,14 +114,15 @@ --texture-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600'%3E%3Cfilter id='g' x='0' y='0' width='100%25' height='100%25' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.8' numOctaves='1' stitchTiles='stitch' seed='7'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncR type='gamma' exponent='4' amplitude='1'/%3E%3C/feComponentTransfer%3E%3CfeColorMatrix values='0 0 0 0 0.98 0 0 0 0 0.98 0 0 0 0 0.98 0.4 0 0 0 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"); /* The link underline (DESIGN.md §2.14, §8): one quick hand-drawn stroke with a single gentle - sag, as a 240×16 tile (12em at the size below) that repeats along the text, so it follows a - link across line breaks with no markup and a typical link shows one whole curve. Smooth on - purpose: no jitter, no wave — the hand is in the curve. The stroke sits near the tile's - bottom edge and the anchor adds a little bottom padding, which is how the line gets its - distance from the text: a background cannot paint below the box. A data URI cannot read + sag. The SVG has no intrinsic size and `preserveAspectRatio='none'`, so `background-size: + 100%` stretches one curve to the link's width — and, with `box-decoration-break: clone`, to + each line of a wrapped link — while `non-scaling-stroke` keeps the line 2.3px at any width. + One curve per line, so there is never a seam where a repeating tile would meet itself. The + curve keeps clear of descenders: its ends sit 0.16em below the box (the anchor's bottom + padding) and it sags away from the text, never toward it. A data URI cannot read `currentcolor`, so the stroke color is baked in and each program theme re-declares the tile in its own `primary-bright` — the one place an accent hex appears outside the token. */ - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='16' viewBox='0 0 240 16'%3E%3Cpath d='M2 10.4C50 13.6 190 13.4 238 10.4' fill='none' stroke='%23facc15' stroke-width='2.3' stroke-linecap='round'/%3E%3C/svg%3E"); + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 16' preserveAspectRatio='none'%3E%3Cpath d='M1.5 12.8C30 14.9 70 14.7 98.5 12.8' fill='none' stroke='%23facc15' stroke-width='2.3' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E"); } /** @@ -133,7 +134,7 @@ --color-primary-bright: #3ecf6e; --color-primary-foreground: #08240f; --color-ring: #3ecf6e; - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='16' viewBox='0 0 240 16'%3E%3Cpath d='M2 10.4C50 13.6 190 13.4 238 10.4' fill='none' stroke='%233ecf6e' stroke-width='2.3' stroke-linecap='round'/%3E%3C/svg%3E"); + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 16' preserveAspectRatio='none'%3E%3Cpath d='M1.5 12.8C30 14.9 70 14.7 98.5 12.8' fill='none' stroke='%233ecf6e' stroke-width='2.3' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E"); } [data-theme="fll"] { @@ -141,7 +142,7 @@ --color-primary-bright: #fb923c; --color-primary-foreground: #241102; --color-ring: #fb923c; - --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='16' viewBox='0 0 240 16'%3E%3Cpath d='M2 10.4C50 13.6 190 13.4 238 10.4' fill='none' stroke='%23fb923c' stroke-width='2.3' stroke-linecap='round'/%3E%3C/svg%3E"); + --texture-underline: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 16' preserveAspectRatio='none'%3E%3Cpath d='M1.5 12.8C30 14.9 70 14.7 98.5 12.8' fill='none' stroke='%23fb923c' stroke-width='2.3' stroke-linecap='round' vector-effect='non-scaling-stroke'/%3E%3C/svg%3E"); } @layer base { @@ -214,10 +215,11 @@ color: var(--color-primary-bright); text-decoration-line: none; background-image: var(--texture-underline); - background-repeat: repeat-x; + background-repeat: no-repeat; background-position: 0 100%; - background-size: auto 0.8em; - padding-bottom: 0.1em; + background-size: 100% 0.8em; + padding-bottom: 0.16em; + box-decoration-break: clone; transition: color var(--duration-micro) var(--ease-toggle); } From 370eb818ef8149ffda1d9f8cf33cacbc0c58a5ca Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 00:11:00 +0000 Subject: [PATCH 18/20] ci: put the LCP waterfall behind a Lighthouse failure in the job log The Lighthouse gate has failed on every run since it was added, on the LCP assertion alone, by 40 to 200 ms, while the same build lands at 1.8 s locally. The summary now explains a run instead of scoring it: Lighthouse and Chrome versions with the benchmark index, then per URL the LCP element, its four phases, and the request waterfall Lighthouse observed. It goes to stdout as well as the step summary, so a failed run's reasons are readable through the Actions API without downloading the report artifact. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vaq66ucygSHc84MJXz1cac --- .github/workflows/ci.yml | 4 +- tools/ci/lighthouse-summary.ts | 93 ++++++++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9830fb7..7a14ecc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,9 +83,11 @@ jobs: - name: Lighthouse CI run: pnpm dlx @lhci/cli@0.15.1 autorun --config=tools/ci/lighthouserc.json + # Into the log as well as the summary, so the waterfall behind a failed LCP assertion is + # readable from the API without downloading the report artifact. - name: Summarize if: ${{ !cancelled() }} - run: node tools/ci/lighthouse-summary.ts >>"$GITHUB_STEP_SUMMARY" + run: node tools/ci/lighthouse-summary.ts | tee -a "$GITHUB_STEP_SUMMARY" - uses: actions/upload-artifact@v5 if: ${{ !cancelled() }} diff --git a/tools/ci/lighthouse-summary.ts b/tools/ci/lighthouse-summary.ts index 688eae7..e25afdc 100644 --- a/tools/ci/lighthouse-summary.ts +++ b/tools/ci/lighthouse-summary.ts @@ -1,13 +1,17 @@ /** - * Renders the Lighthouse CI run in `.lighthouseci/` as a Markdown job summary: median category - * scores per URL, then every failed assertion. + * Renders the Lighthouse CI run in `.lighthouseci/` as Markdown: median category scores per URL, + * every failed assertion, then for each URL what the LCP was and the requests in flight before + * it. The last part is what answers "why did LCP move" without downloading the report artifact: + * on the simulated Slow 4G link every request costs a fixed latency and every kilobyte costs + * time, so the waterfall is the budget. * - * node tools/ci/lighthouse-summary.ts >>"$GITHUB_STEP_SUMMARY" + * node tools/ci/lighthouse-summary.ts | tee -a "$GITHUB_STEP_SUMMARY" */ import { existsSync, readFileSync } from "node:fs"; interface RunSummary { isRepresentativeRun: boolean; + jsonPath: string; summary: Record; url: string; } @@ -22,12 +26,45 @@ interface Assertion { url: string; } +interface NetworkRequest { + networkEndTime: number; + networkRequestTime: number; + priority?: string; + resourceType?: string; + transferSize: number; + url: string; +} + +interface LcpPhase { + phase: string; + timing: number; +} + +/** The slice of a Lighthouse report this script reads. */ +interface Report { + audits: { + "largest-contentful-paint": { numericValue: number }; + "largest-contentful-paint-element": { + details?: { + items: [{ items: [{ node: { nodeLabel: string } }] }, { items: LcpPhase[] }]; + }; + }; + "network-requests": { details: { items: NetworkRequest[] } }; + }; + environment: { benchmarkIndex: number; hostUserAgent: string }; + lighthouseVersion: string; +} + const CATEGORIES = ["performance", "accessibility", "best-practices", "seo"]; const read = (path: string): T[] => // SAFETY: both files are written by @lhci/cli in the shape declared above. existsSync(path) ? (JSON.parse(readFileSync(path, "utf8")) as T[]) : []; +const report = (path: string): Report => + // SAFETY: a Lighthouse report; only the audits declared in `Report` are read. + JSON.parse(readFileSync(path, "utf8")) as Report; + const runs = read(".lighthouseci/reports/manifest.json"); const assertions = read(".lighthouseci/assertion-results.json"); @@ -36,14 +73,17 @@ const score = (value: number): string => { const percent = Math.round(value * 100); return `${percent >= 90 ? "🟢" : percent >= 50 ? "🟠" : "🔴"} ${String(percent)}`; }; +const ms = (value: number): string => `${String(Math.round(value))} ms`; +const kb = (bytes: number): string => `${(bytes / 1024).toFixed(1)} KB`; const lines = ["## Lighthouse", ""]; +const representative = runs.filter((r) => r.isRepresentativeRun); -if (runs.length === 0) { +if (representative.length === 0) { lines.push("No reports were written."); } else { lines.push(`| URL | ${CATEGORIES.join(" | ")} |`, `|---|${"---|".repeat(CATEGORIES.length)}`); - for (const run of runs.filter((r) => r.isRepresentativeRun)) { + for (const run of representative) { const cells = CATEGORIES.map((c) => score(run.summary[c] ?? 0)); lines.push(`| \`${path(run.url)}\` | ${cells.join(" | ")} |`); } @@ -61,4 +101,47 @@ if (failed.length > 0) { } } +if (representative.length > 0) { + const [first] = representative; + if (first !== undefined) { + const { environment, lighthouseVersion } = report(first.jsonPath); + const chrome = /Chrome\/[\d.]+/u.exec(environment.hostUserAgent)?.[0] ?? "Chrome"; + lines.push( + "", + `### Largest Contentful Paint, median run per URL`, + "", + `Lighthouse ${lighthouseVersion}, ${chrome}, benchmark index ${String(Math.round(environment.benchmarkIndex))}.`, + ); + } + + for (const run of representative) { + const { audits } = report(run.jsonPath); + const lcp = audits["largest-contentful-paint"].numericValue; + const element = audits["largest-contentful-paint-element"].details; + const label = element?.items[0].items[0]?.node.nodeLabel ?? "(no element)"; + const phases = + element?.items[1].items.map((p) => `${p.phase} ${ms(p.timing)}`).join(", ") ?? ""; + lines.push("", `**\`${path(run.url)}\`** — LCP ${ms(lcp)}, ${label}`, "", phases, ""); + + const requests = audits["network-requests"].details.items.filter( + (r) => !r.url.startsWith("data:"), + ); + const total = requests.reduce((sum, r) => sum + r.transferSize, 0); + lines.push( + "
", + `${String(requests.length)} requests, ${kb(total)} transferred`, + "", + "| Start | End | Size | Priority | Type | Path |", + "|---:|---:|---:|---|---|---|", + ); + for (const r of requests) { + lines.push( + `| ${ms(r.networkRequestTime)} | ${ms(r.networkEndTime)} | ${kb(r.transferSize)} | ` + + `${r.priority ?? ""} | ${r.resourceType ?? ""} | \`${path(r.url)}\` |`, + ); + } + lines.push("", "
"); + } +} + console.log(lines.join("\n")); From 1a5a9ad64f219bc759ccf6a9ee20376bdd0c1c66 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 00:58:16 +0000 Subject: [PATCH 19/20] perf: measure Lighthouse over HTTP/2, subset the fonts, add a 672px ladder step The Lighthouse gate had failed on every CI run since Phase 09 added it, on LCP alone, by 40 to 200 ms on four of the six URLs. Two causes, both now recorded in docs/adr/0018-lighthouse-over-http2.md. The runner's Chrome 152 fetches every lazy image within about three viewports during the initial load, which Chrome 141 did not, and Lighthouse's simulation counts every request that ends before the paint against LCP. And astro preview serves HTTP/1.1, where the simulation charges a handshake per parallel connection; Cloudflare serves HTTP/2. The same build measures 450 ms less LCP over the transport visitors get. tools/ci/serve.ts serves dist/ over HTTP/2 and TLS with a self-signed certificate it makes with openssl, gzip on the text types, and lighthouserc.json starts it in place of astro preview. The four latin faces are subset to the characters English copy uses, 12 KB off the fonts every page loads (docs/adr/0017-font-subset.md), with tabular figures kept for the numerals. Photographs 92vw wide on a phone gain a 672px variant, which a 360px screen at 2x and Lighthouse's 412px at 1.75x both pick instead of 768 or 840. Chrome 152, median of three, before and after: / 2138 -> 1577 ms, /about/ 2117 -> 1429, /programs/frc/robots/ 2121 -> 1428, /sponsors/ 2040 -> 1503. Font preloads were re-measured over HTTP/2 and change nothing; ADR 0008 stands. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vaq66ucygSHc84MJXz1cac --- docs/adr/0017-font-subset.md | 78 ++++++++++ docs/adr/0018-lighthouse-over-http2.md | 72 ++++++++++ docs/tooling.md | 20 ++- knip.jsonc | 5 +- plan/todo.md | 18 +-- src/components/ui/FeatureCard.astro | 2 +- src/components/ui/PhotoTimeline.astro | 2 +- src/components/ui/VideoLink.astro | 2 +- src/pages/programs/frc/robots.astro | 2 +- src/styles/fonts.css | 31 ++-- ...architects-daughter-latin-400-normal.woff2 | Bin 13156 -> 12548 bytes .../fonts/inter-latin-wght-400-700.woff2 | Bin 36080 -> 28248 bytes .../fonts/orbitron-latin-wght-normal.woff2 | Bin 11800 -> 11044 bytes .../source-code-pro-latin-wght-normal.woff2 | Bin 22044 -> 18800 bytes tools/ci/lighthouserc.json | 18 +-- tools/ci/serve.ts | 133 ++++++++++++++++++ 16 files changed, 334 insertions(+), 49 deletions(-) create mode 100644 docs/adr/0017-font-subset.md create mode 100644 docs/adr/0018-lighthouse-over-http2.md create mode 100644 tools/ci/serve.ts diff --git a/docs/adr/0017-font-subset.md b/docs/adr/0017-font-subset.md new file mode 100644 index 0000000..bb369e0 --- /dev/null +++ b/docs/adr/0017-font-subset.md @@ -0,0 +1,78 @@ +# 0017 — The latin faces are subset to the characters English copy uses + +- **Status:** accepted +- **Date:** 2026-09-07 +- **Amends:** [0011](0011-inter-weight-axis.md), [0014](0014-vendored-fonts.md) + +## Context + +The four latin faces in `src/styles/fonts/` were Google Fonts' `latin` builds as fontsource +packages them: 518 glyphs for Inter, covering Latin-1, spacing modifiers, combining marks, the +whole General Punctuation block, and the stylistic alternates that OpenType features reach. The +site's copy, across every built page, uses printable ASCII plus ten characters outside it: the +dashes, curly quotes, the ellipsis, ©, ®, and ≥. + +Fonts are the third request hop on every page (HTML, then the stylesheet, then the faces the +stylesheet names) and Lighthouse's Slow 4G simulation charges every kilobyte of them against LCP, +which the gate was missing by 40 to 200 ms (`0018-lighthouse-over-http2.md`). + +## Decision + +Each latin face is subset to the ranges below with fonttools, output committed, `unicode-range` in +`fonts.css` restated to the same set so a browser fetches a face only for text it can render: + +``` +U+0020-007E printable ASCII +U+00A0-00FF Latin-1 supplement: ©, ®, °, accented vowels, … +U+2010-2027 dashes, quotes, bullet, ellipsis +U+2030-203A per mille, primes, single guillemets +U+20AC € +U+2122 ™ +U+2212 minus +``` + +| Face | Before | After | +| ----------------------------- | ------: | ------: | +| Inter, wght 400–700 | 36.1 KB | 28.2 KB | +| Source Code Pro, wght 200–900 | 22.0 KB | 18.8 KB | +| Orbitron, wght 400–900 | 11.8 KB | 11.0 KB | +| Architects Daughter | 13.2 KB | 12.5 KB | + +Inter's latin-ext face is untouched: it is fetched only for a character outside the latin set, +which no page has. + +The OpenType features kept are fonttools' defaults plus `tnum`, `pnum`, and `ccmp`: +`font-variant-numeric: tabular-nums` is set on `time`, `data`, and the stat numerals, and the +tabular figures have to be in the file for it to mean anything. Name records 0–6, 13, and 14 are +kept so the copyright and OFL notice travel with each file. + +## Regenerating + +From the fontsource 5.3.0 latin builds (`docs/adr/0014-vendored-fonts.md` says where the Inter +instance comes from), with `fonttools` and `brotli` installed: + +```sh +pyftsubset .woff2 \ + --unicodes="U+0020-007E,U+00A0-00FF,U+2010-2027,U+2030-203A,U+20AC,U+2122,U+2212" \ + --layout-features+=tnum,pnum,ccmp --name-IDs=0,1,2,3,4,5,6,13,14 \ + --flavor=woff2 --output-file=src/styles/fonts/.woff2 +``` + +The range in the command and the `unicode-range` in `fonts.css` are the same set; change both. + +## Alternatives considered + +- **Trim Source Code Pro's and Orbitron's weight axes**, as 0011 did for Inter. Measured in + Phase 09 at 3.2 KB and 0.7 KB; the glyph set was where the bytes were. +- **Subset to the characters actually present in the build.** Smaller still, but a news post + with an é would render its accent in the fallback face until someone regenerated the fonts. + Latin-1 is the widest set English copy reaches into without becoming another language. +- **Drop Inter's 700.** DESIGN.md §3 sanctions 400/500/600, but markdown `**bold**` in the event + and news bodies resolves to 700, and a clamped 600 is a design change 0011 declined to make. + +## Consequences + +- A character outside the ranges above renders in the system fallback face. The site's copy has + none; a new one shows up in the browser, not the build. +- No hinting was dropped (`--no-hinting` measured under 200 bytes) and no glyph the copy uses is + gone: the build's text was checked against each subset's character map. diff --git a/docs/adr/0018-lighthouse-over-http2.md b/docs/adr/0018-lighthouse-over-http2.md new file mode 100644 index 0000000..a1daf4c --- /dev/null +++ b/docs/adr/0018-lighthouse-over-http2.md @@ -0,0 +1,72 @@ +# 0018 — The Lighthouse gate measures the build over HTTP/2 and TLS + +- **Status:** accepted +- **Date:** 2026-09-07 +- **Amends:** [0007](0007-lighthouse-ci-gate.md); confirms [0008](0008-no-font-preloads.md) + +## Context + +The Lighthouse gate had failed on every CI run since Phase 09 added it, on the LCP assertion +alone: four of the six budgeted URLs between 2036 and 2201 ms against a 2000 ms budget, while the +same build measured 1730–1820 ms on a developer machine. Two things differed. + +**Chrome.** The runner's Chrome 152 fetches every `loading="lazy"` image within roughly three +viewports during the initial load; the Chrome 141 the local runs used fetched one. On the homepage +that is 115 KB more on the wire before the hero paints, on `/about/` 211 KB, and Lighthouse's +Lantern simulation shares its 1.6 Mbps link equally among requests in flight, whatever their +priority, and counts every request that ended before the observed paint. Reproduced locally with +Chrome 152 for Testing to within 50 ms of CI. + +**Transport.** `astro preview` serves HTTP/1.1 without TLS. Lantern simulates the protocol it +observes, and over HTTP/1.1 every parallel request beyond the first six waits for a connection and +every new connection is charged a handshake at the simulated 150 ms round trip. Cloudflare Pages +serves HTTP/2 over TLS, where one connection carries everything. The gate was measuring a +transport the site is never served on, and on Chrome 152 that transport was worth 450 ms of LCP. + +Measured, Chrome 152, median of three, milliseconds: + +| URL | CI, as found | fonts subset + 672 step, HTTP/1.1 | same build, HTTP/2 + TLS | +| ----------------------- | -----------: | --------------------------------: | -----------------------: | +| `/` | 2138 | 2037 | 1577 | +| `/about/` | 2117 | 2033 | 1427 | +| `/programs/frc/robots/` | 2121 | 1887 | 1455 | +| `/sponsors/` | 2040 | 1881 | 1502 | + +## Decision + +1. **`tools/ci/serve.ts` serves `dist/` for the gate**: HTTP/2 over TLS with a self-signed + certificate it generates with `openssl` on first run, gzip on the text types Cloudflare + compresses, `404.html` for a missing path. `lighthouserc.json` starts it in place of + `astro preview` and launches Chrome with `--ignore-certificate-errors`. +2. **The latin faces are subset** to the characters English copy uses, 12 KB off the fonts every + page loads (`0017-font-subset.md`). +3. **Image ladders gain a 672px step** where a photograph is 92vw wide on a phone. A 360 CSS px + screen at 2x and Lighthouse's 412 px at 1.75x both ask for about 665 device pixels and were + being answered with the 768 or 840 px variant. +4. The LCP budget stays at 2000 ms. With the transport the site is served on, the tightest median + is 1577 ms. + +## Alternatives considered + +- **Preload the three above-the-fold faces.** Over HTTP/1.1 it recovered 100–300 ms on four + pages and nothing on the homepage; over HTTP/2 it changed nothing (1578 vs 1577 ms on `/`, 1428 + vs 1427 on `/about/`). 0008 stands. +- **Inline the stylesheet.** Removes a request hop, but puts the fonts and the hero image on the + wire at the same moment; measured 150–200 ms worse on the homepage. +- **Pin Chrome for the gate.** Reproducible, and blind: Chrome 152's lazy-loading is what + visitors run, so the gate should see it. +- **Raise the budget.** Would have hidden a real 450 ms measurement error rather than fixing it. +- **Reduce the below-the-fold photographs further.** They are already q70 and right-sized; what + remained was the transport. + +## Consequences + +- `openssl` is a requirement of the Lighthouse job and of running `lhci autorun` locally. It is on + every GitHub runner image and on macOS. +- Lighthouse's `uses-http2` diagnostic passes for the first time, and the reports describe the + site as visitors get it. +- LHCI kills the server it started, so the local `astro preview stop` step in `docs/tooling.md` is + gone. +- Chrome on the runner is whatever `ubuntu-latest` ships. A future Chrome can move the numbers + again; the job summary now prints the waterfall behind every URL's LCP so the cause is readable + from the log. diff --git a/docs/tooling.md b/docs/tooling.md index 029a4a1..a6af554 100644 --- a/docs/tooling.md +++ b/docs/tooling.md @@ -40,6 +40,9 @@ TypeScript scripts run directly by Node (type stripping, no build step), each be | `check:meta` | Every built page's head: unique title/description, og:image | CI, after build | | `assets:og` | Render the OG cards in `src/assets/og/` | by hand | +Two more under `tools/ci/` have no script because Lighthouse CI runs them: `serve.ts` serves +`dist/` over HTTP/2 and TLS for the audit, and `lighthouse-summary.ts` writes the job summary. + Only erasable TypeScript syntax (no enums, namespaces, or parameter properties); `tools/tsconfig.json` enforces it. @@ -72,9 +75,11 @@ and pushes to `main` and `staging`, in three jobs: - **Check**: typecheck, lint, format, knip, and the repo checks as separate steps, all of which run even when an earlier one fails. ESLint findings become inline annotations on the PR. - **Build**: `pnpm build`, `check:meta`, and an offline link check over `dist/` (lychee). -- **Lighthouse**: `@lhci/cli` via `pnpm dlx` against `astro preview` serving the build artifact - (`docs/adr/0007`), three runs per URL over six page shapes. Median scores per URL and every - failed assertion go to the job summary; full reports upload as an artifact. +- **Lighthouse**: `@lhci/cli` via `pnpm dlx` (`docs/adr/0007`) against `tools/ci/serve.ts`, which + serves the build artifact over HTTP/2 and TLS the way Cloudflare does (`docs/adr/0018`), three + runs per URL over six page shapes. The job summary and the log carry the median scores per URL, + every failed assertion, and for each URL the LCP element, its phases, and the request waterfall; + full reports upload as an artifact. ### Performance budgets @@ -92,11 +97,12 @@ and pushes to `main` and `staging`, in three jobs: | Script transfer size | < 35 KB | | Total page transfer size | < 1 MB | -Mobile emulation with simulated throttling, so transfer size dominates. `astro preview` gzips, -which keeps the measurement comparable to Cloudflare. Locally: +Mobile emulation with simulated throttling, so transfer size and request count dominate, and the +simulation follows the protocol it observes: HTTP/2 with gzip, as served. Locally (`openssl` on +the path for the self-signed certificate): ```sh pnpm build -pnpm dlx @lhci/cli@0.15.1 autorun -pnpm exec astro preview stop # the preview server outlives lhci +pnpm dlx @lhci/cli@0.15.1 autorun --config=tools/ci/lighthouserc.json +node tools/ci/lighthouse-summary.ts ``` diff --git a/knip.jsonc b/knip.jsonc index c577e39..8e646fb 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -1,7 +1,8 @@ { "$schema": "./node_modules/knip/schema.json", - // Cloudflare Pages Functions are file-routed, which knip has no plugin for. - "entry": ["functions/**/*.ts"], + // Cloudflare Pages Functions are file-routed, which knip has no plugin for; Lighthouse CI + // starts tools/ci/serve.ts from lighthouserc.json, which knip does not read. + "entry": ["functions/**/*.ts", "tools/ci/serve.ts"], // tools/lint/anti-slop/shared is vendored upstream source (see its VENDOR.md). "ignore": ["legacy/**", "tools/lint/anti-slop/shared/**"], } diff --git a/plan/todo.md b/plan/todo.md index 459c98d..76f8210 100644 --- a/plan/todo.md +++ b/plan/todo.md @@ -74,12 +74,12 @@ item says what is already in the repository waiting for it. redirect stub out of season and the list uses `/about/` instead. When a live event exists and you want its shape budgeted, add its URL to `lighthouserc.json` — and take it out again when the season ends. -- [ ] **Watch item, not an open problem: the LCP budget is met, tightest median 1807 ms against - 2000 ms.** This was a real failure and is closed — trimming Inter's weight axis - (`docs/adr/0011-inter-weight-axis.md`) took 12 KB off the critical path and collapsed a - bimodal distribution that had two pages running over budget. Every run on every budgeted URL - is now under 2000 ms, and `lighthouserc.json` asserts on the median rather than the best run. - If CI ever goes red here, the levers left are Source Code Pro (22 KB, owns the numerals and - spec labels) and Orbitron (11 KB, the display voice). Both are DESIGN.md §3 decisions, and - axis-trimming them was measured at 3.2 KB and 0.7 KB — so cutting a face outright is the only - meaningful move remaining. +- [ ] **Watch item, not an open problem: the LCP budget is met, tightest median 1577 ms against + 2000 ms.** The gate had been red on every CI run since Phase 09: the runner's Chrome 152 + fetches below-the-fold images during the initial load where Chrome 141 did not, and + `astro preview`'s HTTP/1.1 was costing 450 ms of simulated handshakes that Cloudflare's + HTTP/2 never pays. Closed by measuring over HTTP/2 (`docs/adr/0018-lighthouse-over-http2.md`), + subsetting the fonts (`docs/adr/0017-font-subset.md`), and a 672px image ladder step. If CI + goes red here again, the job log now prints each URL's LCP element, phases, and request + waterfall; start there. The levers left on the fonts are the Source Code Pro and Orbitron + weight axes, measured at 3.2 KB and 0.7 KB. diff --git a/src/components/ui/FeatureCard.astro b/src/components/ui/FeatureCard.astro index 0da9c6b..fd7046a 100644 --- a/src/components/ui/FeatureCard.astro +++ b/src/components/ui/FeatureCard.astro @@ -83,7 +83,7 @@ const plateIcon = image === undefined ? icon : undefined; sizes="(min-width: 64rem) 30rem, (min-width: 48rem) 45vw, 92vw" src={image} width={900} - widths={[400, 640, 768, 900]} + widths={[400, 672, 768, 900]} /> ) diff --git a/src/components/ui/PhotoTimeline.astro b/src/components/ui/PhotoTimeline.astro index 69f046f..78cdaca 100644 --- a/src/components/ui/PhotoTimeline.astro +++ b/src/components/ui/PhotoTimeline.astro @@ -38,7 +38,7 @@ const { label, photos, class: className } = Astro.props; sizes="(min-width: 48rem) 30rem, 92vw" src={data.photo} width={960} - widths={[480, 768, 960]} + widths={[480, 672, 960]} /> {data.caption !== undefined && ( diff --git a/src/components/ui/VideoLink.astro b/src/components/ui/VideoLink.astro index 82c845f..ab332c0 100644 --- a/src/components/ui/VideoLink.astro +++ b/src/components/ui/VideoLink.astro @@ -40,7 +40,7 @@ const { href, label, poster, class: className, rel = "noopener", ...attrs } = As sizes="(min-width: 48rem) 48rem, 92vw" src={poster} width={1024} - widths={[640, 840, 1024]} + widths={[672, 840, 1024]} />
{data.logo !== undefined && ( 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 a=S58sYE7Y9m*QjBNAueLT_$Hg+q4)oXhB2( literal 13156 zcmV-qGn>qJPew8T0RR9105fC&3;+NC09YIV05cE(0RR9100000000000000000000 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 K8SlUs0002?ye;Vf diff --git a/src/styles/fonts/inter-latin-wght-400-700.woff2 b/src/styles/fonts/inter-latin-wght-400-700.woff2 index e75ab5eb77ccaf43e01df4dd7e8b2e58fc2f4e7e..140b674cf1838d89d298cd57d7a4f035e259d4ae 100644 GIT binary patch literal 28248 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%0D=kz3c?N0Z6-d8;neB?utj$7MrIDp* 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 z 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 R3LwZIlGIK1k;z8@005jRp_%{y literal 36080 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 diff --git a/src/styles/fonts/orbitron-latin-wght-normal.woff2 b/src/styles/fonts/orbitron-latin-wght-normal.woff2 index 39d9db55bdf48b839d360769987abb69ae44ccfb..2302bca02104178f04b9b99bfd1efc3b81ff70d3 100644 GIT binary patch literal 11044 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 literal 11800 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

3bNFmBv9b>LvJJic+O4O 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 { + const cert = join(CERT_DIR, "cert.pem"); + const key = join(CERT_DIR, "key.pem"); + if (!existsSync(cert) || !existsSync(key)) { + mkdirSync(CERT_DIR, { recursive: true }); + execFileSync( + "openssl", + [ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-days", + "2", + "-subj", + "/CN=localhost", + "-addext", + "subjectAltName=DNS:localhost,IP:127.0.0.1", + "-keyout", + key, + "-out", + cert, + ], + { stdio: "ignore" }, + ); + } + return { cert: readFileSync(cert), key: readFileSync(key) }; +}; + +/** The file a request path resolves to, and the status it deserves. */ +const resolve = (pathname: string) => { + const direct = join(DIST, decodeURIComponent(pathname)); + if (existsSync(direct)) { + if (!statSync(direct).isDirectory()) { + return { path: direct, status: 200 }; + } + const index = join(direct, "index.html"); + if (existsSync(index)) { + return { path: index, status: 200 }; + } + } + return { path: join(DIST, "404.html"), status: 404 }; +}; + +const server = createSecureServer({ ...certificate(), allowHTTP1: true }); + +server.on("stream", (stream: ServerHttp2Stream, headers: IncomingHttpHeaders) => { + const pathname = new URL(headers[":path"] ?? "/", `https://localhost:${String(PORT)}`).pathname; + const { path, status } = resolve(pathname); + const extension = extname(path); + let body = readFileSync(path); + const response = new Map([ + [":status", String(status)], + ["content-type", TYPES.get(extension) ?? "application/octet-stream"], + ["cache-control", "public, max-age=0, must-revalidate"], + ]); + if (COMPRESSIBLE.has(extension)) { + body = gzipSync(body); + response.set("content-encoding", "gzip"); + } + response.set("content-length", String(body.length)); + stream.respond(Object.fromEntries(response)); + stream.end(headers[":method"] === "HEAD" ? undefined : body); +}); + +server.listen(PORT, () => { + console.log(`serving ${DIST}/ over HTTP/2 at https://localhost:${String(PORT)}`); +}); + +for (const signal of ["SIGINT", "SIGTERM"] as const) { + process.on(signal, () => { + server.close(); + process.exit(0); + }); +} From 97bb6ccbcf8baf93ca351d8585f58c671c8ce262 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 7 Sep 2026 00:58:52 +0000 Subject: [PATCH 20/20] ci: tell knip that serve.ts calls the system openssl Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Vaq66ucygSHc84MJXz1cac --- knip.jsonc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/knip.jsonc b/knip.jsonc index 8e646fb..702a357 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -3,6 +3,8 @@ // Cloudflare Pages Functions are file-routed, which knip has no plugin for; Lighthouse CI // starts tools/ci/serve.ts from lighthouserc.json, which knip does not read. "entry": ["functions/**/*.ts", "tools/ci/serve.ts"], + // serve.ts shells out to the system openssl for its certificate; it is not an npm binary. + "ignoreBinaries": ["openssl"], // tools/lint/anti-slop/shared is vendored upstream source (see its VENDOR.md). "ignore": ["legacy/**", "tools/lint/anti-slop/shared/**"], }