`FontFamily.axes` ({ wght, opsz }) was declared but never consumed — the
TYPOGRAPHY_ENGINE_RFC §5 promise was unmaterialized. Wire it:
- axes.wght -> when a face omits `weight`, @font-face emits the range
(`font-weight: 100 900`) so one variable face spans the whole axis
- axes.opsz -> `:root { font-optical-sizing: auto }` so the optical-size
axis tracks the rendered font-size
Engine-only, inert until a theme declares axes (shipped fonts are static
TTFs — same posture as wide-gamut color: ready, not yet exercised by
assets). Zero change to generated/base.css (no family declares axes).
`npm run check` 0 errors; new test asserts the emission; the generated-css
test confirms base output unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TYPOGRAPHY_ENGINE_RFC Phase 3 (engine tokens). Additive scales, pure CSS, no binaries.
- config-types: TypographyPrimitiveSet += tracking / leading / features / measure
(optional Record<string,string>).
- render-css: emits --tracking-{k} / --leading-{k} / --font-feature-{k} / --measure-{k}.
- typography.ts defaults: tracking (tighter…wider), leading (none…loose), features
(tabular = "tnum"+"lnum" for data, oldstyle/smallcaps/ligatures), measure (54/66/78ch).
+ optical tracking baked into the size tokens: small text slightly looser
(xxs +0.01em), display tighter (xxxl -0.02em) — was all 0.
- config.ts: validates the 4 scales (CSS-value maps).
- generated/base.css regenerated. test: scales + optical tracking emission.
Additive tokens (recipes/components consume var(--tracking-tight) etc.) so the scales
are zero-risk; the only rendered change is the gentle optical tracking on headings/small
text. check 0 errors; eidos suite green (3 pre-existing words failures only).
Next (Phase 3b): wire the component props (wrap: balance/pretty, numeric: tabular,
tracking/leading/measure) on <Text>/<Heading>.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TYPOGRAPHY_ENGINE_RFC Phase 2. Fonts are theme data (each theme owns its families), so
@font-face becomes config-driven + generated — like the color palette — instead of a
separate hand-written CSS file. This matches next/font / Fontaine (config -> @font-face),
above the token-only frameworks (Radix/Tailwind/MUI) that leave loading to you.
- config-types: FontFamily += faces (FontFace[]) / axes (FontAxes) / fallback
(FontFallback, metric-override) / display / preload. Additive — the family stack still
works from `family`+`fallbacks`.
- render-css: renderFontFaceBlocks generates @font-face per face from the config, deduped
by the real font name (a font shared across slots — Lora as secondary+display — emits
once). Optional metric-override fallback @font-face (anti-CLS) injected into the stack
as `'{family} Fallback'` when declared. Emitted first in renderStaticCss.
- typography.ts: the BASE THEME's 14 @font-face migrated from fonts.css into the config
(faces). TTF today (the theme's choice); a theme swaps to woff2/variable + fallback
metrics by editing config only.
- index.css: drops `@import './themes/fonts.css'` — the @font-face now ships in
generated/base.css. (fonts.css superseded; left in place, no longer imported.)
- generated/base.css regenerated (14 @font-face, Lora deduped). test: @font-face
generation + dedup; merge-without-mutation assertion updated for the faces field.
Verified in browser: 3 families registered, files resolve (200), fonts load on demand
(swap). check 0 errors; eidos suite green (3 pre-existing words failures only).
Deferred (capability typed, theme adopts when it has the assets): variable woff2,
metric-override numbers (need fontkit/precompute), <link rel=preload> (head markup).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
TYPOGRAPHY_ENGINE_RFC Phase 1. Additive on TypographyPrimitiveSet, behind the frozen
token contract (--font-size-X keeps its name; only the value formula changes, like
color --scale-* hex -> oklch()).
- type-scale.ts (pure, isomorphic, no canvas): the Utopia clamp() formula. fluidClamp /
resolveTypeSize / isFluidSize. rem-based (a11y: scales with browser font-zoom).
- config-types: TextMetric.size accepts `string | FluidSize` ({min,max,minVw?,maxVw?}).
Plain length strings still valid -> backward-compatible.
- render-css appendTypographyDeclarations: emits calc(resolveTypeSize(size) * --scaling)
-> a fixed rem or a fluid clamp; the --scaling axis composes on top.
- config.ts: validateSizeValue accepts a FluidSize (validates min/max/minVw/maxVw) so
the base config validates (was the cascade root — FluidSize objects failed the
string-only CSS-value check).
- typography.ts: sizes in rem; headings (lg/xl/xxl/xxxl) fluid (min @480px -> max
@1280px, max = previous fixed px so desktop is unchanged); body (md) fixed. hero/h1/h2
drop the manual { base, md } responsive sizes — the clamp covers the viewport.
- generated/base.css regenerated. type-scale.test.ts (6 tests). 2 config-test assertions
updated to the new rem/clamp values.
Verified in browser: --font-size-xxxl 40px @480 -> 80px @1280; xxl 32->48; lg 18->20;
md 16 fixed. check 0 errors; eidos suite green (3 pre-existing words failures only).
canvas-text/<SText> unaffected (reads getComputedStyle real font, measures the clamp).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finishes the THEMING_AUDIT P3 backlog.
P3-5 (fixed) · appendScaledMetricDeclarations: the `parseFloat(raw) === 0` guard let
non-numeric values (auto / var() / calc()) fall into `calc(x * …)` = invalid CSS. Now
only finite, non-zero numbers are scaled; zero + non-numeric emit verbatim. No change
to the base config output (all values numeric) — pure robustness.
P3-8 (fixed) · index.ts no longer re-exports the raw render-* fns. The public render
API is the ActiveEidos class (gated by assertValid() + active config); ./lib/render-css
stays reachable for internal/tooling use. Redirected the one internal consumer
(active-eidos-config.test.ts) to import renderThemeCss from the module.
Triaged the rest with rationale (audit updated):
- P3-4 deferred · density wins by deterministic source order (stable); the :where(:root)
restructure to also support scoped density is high-cost for a theoretical nit.
- P3-6 already resolved · dispose() routes documentElement via dom (no direct access).
- P3-7 deferred · ActiveEidos reactivity is callback-driven (apply() on pref change) by
design; a full runes conversion is a risky refactor with no bug to justify it.
- P3-9 deferred · orphan _accent forwarders — low-value recipe surgery with cascade risk.
All P3 now fixed-or-decided; only the P2 secondary halves (contract pruning + bare
identifier color validation, both edge-case) remain, deferred as low-value.
check 0 errors · eidos suite green (3 pre-existing words-track failures unrelated).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tertiary was `gray` — literally the same scale as neutral, and secondary
(slate) is a near-gray too, so the three desaturated roles blurred together.
Per Material 3's tertiary rule (rotate the primary hue ~60deg), tertiary now
uses `indigo`: purple's cool neighbor, saturated enough to be distinct, in a
hue band no intent occupies. Updated the one test that pinned tertiary=gray;
regenerated base.css.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the synthetic step-9-at-opacity alpha ramp with a real compositing inverse: each --scale-X-aN is the translucent color that, painted over the scale's background (white for light scales, black for dark, by step-1 luminance), reproduces the opaque solid step N (Radix model). Computed in render-css.ts for ANY theme (base + custom), so alpha is consistent-by-construction with the solid scale and cannot drift. alphaScales opt-in still wins verbatim; non-hex solids fall back to the legacy color-mix ramp. Verified: --scale-purple-a9 = rgb(92 0 173 / 0.6941) over white = #8e4ec6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>