Rule handbook metadata

Rule
DET.CARD.ACTION_LIMIT · lane deterministic
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.py copies the After example HTML from this handbook page onto the defect fixture, then invoke-det-ruleset-remediation-verify.sh re-audits and expects zero findings for this rule.
  • Production sites: run-website-ux-remediation-loop.sh runs handbook_html_patch (HTML patches in lib/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—typically btn btn-forge or a single btn btn-primary—plus optional low-emphasis btn btn-forge-outline or btn btn-cyan-outline siblings.
  • Link cards use the whole surface as the primary: <a class="forge-card breathe-link" href="…"> with no additional btn-forge buttons inside the same root.
  • Nested cards are scoped independently: a primary inside a child .forge-card does not count against the parent container.
  • Toolbar cards are skipped when data-card-kind="toolbar", data-ks-type / data-ks-name includes toolbar, classes toolbar-card / card-toolbar apply, or the card contains [role="toolbar"] / .btn-toolbar.
  • Nav, pagination, breadcrumb, dropdown, and cookie regions are excluded even when markup uses .card classes.
  • metrics.cardActionLimitReport.violations is empty after crawl; primaryCount ≤ 1 for every scanned non-toolbar card.

Failing signals

  • too-many-primary-actions: A .forge-card or .card reports primaryCount=2 or higher (for example labels="Start tour | Read docs" both on btn-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-link on the root and inner btn btn-forge buttons—double primaries on one card root.
  • Marketing or product cards reuse hero semantics inside a card footer (multiple btn-forge without toolbar contract).
  • Page may pass DET.CTA.HIERARCHY per 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.

Start tour Read docs

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):

  1. Pick one owner — keep the highest-intent label on btn btn-forge; demote siblings to btn btn-forge-outline, btn btn-cyan-outline, or plain forge-support text links.
  2. Prefer link-card pattern — when the whole tile navigates, use <a class="forge-card breathe-link"> alone; remove inner filled buttons.
  3. Split goals — separate “start” and “learn” into two .col-md-4 cards in the same row g-3 grid instead of two primaries in one footer.
  4. Toolbar contract — when multiple filled toggles are required, set data-card-kind="toolbar", add role="toolbar" or .btn-toolbar, and document the exception in the design contract.
  5. Re-run analyze-website-ux.mjs on pages with outcome grids; pair with DET.BUTTON.GROUP.MAX when the overcrowding is a horizontal hero row rather than a card footer.