Conversation
Complete phased plan for the Next.js -> Astro rewrite: toolchain (pnpm/mise/oxlint/oxfmt/ESLint/Prettier split), design system, shadcn-convention Astro primitives, content collections, page ports, event landing pattern, asset/performance budgets, SEO/AI-SEO, analytics, and cutover. plan/00-overview.md records all locked decisions from the planning interview. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
…ules DESIGN.md is now authored with the plan (grounded in the extracted legacy palette, fonts, and live-site audit) rather than deferred to Phase 02, which becomes pure implementation. Phase 06's gate becomes a conformance check. New decision D25: no private/internal content on the main domain - _headers noindex reduced to preview/staging hosts only, dropping /team/*, /image/*, /video/* and enabling media indexing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
…GN.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
Settled in owner review against Brand Guidelines v1: brand-literal recessed surfaces (#262626 page / #171717 machined-pocket cards, grid floor on feature moments), AAA text ramp, fill-vs-text accent token pairs for all brand colors, Science Blue as informational + reserved light-theme primary, Source Code Pro as the data voice, engineering grid motif replacing the circuit pattern, real logo lockups in chrome, sticky header, /programs hub behind the nav dropdown, full-height mobile menu, and brand naming rules. Recorded as D26; navbar/footer specs and the new /programs page added to phases 02/05/07. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
Between-section devices for the dark ground: ambient pools, ghost section numerals (from the brand guidelines' section pages), ruler dividers, and tint bands - exactly one per boundary, with a restraint rule. Closes the flat/sterile-background review feedback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
…on marks The engineering grid now runs on the page ground itself (<=3%) so the dark surface reads as the drawing sheet the pockets are cut into; pools/numerals/ruler opacities raised per review; corner registration marks added as a feature-pocket garnish. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
Review finding: a page-level grid competes with the V2+V3 feature pocket floors and dulls the recess effect. Grid stays scoped to heroes, section breaks, and feature-pocket interiors; ground modulation comes from pools, numerals, ruler dividers, tint bands, and registration marks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
New DESIGN.md motif family with the t-shirt art as normative reference: highlight capsules (the Real Skills/Robots/Fun treatment), the engineering-drawing title block, scribed wireframe lineart, and labeled leader-line callouts - plus a register budget capping distinct blueprint devices at 4 per viewport. Phase 02 gains the task of sourcing the wireframe gear-bulb vector from the merch files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
Third metaphor layer: the engineer's hand markup over the machined sheet - chalk ovals (replacing the rejected pill capsules), highlighter swipes, chalk underlines, and sketch arrows, with a draw-on entrance as the one sanctioned decorative animation. Machined strokes stay perfectly straight; hand strokes stay imperfect - the contrast is deliberate. Register budget covers all three layers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
…adii Per design review: every hand-markup device ships >=3 SVG path variants (adjacent instances never match); highlighter swipes vary rotation/inset/alpha; chalk underlines work inline under short phrases as the sanctioned in-prose emphasis; sketch arrows may only target handwritten annotation text (new Architects Daughter annotation face, markup-only); machined/scribed layers stay perfectly geometric. Radius scale unified to the one-end-mill system: 4/8/12, 16px and pills retired. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
The overhaul lands as one gh-stack: this plan/DESIGN branch is the bottom layer targeting staging, each phase is a branch stacked on the previous phase's branch, and the whole stack cascade-merges into staging at cutover - no long-running astro-rewrite branch, no manual merge sequencing. Restack rules, per-layer CI/preview notes, and cutover mechanics updated; stale radius-xl references in phase 02 cleaned up and hand-markup primitives added to its task list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DU6XwSGRUDZtz5DuXjzKnn
…ain, CI Park the Next.js site in legacy/ and stand up the Astro project it will be replaced by, with the full final toolchain in place before any UI exists. - mise pins node 26.7.0 + pnpm 11.22.0 with committed checksums; tasks forward to package.json scripts, which stay the single source of truth. - pnpm-workspace.yaml enforces a 7-day release cooldown and denies install scripts except sharp and esbuild. Dependency pins are therefore the newest version at least a week old, not the newest version. - Astro 7 static scaffold: sitemap integration, Tailwind v4 via the vite plugin, strictest tsconfig, cn re-exported from a single sanctioned site. - oxlint (type-aware, with anti-slop vendored from dmmulroy/anti-slop) and oxfmt own .ts/.js/.json/.css; ESLint (typed + jsx-a11y-strict) and Prettier own .astro and .md. Ownership table and invocation quirks in docs/tooling.md, rationale and the oxc migration seam in docs/adr/0001. - knip, .editorconfig, VS Code settings, AGENTS.md (CLAUDE.md symlink), and a PostToolUse hook that routes each edited file to its owning toolchain and feeds lint failures back. - CI gates every PR on check + build + an offline link check over dist/. Typechecking functions/ surfaced a real bug: an absent CF-Connecting-IP header was being sent to Turnstile as the string "null". The header is optional, so it is now omitted when missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
Astro emits root-relative hrefs (/_astro/index.*.css). In --offline mode lychee cannot resolve those against a local file tree without knowing what the root is, so it errored on every page. CI's check and build steps passed; this was the only failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
Accessibility linting was inert. `eslint-plugin-jsx-a11y` is an optional peer of `eslint-plugin-astro`, so `astroConfigs["jsx-a11y-strict"]` registered zero rules and an alt-less `<img>` passed `pnpm lint`. It is now a direct devDependency, and `pnpm-workspace.yaml` allows eslint 10 against its stale `^9` peer range. The `clsx`/`tailwind-merge` and `legacy/*` import bans lived only in `.oxlintrc.json`, which ignores `**/*.astro` — the file type the site is built from. `eslint.config.ts` restates them. `functions/` enabled `strict` alone, so the only code handling untrusted request input typechecked more loosely than `src/`. It now carries the same `noUncheckedIndexedAccess`/`exactOptionalPropertyTypes` flags as the root. The format-lint hook's `run()` propagated its `&&` chain status, so a missing binary was indistinguishable from a lint failure: before `pnpm install`, any `.ts` edit was rejected with an empty message. Missing tools now return a sentinel and let the edit through. Tailwind's source detection scanned the checked-in `legacy/` tree, emitting utilities for the retired Next.js site into the stylesheet every page links (47,119 -> 9,910 bytes here, with identical HTML). `@source not` excludes it. CI reinstalled the whole dependency tree from the network each run; mise-action caches tool binaries, not pnpm's store. Added an `actions/cache` step keyed on the lockfile and `--prefer-offline`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
`oxfmt --check` was failing CI on two files: `.vscode/settings.json` (a `.json` file, so no trailing commas) and `knip.jsonc` (a `.jsonc` file, where oxfmt keeps them). Applied the formatter's own output. Removing `src/lib/cn.ts` left `cnfast` with no importer, which knip's `dependencies: "error"` rule reports. The dependency now arrives in Phase 02 alongside the first component that merges classes; `plan/01` says so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
`pnpm typecheck` runs `tsgo -p functions` instead of `tsc -p functions`, via `@typescript/native-preview`. Measured on this tree: 961 ms -> 182 ms. Verified it enforces the same config — the seeded `noUncheckedIndexedAccess` and assignability probes both fail as they did under tsc. `astro check` stays on the JavaScript compiler, so `typescript` remains a dependency. `@astrojs/check` peers `typescript: ^5.0.0 || ^6.0.0` and its language server is built against that compiler's API; the native preview exports only `version` and `versionMajorMinor`, shipping no `typescript.js` or `tsserver.js` that could stand in. `typescript-eslint` independently peers `>=4.8.4 <6.1.0`, so the JS side cannot move to 7 either. Both constraints are recorded in docs/tooling.md with the version to re-check. oxlint's type-aware pass was already on the Go toolchain via `oxlint-tsgolint`, which ships its own `tsgolint` binary — no overlap with the native preview. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
Vendored @nkzw/oxlint-config (MIT) to tools/lint/nkzw/ and extended it by path from .oxlintrc.json. 146 of its 185 rules apply here; tools/lint/nkzw/VENDOR.md records what was dropped and why — 35 react rules (this site ships no client-side framework), 3 @nkzw rules and 1 no-only-tests rule needing packages we are not installing, and 2 overrides for upstream's own directory layout. Upstream's `.ts` override is kept: it disables the correctness rules TypeScript already covers. Every rule is now an error. `perf` moved from warn to error; `style` stays off. oxlint reported 34 violations across the existing tree; all are fixed. Three needed a decision rather than autofix: - `functions/types.ts` carried `result?: any` behind a `biome-ignore` comment for a linter this repo no longer uses. It is `unknown`, which is what the two call sites pass anyway. - `eslint.config.ts` used a namespace import for the Astro parser; the package has no default export, so it names `parseForESLint` — the interface ESLint actually consumes. - `no-console` is off under `functions/**`. A Worker's console is its log stream, which is not the unintended-logging case the rule guards. Also fixed a pre-existing hole the new config surfaced: the `legacy/` import ban used `legacy/*`, and a single star matches one path segment — so `legacy/data/config` and every deep relative path were allowed. Both linters now use `["legacy/**", "**/legacy/**"]`, verified at four depths. Trailing commas are now explicit: `all` for code, `none` for JSON/JSONC/JSON5 via an oxfmt override, matched in Prettier for `.astro` frontmatter. `.prettierrc` became `.prettierrc.json` so the JSON override actually matches it — otherwise oxfmt wanted a trailing comma in Prettier's own config. Format-on-edit had a gap: oxfmt's directory scan covers `.yaml`/`.yml`/`.toml`, so an unformatted one fails `pnpm check`, but the hook did not route those extensions. It formats them now (no lint pass — oxlint has no rules for them). VS Code gets an explicit formatter per owned language rather than relying on the global default, plus `eslint.validate` for `.astro`. Astro cannot move to TypeScript 7 yet, and the reason is upstream: TS 7 exposes no stable programmatic API, so every Volar-based language server is pinned to TS 6 (withastro/roadmap#1321). Recorded in docs/tooling.md with the maintainer quote and what to watch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
Turn DESIGN.md into working CSS: the full token set, fluid type scale, self-hosted fonts, program themes, the motif components, and a /styleguide page that proves all of it. Contrast is verified at build time, not by eye. /styleguide computes every pair required by DESIGN.md §9 and throws with the offending pairs listed if one drops below its floor, so a token edit cannot quietly break accessibility. That verification caught two defects in the spec, amended here per DESIGN.md §11: - Science Blue's fill used a white label, which measures 3.5:1 — below AA, on fills that carry chip-sized text. Changed to the brand near-black (4.9:1), which also makes blue consistent with every other fill. - body's stated ratio was "≈11.5:1"; it measures 10.2:1. Still AAA. Also added a destructive pair, which §8 required but the doc never valued, and pinned the highlighter swipe at 25% alpha — the only value in the documented range that keeps white text at AAA. Fonts are declared face by face rather than through the @fontsource CSS, so only latin subsets reach the build: five woff2 files, no cyrillic or greek. Program themes remap exactly four tokens — primary, primary-bright, primary-foreground, ring — diff-checked in the built CSS. The brief said three; DESIGN.md §2 says four, and without primary-bright a program page's links and stats would stay yellow on a green page. Hand-markup strokes carry pathLength="100" so the draw-on animation's dash math is in percent; a hard-coded length truncates any path longer than the guess, which is what first left the chalk ovals as open arcs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
Three DESIGN.md §8 rules were declared but not in force. In-prose links set only underline thickness and offset, and preflight's `text-decoration: inherit` resolved the line to `none`, so links were colour-only; `spec-chip` used an opaque `currentcolor` border where the doc asks for 40% alpha; and adding `svg` to the base `max-width: 100%` rule clamped ChalkOval's deliberate 2rem overshoot, since `max-width` cannot be overridden by a `width` utility. `SketchArrow direction="right"` was a silent no-op: Tailwind v4 implements `-rotate-45` with the individual `rotate` property, which the per-variant inline `style` overrode. Direction is now summed into that one value; mirroring stays a class because `scale` does not collide. `--color-body` and `--text-body` both claimed `.text-body` and the colour won, leaving the size token unreachable and the type-ramp row demonstrating a colour. Added `@utility text-copy` and pointed the ramp at it. `/styleguide`'s contrast gate compared a hand-typed copy of the palette against itself, so a token edited only in the stylesheet left the build green. `@/lib/tokens` now parses the `@theme` and `[data-theme]` blocks out of `global.css`, and the program themes' `primary-bright` accents are gated too — with the theme list read from the stylesheet, so a new program cannot ship an unchecked accent. Verified: dimming `--color-muted`, renaming a token, breaking the FLL accent, and changing the swipe alpha each now fail the build. DESIGN.md's accent ratios disagreed with the repo's own verifier. Danger Orange is 6.6:1, not 7.9:1 — the old number sat above the 7:1 AAA line while the real value sits below it. Corrected alongside Safety Yellow (9.8), Science Blue (5.9), destructive text (7.9) and the swipe (7.6), and the measured value now renders per theme on the page. `Callout` emitted `<figcaption>` with no `<figure>` anywhere on the page. It renders `<p>` by default; `as="figcaption"` is opt-in and the caller owns the figure. `ambient-pool-neutral` overrides only a `::before` background, so used alone it emitted no pseudo-element — documented as the modifier it is. Quality: the §2.3 engineering grid was defined twice, in CSS and as an inline style with a raw `#FAFAFA`, already disagreeing on alpha; it is now one `@utility engineering-grid` driven by custom properties off `--color-foreground`. The §13 stroke contract was duplicated across three hand-markup devices, which had already drifted on `vector-effect`; it is now `@utility hand-stroke` plus a shared tone mapping. RegistrationMarks encoded one bracket as four hand-computed paths. The four `public/image/svg/logo-*.svg` files were byte-identical to `src/assets/brand/` and unreferenced, deploying 48 KB unhashed. The page was `noindex` and in the sitemap — a "Submitted URL marked 'noindex'" error. Added a sitemap filter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
The zero-JS primitive layer: Button, Card and its sub-parts, Badge, Input,
Textarea, Label, FieldError, Separator, Accordion, Dialog, Carousel, Icon,
Skeleton — every one on /styleguide in all variants and all three themes.
Interactivity stays as high up the ladder as it can: Accordion is native
<details> with the name attribute for exclusive open, Dialog is native
<dialog> so the browser owns the focus trap and Esc, Carousel is a
scroll-snap track. Together the whole styleguide ships 835 bytes of inlined
script, and a page built from only the static primitives ships none.
Two silent bugs surfaced here, both of which would have spread across every
page:
cn() was dropping font sizes. Tailwind builds text-* utilities from both
--text-* and --color-*, and the merge step only knows Tailwind's stock scale,
so it treated every text-* class as one conflict group and kept the last.
cn("text-primary-foreground", "text-body") collapsed to text-body, which
rendered every primary button's label in body gray on Safety Yellow — 1.3:1,
measured in the browser. @/lib/cn now registers the DESIGN.md §3 type scale as
the font-size group; those buttons measure 11.7:1, and size-plus-color pairs
keep both classes.
Relatedly, text-body is a color and not a size, so Button, Input, and Textarea
were asking for a size and getting none. They use text-copy now.
Icons follow ADR 0002: @tabler/icons inlined at build, no astro-icon or
Iconify. The package's exports map rewrites every subpath including
package.json, so the icons directory is located through a known icon instead.
CVA recipes live in sibling *.variants.ts files, since Astro forbids exporting
values from a component. That also lets one component reuse another's recipe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
Four primitives had behavior that was declared but never ran. Carousel's scoped `[data-carousel-track] > *` was rewritten by Astro to require this component's own scope attribute, which slotted slides never carry — so `scroll-snap-align`, `flex: 0 0 100%` and the `flex-basis` media rule matched nothing. Slides collapsed to content width, nothing snapped, and `itemBasis` was dead. `:global(*)` on the child escapes the scope while keeping `define:vars`. FieldError's `empty:hidden` could never match: `:empty` requires no child nodes and the `<Icon>` was unconditional, so a placeholder rendered a bare red alert triangle. The icon is now gated on slot content. Button spread `disabled` onto the `<a>` path, where the attribute is invalid and `:disabled` never matches — a disabled link rendered at full opacity, fully clickable, and typechecked cleanly. It maps to `aria-disabled` (which the variants already style) and the `href` is dropped. `Props` also extended only `HTMLAttributes<"button">`, so every external link button the site needs failed `astro check`; it now picks up `download`/`hreflang`/`rel`/`target`. Accordion's `name` became an unused `data-accordion-name` with no consumer, so the wrapper's documented exclusive-open was a no-op — the styleguide worked only because it repeated `name` on each item. Dropped the prop; the JSDoc and the `shadcn-astro` worked example now say where `name` belongs. Icon emitted `stroke`/`stroke-width` unconditionally, but Tabler's filled sources carry no stroke, so filled glyphs inflated ~1px on every edge and thickened narrow details. Carousel's explicit `behavior: "smooth"` bypassed the reduced-motion `scroll-behavior: auto !important` in global.css; the track's `scroll-smooth` class supplies it instead. The styleguide's section numerals ran 1–6, 8, 7. Quality: `Field.variants.ts` holds the recipe `Input` and `Textarea` were copy-pasting — five of six lines byte-identical, which is the reuse the sibling-variants convention exists for. `Button` gained a `pocket` variant so the Carousel arrows and the Dialog close compose it rather than hand-building icon buttons; the Dialog's was `p-1`, below the 44px minimum the shared recipe enforces. `tools/checks/cn-font-size-group.mjs` makes `cn`'s font-size group drifting from the `--text-*` tokens a `pnpm check` failure instead of a comment — that invariant fails invisibly, and it already cost this phase a 1.3:1 contrast bug. The brief's carousel keyboard criterion had been rewritten in place to describe what shipped. Restored, with the arrow decision recorded under "Deviations from this brief" alongside the rest. Newly-live jsx-a11y rules (see PR #15) caught two real violations: `href="#"` on the styleguide's demo links, and `tabindex="0"` on the carousel track. The track is a keyboard-reachable scroll container, so it is `role="region"` with a name, and that one role is added to `no-noninteractive-tabindex`'s allowlist — dropping the tabindex would make the slides keyboard-unreachable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
Everything that changes over time moves out of code and into zod-validated markdown. Adding a sponsor is now a logo file plus five lines of frontmatter. Migrated: 10 sponsors (7 active, 3 as active: false), 6 FAQ answers, both seasonal events, 6 robots, 14 team photos. Copy comes across verbatim, typos included — revision is Phase 12 (D8). Former sponsors are records with active: false rather than commented-out code, so we can still say who supported us in 2022. src/data/site.ts replaces every hardcoded constant the legacy pages carried: the four external URLs from data/config.ts, both Google Calendar IDs, the GA4 measurement ID, the workspace address and directions link, socials and contact. Schemas stay flat on purpose (D2) — strings, enums, booleans, dates, numbers, images — which is what keeps a git-backed CMS a later addition rather than a restructuring. events therefore carries locationName/locationAddress instead of a nested object. Two things worth knowing: Collection names avoid slashes (frcRobots, not frc/robots) while the content still nests under frc/ and fll/ on disk as D18 requires. Astro writes each collection's editor JSON schema to .astro/collections/<name>.schema.json without creating intermediate directories, so a slashed name warned on every build and silently dropped frontmatter autocomplete. news/template.md is a real entry with draft: true rather than a glob-excluded _TEMPLATE.md. An excluded template drifts from the schema unnoticed and leaves the collection empty, which warns on every build; as an entry it is validated and still never renders. "Test Sponsor" was deliberately not migrated — it is the one commented-out legacy entry that is a test fixture, not a former sponsor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
`image()` in the schemas commits every moved asset to astro:assets, but pnpm's isolated layout keeps Astro's own `sharp` where the bundled image service cannot resolve it — the first page to render a collection image gets one `MissingSharp` warning per asset and an unoptimized passthrough. Reproduced, then declared `sharp` in dependencies with ADR 0003. `z.object` strips unknown keys, so a typo'd field name built clean and silently dropped the value — the one thing `docs/content.md` promises it does not do. The acceptance criterion had been verified with a bad *value*, which always failed correctly. All seven schemas are `z.strictObject`; `sinceYear` for `since` now exits 1 with `Unrecognized key`. The `program` enum accepted `sc2` (the open house uses it) while `programs` had only `frc` and `fll`, so `programs[entry.data.program]` was undefined for that event. The enum derives from `PROGRAM_KEYS` and the map `satisfies` a total record over it. `astro.config.ts` reads `site.url` rather than declaring the canonical origin a second time. `z` from `astro:content` is deprecated with removal slated for the major already pinned — switched to `astro/zod`, and `z.string().url()` to Zod 4's `z.url()`, clearing 53 check hints. Content that legacy had and no schema field could hold: the kickoff's two season teaser URLs and two game-hint links (`teaserUrls`/`hintUrls`/`hintLabels`, flat per D2), its hero subtitle, which had become an `##` with no section under it, Robo Fett's "2020-2021" two-season label, and the Viper and TroubleClef wordmark overlays. The kickoff `description` is legacy's own again rather than new prose. Reference integrity is now enforced where the reference is declared: `tools/checks/content-references.mjs` fails `pnpm check` on a dangling `faq` slug, which Astro only logs before exiting 0. Quality: the workspace address and directions URL were written into every event's frontmatter next to the copies in `site.ts`; location is optional and defaults from there, so only an off-site event carries one. `displayDate` restated `start`/`end` as prose, and the open house stated its date twice more — in body copy and in an FAQ answer; `src/lib/event-date.ts` formats the timestamps, and Phase 08's acceptance criteria require closing its knip seam. Dropping the blanket `src/data/site.ts` knip entry restored export-level checking there, with `@public` on the two exports that have a known future consumer. The moved masters were camera-resolution — up to 6000px and near-lossless — so `<Image>` derived variants from them that came out larger than the source and cost ~22s of sharp time per build. `tools/assets/optimize-sources.mjs` (plan/09 §2) caps sources at 2560px: 17.8 MB saved across 12 files. `openhouse-header.webp` went back to `public/image`, where Phase 09's inventory-based prune can still see it — nothing references it. Also: `src/data/site.ts` described a `functions/api/calendar/` that does not exist, and `docs/content.md` linked to a route that does not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
The chrome every page shares, with SEO correctness built into the type signature rather than left to each page: BaseLayout requires title and description, so omitting either is a compile error instead of something a reviewer has to catch. Seo emits title, description, canonical, the full Open Graph set with image dimensions, and a Twitter card. No keywords meta — legacy carried 22 terms for a signal search engines dropped years ago. jsonld.ts holds typed builders: NGO (the specific type for a nonprofit, not bare Organization) on every page, WebSite on the homepage, and breadcrumbs ready for the nested pages in Phases 07-08. Navbar follows D26: the real lockup, full-width on desktop and the square mark on mobile, never a rebuilt gear plus the name in Inter. Programs is a real link to /programs with a dropdown in addition, so touch and no-JS both get a destination — verified that the panel opens on keyboard focus and Tab moves into it. The mobile sheet is full-height with 44px targets, locks body scroll, and closes on Esc with focus returned. The only script is the ~25-line toggle. Footer is a recessed card band carrying the lockup and the engineering title block. It points Donate at the real /donate page rather than legacy's /wiki/donations, and adds a Programs column — with the mobile sheet closed, the footer is otherwise the only place a program link appears. sharp becomes a direct dependency: the first real <Image> use made astro:assets need it, which allowBuilds already anticipated. The icon set is rendered from the square mark rather than committed by hand; the apple-touch icon is flattened onto the brand ground because iOS ignores transparency. exactOptionalPropertyTypes forced one signature choice worth knowing: a layout that forwards optional props passes explicit undefined, which that flag treats as distinct from an absent prop, so the receiving props are declared `?: T | undefined` instead of filtering props at every call site. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
The app shell links to the full site's routes, but Phase 07 builds them, so the offline link check failed on every page carrying the header and footer: 65 errors across ten routes. That is a correct finding about an unavoidable ordering problem, not a tooling fault. .lycheeignore excludes exactly those ten routes and nothing else, so the gate stays live for assets, anchors, and any link outside the list — verified by seeding a bogus href, which still fails the check. An exclusion list like this is a trap once its pages land: the entry silently keeps that page out of the link check forever. tools/checks/stale-link-ignores.mjs fails CI if an excluded route now exists in dist, so the list can only shrink. Phase 07's acceptance criteria now require the file to be gone. Verified locally with lychee 0.24.2 using the workflow's exact flags: 0 errors, and the guard correctly fails when a landed route is left excluded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YRfxMh7FLjQtDbb1BEsCbR
Four things in the mobile menu did not work. `Icon.astro` has no rest spread, so `data-menu-icon-open`/`-close` never reached the DOM and the hamburger never became an X — the `?.` swallowed the nulls and TypeScript does not check hyphenated JSX attribute names. Backdrop close was unreachable: the sheet's `<nav class="h-full">` is exactly the sheet's box, so `event.target === sheet` could never be true. Crossing `md` with the sheet open left the page scroll-locked and unscrollable with no visible control, since both the sheet and its toggle are `md:hidden`. And nothing made the page behind the opaque sheet inert, so Tab past the last item walked every invisible link in `main` and `footer`. The sheet now keeps one piece of state — `aria-expanded` — with CSS deriving the icon swap and the scroll lock from it, a `matchMedia` listener closing it at `md`, and `inert` on everything in the shell that is not the toggle. Verified in a real browser: icon swap, Esc, scroll lock, inert (only the close button stays focusable), and the breakpoint close all behave. The Programs panel was a `:focus-within` disclosure with no dismissal path, which could not meet this phase's own "Esc closes" criterion (§9) or WCAG 2.2 SC 1.4.13 — and that criterion had been rewritten to describe what shipped. It is a native `popover` now: Esc, light-dismiss, top-layer and the `expanded` state come from the platform with no script, and the criterion is restored. The sticky header animated `height` on a scroll timeline, relayouting itself and reflowing the page on every frame through the first 120px, against §6's "only `opacity` and `transform` animate" — it animates `scale`. The header logo was `loading="lazy"`, invisible to the preload scanner despite being an LCP candidate; it is `priority`. `min-h-dvh` on a block `<body>` stretched nothing, leaving a lighter strip below the footer band on tall viewports; the body is a flex column and `main` absorbs the slack (measured: 0px gap at 1600px tall). `stale-link-ignores.mjs` silently dropped any entry it could not parse, so a malformed exclusion escaped the staleness guard forever while lychee still applied it — exactly the permanent blind spot it exists to prevent. It fails loud now. The `og:image:width`/`height` block was dead for every page, since the default `ogImage` is a string; the default card's dimensions travel with its path in `site.ts`. The manifest's maskable entry reused the unpadded 512, whose artwork spans ~88% of its box, so a launcher mask clipped the gear teeth — a padded render at 72% is a separate file. Quality: the route inventory was declared four times across two components and the 404 page, already diverged, with the Footer re-typing program names `site.ts` already held; it is one `nav` structure there, and the desktop/sheet split is a `surfaces` field rather than a discrepancy. DESIGN.md §8's link contract had no implementation home — twelve call sites wrote `no-underline` and no chrome link underlined on hover; `ui-link` and `external-link` are utilities, as is §5's container rule, which was a class string at five sites. The Footer declared its socials twice and re-implemented `CardTitle`. `site.webmanifest` is a generated route reading `site.ts` instead of restating the org name, a third description and both brand colours. `organization`/`webSite` are constants, not zero-argument factories. Dropping the `cn.ts`, `fonts.ts` and `site.ts` knip entries — all genuinely referenced as of this PR — restored export-level checking in the two most central modules; `jsonld.ts`'s entry became `@public` on the one unused export. Also fixed a regression this PR nearly introduced: the first draft toggled the menu icons from a scoped `<style>`, which Astro rewrites to demand this component's scope attribute — the same failure the Carousel had, since `Icon.astro` renders its own root. `:global()` on the icon side, verified in the built CSS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BX5PrKuYNRLVxiEj3eejhs
Reuse and altitude: - nav data now actually drives the chrome: the Programs disclosure is a panel flag, the sheet's Donate button is a sheet-cta surface, the footer references nav.calendar instead of nav.programs[3], and the dead "footer" surface value is gone - browser-chrome colors read the CSS tokens (Seo, manifest); theme-color follows the program theme; the icon inventory lives in site.icons and is projected into head links, the manifest, and JSON-LD - ProgramLayout derives its props from BaseLayout, restoring ogType and noindex passthrough; og:image:alt travels with the image - button/badge variant inventories are exported and the styleguide renders them, durations, and program themes from the same sources the build gates on (the missing pocket variant now shows) - hand-markup register shares its variant type and base classes; --breakpoint-md is a declared token read by CSS and the sheet script Efficiency: - the navbar fetches one logo per viewport via <picture> instead of eagerly downloading both variants on every load - tick-strip SVGs collapse to single paths (styleguide HTML -32 KB); programColor memoizes its stylesheet scan; the carousel reads the track gap instead of hardcoding 16; tool scripts do one read per collection and one sharp pipeline per image Also deletes edit-history and restating comments per the comment policy, and drops the unused cloudflareBeaconToken placeholder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Eyebrows take primary-bright (fill-vs-text law); one carrying a numeral takes the spec-label voice. - The closing tagline keeps each "Real X." pair together, stacks below sm, and the chalk ovals clear their neighbours. - Section padding is never doubled: the footer drops mt-24 and adjacent sections on one ground share a padding. - FIRST® stands upright in Orbitron headings, its ® drawn by Inter through the display stack. - Only standalone dates take the mono voice; the donate form and its card title rules align. - Meta-refresh stub pages (openhouse, kickoff) also emit a 301 in _redirects. - DESIGN.md §1, §3, §4 record the rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFeYttZCfuyKpR2hJqLfK4
…wipes - FRC hero h1 is two lines; the rest of the sentence leads the subhead. - Donate's hero carries the PayPal button; Sponsors' hero carries "Become a sponsor". - SponsorTier picks its column count from the sponsor count, and a lone sponsor takes SponsorCard's full-width feature layout. - Highlight swipes run 35% at display sizes and 25% at body size; the styleguide build check measures each against its floor. - DESIGN.md §2.13 and §5 record the rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFeYttZCfuyKpR2hJqLfK4
Depth drawn rather than rendered: cut into metal, laid on the table. - band (was tint): #121212 drafting table with a primary top edge, the brand edge vignette and a feathered two-weight grid; pockets on it become raised plates. A build step fails any page where two bands touch, footer included. - Buttons are keycaps: a hard 3px offset that lifts on hover and presses flat; the outline keycap's edge meets 3:1. Reduced motion keeps the face still. - Pockets cut deeper with a visible hover; framed media takes a 6px drafted edge; photo heroes carry the grid on their copy side. - Header (md+) and footer use the brand's Light lockup; mobile keeps the colour mark. - The styleguide shows the surfaces and states, and its contrast check covers band, plate, grid crossings and control edges. - DESIGN.md §2, §2.3, §2.4, §4–§8 and §10 record the rules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFeYttZCfuyKpR2hJqLfK4
The panel is a popover in the top layer, where `absolute top-full` resolves against the viewport, so it opened just below the bottom of the screen. CSS anchor positioning places it under the nav item, with a toggle-time fallback where anchors are unsupported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFeYttZCfuyKpR2hJqLfK4
The grain tile's gamma goes 4 → 5 and its alpha 0.4 → 1: the spread rises from ~4 to ~6 levels while the mean lift moves only ~5 → ~8, so the sheet reads as textured without lightening. DESIGN.md §2 records the new figures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFeYttZCfuyKpR2hJqLfK4
The staging rule matched one host by name, so any other preview subdomain (astro.scstem.org) was indexable. https://*.scstem.org/* covers them all; the apex, production, does not match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RFeYttZCfuyKpR2hJqLfK4
Tighter heroes, intros and CTAs across home, programs, FLL, FRC, about,
sponsors, donate, get-involved, contact, calendar, 404, the events and
the FAQ. Each pitch now lives on one page instead of being copied
between several. Facts corrected: two programs, founded 2012, no drone
teams or maker spaces, kickoff from noon. Grammar and typo fixes along
the way ("premier", "partner with", "teamwork", "begins", en dashes).
The robots page is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RFeYttZCfuyKpR2hJqLfK4
overhaul: cutover pre-flight, parity records, legacy removal
Moves every design token into src/styles/tokens.css: plain CSS with no Tailwind, so the team wiki (Starlight) and web apps can import the same file. global.css maps it onto Tailwind with `@theme inline`, so utilities read the tokens where they are used and a switch on any element reaches everything inside it. Three switches: data-mode (dark default, light), data-accent (yellow default, green, orange) and data-register (blueprint). Colors are written once as light-dark(light, dark); the grain and the link underline, which light-dark() cannot carry, switch in [data-mode] blocks. Program pages now set data-accent from programs[key].accent in place of data-theme. @/lib/tokens resolves a token for any finish, accent and register and throws on anything it cannot parse. /styleguide gates every text, accent, swipe, tier and control-edge pair in all twelve combinations. The light and blueprint values are in place but nothing switches to them yet, so the dark site renders as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
DESIGN.md takes every decision from the review: the three switches and the portable token file; the clear-anodized light finish with ink links and a yellow marker; the tuned light scrim; dark first with a remembered toggle; yellow, green and orange accents; the cyanotype and whiteprint blueprint register with its drawing frame; the felt-marker stroke and drifting underline; six call-out devices with their rules; grid bubbles, construction-line registration marks and lit pocket floors; nine text roles on three line heights; nine spacing steps by convention; three radii by kind of object; IBM Carbon icons. plan/todo.md records the open question the review left: frame or soft margins for blueprint pages outside the wiki. AGENTS.md points at tokens.css. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
Type (DESIGN.md §3): nine roles on three line heights — tight 1.1, snug 1.3, relaxed 1.6. body-lg becomes lead and the reading size becomes copy, so text-copy is a real token rather than a hand-written utility beside the body color. Eyebrows are labels (Source Code Pro caps), so eyebrow.ts and the Orbitron eyebrow utility go; stats are the data font at h2 size. Spacing (§4): tokens.css names the nine steps by their Tailwind numbers (--sc2-space-1 … 24) as the convention; other values stay allowed. Icons (§8, docs/adr/0023-carbon-icons.md, superseding 0002): @carbon/icons 11.88.0 replaces @tabler/icons. Icon.astro inlines the 32px masters filled in currentColor, and every call site uses Carbon's names. Its telemetry postinstall is denied in pnpm-workspace.yaml. Also fixes the token commit's transitions: twelve duration-(--duration-*) and ease-(--ease-*) classes read variables that @theme inline no longer emits, and now read the --sc2-* tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
Grid bubbles (DESIGN.md §2.6) replace ghost numerals: GridBubble draws the circled section number and a dash-dot center line across the container, and Section's atmosphere="numeral" becomes "bubble". GhostNumeral and the numeral type tokens go. RegistrationMarks (§2.8) draws construction lines crossing just outside each corner of the pocket, after the lockup on the t-shirt front, in place of inset brackets. Pockets take the lit floor (§2): a shade-to-lit overlay on the fill, so hover still changes only the fill. The feature grid is a layer above it through --pocket-grid, and plates on a band stay flat. /styleguide gates text on both ends of the floor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
Hand markup (DESIGN.md §2.12–2.20): - The shared stroke is the felt marker: 4.5px at 90% opacity. Marks take the accent's marker color (text-mark), and handwriting takes its text color, since the light finish's yellow marker fails as text. - Link underlines drift upward toward their end, in three variants rotated among sibling links with :nth-of-type. The eighteen strokes (three variants, three accents, two finishes) are written into tokens.css by `pnpm assets:underlines` from the paths and the colors the file declares. Links are ink on the light finish, where the yellow stroke alone marks them. - RevisionCloud: CSS-drawn scallops that fit any content, with a delta tag. - StrikeCorrect: a struck word with a handwritten correction; assistive tech reads the correction only. - ChalkOval takes around="control" to ring a button; SketchArrow gains up-left and up-right so a leader note can point at a control. Tick marks and margin brackets stay in DESIGN.md until a page uses them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
Every page opens dark (DESIGN.md §5). ModeToggle, a neutral keycap in the header and a labelled row in the mobile menu, switches to the clear-anodized light finish and remembers the choice in localStorage; an inline, blocking script in BaseLayout applies it before first paint, so a light-mode visitor never sees a dark flash. The light: variant styles the light finish. The header and footer lockup is now one file drawn as a CSS mask and filled with the logo token (brand White or Black), so both finishes share a single fetch; below md the square mark stays. Photo headers take the tuned light scrim, solid further across the copy with the photo pulled back slightly (§7). Sponsor and FIRST logos, all drawn for a dark ground, keep it on the light finish on a print token that is transparent on the dark one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
BaseLayout takes register="blueprint" (DESIGN.md §2): the page wears the cyanotype or whiteprint tokens, and its main is a drawing sheet. From xl up the drafting grid fills the page margins, and DrawingFrame lays plain ground inside one frame line at the container's edge, with zone numbers along the top and letters down the left, so no grid line crosses text. Photos on a blueprint page go grayscale under the print's scrim. The 404 is the first page in the register: the sheet that is not in the set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
The `light:` variant and the hero's light-finish photo filter now skip anything inside a `data-mode="dark"` element, so a dark panel on a light page keeps its dark styling. Also brings the primitives README token note and the tooling script table up to date with tokens.css and assets:underlines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
/design-language/ explains the design system for the people who build with it: the idea, the three switches (with a live preview panel), which token to reach for, type, space and radius, surfaces, components, the marker, the blueprint register, recipes and a ship checklist. It is a draft on its way to the team wiki. Nothing links to it, it sets noindex (so the sitemap leaves it out), and _headers adds X-Robots-Tag: noindex. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
Astro does not unwrap `:global()` nested inside `:not()`, so the dark-island exclusion made the selector invalid and the filter matched nothing. The rule is now global as a whole. Also adds the todo to move /design-language/ to the wiki and take it off the site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X6mmyYzaUyrZPwBpPMpN6u
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.