Rule handbook metadata
- Rule
DET.CARD.ACTION_LIMIT· lanedeterministic- Page status
- current
- page_version
7f989d49b54debbbf250a24bc84c6bc5f9dedadedc654dd4e15989008004f934- generated_at
- 2026-05-19T20:30:00.000Z
- registry_fingerprint
2ce40848effce579d3e4879f6ca85535183a14db201870a6c007da424624550c
How this rule is fixed
Pilot deterministic fixer: handbook_after · harness mode standalone · verify expect_rule_clean.
- Harness:
apply-harness-fixture-remediation.pycopies the After example HTML from this handbook page onto the defect fixture, theninvoke-det-ruleset-remediation-verify.shre-audits and expects zero findings for this rule. - Production sites:
run-website-ux-remediation-loop.shrunshandbook_html_patch(HTML patches inlib/ux-deterministic-fixers/fixers/patches/) before invoking the Cursor agent when the quality gate still fails.
Detection module: docs/design/ux-audit/deterministic-design-rules.md#det-card-action-limit. Scroll down for Before / After examples and Evidence and remediation steps.
Purpose
Kitchen Sink cards (.forge-card, Bootstrap .card, [data-card], showcase preview-card tiles) are meant to answer one question and offer one decisive next step. When a card footer stacks two or more filled primaries (btn btn-forge, btn btn-primary), scanners cannot tell which action owns the tile and conversion metrics blur across competing labels.
This deterministic rule scans visible card roots in Playwright, counts primary actions inside each card (excluding nested child cards), and allows at most one (MAX_PRIMARY_ACTIONS_PER_CARD = 1). Outline and secondary controls (btn-forge-outline, btn-cyan-outline, btn-outline-*, btn-secondary, btn-link) do not count toward the cap. Cards that declare a toolbar-card contract may host multiple primaries when the pattern is intentional (filter rails, editor toolbars, presentation stage controls).
Plan: Audit outcome grids, product tiles, and handbook callout cards on a crawl URL. Do: Keep one btn-forge (or link-card surface) per card; demote siblings to outline buttons or move overflow into a toolbar card. Check: cardActionLimitReport.violations is empty. Adjust: Split multi-goal tiles into separate cards or adopt data-card-kind="toolbar" with [role="toolbar"] / .btn-toolbar when multiple filled actions are required.
Passing signals
- A
.forge-card.breathe-static(or.card) body ends with one filled primary—typicallybtn btn-forgeor a singlebtn btn-primary—plus optional low-emphasisbtn btn-forge-outlineorbtn btn-cyan-outlinesiblings. - Link cards use the whole surface as the primary:
<a class="forge-card breathe-link" href="…">with no additionalbtn-forgebuttons inside the same root. - Nested cards are scoped independently: a primary inside a child
.forge-carddoes not count against the parent container. - Toolbar cards are skipped when
data-card-kind="toolbar",data-ks-type/data-ks-nameincludestoolbar, classestoolbar-card/card-toolbarapply, or the card contains[role="toolbar"]/.btn-toolbar. - Nav, pagination, breadcrumb, dropdown, and cookie regions are excluded even when markup uses
.cardclasses. metrics.cardActionLimitReport.violationsis empty after crawl;primaryCount ≤ 1for every scanned non-toolbar card.
Failing signals
too-many-primary-actions: A.forge-cardor.cardreportsprimaryCount=2or higher (for examplelabels="Start tour | Read docs"both onbtn-forge).- Severity: major when two primaries compete; critical when three or more filled primaries appear in one card (
primaryCount > maxAllowed + 1). - Evidence strings include
primary_actions=N max=1 card="div.forge-card…"and truncated button labels. - A static tile combines
forge-card breathe-linkon the root and innerbtn btn-forgebuttons—double primaries on one card root. - Marketing or product cards reuse hero semantics inside a card footer (multiple
btn-forgewithout toolbar contract). - Page may pass
DET.CTA.HIERARCHYper viewport region yet still fail here because each card is its own conversion surface.
Before example
Before (failing example)
Outcome
Governed delegation
Shape intent, delegate safely, and keep human review on the critical path.
Failing KS markup: static outcome card with two filled btn-forge actions in the footer—two primaries on one card root.
After example
After (passing example)
Outcome
Governed delegation
Shape intent, delegate safely, and keep human review on the critical path.
Passing KS markup: one filled primary plus an outline secondary in the same footer—only btn-forge counts as primary.
Alternative passing pattern when multiple filled controls are intentional—declare a toolbar card:
<div
class="forge-card toolbar-card p-2"
data-card-kind="toolbar"
data-ks-type="component"
data-ks-name="filter-toolbar-card"
hash="Tbr"
data-ks-hash="Tbr"
>
<motion.div class="btn-toolbar gap-2" role="toolbar" aria-label="Filter views">
<button type="button" class="btn btn-forge btn-sm">All</button>
<button type="button" class="btn btn-forge btn-sm">Active</button>
<button type="button" class="btn btn-forge btn-sm">Archived</button>
</motion.div>
</div>
Evidence and remediation
Evidence: Auditor field metrics.cardActionLimitReport with maxAllowed: 1, cardCount, and violations[] entries (kind: too-many-primary-actions, primaryCount, selectorHint, labels). Findings message: A card exposes more than one primary action… with primary_actions=N max=1 in evidence. Reproduce in DevTools by selecting the card root and counting visible .btn-forge / .btn-primary descendants (ignore .btn-forge-outline and .btn-secondary).
Remediate (in order):
- Pick one owner — keep the highest-intent label on
btn btn-forge; demote siblings tobtn btn-forge-outline,btn btn-cyan-outline, or plainforge-supporttext links. - Prefer link-card pattern — when the whole tile navigates, use
<a class="forge-card breathe-link">alone; remove inner filled buttons. - Split goals — separate “start” and “learn” into two
.col-md-4cards in the samerow g-3grid instead of two primaries in one footer. - Toolbar contract — when multiple filled toggles are required, set
data-card-kind="toolbar", addrole="toolbar"or.btn-toolbar, and document the exception in the design contract. - Re-run
analyze-website-ux.mjson pages with outcome grids; pair withDET.BUTTON.GROUP.MAXwhen the overcrowding is a horizontal hero row rather than a card footer.