`motir-marketing` has no design-asset guard lane — and a PORT of motir-core's mock scanner would be green on all 54 sites it was asked to catch
Found while running MOTIR-3985, which ADOPTED motir-core's answer that a design board's chrome owes AA (MOTIR-3054) and swept the 54 sites. That card's third obligation was "give the repository a lane, or say why it has none", and the reason it has none is not cost — it is that the obvious lane does not work, which is a finding rather than a deferral.
The decision is now enforced by nothing
design/marketing/design-notes.md § A design board's CHROME owes AA states the rule and
.github/workflows/ci.yml runs lint · typecheck · build · test. vitest.config.mts's include is
tests/**/*.test.ts(x) and no spec in tests/ reads design/** for contrast at all — the closest,
tests/aaMatrix.test.ts, rules on --el-* token pairs read off rendered React components via
tests/support/paintedInks.ts, which resolves Tailwind CLASSES and cannot see a .mock.html
stylesheet. So the next asset authored here re-introduces the defect and CI is green.
⚠️ AND THE PORT IS THE TRAP — motir-core's scanner is STRUCTURALLY BLIND to this repository's defect
The natural fix is to copy motir-core's tests/theme/inkContrastMockScan.ts + vitest.design.config.ts.
Measured against the 54 sites MOTIR-3985 swept, that port reports ZERO.
scanMock's muted arm resolves a surface with ownSurface, which reads a bg-(--el-*) utility, an
inline background: var(--el-*), or a stylesheet background: var(--el-*); declaredToken returns
null for anything that is not a var(--el-…). It then rules the resolved surface against
TINTED_SURFACE_TOKENS, a list of --el-* NAMES.
These two mocks paint their annotation board on body { background: #f4f3f1 } — a raw scaffold
colour the assets own outright, by the same convention motir-core's own mocks use for a sheet. The
walk therefore resolves null at every one of the 54 elements, finds no tinted token, and abstains.
Two of the sites were not even a --el-* INK (#1aae39 at 2.65:1, #e03131 at 4.07:1), which no arm
of that scanner rules on either.
A guard whose blind spot is exactly the population it was installed for reads as a verdict. That is the thing this card exists to avoid shipping, and it is why the lane is a design question rather than a copy.
The scan that DOES see it, and what it costs
The measurement that found all 54 — and that MOTIR-3985 re-ran to
verify 54 → 0 — loads each mock in headless chromium, walks every element carrying its own text
node, reads its resolved color against its effective background (compositing ancestor fills), and
rules at that element's OWN size and weight. It is total by construction: it starts from the
elements, not from a list of pairs, so a raw hex is measured exactly like a token. Reproduced on that
card's branch:
scanned 502 text-bearing elements; 1 below 1.4.3
1 3.85 design-showcase.mock.html span.i 13px/500 #6c5cdd on #0f0f0f — "☾"
(That one entry is the already-dispositioned decorative ☾ glyph — an accessible name in its own
sibling label, clearing 1.4.11's 3:1 — recorded in design-notes.md and not a 1.4.3 failure.)
The cost is a browser this repository does not depend on. package.json has no playwright /
@playwright/test; MOTIR-3985's scan and both .png re-exports
borrowed motir-core's installed chromium. So the lane means: a devDependency, a
playwright install chromium step, a vitest.design.config.ts, and a design-guards job — added to
deploy's needs in the same change, per ci.yml's own header ("do not declare a gate before its
job exists").
⚠️ jsdom is NOT a cheaper substitute, and it was measured rather than assumed. It is already a
devDependency and the suite already runs on it, so it is the first thing to reach for. Probed against
design-showcase.mock.html, jsdom's getComputedStyle applies the class cascade for color — and
then drops two things this scan cannot do without: it does not expand a font: SHORTHAND (.rule's
font: 600 10px … reads back as 16px / normal, so every 1.4.3 large-text call is wrong), and it
does not apply background: var(--el-page-bg) (.frame reads back rgba(0,0,0,0), so the surface
walk resolves the sheet where the asset paints white). Both failures are silent and both fall on the
side that under-reports. That is this card's own trap one layer down — do not re-derive it.
Acceptance criteria
- A lane exists that FAILS on the pre-MOTIR-3985 state of both
mocks. Assert it against a FIXTURE carrying the real failing markup (a
#f4f3f1sheet with a--el-text-mutedcaption on it, and a raw-hex ink), not against the swept assets — a guard whose only evidence is a green tree on a tree that already passes is the shape this card is about. - The lane rules on
::before/::aftergenerated content as well as elements, or its header states that it does not and why.design-showcase.mock.html's.fold::after("FOLD — 900px", 10px/700,#e03131) is the live instance:document.querySelectorAll('*')cannot reach it, and agetComputedStyle(el, '::after')probe measures it at 4.51:1 on#ffffff— clearing 1.4.3 by 0.01, on a raw hex nothing measures. ci.ymlgains the job ANDdeploy'sneedsgains its name, in the same change.- The lane runs on EVERY branch prefix (a
design/*diff is the only kind that can break it, andci.ymlhere has no path filtering today — confirm that before assuming a skip exists to fix). pnpm lint,format:check,typecheck,build,test.
Boundary
Not the sweep — MOTIR-3985 swept the 54 sites and recorded the
decision; this card only makes the decision enforceable. Not motir-core — nothing here changes
inkContrastMockScan.ts; it is cited as the guard that does not transfer, and the reason is a
property of THIS repository's assets (a raw sheet), not a defect in it. Not the component layer —
tests/aaMatrix.test.ts already rules on what the site paints and is untouched. Not the .png
re-export tool — that is the other thing the same devDependency pays for, and it is
MOTIR-4003, which consumes this card's playwright pin. The two
were one card until likely-over-gate-sizing fired on the pair at 90 minutes.
Advisory dispositions
validate_work_item after the split returns ONE blocker and ONE advisory, and both are correct.
blocker— MOTIR-3985 atin_progress— INTENDED, not a defect. This card's first acceptance criterion asserts the lane FAILS on the pre-sweep state of both mocks, which is a fixture derived from that card's diff; and landing the lane before the sweep merges would turnmainred. The edge is the onelog-bug.md's EDGE TEST asked for, and it is why this card sits in MOTIR-656 rather than under the epic.reference· advisory — names MOTIR-4003 attodo— NO EDGE IS OWED, and the direction is the whole answer. This card does not consume the render script; the render script consumes theplaywrightpin this card adds. The edge exists and runs the other way (MOTIR-4003 blocked_by MOTIR-4001). The reference here is a Boundary pointer naming what this card deliberately does NOT do, which is exactly the shape gate 4's "until proven otherwise" clause exists for.