Skip to content

fix(web): keep the contrast slider from flattening text hierarchy - #70

Merged
rynfar merged 1 commit into
pylonfrom
fix/appearance-contrast-hierarchy
Aug 24, 2026
Merged

fix(web): keep the contrast slider from flattening text hierarchy#70
rynfar merged 1 commit into
pylonfrom
fix/appearance-contrast-hierarchy

Conversation

@rynfar

@rynfar rynfar commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

The last open finding from the #68 review, now measured in a browser rather than reasoned about.

The problem

#7906's contrast boost mixes every foreground role toward one shared target. Per CSS Color 5 an omitted color-mix() percentage is 100% - other, so at a full 100% boost the inner mix gets 0% and each token resolves to the target itself.

Measured against the real Pylon light-theme tokens, reading back getComputedStyle().color:

contrast --contrast-foreground L --contrast-muted-foreground L separation
100 (default) 0.274 0.552 0.278
160 0.110 0.221 0.111
180 0.055 0.110 0.055
200 (max) 0 0 0

At 200, --contrast-foreground, --contrast-muted-foreground, --contrast-placeholder and --contrast-icon-muted all come back oklab(0 0 0). Body text, muted timestamps, icon labels and placeholder text render identically — so an empty composer is indistinguishable from a filled one, and the whole secondary hierarchy disappears at the setting's own maximum. It degrades well before the maximum, too.

The fix

Scale the foreground mix by 0.6, so the maximum lands at a 60% boost instead of 100%:

fg L muted L separation
default 0.274 0.552 0.278
200 as shipped 0 0 0
200 proposed 0.110 0.221 0.111

The slider keeps its full 50–200 range and its direction; the top is still clearly darker than the default; the roles stay apart. The ratio between roles was already preserved at every level — it's the absolute separation that collapses — so capping the mix is enough.

Borders keep the full curve. They carry a single role, so converging on the target costs no hierarchy, and their existing quarter-weight already bounds them at 25%.

0.6 is a judgment call. 0.5 gives separation 0.139 if you want more headroom; the constant is named and documented at the one place it's applied.

Notes

  • Arrived with #7906 in chore(upstream): adopt the 2026-08-23 T3 Code batch #68. Not a Pylon-authored defect, and upstream has the same behavior.
  • Verification was a standalone CSS harness driving the real token definitions, because the point in question is how the browser resolves nested color-mix() — not app wiring.
  • vp test run apps/web/src/appearanceContrast.test.ts — 4 passed; the max-boost test now also asserts the boost is not 100%.
  • @t3tools/web typecheck clean; vp check clean on both files.

Claude Opus 5 in Pylon.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

The appearance contrast boost mixes every foreground role toward one shared
target, so at a full 100% they all resolve to exactly that target. Measured in a
browser against the real tokens: at the slider's own maximum of 200,
`--contrast-foreground`, `--contrast-muted-foreground`, `--contrast-placeholder`
and `--contrast-icon-muted` all come back `oklab(0 0 0)`. Body text, muted
timestamps and placeholder text become the same colour, so an empty composer
reads as a filled one.

It degrades before the maximum too — lightness separation between normal and
muted text runs 0.278 at the default, 0.111 at 160, 0.055 at 180, then 0.

Scaling the foreground mix to 0.6 keeps the top of the range clearly darker than
the default while holding the roles apart, and leaves the slider's range and
direction untouched. Borders keep the full curve: they carry a single role, so
converging costs no hierarchy, and their quarter-weight already bounds them.

Arrived with #7906 (upstream) in #68; not a Pylon-authored defect.
@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S labels Aug 23, 2026
@github-actions

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 13.4 KiB 13.4 KiB −13 B (−0.1%) 15.1 KiB
Codex Thread snapshot wire 6.9 KiB 6.9 KiB +2 B (+0.0%) 7.3 KiB
Codex Live turn WebSocket wire 6.6 KiB 6.5 KiB −15 B (−0.2%) 7.8 KiB
Codex Live turn WebSocket decoded 55.0 KiB 55.0 KiB 0 B (0.0%) 66.4 KiB
Codex Live turn messages 16 16 0 (0.0%) 21
Claude Total thread wire 13.4 KiB 13.4 KiB −4 B (−0.0%) 15.1 KiB
Claude Thread snapshot wire 6.9 KiB 6.9 KiB −5 B (−0.1%) 7.3 KiB
Claude Live turn WebSocket wire 6.6 KiB 6.6 KiB +1 B (+0.0%) 7.8 KiB
Claude Live turn WebSocket decoded 55.8 KiB 55.8 KiB 0 B (0.0%) 66.4 KiB
Claude Live turn messages 16 16 0 (0.0%) 21

Baseline: ea55ccb · PR result: f9d1de8 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 109.4 KiB
  • Claude decoded thread snapshot: 110.0 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@rynfar
rynfar merged commit fe83b3c into pylon Aug 24, 2026
14 checks passed
@rynfar
rynfar deleted the fix/appearance-contrast-hierarchy branch August 24, 2026 06:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant