ADR-092 ActifActive governance

Safari `:visited` hex-fallback incident, consolidated record (amends ADR-047, ADR-059, ADR-060)

DateDate
2026-08-28
DécideursDecision makers
Guilherme Negreiros — Design System Lead

Context

ADR-047 (2026-06-05) established a system-wide rule: navigation elements never carry a distinct :visited color, realigned to the unvisited state via a semantic token.

WebKit blocks var() resolution inside :visited rules for security reasons — a defense against history-sniffing attacks that infer a user's browsing history from computed styles. var() is accepted as syntax but silently ignored at apply time, so a :visited rule written as color: var(--agtc-semantic-color-text-secondary) alone never resolves in Safari: the link keeps the browser's default :visited color instead of the token value, defeating ADR-047's rule on that one browser.

This ADR consolidates the incident record that was previously scattered across three separate documents — ADR-047's original text (which did not yet know about the Safari constraint), a post-incident note added to ADR-059, and a corrected timeline in ADR-060 (PR #64, 2026-08-24) — into a single dedicated record, and is the canonical reference going forward.

What happened

Four commits, spanning three weeks:

DateCommitEvent
2026-05-30b5850cf:visited neutralization added as var() alone, no fallback — passes on Chrome/Firefox, silently fails on Safari
2026-06-06fb7b5a5First fix, bundled into a broader dark-section/visited-state/active-nav pass — adds the literal hex value before the var() declaration as a Safari fallback
2026-06-156b42f21Regression: the semantic-tokens-hierarchy audit (ADR-059) scans site/build.js for hardcoded values, flags the color:#hex;color:var(...) pair in the :visited rules as an obsolete IE11-style fallback, and strips the hex line
2026-06-15ce994bfSame-day fix: hex fallback restored, and the exception documented durably in .claude/rules/no-visited-nav.md and ADR-059, so a future audit doesn't repeat the same misclassification

The regression and its fix landed the same day — the audit tooling had no way to distinguish "hardcoded value that should route through a token" from "browser-mandated literal that must stay paired with the token," because both look identical at the syntax level (color:#hex;color:var(...)).

Decision

1. The hex-then-var() pairing in :visited rules is a permanent, required pattern — not a stopgap and not IE11-era cruft — for as long as WebKit blocks var() resolution inside :visited. It is documented as an explicit exception in .claude/rules/no-visited-nav.md (§Safari exception), which any future automated token audit must treat as allow-listed rather than flag as a violation. 2. The literal hex value must always be the resolved value of the paired semantic token, kept in sync manually — this is not a hardcoded value in the sense of tokens-system.md, since it carries no independent design decision; it is a browser-compatibility mirror of the token. 3. ADR-047, ADR-059, and ADR-060 are not rewritten to remove the incident material they already carry (their post-incident notes and corrected timeline stay as the in-context record at each decision point) — this ADR is the single place a reader goes for the full incident end-to-end, cross-linked from the other three.

Reference implementation

/* Hex value resolved BEFORE the var() — Safari applies the hex, Chrome/Firefox apply
   var(). Declare before :hover/.active rules per ADR-047 (later selector must win at
   equal specificity). */
.top-nav a:visited { color: #646464; color: var(--agtc-semantic-color-text-secondary); }

/* Dark theme: same pattern with the resolved dark value */
[data-theme="dark"] .top-nav a:visited { color: #a4abb8; color: var(--agtc-semantic-color-text-secondary); }

Applied identically across every navigation surface in scope for ADR-047 (top-nav, sidebar, toc, footer-links, audit-footer-link, icon buttons).

Accessibility (WCAG 2.2)

No regression versus ADR-047's original assessment: the hex fallback resolves to the exact same color the token already resolves to elsewhere, so contrast ratios validated under ADR-047 are unchanged. The fix restores parity across browsers rather than introducing a new visual state — before the fix, Safari users saw the browser's default purple :visited tint on navigation, an inconsistency the rest of ADR-047 was written to eliminate.

Rejected alternatives

Rejected: reintroduces exactly the cross-browser inconsistency ADR-047 exists to remove, only silently and on one browser family.

(parse selector context, not just the value pattern) instead of relying on a documented allow-list.** Deferred, not rejected outright: a structural fix is more robust but a larger change to scripts/audit-tokens.js's matching logic; the allow-list documented in no-visited-nav.md closes the immediate gap at near-zero cost and is tracked as a candidate follow-up rather than blocking this record.

Rejected: ADR-047 predates the discovery and its original text is part of the historical record of what was decided and known at the time (2026-06-05, before the regression); folding the incident into it retroactively would obscure that the constraint was learned the hard way three weeks later, not designed in from the start.

Consequences

Safari exception pattern (§Safari exception) — this ADR does not change that file, it documents why the pattern exists.

no-visited-nav.md's exception list before flagging a :visited hex-then-var() pair as a violation, to avoid repeating the 2026-06-15 regression.

material already present in ADR-047, ADR-059, and ADR-060.

← ADR-091 ADR-093 →