Uh oh!
There was an error while loading. Please reload this page.
refactor(ui): name one text role at every renderer call site - #1893
Merged
Conversation
A text style is one role, not four independent picks. #1857 converged the sizes and #1878 the leadings; both removed the divergence of the day without removing the ability to diverge again, because size, leading, weight and family stayed four separate choices at each of 348 call sites. The `font:` shorthand is the only mechanism CSS has that makes them inseparable. Its reset semantics are why it fits rather than a hazard to work around: it resets weight, style, variant and family, and the role token supplies them, so no site can move one and leave the others. Composed from Astryx's generated atoms only, so the ladder stays a pure function of typography.scale. Family is not part of the call-site triplet: `font:` requires a family slot that would override the one monospace authority in this file, so the slot reads --maka-font-family, rebound on :where(code, kbd, samp, pre) and inherited from there. Measured behavior-neutral: 12262 text elements across 86 surfaces (24 live app states, 62 Storybook stories) compared before and after, 0 changed.
381 declaration blocks across 44 stylesheets now declare one `font: var(--maka-text-<role>)` and nothing else about type. Exactly one raw font longhand survives in the renderer — the monospace stack in maka-tokens.css that owns the family axis. Roles are assigned from MEASURED computed styles, not from declared values, so the ~30 selectors that took their weight from the UA or an ancestor keep it: 187 of the 385 blocks render in at least one of the 100 surfaces the harness reaches and were classified from what the browser reported there. Three things measurement caught that reading the CSS did not: - `.maka-hero h1` declares 28px but every probed instance is also `.maka-hero-empty-chat h1` at 20px, so the measured role described the other rule. Taking it would have shrunk the onboarding hero, the one surface that keeps the large scale and that no fixture renders. A measured role is now rejected when it disagrees with the block's own declared size. - Per-rule redundancy is not compositional: two rules that both pin 12px on one element are each individually removable and jointly load-bearing. Deleting on that basis drifted 85 elements 12px→14px, so this commit only migrates; deletion is decided against the final tree. - A role composed on :root freezes its family slot there — custom property var() references are substituted where DECLARED, and the resolved string is what inherits. A real <pre> whose --maka-font-family read Geist Mono still computed -apple-system. The role table is anchored on `:root, :where(code, kbd, samp, pre)` so code elements compose their own mono-family roles. `.maka-tool-name` also loses a latent defect: measured 16px against a 1.6667 ratio inherited from the supporting role, 26.67px, off the 4px grid.
The 348-call-site migration was assigned from measured computed values, so the roles it picked were the ones the browser reported — which is exactly why re-measuring the migrated tree found the places where the measured value described something other than the call site's intent. Every change below is a diff bucket from that re-measurement, named and closed. Markup, not a call-site family. A slug, an id, a task key, a capability id, a memory path, a tool name, a summarized error — these are code, and the role table composes the code family for the code element group, so saying so in the markup is also what makes them render monospaced. 8 render sites move to <code>; `SettingRow`'s `mono` prop now picks the element. UA bold is not a role. 17 blocks selected strong/h*/th and declared no weight of their own, so they took the UA's 700 by accident. Naming a body or supporting role resets that to 400 — measured, 21 elements went 700→400 before this. They are section titles at 14px and 12px, which is heading-4 and heading-5: same size, same leading, weight 700→600 (Astryx has no 700). Page titles keep their size, not their weight. `.settingsPageHeader h2`, `.maka-module-main-header h2` and `.maka-plan-heading h2` were display sizes carrying a hand-rolled semibold. Matching on weight picked heading-1 and collapsed 66 page titles to 20px, level with the section headings under them. Display roles keep 25px/22px and take Astryx's display weight of 400. A family-only rule hides its size. `.maka-mcp-*` set only font-family, and its three selectors inherited two different sizes — 14px in the card body, 12px in the description. One role could not express that, so the rule splits: code role for the 14px pair, supporting for the description. `.maka-nav-kbd` opts back out of mono. It read `font-family: inherit` before; as a <kbd> it now composes the mono role table. It names the family axis instead — one declaration, and the role stays whole. Remaining measured delta is 206 of 16,063 elements across 86 surfaces, every bucket a convergence this PR intends: 66 page titles 600→400, 32 badges and 28 rail prompts 500→600, 21 strong 700→600, 30 family changes where mono was decoration rather than code, and 13 hand-rolled 12px/500 sites landing on supporting, their emphasis already carried by color or pill chrome.
Size, leading, weight and family were four independent properties, so each needed its own guard: a ban on product `--leading-*` tiers, on literal ratios, on `!important` sizes and leadings, on em/rem multipliers, and a pairing check that resolved a block's size and leading through the generated theme to prove they named the same tier. Every one of those describes a way for the four to come apart. None of them is expressible now. A call site that cannot write a font longhand cannot write a literal ratio, an em multiplier, a forced size, or a mismatched pair — so they collapse into one assertion: outside the role table, every text style is `font: var(--maka-text-<role>)` and no stylesheet declares font-size, line-height, font-weight or font-family. `findLeadingPairingOffenders` is deleted rather than kept alongside it; two authorities on one question means the weaker one is the one people read. `findFontShorthandOffenders` inverts instead of tightening. The shorthand was banned because it hides bare weights and leadings; it is now the only legal form, because it is the one CSS mechanism that makes the four inseparable. The replacement also checks that every role NAMED is a role DEFINED — a var() that resolves to nothing makes the whole declaration invalid at computed-value time, so the element silently keeps what it inherits. That arm found a live defect: `--maka-text-display-1` was referenced by the hero and defined nowhere, and every other check in the file was green. Two things text cannot prove keep their own guards. The role table's shape is asserted in the contract (each role composed only from its own Astryx atoms, declared on both anchors, family axis pinned once per anchor). What those resolve to in a document is measured in e2e: a <pre> and a <div> naming the same role must agree on size, weight and leading and differ only in family — the substitution-timing fact that made the first, single -anchor role table wrong while reading perfectly in CSS. Seven mutations verified, each restored with cp: a longhand at a call site, a hand-composed shorthand, an undefined role, a role built from a foreign atom, a single-anchor table, a third family authority, and an Astryx atom rebound to a literal. Desktop 1323 pass, ui 239 pass.
Six product tokens reached zero consumers when call sites stopped naming sizes and families, and check-dead-css found all six: --font-size-heading / stat / ui / caption, --font-sans, --font-default. They existed so a call site could say "this is the caption size" or "this is the mono stack" — a role says both, together with the weight and the leading, so a second set of names for the same rungs is exactly the second authority the convergence exists to remove. The role table now reads --font-family-code directly, and nothing in the renderer aliases an Astryx type name. The contract that pinned those aliases is replaced by the stronger thing they were approximating: there is no product name for a size or a family at all. --font-weight-bold survives as the top rung of the series the generated theme reads, in RESERVED_SCALE_TOKENS with that reason. Also caught rebasing onto #1876: `.maka-session-rename-input` declared `font: inherit` before its longhands, so migrating the longhands to a role left the reset winning and the role line dead. `findTextRoleOffenders` now counts font declarations before reading the last one — a block that declares a role and then overrides it reads as legal to any check that asks only what the browser uses. Eight mutations verified with cp. Measured against 105ec3e rather than the branch point, since #1876 moved the session rail under this work: 118 of 16,076 text elements across 86 surfaces change, every one a bucket this PR intends.
Review found four sites where naming a role changed more than the role. The `font:` shorthand resets `font-variant-*`, `font-style` and the feature settings along with the four axes it names. Within a rule every site already declared those after the role; across rules two did not. `.maka-stat-tile-value` held the tabular figures the tiles exist to align while the role sat in the `-outline` / `-filled` modifiers, so the role won and the digits stopped aligning; reordering makes the base rule the later one. `code.settingsReadOnlyValue` restates the declaration because it is the higher-specificity selector. A grouped rule that hands a selector a role, followed by that selector's own rule handing it another, leaves the first dead — and a later retune of the group moves every other member while this site silently stays put. `.plan-proposal-kicker` had already drifted that way, from the 12px tier to the 14px one; the 12px tier offers 400 and 600, and an eyebrow over a title takes the 600. `.maka-agent-graph-section-label` was the same shape without a size change. `data-mono` said in an attribute what the `<code>` element already says.
The collapse to one guard was real for the shape it checks and a regression for what it can see. Three holes, each found by mutation: Scope. `readCallSiteCss()` skipped all of maka-tokens.css so the role table could write its one family longhand. That file is 1400 lines, ~40 of them ordinary component rules, and they were exempt from the only remaining guard — a literal ratio, an em multiplier, an `!important` size and a hand-composed shorthand all passed there while failing everywhere else. The exemption is now the one declaration that needs it: `font-family` on the code element group. Level. `--maka-text-*` and `--maka-font-family` are the vocabulary this branch introduced and nothing checked what they were bound to. One rule rebinding a role to `700 44px/1.05 Impact` re-chose all four axes while its call site still named exactly one role. Astryx's atoms were checked, but only against a leading digit, so `calc(2.5)` and `max(24px, 1rem)` walked through. Both arms are now one rule: a type token may be rebound to another token, never to a value. Parser. The hand-rolled brace walk dropped at-rule bodies whole, so a longhand inside a rule-nested `@media` — the shape Astryx itself uses for coarse pointers — was invisible; it also treated a brace inside a string as structural, and had lost the case-insensitivity its predecessor had. Replaced with postcss, which is already what Vite parses this same CSS with. A rule-nested at-rule is its own cascade context, so a responsive role swap is not counted as a duplicate. One arm is new rather than restored: a selector may be given a role by only one rule per cascade context. That is the cross-rule form of the dead-role bug the in-rule count already caught. The e2e step that read the four deleted size aliases now measures the roles instead. It had been failing since those names were deleted, and because a failing step aborts the test, every later step — the off-grid sweep and both two-anchor probes — had stopped running. The two-anchor probe also pins the three non-family axes to the supporting tier's own values; equality between the two probes alone would hold if the role stopped resolving for both. Mutation-verified (cp backup/restore, no git): longhand / literal ratio / hand-composed shorthand inside maka-tokens.css, role token rebound to a value, family axis rebound to a stack, atom rebound to calc(), longhand in a rule-nested @media, upper-cased longhand, a second role rule for one selector, role table on :root alone, family longhand moved off the code anchor — 11 of 11 caught, baseline clean.
… role costs `--font-weight-bold` had no consumer once call sites stopped declaring weight, and the generated theme reads only normal / medium / semibold. It survived on a check-dead-css allowlist entry whose stated reason was that the scale reads as a scale with it present — an aesthetic, not a constraint, and a product name for an axis the roles own. Four things the role table implies and did not say: the shorthand resets font sub-properties, so any the site needs must come after it; a 12px site that titles something below it takes heading-5 and everything else takes supporting, which is what makes the hand-rolled 500s reviewable; 97 call sites name a heading role for its weight rather than because they are headings, so a `weights.heading` retune moves them; and `large` / `heading-6` are absent because nothing uses them, not because the projection is partial. Astryx does support product roles through defineTheme component overrides — staying a pure projection of the vendor set is a choice, and now says so. Display on a page title is a deliberate override of Astryx's own guidance, recorded where the override is. Every heading role is weight 600 and tops out at 20px, so matching on weight would collapse a 25px page title level with the section headings under it. Also three comments naming tokens this branch deleted.
Astro-Han
marked this pull request as ready for review
August 2, 2026 09:10
Uh oh!
There was an error while loading. Please reload this page.
Astro-Han added a commit
that referenced
this pull request
Aug 2, 2026
Squashed for rebase onto #1893; final message written at the end.
Astro-Han added a commit
that referenced
this pull request
Aug 2, 2026
… exemption Review of the previous revision found three ways the guards were narrower than their own documentation, all of the same shape: a check that reports green because it cannot see the thing it claims to govern. - `isChip` required a rule to declare its own type. A compound chip delegates type to a child and a simple one may just inherit, so the arm was hiding three shipping chips with the #1879 defect while reporting the other 15 green. Two arms — pill radius and padding — see all 20 pill boxes, and no longer read a vocabulary that #1893 already moved once, so the unanchored `chips.length >= 8` floor that watched for exactly that drift is deleted with it. - The exemption set was one list honoured by skipping, so a mutation adding `height: 1px` to a rule excused as component-sized stayed green. It is now three reason-groups, each asserted: WRAPS must declare no block size (and is the wrap contract's own list rather than a second copy of it), COMPONENT_OWNED must declare none at all, PINNED_OFF_RULER must still be pinned. - The Badge contract read the unconditional view, on which `.settingsHealthBlockerBadge` — which lives entirely inside a breakpoint — merged to nothing, so the exemption naming it suppressed nothing. It reads a conditional-inclusive view now and asserts that the release is real. Also: `min-height`/`max-height`/`padding-inline-start` all constrained a box while passing both checks; the ruler accepted tiers it does not define; and the JSX scanner ended a tag at the first `>` in a prop, and then, once rewritten, at an apostrophe inside a prop comment — which silently dropped three live call sites until the mutation sweep caught it. Four boxes the widened scan surfaced, each measured live before its tier: `.maka-quote-chip-collapsed` and `.maka-deep-research-run-count` at 24px natural (pixel-neutral on `--h-control-sm`), `.maka-firstrun-step` at 29px (`--h-control-lg`, +3px), and `.maka-mcp-install-button`, a bare 34px square that swaps with a 28px `Button size="sm"` in the same slot.
Astro-Han added a commit
that referenced
this pull request
Aug 2, 2026
* fix(ui): give the single-line chip family a box the component owns Squashed for rebase onto #1893; final message written at the end. * fix(ui): derive the chip population from shape alone and assert every exemption Review of the previous revision found three ways the guards were narrower than their own documentation, all of the same shape: a check that reports green because it cannot see the thing it claims to govern. - `isChip` required a rule to declare its own type. A compound chip delegates type to a child and a simple one may just inherit, so the arm was hiding three shipping chips with the #1879 defect while reporting the other 15 green. Two arms — pill radius and padding — see all 20 pill boxes, and no longer read a vocabulary that #1893 already moved once, so the unanchored `chips.length >= 8` floor that watched for exactly that drift is deleted with it. - The exemption set was one list honoured by skipping, so a mutation adding `height: 1px` to a rule excused as component-sized stayed green. It is now three reason-groups, each asserted: WRAPS must declare no block size (and is the wrap contract's own list rather than a second copy of it), COMPONENT_OWNED must declare none at all, PINNED_OFF_RULER must still be pinned. - The Badge contract read the unconditional view, on which `.settingsHealthBlockerBadge` — which lives entirely inside a breakpoint — merged to nothing, so the exemption naming it suppressed nothing. It reads a conditional-inclusive view now and asserts that the release is real. Also: `min-height`/`max-height`/`padding-inline-start` all constrained a box while passing both checks; the ruler accepted tiers it does not define; and the JSX scanner ended a tag at the first `>` in a prop, and then, once rewritten, at an apostrophe inside a prop comment — which silently dropped three live call sites until the mutation sweep caught it. Four boxes the widened scan surfaced, each measured live before its tier: `.maka-quote-chip-collapsed` and `.maka-deep-research-run-count` at 24px natural (pixel-neutral on `--h-control-sm`), `.maka-firstrun-step` at 29px (`--h-control-lg`, +3px), and `.maka-mcp-install-button`, a bare 34px square that swaps with a 28px `Button size="sm"` in the same slot. * test(desktop): close the guard holes three re-reviews found in the chip contracts Every finding below was reproduced as a mutation that passed green, and each fix is re-verified by the same mutation failing. Four root causes, three of which delete something. The predicate read text, not declarations. `/padding/` also matches `background-clip: padding-box`, which is how a scrollbar thumb — padding-less, and a box no reader perceives as an object — entered the chip population and earned an exemption group of its own. The group is deleted with the substring that created it, and the padding vocabulary becomes one shared authority. The same guard-narrower-than-CSS mistake, twice more. The Badge contract's `OWNED` gained the logical padding family and lost `padding-top/right/bottom/ left`, so `padding-left: 9px` on a Badge className redrew the component's box while green. And the off-ruler exemption proved "still pinned" with the constrains-a-block-size vocabulary, so rewriting its `height` as `min-height` handed the box back to the line box and stayed green. Which merge view follows from which question, applied to all three sites rather than one. A must-NOT contract has no conditional distinction: pinning a wrapping row, or re-adding the height override #1879 removed, inside `@media (max-width: 620px)` passed the unconditional view. A must-HOLD PAIR needs the opposite — the flattened view let a Badge release `height` in one breakpoint and `white-space` in the mutually exclusive one, releasing the box at no viewport at all, so releases are now read per cascade context. A scan that cannot read a call site has to say so. `<Badge {...{ className: 'x' }} />` is legal JSX that the static scan cannot read and the `className={` scan does not match, so both contracts concluded there was nothing to govern. Spreads join computed classNames as unreadable; `//` prop comments parse. (TypeScript 7 is the Go port and ships no JS compiler API, so an AST is not available to reach for — but the property that matters is loud-vs-silent, not lexer-vs-parser.) Three smaller ones: rungs are read from the ruler's own `:root` scope, so a `--h-control-*` declared under `.dark` is no longer a tier a chip may name; `--radius-pill` must exist, because renaming it reported CHIP POPULATION 0 and passed; `.maka-sidebar-update-button` returns to COMPONENT_OWNED, where an entry that was dead weight as a skip is the only assertion reaching it. The e2e no longer credits the tier-choice arithmetic to a check that does not exist. Measured with a throwaway probe: all four newly pinned chips resolve to zero nodes in both booted fixtures, so that choice is unasserted, and the comment says so. Verified: 10/10 mutations caught, each confirmed to land before being trusted. lint, format:check, typecheck, desktop 1364, ui 241, e2e 77, check-dead-css.
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 freeto 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.
Summary
A text style is one indivisible role — size, leading, weight and family chosen together — so a call site names a role and declares nothing else about type. #1857 converged the sizes and #1878 the leadings; both removed the divergence of the day without removing the ability to diverge again, because the four properties stayed four separate choices per site. This makes them one choice.
The vehicle is the
font:shorthand, which #1878 banned outright. That ban is inverted, not lifted: the shorthand is the only mechanism CSS has that makes the four inseparable, and its reset semantics — it resets weight, style, variant and family — turn from hazard into guarantee once the only legal value is a role token. The alternative the issue names, Astryx's<Text>/<Heading>components, cannot carry this surface:Text.d.tstypesasas'span' | 'p' | 'div' | 'label' | 'h1' | 'h2' | 'h3', which terminates 142 of the 348 selectors (strong,small,li,dt,code,pre,h4,th,time), and it exposessize/weightoverride props, so the triplet would not actually be indivisible.Roles are Astryx's 14, composed from its generated atoms only. There are no product roles: the 42 blocks whose measured values had no Astryx equivalent were hand-rolled picks the ladder never offered — every Astryx heading is 600, yet 10 blocks used 700; the 12px tier offers 400 and 600, yet 11 used 500.
Two things had to move out of the call site for this to work. Family is an inheritance axis (
--maka-font-family), because the shorthand's family slot is mandatory. And the role table is anchored on:rootand:where(code, kbd, samp, pre), becausevar()inside a custom property is substituted where the property is declared and the resolved string is what inherits — a table composed only on:rootfreezes the sans stack into every role. That is not a code-reading conclusion: measured,.maka-tool-diff-body, a real<pre>, read--maka-font-family: "Geist Mono Variable"and computedfont-family: -apple-system. With the second anchor every code element gets the mono role for free, which is why 8 render sites moved to<code>markup instead of gaining a product role.Six guards collapse into one. A ban on product
--leading-*tiers, on literal ratios, on!importantsizes and leadings, on em/rem multipliers, and the size↔leading pairing check are each a way for the four to come apart — none is expressible once a call site cannot write a font longhand.findLeadingPairingOffendersis deleted rather than kept beside the new check; two authorities on one question means the weaker one is the one people read. Six product tokens (--font-size-heading/stat/ui/caption,--font-sans,--font-default) reached zero consumers and are deleted with them.Closes#1875.
Verification
Measured against
105ec3e87, not the branch point, since #1876 moved the session rail under this work.Computed-style regression net. 16,076 text elements across 86 surfaces (15 e2e windows + every settings page and module hub via a navigation walk + 62 Storybook stories), four properties each, captured from a pristine-main worktree and re-captured here. 118 elements differ, every one a convergence this PR intends:
<strong>section titles — Astryx has no 700Defects the measurement found that reading the code did not. Each is recorded in the commit that fixes it:
.maka-hero h1was assigned from a measured value that described a different rule masking it; per-rule redundancy is not compositional (deleting 79 individually-redundant rules drifted 85 elements 12px→14px); custom-property substitution is not lazy (the single-anchor role table above);--maka-text-display-1was referenced by the hero and defined nowhere, so the whole declaration was invalid and the element silently inherited;.maka-session-rename-inputdeclaredfont: inheritafter its new role, leaving the role line dead. Also fixed a latent off-grid bug:.maka-tool-namerendered 16px against an inherited 1.6667 ratio = 26.67px.What the net did not cover, and what review found there. The 118 are every element the walk rendered; 188 of the 371 role-declaring rules were never on a rendered surface, and four defects were living in that gap. Two were visible:
.plan-proposal-kickerhad moved from the 12px tier to the 14px one, because a grouped rule gave it a role and its own rule gave it another — the cross-rule form of the dead-role bug, and the reason the contract now has an arm for it. Two were resets the net does not sample:font:clearsfont-variant-numeric, and the stat tiles'tabular-numssat in a rule the role's rule outranked. Six more rules in the gap drop--font-monofor the sans stack. Those six are the same call as the nine measured ones — mono was decoration, not code — and unlike the four above the cascade settles it without a render: none of the six is acode/kbd/samp/preelement, so the family slot resolves to the body stack and the other three axes are unchanged. Fifteen sites in total, not nine.Mutation verification — 19 mutations, each backed up and restored with
cp(nogit checkout/restore/stash). The first eight cover the guard as originally written: a font longhand at a call site, a hand-composed shorthand, an undefined role, a role built from a foreign atom, a single-anchor table, a third family authority, an Astryx atom rebound to a literal, and a secondfont:declaration after a role. Review pointed out that all eight only re-trigger assertions already written, and none touches a boundary this PR introduced — so eleven more do: a longhand, a literal ratio and a hand-composed shorthand insidemaka-tokens.css; a role token rebound to a value; the family axis rebound to a literal stack; an Astryx atom rebound tocalc(); a longhand inside a rule-nested@media; an upper-cased property name; a second role rule for one selector; the role table anchored on:rootalone; and the family longhand moved off the code anchor. All nineteen fire; the tree restores green.Visual evidence. Live-app captures,
mainon the left and this branch on the right, composed with ImageMagick (+append) — no generative model touched the pixels.Module hub — page title 25px 600→400, skill ids stay mono as <code> markup
Settings · 通用 — page title 22px 600→400, size and leading unchanged
Settings · 记忆 — strong 700→600, filename stays mono while the prose beside it drops to sans, path is now <code>
Settings · 权限与能力 — capability ids as <code>, section titles on heading roles
Checks run:
lint✅ ·format:check✅ ·typecheck✅ ·@maka/desktop test1339 ✅ ·@maka/ui test239 ✅ ·desktop e2e73/73 ✅ ·check-dead-css --check✅ ·astryx:theme --check✅.Not run: no per-provider or new E2E fixture was added — the three new invariants that need a live document are steps inside the existing
e2e/type-scale.spec.tsprobe.Review focus
Page titles lose their weight, not their size.
.settingsPageHeader h2,.maka-module-main-header h2and.maka-plan-heading h2were display sizes (22px/25px) carrying a hand-rolled semibold. Astryx's display roles are weight 400 by design. Matching on weight instead would pickheading-1and collapse 46 page titles to 20px, level with the section headings under them — so this trades weight to keep hierarchy. It is the most visible change in the PR and the one most worth disagreeing with, and it is a deliberate override of Astryx's own guidance, which reserves display for hero, marketing and data-callout text and says not to use it for document headings. Recorded at the call site rather than only here.The component route is closed by the vendor's type union, not just by
as.Text'sasprop accepts onlyspan | p | div | label | h1 | h2 | h3, which strands thestrong/small/li/dt/code/preselectors — but the harder blocker isBuiltinTextType, which has no heading roles at all: 99 call sites nameheading-1..5and<Text>cannot express any of them.Headingcan, and unlikeTextit exposes nosize/weightoverrides — the divisibility argument applies toTextonly. For page headingsHeadingremains the more idiomatic seam; this PR does not move markup that far.Redundant role declarations are kept, deliberately. The joint-redundancy probe says 79 of 371 role-declaring rules change nothing when removed — but only 183 of the 371 are exercised by any surface the harness reaches, so 51% of the vocabulary is unverifiable, and this PR has already been bitten twice by "the measured value described a different rule than the one I was editing". Deleting on that evidence would be asserting redundancy about rules the measurement never saw. A no-op
font: var(--maka-text-body)costs nothing structurally: the point of the role token is that a call site can no longer diverge into four values.