Skip to content

overhaul: 02 design system - #16

Open
CS-5 wants to merge 3 commits into
overhaul/01-foundationfrom
overhaul/02-design-system
Open

overhaul: 02 design system#16
CS-5 wants to merge 3 commits into
overhaul/01-foundationfrom
overhaul/02-design-system

Conversation

@CS-5

@CS-5 CS-5 commented Aug 28, 2026

Copy link
Copy Markdown
Member

Layer 2 of the overhaul stack, on overhaul/01-foundation. plan/02-design-system.md.

DESIGN.md turned into working CSS. ⚠️ This PR amends DESIGN.md — see "Design amendments" below, it needs your eyes.

What's here

Tokens (src/styles/global.css) — the full §2–§4/§6 set as a Tailwind v4 @theme block: the recessed surface system, the AAA text ramp, fill/text accent pairs, sponsor tiers, the fluid type scale as clamp() between 360px and 1440px, the three-value radius scale, motion durations and easings.

Program themes remap exactly four tokens — primary, primary-bright, primary-foreground, ring — and nothing else, diff-checked in the built CSS.

Fonts, self-hosted. Declared face by face rather than importing the @fontsource CSS, because those ship every subset: this way dist contains 5 latin woff2 files and no cyrillic, greek, or vietnamese. src/styles/fonts.ts exports the two above-the-fold URLs for Phase 05's preload.

Motifs as components so pages can't reinvent them: Pattern (engineering grid + dimension ticks), GhostNumeral, RulerDivider, RegistrationMarks, TitleBlock, Callout, and the hand-markup set — ChalkOval, ChalkUnderline, SketchArrow, each with three distinct paths. Plus utilities for the pocket anatomy, accent rules, media frame, highlighter swipe, ambient pools, spec labels and chips.

/styleguide — every token, ramp, and motif rendered once, with computed contrast ratios, and each section repeated under data-theme="frc" and "fll". Noindexed.

Contrast is verified at build, not by eye

The page computes every pair DESIGN.md §9 requires and throws with the offending pairs listed if one drops below its floor. Verified by temporarily dimming body to #8A8A8A:

Error: DESIGN.md §9 contrast floors violated — fix the tokens in src/styles/global.css:
  body (#8A8A8A) on #262626 is 4.3:1, below its 7:1 floor
  body (#8A8A8A) on #171717 is 5.1:1, below its 7:1 floor

A token edit can't quietly break accessibility now.

Design amendments (DESIGN.md §11) — please review

That verification caught two real defects in the spec:

  1. Science Blue's fill label was failing AA. #FAFAFA on #3B82F6 measures 3.5:1, and blue fills carry chip-sized text (calendar/event chips, data UI). Changed the label to the brand near-black #1717174.9:1, which also stops blue being the one fill with a white label. Science Blue's own hex is untouched, so its reservation as the future light-theme primary stands.
  2. body's documented ratio was wrong — "≈11.5:1" vs. a measured 10.2:1. Still AAA; the decision holds, only the number changed.

Two gaps filled:

  1. Added a destructive pair (#DB262F/#FAFAFA fill at 4.6:1, #FCA5A5 text at 8.0:1). §8 requires "the destructive text token" but no value existed anywhere in the doc.
  2. Highlighter swipe pinned at 25% alpha — the only value in the documented 25–35% range that keeps white text at AAA (7.7:1 on the ground, 9.2:1 on card). §2.13 asked for this to be verified on /styleguide; the page now prints the measured numbers.

Also corrected Danger Orange's stated 8.0:1 to the measured 7.9:1.

Deviations from the phase brief

  • Four theme tokens, not three. The brief's acceptance line says three; DESIGN.md §2 lists four, adding primary-bright. The doc wins (§11) — and without it, links and stat numerals on a green FRC page would stay yellow.
  • Motif components live in src/components/ui/primitives/, which Phase 03 also populates. DESIGN.md §2.12 calls them primitives; the conventions README arrives with Phase 03.
  • @typescript-eslint/no-unsafe-return is off for .astro. astro-eslint-parser doesn't type template JSX, so every items.map(() => <El />) resolves as error and trips the rule. Frontmatter — where the real logic lives — stays fully typed, and astro check type-checks templates properly. The config comment names the sibling rules that may need the same treatment, and says not to blanket-off the family.
  • The gear-bulb lineart vector is still outstanding (§2b — it needs to come from the merch/brand source files). I deliberately did not commit an approximation, so the scribed-lineart motif is simply absent rather than something to un-draw later. The other scribed devices (TitleBlock, Callout) are done.

Notes

Two mechanics worth knowing, both of which bit during implementation: hand-markup strokes carry pathLength="100" so the draw-on dash math is in percent — a hard-coded length silently truncates any longer path, which is what first left the chalk ovals as open arcs — and the ovals and underlines use vector-effect="non-scaling-stroke", since their SVG is stretched non-uniformly over whatever word it wraps.

I rendered the page at 1280px and 390px and iterated on it; both hold up, and the contrast table scrolls inside its own container rather than the page.


Generated by Claude Code

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
@CS-5
CS-5 force-pushed the overhaul/02-design-system branch from bf8dfc9 to 59ac30c Compare August 28, 2026 00:28

@CS-5 CS-5 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review of the design-system layer. Verified by installing this branch, running pnpm check (green) and pnpm build, then reading the emitted dist/_astro/global.*.css, dist/styleguide/index.html and dist/sitemap-0.xml, and by compiling global.css against the pinned tailwindcss@4.3.3 to confirm what the utilities actually resolve to. Contrast numbers below come from this PR's own src/lib/contrast.ts.

The structure is good and the build-time contrast idea is the right instinct. Eleven findings, four of which change what ships:

Rendering / behavior

  1. global.css — in-prose links get no underline: text-decoration-line is never set, and preflight's a { text-decoration: inherit } resolves it to none. DESIGN.md §8 unimplemented, and links become color-only.
  2. global.css — adding svg to the base max-width: 100% rule clamps ChalkOval's w-[calc(100%+2rem)], so the oval renders word-width and offset 1rem left instead of ringing the word.
  3. SketchArrow.astrodirection="right" is dead: the inline rotate style overrides the -rotate-45 class, since Tailwind v4 rotation uses the rotate property.
  4. global.css--color-body and --text-body both claim .text-body; the color wins, so the body size token has no usable utility (and the type-ramp row demonstrating it silently sets a color).

The guard itself
5. styleguide.astro — the contrast check reads hex literals duplicated into the page, not global.css, so a token edit on the CSS side alone leaves the build green against stale values.
6. DESIGN.md — Danger Orange's "corrected" 7.9:1 is the destructive token's number; #FB923C measures 6.6:1. The error crosses the AAA line, and the guard's AA-only floor can't catch it.

Smaller
7. /styleguide is noindex but still listed in dist/sitemap-0.xml — one-line filter in astro.config.ts.
8. spec-chip's border is opaque currentcolor, while its comment and DESIGN.md §8 both specify 40% alpha.
9. Callout renders <figcaption> with no <figure> anywhere on the page.
10. ambient-pool-neutral silently renders nothing unless composed with ambient-pool.
11. Swipe (7.6 vs 7.7) and Science Blue label (4.8 vs 4.9) doc figures are each 0.1 off the verifier.

Nothing here is structural — 1, 2 and 3 are the ones I'd fix before merge.


Generated by Claude Code

Comment thread src/styles/global.css Outdated
Comment on lines +177 to +181
/* In-prose links are accent-colored and underlined (DESIGN.md §8). */
a {
color: var(--color-primary-bright);
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In-prose links are not actually underlined.

Tailwind's preflight already ships a { color: inherit; text-decoration: inherit }. Since this rule sets only text-decoration-thickness and text-underline-offset — never text-decoration-line — the line style stays inherit, which resolves to none from body. Both declarations here are inert.

Verified in the built stylesheet (dist/_astro/global.*.css):

a{color:var(--color-primary-bright);text-underline-offset:.2em;text-decoration-thickness:1px}

No underline declaration anywhere. So on /styleguide both "An in-prose link" and the handwritten "Become a sponsor" render as accent-colored text with no underline — DESIGN.md §8 ("in-prose links primary-colored and underlined") is unimplemented, and link identification falls back to color alone (WCAG 1.4.1).

Suggested change
/* In-prose links are accent-colored and underlined (DESIGN.md §8). */
a {
color: var(--color-primary-bright);
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
a {
color: var(--color-primary-bright);
text-decoration-line: underline;
text-decoration-thickness: 1px;
text-underline-offset: 0.2em;
}

Generated by Claude Code

Comment thread src/styles/global.css Outdated
Comment on lines +202 to +210
/* Media never overflows its column. */
img,
picture,
video,
svg {
display: block;
max-width: 100%;
height: auto;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding svg to this rule breaks ChalkOval.

ChalkOval.astro:42 sizes its SVG as -inset-x-4 … w-[calc(100%+2rem)] so the stroke overshoots the word by 1rem on each side. max-width is a different property from width, so the utilities-layer width cannot override this base-layer max-width: 100% — the computed width collapses to the containing block's width while left stays at -1rem.

Result: the oval renders word-width and shifted 1rem to the left, ending before the word does, instead of ringing it. This is exactly the failure mode the component's own comment warns about ("a path covering only part of the box lands beside the word instead of around it") — reintroduced from the stylesheet side.

Preflight already covers img, video; it deliberately leaves svg out. Either drop svg here, or give the oval/underline SVGs max-width: none.

Suggested change
/* Media never overflows its column. */
img,
picture,
video,
svg {
display: block;
max-width: 100%;
height: auto;
}
/* Media never overflows its column. */
img,
picture,
video {
display: block;
max-width: 100%;
height: auto;
}
svg {
display: block;
}

Generated by Claude Code

className,
)}
fill="none"
style={`rotate: ${rotations[variant]}`}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

direction="right" is a silent no-op.

Tailwind v4 implements rotation with the individual rotate property, not a transform. Compiled against the pinned tailwindcss@4.3.3:

.-rotate-45 { rotate: calc(45deg * -1); }

That is the same property this inline style sets, and an inline style always beats a class. So flip.right on line 33 never applies: <SketchArrow direction="right" /> renders at rotations[1] = 0deg, i.e. identical to down-right. Variants 2/3 land at -3deg/2deg rather than -45deg.

down-left survives only because -scale-x-100 sets scale, a different property.

Fold the direction into the same value, e.g.:

const directionRotation = { "down-right": "0deg", "down-left": "0deg", right: "-45deg" } as const;
// style={`rotate: calc(${rotations[variant]} + ${directionRotation[direction]})`}

Generated by Claude Code

Comment thread src/styles/global.css

/* --- Text ramp (DESIGN.md §2) ----------------------------------------------------------- */
--color-foreground: #fafafa; /* 14.5:1 on background — headings, nav, emphasis */
--color-body: #d4d4d4; /* 10.2:1 — all reading copy */

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--color-body collides with --text-body on the class text-body.

Tailwind's text-* namespace serves both colors and font sizes, so --color-body (here) and --text-body (line 82) both claim .text-body. The color wins — the built stylesheet has exactly one rule and no font-size counterpart:

.text-body{color:var(--color-body)}

Two consequences:

  1. --text-body has no reachable utility at all. styleguide.astro:130 lists { token: "body", class: "text-body" } in the type ramp intending to demonstrate the size token; it sets a color instead. It only looks correct because that row inherits body's font size, which happens to be var(--text-body).
  2. Any future page writing class="text-body" for sizing silently gets a color change — the kind of thing that survives review because it renders plausibly.

Worth renaming one side (e.g. --color-text-body, or --text-base for the size) so the two namespaces stop overlapping.


Generated by Claude Code

Comment thread DESIGN.md Outdated
| Science Blue | `#3B82F6` / `#171717` | `#60A5FA` (6.0: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` (8.0:1) | FLL theme accent pair |
| Danger Orange | `#F97316` / `#241102` | `#FB923C` (7.9:1) | FLL theme accent pair |

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This correction has the wrong number — the two rows look swapped.

Running the repo's own src/lib/contrast.ts against #262626:

Value Measured
#FB923C (Danger Orange text) 6.6:1
#FCA5A5 (Destructive text) 7.9:1

So 7.9:1 belongs to the destructive row below (which states 8.0:1), and Danger Orange is 6.6:1. The old 8.0:1 was wrong, but 7.9:1 is wrong in a more dangerous way: it sits above the 7:1 AAA line while the real value sits below it. Anyone trusting this table will use #FB923C for reading copy on an FLL-themed page believing it clears AAA.

The build guard can't catch this — styleguide.astro floors every *-bright token at AA_NORMAL, never at the ratio the doc claims. If these documented numbers are meant to be normative, the guard should assert against them too.


Generated by Claude Code

<head>
<meta charset="utf-8" />
<meta content="width=device-width, initial-scale=1" name="viewport" />
<meta content="noindex, nofollow" name="robots" />

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noindex isn't enough — the page is also being submitted in the sitemap.

The sitemap integration is already live on this branch (astro.config.ts:9, sitemap() with no filter), so this page ships in the index from the first deploy. After pnpm build:

<!-- dist/sitemap-0.xml -->
<url><loc>https://scstem.org/</loc></url>
<url><loc>https://scstem.org/styleguide/</loc></url>

A URL that is both in the sitemap and noindex is the "Submitted URL marked 'noindex'" error in Search Console. The PR body defers the exclusion to Phase 10, but the cost of doing it now is one line:

integrations: [sitemap({ filter: (page) => !page.includes("/styleguide") })],

Generated by Claude Code

Comment thread src/styles/global.css
Comment on lines +329 to +338
/* Spec chip — a stamped plate: transparent fill, 40%-alpha border in the chip's own color. */
@utility spec-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.1875rem 0.5rem;
border: 1px solid currentcolor;
border-radius: var(--radius-sm);
background-color: transparent;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 40% alpha in the comment isn't in the code.

border: 1px solid currentcolor is fully opaque, so the tier chips on /styleguide get a border as bright as their label. DESIGN.md §8 asks for "1px 40%-alpha border in the chip's color", and per CLAUDE.md the doc wins. The comment currently describes something that isn't there.

Suggested change
/* Spec chip — a stamped plate: transparent fill, 40%-alpha border in the chip's own color. */
@utility spec-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.1875rem 0.5rem;
border: 1px solid currentcolor;
border-radius: var(--radius-sm);
background-color: transparent;
}
/* Spec chip — a stamped plate: transparent fill, 40%-alpha border in the chip's own color. */
@utility spec-chip {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.1875rem 0.5rem;
border: 1px solid color-mix(in srgb, currentcolor 40%, transparent);
border-radius: var(--radius-sm);
background-color: transparent;
}

Generated by Claude Code

const { reference, side = "left", class: className } = Astro.props;
---

<figcaption

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<figcaption> with no <figure> ancestor.

HTML permits figcaption only as the first or last child of a figure. The built page has neither:

$ grep -c '<figure' dist/styleguide/index.html
0
$ grep -o '<figcaption[^>]*>' dist/styleguide/index.html
<figcaption class="spec-label flex items-center gap-2 text-muted mt-3">

At the one call site (styleguide.astro:429-430) the Callout is a sibling of the media div, so assistive tech gets no figure/caption relationship and validators flag the document. Since the component is also documented for "detail annotations on a hero" — where there is no figure at all — the element choice is doing work it can't do.

Either wrap the media in a <figure> inside the component (media in a slot), or emit a plain <p>/<div> and make callers responsible for the <figure>.


Generated by Claude Code

Comment thread src/styles/global.css
Comment on lines +370 to +378
@utility ambient-pool-neutral {
&::before {
background: radial-gradient(
ellipse 80% 60% at 50% 0%,
rgb(250 250 250 / 0.03),
transparent 70%
);
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reads like a peer of ambient-pool, but it only overrides background — no content, position, inset, z-index, pointer-events, and nothing on the host element.

So class="ambient-pool-neutral" on its own generates a ::before with no content, which means no pseudo-element and no pool: it fails silently, with nothing in the build to catch it. Composed as ambient-pool ambient-pool-neutral it works (I checked the emitted order — the override lands after), but nothing here says that's required.

Either repeat the structural declarations so it stands alone, or say in the comment that it is a modifier requiring ambient-pool.


Generated by Claude Code

Comment thread DESIGN.md Outdated

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, ±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`.
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.7: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`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small numbers nit, but this doc is normative-by-copy so it's worth getting exact: src/lib/contrast.ts gives 7.6:1, not 7.7:1, for #FAFAFA over the 25% swipe on #262626 (blend#5B5022). The card figure of 9.2:1 checks out. /styleguide prints 7.6, so the page and the doc it exists to prove disagree on first read.

Same in plan/02-design-system.md: Science Blue's new #171717 label measures 4.8:1, not 4.9:1.

Both still clear their floors — just a numbers fix so the doc matches the verifier.


Generated by Claude Code

CS-5 commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

/simplify pass — quality review (reuse · simplification · efficiency · altitude)

Quality only; correctness findings are in the separate review on this PR. Finding 2 below overlaps that review's styleguide.astro:23 item — I'm restating it because three of the four quality angles landed on it independently, and because the fix is a simplification rather than a bug patch.


1. The engineering grid is defined twice — once as a CSS utility, once as an inline style with a raw hex

src/components/ui/primitives/Pattern.astro:22-38

const opacity = { hero: 0.07, section: 0.05, pocket: 0.04 }[placement];
const cell = placement === "pocket" ? 26 : 28;
...
      background-image:
        linear-gradient(to right, #FAFAFA 1px, transparent 1px),
        linear-gradient(to bottom, #FAFAFA 1px, transparent 1px);
      background-size: ${String(cell)}px ${String(cell)}px;

src/styles/global.css:260-267 (@utility pocket-feature) already draws exactly this grid — same two gradients, same 26px cell — as rgb(250 250 250 / 0.05). So one DESIGN.md §2.3 device now has two independent implementations that already disagree (0.04 vs 0.05 alpha), in two different syntaxes. A change to cell size or stroke alpha has to be made twice.

The color is the worse half. #FAFAFA is the literal value of --color-foreground, and DESIGN.md §2 says "All component color comes from semantic tokens — raw hex values and Tailwind palette classes are banned in components." D15 promises "a light theme / theme switcher is a future token-set addition, not a refactor" — but a white grid is invisible on a light ground, so the light theme becomes a component edit plus two utility edits, in three files a token-block author has no reason to open. This is also the one motif DESIGN.md restricts by placement, so it'll be reused on heroes and section breaks and spread the literal further. (global.css:373 ambient-pool-neutral has the same hardcode as rgb(250 250 250 / …).)

Fix: one @utility engineering-grid in global.css parameterized by --grid-cell / --grid-alpha, built on currentColor (gradients accept it) with text-foreground on the wrapper. pocket-feature composes it; Pattern sets class="engineering-grid" plus two custom properties. The whole style template literal and both lookup tables collapse.


2. The contrast gate verifies a hand-typed copy of the palette, not the tokens

src/pages/styleguide.astro:23-120

const BACKGROUND = "#262626";
const CARD = "#171717";
const textTokens: readonly TextToken[] = [
  { name: "foreground", value: "#FAFAFA", floor: AAA_NORMAL, use: "Headings, nav, emphasis" },
  { name: "body", value: "#D4D4D4", floor: AAA_NORMAL, use: "All reading copy" },

~25 hex values (surfaces, text ramp, fill pairs, tiers), the three radius values, and const SWIPE_ALPHA = 0.25 (line 95) restate global.css:16-53 and the 25% inside @utility highlight-swipe (line 304). The build-time throw compares those literals to each other; global.css is never read.

So the gate cannot detect the thing it exists to detect. Edit --color-body in global.css and the build stays green against #D4D4D4, and the rendered swatch keeps showing the old color. DESIGN.md §9: "Contrast pairs verified live on /styleguide (computed ratios rendered); token changes must re-verify." A token change does not re-verify here.

It's already incomplete in exactly this way: textTokens carries one primary-bright, so the program themes' accent text colors — #3ECF6E and #FB923C, whose ratios DESIGN.md §2 tabulates — are never checked on any surface, even though the page renders both themes.

Fix: give the palette one source. import globalCss from "@/styles/global.css?raw" (or readFileSyncsrc/lib/icon.ts in PR #17 does build-time file reads already), scan the --color-* / --radius-* declarations out of the @theme block into a Record<string, string>, and drive both the swatches and the gate off the parsed map. The arrays then keep only what's genuinely editorial (floor, use) and look values up by name — a missing token becomes a build failure instead of silent drift. Deriving the theme rows from the [data-theme] blocks the same way means a new program can't ship an unchecked accent.


3. The hand-markup stroke recipe is copy-pasted across three primitives

ChalkOval.astro:21-43, ChalkUnderline.astro:19-44, SketchArrow.astro:44-57

      tone === "chalk" ? "text-foreground" : "text-primary",
...
    <path
      d={paths[variant]}
      pathLength="100"
      stroke="currentColor"
      stroke-linecap="round"
      stroke-width="2.5"
      vector-effect="non-scaling-stroke"></path>

ChalkOval and ChalkUnderline have effectively the same body — same variant/tone/class props, same paths/rotations lookup tables, same <span class="relative inline-block"> + stretched preserveAspectRatio="none" svg — differing only in path data and placement classes. The tone ternary appears three times and the five stroke attributes five times. SketchArrow has already dropped vector-effect while the other two kept it, which is the drift arriving on schedule.

DESIGN.md §13 sets these values for the register as a whole ("2–3px, stroke-linecap: round") and mandates ≥3 variants per markup device with more devices coming — so this template gets copied again each time a device is added, and the part that must stay identical across devices is exactly the part that's duplicated.

Fix: either an internal HandStroke.astro owning the wrapper, the tone ternary and the <path> attributes (taking paths / rotations / viewBox / positioning classes as props), or an @utility hand-stroke in global.css beside the existing @utility draw-on carrying fill: none; stroke: currentColor; stroke-linecap: round; stroke-width: 2.5. Either way ChalkOval and ChalkUnderline keep their public API and shrink to a data table plus one element.


4. Four hand-written corner paths where one path and three rotations do it

src/components/ui/primitives/RegistrationMarks.astro:15-27

const corners = [
  { key: "tl", path: "M1 13 L1 1 L13 1" },
  { key: "tr", path: "M11 1 L23 1 L23 13" },
  { key: "br", path: "M23 11 L23 23 L11 23" },
  { key: "bl", path: "M13 23 L1 23 L1 11" },
] as const;

const position = { tl: "top-0 left-0", tr: "top-0 right-0", ... } as const;

I checked the geometry: every path is the tl bracket rotated 90° about (12,12) — M1 13 L1 1 L13 1 rotated 90° is exactly M11 1 L23 1 L23 13. So the bracket's arm length and inset are encoded four times, and changing the mark means recomputing four coordinate strings by hand and keeping a second keyed map in sync.

Fix: one const BRACKET = "M1 13 L1 1 L13 1" and one list of position+rotation classes — ["top-0 left-0", "top-0 right-0 rotate-90", "right-0 bottom-0 rotate-180", "bottom-0 left-0 -rotate-90"]. The corners/position pair becomes a single array and the path is edited once.


5. The four brand SVGs are duplicated, not moved — the public/ copies still deploy

src/assets/brand/logo-black-full.svg, logo-color-full.svg, logo-color.svg, logo-white-full.svg

These are byte-identical to the existing public/image/svg/* (same git blob hashes — e.g. f51e8055… for logo-black-full.svg in both places), and both copies are still present as of 05-app-shell.

Cost: 48,422 bytes uploaded on every Cloudflare Pages deploy for files nothing references, served unhashed from public/ (no fingerprint, no immutable caching) rather than the fingerprinted _astro/ copies Astro emits. Plus two sources of truth for the logo.

Fix: delete public/image/svg/logo-*.svg. legacy/ is reference-only and never built, so nothing needs them at that path.


Checked and cleared

The font setup is the strongest part of this PR and I want to say so explicitly: src/styles/fonts.css + fonts.ts are self-hosted, latin-only @font-face declarations hand-written to keep cyrillic/greek out, font-display: swap on every face, one variable file per family, and exactly the two above-the-fold faces preloaded. I confirmed in built dist/index.html that the two rel="preload" hrefs match the url()s in the emitted CSS byte for byte (inter-latin-wght-normal.Dx4kXJAl.woff2, orbitron-latin-wght-normal.Du7j2Vp2.woff2), so there's no double fetch. No finding.

Also cleared: the styleguide's utilities are not leaking into the shared bundle (removing the page shrinks the CSS by 1.4 KB raw / 227 bytes gzipped).


Generated by Claude Code

This was referenced Aug 28, 2026
claude added 2 commits August 28, 2026 11:06
# Conflicts:
#	eslint.config.ts
#	src/styles/global.css
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants