Every surface this package renders targets WCAG 2.2 level AAA. Apply this whenever you build, extend, or touch one, in the same change as the slice. Accessibility is part of "done," not a later pass. The surfaces today:
php artisan boost:add-skill robot-council/core
Save this content to: .cursor/rules/accessibility.md
# Rule — accessibility: WCAG 2.2 AAA as the target, with cognitive (COGA) and low-vision support
Every surface this package renders **targets [WCAG 2.2](https://www.w3.org/TR/WCAG22/) level AAA**. Apply this whenever you build, extend, or touch one, in the **same change** as the slice. Accessibility is part of "done," not a later pass. The surfaces today:
| Surface | Route | View |
| --- | --- | --- |
| Overview | `dashboard` | [`livewire/dashboard.blade.php`](../../resources/views/livewire/dashboard.blade.php), with [`livewire/fleet-totals.blade.php`](../../resources/views/livewire/fleet-totals.blade.php) |
| Lanes | `dashboard/lanes` | [`livewire/lanes.blade.php`](../../resources/views/livewire/lanes.blade.php) |
| Queue | `dashboard/queue` | [`livewire/task-board.blade.php`](../../resources/views/livewire/task-board.blade.php) |
| Agents | `dashboard/agents` | [`livewire/agents.blade.php`](../../resources/views/livewire/agents.blade.php) |
| Locks | `dashboard/locks` | [`livewire/locks.blade.php`](../../resources/views/livewire/locks.blade.php) |
| Change feed | `dashboard/feed` | [`livewire/change-feed.blade.php`](../../resources/views/livewire/change-feed.blade.php) |
| Administration | `dashboard/administration` | [`livewire/administration.blade.php`](../../resources/views/livewire/administration.blade.php) |
| Access | `dashboard/access` | [`livewire/access-lists.blade.php`](../../resources/views/livewire/access-lists.blade.php) |
| Seat settings | `dashboard/seats` | [`livewire/seat-settings.blade.php`](../../resources/views/livewire/seat-settings.blade.php) |
| Waiting on me | `dashboard/waiting` | [`livewire/waiting-on-me.blade.php`](../../resources/views/livewire/waiting-on-me.blade.php) |
| Enrollment | `enroll` | [`enroll.blade.php`](../../resources/views/enroll.blade.php) |
| Signed out / sign-in expired | `signed-out`, and the callback's failure path | [`signed-out.blade.php`](../../resources/views/signed-out.blade.php), [`sign-in-expired.blade.php`](../../resources/views/sign-in-expired.blade.php) |
All of the dashboard pages sit inside [`layouts/dashboard.blade.php`](../../resources/views/layouts/dashboard.blade.php), and every page shares [`partials/head.blade.php`](../../resources/views/partials/head.blade.php). A new surface joins this table in the change that adds it.
**AAA is the target, AA is the floor.** WCAG itself says full AAA conformance is not achievable for all content, so AAA is something to build toward, never a compliance claim. **AA is the minimum** every shipped surface must meet. When an AAA criterion genuinely cannot be met for a piece of content, meet AA and **say why AAA was out of reach**, in the pull request and, where the reason will outlive it, in a comment beside the markup. Never drop the bar silently.
**This raises the bar the dashboard was built to.** Until this rule, the standard was set ticket by ticket and aimed at AA: #197 and #229 raised dim text to the AA ratio, and the theme overrides in [`resources/css/dashboard.css`](../../resources/css/dashboard.css) are each justified against AA. So the existing surfaces do not meet this rule yet, and bringing them up to it is tracked per section below rather than assumed.
**Three layers, WCAG first.** [WCAG 2.2](https://www.w3.org/TR/WCAG22/), the published Recommendation, is the spine and the **tiebreaker**. Where it is thin on cognitive and low-vision needs, two W3C task-force resources extend it: the **COGA techniques** and the **Low Vision requirements**, distilled in their own sections below. Both are *editor's drafts* and **subordinate to WCAG**: apply them to go *beyond* WCAG, never to contradict it. If either conflicts with the latest published WCAG, **WCAG wins**. Their versions, and the instruction to keep them current, are under *Source status*.
## How to apply
- **Semantic, keyboard-first markup.** Real headings, landmarks and lists. Every interactive control is reachable and operable by keyboard, with a visible focus indicator, and nothing traps focus. The surfaces most at risk are the ones built from daisyUI's CSS-only widgets: the `drawer` sidebar in the layout, which opens through a hidden checkbox, and any `dropdown` or `collapse`. Tracked in #400.
- **Prose semantics: the right inline element for a term.** In running prose, mark a term with the element that carries its *meaning*, not a `<span>` with a class, which carries none:
- a code-level **identifier** (a task id, a lock name such as `branch:feature/foo`, an ability such as `coordinator:direct`, a harness or machine label): use **`<code>`**, not `<var>` (a placeholder) or `<samp>` (program output);
- a term's **defining instance**, where the prose introduces it: use **`<dfn>`**;
- an **abbreviation** (CI, MCP, PR): use **`<abbr title="expansion">`**, and spell a load-bearing one out on first use, which is COGA's expand-on-first-use;
- **importance**: use `<strong>`; **stress emphasis**: use `<em>`;
- **`<mark>`** only for content relevant to what the reader is doing right now, such as the row a filter matched. Never general emphasis, and back any meaning it carries with text, because most screen readers do not announce it by default.
- **Never** put load-bearing meaning in a `title=` attribute alone: keyboard, touch and screen-reader users cannot reliably reach it. `<abbr title>` is the one sanctioned use, and even then a load-bearing expansion is also written out on first use. The dashboard has no `title=` today (counted 2026-09-25 across `resources/views`), and it should stay that way.
- **Name, role, value for every control.** Every button, filter, toggle and form field has an accessible name and announces its state: the Agents, Locks and Administration filters already announce their state through `aria-pressed`, and the Queue's status select through its selected option; and must also show it visibly by more than `btn-primary`'s color (text, a boundary, or both); the sidebar toggle says whether the menu is open. An icon-only control carries `sr-only` text or `aria-label`.
- **Contrast at the AAA threshold, and never color alone.** Text reaches **7:1** at normal size and **4.5:1** at large size (24px, or 18.66px bold). Measure with the [`wcag-contrast`](../skills/wcag-contrast/SKILL.md) skill against **both** themes in `dashboard.css`, since a pair that passes in light can fail in dark. **A daisyUI semantic color is visual meaning only.** `badge-success`, `badge-warning`, `badge-error`, `text-error`, the theme's `primary` and every other semantic token reach no assistive technology, and a reader who cannot tell the hues apart gets nothing from them. So a session's status, a lock's state, a lane's condition and a task's status each read as a **word**, with the color as reinforcement. Tracked in #398.
- **Respect user preferences.** Honor `prefers-reduced-motion`: the dashboard declares nothing for it today, and daisyUI's own transitions are short, but anything added that moves, auto-scrolls or animates must stop under it. Never override the user's text size or zoom.
- **Readable text blocks ([SC 1.4.8](https://www.w3.org/TR/WCAG22/#visual-presentation), AAA).** A *block of running prose*, such as a page's lede, an explanatory paragraph, an empty state or a narration body in the change feed, gets a **font-size-independent** measure (a `rem` cap near 68 characters at the base size, such as `max-w-xl`, which is 36rem, rather than a `ch` measure, which varies with the element's own font size), line spacing **of at least 1.5** (`leading-relaxed` or more, not the tighter leading that Tailwind's small-text utilities carry), **never justified** text, and paragraph spacing at least 1.5 times the line spacing where paragraphs stack. It applies to **blocks of text**, not to single-line labels, badges, table cells or button rows, whose tight leading is intentional. SC 1.4.8 frames every one of its items, including the 80-character width and resizing to **200%** with no horizontal scroll, as something a *mechanism* must make available ([1.4.8 Note 1](https://www.w3.org/TR/WCAG22/#visual-presentation)), so a consistent reflow-safe cap is the correct reading rather than an exact character count, and the 200% resize with no page-level horizontal scroll is also [SC 1.4.10](https://www.w3.org/TR/WCAG22/#reflow)'s requirement, which is binding at AA.
- **Comfortable default text size: body 16px, never below 12px.** Body and content text defaults to **16px** (`text-base`). **14px (`text-sm`) is the floor for anything read as content**, such as dense secondary text or a caption that is a sentence. **12px (`text-xs`) is for micro-labels only**: badges, table column headers, timestamps beside a row, ids. Never use anything smaller, including an arbitrary `text-[11px]`. WCAG sets no minimum, because it relies on zoom, but a small default makes every low-vision user zoom. **Density is never a reason to shrink content below the floor**: if a surface feels crowded at 16px, loosen the layout. **Watch daisyUI's size modifiers, which set their own font size:** measured in the built `resources/dist/dashboard.css` on 2026-09-25, `btn-sm` sets **12px**, `btn-xs` sets **11px** and `badge-xs` sets **10px**, so every `btn-xs` and `badge-xs` is sub-12px text whatever utility sits beside it; `badge-sm` sets 12px. `badge-xs` is the smallest text the dashboard ships today, on the change feed's `coordinator` badge. This guidance is what #310 applies; the scale itself is stated there, in one place.
- **Verify, don't assume.** Before the pull request, for each surface the change touches:
- measure every new or changed color pair with the [`wcag-contrast`](../skills/wcag-contrast/SKILL.md) skill, in both themes;
- tab through the surface: every control reached in a sensible order, focus visible, nothing trapped;
- check it with a screen reader (VoiceOver on macOS, NVDA on Windows): headings, landmarks, control names and states, and the text that stands in for color;
- use it **by touch at 390px wide**, and with an on-screen keyboard, per the pointer section below;
- zoom it to **200%** and **400%**, and apply the text-spacing values from the low-vision section below.
Treat an accessibility regression like a failing test.
- **The automated check is a floor, and a narrow one.** [`tests/Browser/DashboardAccessibilityTest.php`](../../tests/Browser/DashboardAccessibilityTest.php) (#403) loads every surface in a real headless Chromium with a seeded fleet, in both themes, and fails the gate on any serious or critical axe-core violation among the WCAG A and AA rules. It reports the AAA rules to `build/axe-aaa.json` without failing on them, and adds three checks axe cannot make: that no status is carried by color alone, that status words survive forced colors, and that every control meets its target size. Each check has a planted canary that must fail, so a clean run means the check looked. It runs as the `browser` job in CI and locally with `composer test:browser`, after `npx playwright install chromium`. **Treat it as necessary, never sufficient.** axe reliably automates only about 30 to 40 percent of WCAG (contrast, names and states, reflow), and nothing in the suite can judge plain language, focus order, or whether a state change was announced; the manual steps above stay the load-bearing pass.
## Pointer, touch and target size: WCAG 2.5 input modalities
A keyboard, screen-reader and contrast pass never exercises *pointer modality*, so tap-target size, touch zoom and hover-only affordances go unchecked unless they are looked for on purpose. WCAG 2.2's [Guideline 2.5, Input Modalities](https://www.w3.org/TR/WCAG22/#input-modalities) is the standard; apply it in the same change as the slice.
- **Target size: 44 by 44 is the AAA target ([SC 2.5.5](https://www.w3.org/TR/WCAG22/#target-size-enhanced)), 24 by 24 the floor ([SC 2.5.8](https://www.w3.org/TR/WCAG22/#target-size-minimum), AA).** In daisyUI's terms, measured in the built stylesheet on 2026-09-25 with the default `--size-field` of `0.25rem`: **`btn` is 40px tall, `btn-sm` 32px and `btn-xs` 24px**, and a `badge` is 24px, `badge-sm` 20px and `badge-xs` 16px. **None of them reaches 44px**, and `btn-xs` meets the 24px floor exactly, with no margin against it. **Every control on the dashboard reaches 44px** (#480). That covers the consequential ones: approving or denying an enrollment, revoking an installation or a session, approving, denying or imposing a role, parking or waiving a lane's work, exempting a seat, and signing out. It also covers the **touch-primary** ones #480 reclassified from dense: filters, pagers, "Show all", every form field, the navigation rows, and a link that stands alone in a table cell. A button takes `btn-target`; a text, number, date or time field and a select are 44px through the stylesheet with no class needed; a checkbox is reached through the label wrapped round it, which the view makes `min-h-11`; a lone link in a cell is `min-h-11 min-w-11`. Where a row of filters would wrap badly at that size, change the control rather than shrink it, as the Queue's status filter became a select. No control names an `xs` or `sm` size modifier, and `DashboardThemeTest` refuses one on a button, field, checkbox, radio or toggle. A control smaller than 24px ships only under one of SC 2.5.8's exceptions: **Spacing** (a 24px-diameter circle around it overlaps no other target or its circle), **Equivalent** (the same action has a qualifying control on the same page), **Inline** (it sits inside a sentence or block of text), **User agent control** (its size is the browser's, unmodified), or **Essential** (that presentation is essential, as in a dense data visualization). The dashboard has no case for Essential today. When a control meets only AA and could have met AAA, say why. Tracked in #399 and #480.
- **Single-pointer alternatives to gestures ([SC 2.5.1](https://www.w3.org/TR/WCAG22/#pointer-gestures), A; [SC 2.5.7](https://www.w3.org/TR/WCAG22/#dragging-movements), AA).** Any path-based or multipoint gesture needs a single-pointer alternative without a path, and any drag needs a single-pointer alternative without a drag, unless the gesture is essential. The dashboard has none today. A reorderable queue or a draggable lane card would be the first, and it would ship with buttons that do the same thing. A keyboard path does **not** discharge this obligation, because a head-pointer, eye-gaze or speech user has neither the gesture nor the keyboard.
- **Pointer cancellation ([SC 2.5.2](https://www.w3.org/TR/WCAG22/#pointer-cancellation), A).** Act on the **up-event**, never the down-event, so a mis-aimed tap can be abandoned by sliding off before release. A native `<button>`, a form submit and Livewire's `wire:click` all do this already, so in practice the rule is: **don't bind a consequential action to `pointerdown` or `mousedown`.**
- **No meaning in hover or `title=` alone.** A `title=` tooltip is unreachable by touch and keyboard. The same point is made under prose semantics, and it is repeated here because it is a pointer failure too: meaning belongs in visible text or `sr-only` text.
## Cognitive and learning disabilities: COGA techniques
The [COGA techniques](https://w3c.github.io/coga/techniques/) address memory, attention, language and executive-function load, which is where WCAG is thinnest. The dashboard is demanding here: its readers are looking at a fleet of agents described in a vocabulary of its own (*seat*, *lane*, *placement*, *hand-back*, *fence*, *stale* against *gone*, *coordinator*, *directive*, *narration*). Apply, in the same change as the slice:
- **Explain the vocabulary in place.** Every term of art the dashboard shows carries a plain-language explanation next to it: an inline gloss, a `<dfn>` on first use, or a link to where it is explained. Never a bare term. A developer opening the dashboard for the first time must not need the README to read the Agents page. (COGA *writing style* and *help meaning*.) Tracked in #402.
- **Plain, concrete language.** Short sentences, active voice, one idea at a time. Expand or avoid abbreviations. Lead with the keyword ("Gone — no contact for 30 minutes", not "The session, having not been heard from, is now gone"). Empty states, errors and confirmations need this most.
- **Be predictable across surfaces.** The same control means and does the same thing everywhere: a status badge, a filter row, a session's name and a timestamp render and behave the same on the overview, Agents, Locks, Lanes and the Queue. Consistent layout, wording and placement; no page-specific surprises.
- **Clear structure and visible affordances.** Real headings and chunked content. Actionable things *look* actionable: a filter that is not selected still reads as a control, not as flat text (#309 fixed the instance of this that shipped, a `btn-ghost` filter with no boundary at rest). Link text says where it goes, never "click here". Every icon has a text label.
- **Rapid, direct feedback.** Every consequential action confirms in words: "Enrollment approved for josh-office", "Seat exempted from placement", "Role request withdrawn". A control shows its new state as soon as it is activated, and an error appears next to the field it is about, in plain language, with what to do next. Tracked in #402.
- **Don't trap the user in time, and don't lose their work.** Most dashboard views refresh themselves with `wire:poll`, every `robot-council.dashboard.poll_seconds` (default 5). A refresh must never discard what a person is part-way through: a half-typed field, an open menu, a selected filter, a scroll position. Nothing has a time limit a person cannot extend, and anything long-running shows its progress and a way to stop.
- **Orient on return.** After a navigation, or a refresh that changes what is shown, the page still says where the reader is (its heading, and the current item in the sidebar marked with `aria-current`) and what just changed.
- **Adaptable by construction.** Rich semantic HTML, with ARIA roles and states only where HTML has none, so a user's assistive tools can re-present the content. No blinking, no auto-scroll, no surprise pop-ups.
## Low vision: requirements
The [Low Vision requirements](https://w3c.github.io/low-vision-a11y-tf/requirements.html) are mostly about **not breaking the user's own customizations**: zoom, enlarged text, restyled spacing, overridden colors. The readable-text bullet above ([SC 1.4.8](https://www.w3.org/TR/WCAG22/#visual-presentation)) already covers measure, line spacing, justification and 200% resize. In addition:
- **Scale with the user, not against them.** Size text and layout in relative units (`rem`, `em`, `%`, `ch`), never fixed `px` that ignores the user's base font size; Tailwind's spacing and type scales are already `rem`. Content stays usable, and loses nothing, at **200% zoom**, and reflows to a single column with one scroll direction at **400%** ([SC 1.4.10 Reflow](https://www.w3.org/TR/WCAG22/#reflow)). **A wide `table` is the surface most at risk**, and the dashboard is mostly tables: at 400% a table needs its own horizontal scroll container (`overflow-x-auto` around it) so that the *page* does not scroll sideways, or it needs to restack into rows. Tracked in #401.
- **Survive restyled text.** Nothing is lost when the user forces the [SC 1.4.12 text-spacing](https://www.w3.org/TR/WCAG22/#text-spacing) values: line height at least 1.5 times the font size, paragraph spacing at least 2 times, letter spacing at least 0.12em and word spacing at least 0.16em. Nothing clips, overlaps or is cut off. **Verify** by applying those values to each surface with a text-spacing bookmarklet or a user stylesheet. **Patterns to watch:** a fixed `h-*` on a container that holds text (prefer `min-h-*`); `overflow-hidden` or `line-clamp-*` on a block of text; and `truncate` or `whitespace-nowrap` inside a fixed-width box. No view uses `truncate`, `line-clamp` or `text-ellipsis` since #401, which found the three in the layout's header cut off under the text spacing; `DashboardThemeTest` refuses one. A long identifier wraps instead, through the stylesheet's `overflow-wrap: break-word` on `body`.
- **Don't fight user and system display settings.** Never disable browser zoom: the shared viewport meta in `partials/head.blade.php` is `width=device-width, initial-scale=1` and must never gain `user-scalable=no` or a `maximum-scale`. Honor `prefers-reduced-motion`, `prefers-contrast` and **`forced-colors`** (Windows High Contrast). Under forced colors every daisyUI semantic color is replaced by the system's, so a badge's status and a selected filter lose their color entirely and must still read from their text or border. That is never-color-alone, checked again in forced-colors mode.
- **Keep the point of regard.** When a surface re-renders, as every `wire:poll` refresh does, and when a filter changes or a menu opens, keep the user's focus and scroll position where they were. A refresh that moves focus to the top of the page, or re-announces the whole table to a screen reader, is a failure. Tracked in #401.
- **Let elements be restyled independently.** Use element-level semantics (`<strong>`, `<em>`, `<code>`, `<cite>`, headings, lists, table headers with `scope`) so a user stylesheet can restyle headings, links and body text separately. Don't bury meaning in undifferentiated `<span>` and `<div>` elements.
## Source status: keep these current
Both task-force resources are **W3C Editor's Drafts**, not Recommendations: citable guidance, but moving targets, and **subordinate to WCAG** (above):
- **COGA techniques**: Editor's Draft **19 December 2025**, https://w3c.github.io/coga/techniques/
- **Accessibility Requirements for People with Low Vision**: Editor's Draft **28 March 2023**, https://w3c.github.io/low-vision-a11y-tf/requirements.html
**Precedence:** the latest published **WCAG** always wins a conflict. Between an editor's draft and a more mature version of the *same* document, the more recent and more published one wins. **Update this rule** when either advances, whether to a newer editor's draft or, taking precedence, to a published version (a W3C Note, or a Candidate, Proposed or final Recommendation). When you next do accessibility work, look at both URLs; if the date or maturity has moved past what is recorded here, read the current version and reconcile this rule's guidance and citations with it. WCAG itself stays pinned to the published [2.2 Recommendation](https://www.w3.org/TR/WCAG22/); re-pin it when a later WCAG, such as 3.0, is published.
## Provenance
Ported from [`cairnorg/cairn`'s accessibility rule](https://github.com/cairnorg/cairn/blob/793d3eae3b18fe4c0f250a9632f49787a914bef6/.claude/rules/accessibility.md), pinned at `cairnorg/cairn@793d3ea`, for #397. That rule names its own surfaces, components and tools; every one of them was replaced here with this repository's, or dropped where this repository has no counterpart. What was dropped, so that nobody goes looking for it: an in-app vocabulary page, a component-tone palette, two accessibility MCP servers, and a committed browser accessibility suite. The last is the gap #403 would close. When the source changes, compare it against the pinned commit rather than re-porting it whole.
## The DRY line
This file is the canonical statement of the standard: WCAG 2.2 AAA, plus the COGA and Low Vision sections above. It distills the subset of each resource that applies to this dashboard; the **exhaustive** lists of techniques and requirements stay at the source URLs, to be cited rather than copied here. [`CLAUDE.md`](../../CLAUDE.md) carries a one-line pointer here and does not restate the standard. **Measuring a contrast ratio** is the [`wcag-contrast`](../skills/wcag-contrast/SKILL.md) skill; this rule says which ratio a surface must reach. **The type scale** is #310's to state in one place; this rule states only the floors it must respect. When a surface's approach turns on a design decision (an irreducible gap against AAA, a trade between accessibility and density), record what was decided, why, and what was rejected **on the issue**, per [`design-decision-forks`](design-decision-forks.md). This rule stays the short standing directive.
Every surface this package renders targets WCAG 2.2 level AAA. Apply this whenever you build, extend, or touch one, in the same change as the slice. Accessibility is part of "done," not a later pass. The surfaces today:
| Surface | Route | View |
|---|---|---|
| Overview | dashboard |
livewire/dashboard.blade.php, with livewire/fleet-totals.blade.php |
| Lanes | dashboard/lanes |
livewire/lanes.blade.php |
| Queue | dashboard/queue |
livewire/task-board.blade.php |
| Agents | dashboard/agents |
livewire/agents.blade.php |
| Locks | dashboard/locks |
livewire/locks.blade.php |
| Change feed | dashboard/feed |
livewire/change-feed.blade.php |
| Administration | dashboard/administration |
livewire/administration.blade.php |
| Access | dashboard/access |
livewire/access-lists.blade.php |
| Seat settings | dashboard/seats |
livewire/seat-settings.blade.php |
| Waiting on me | dashboard/waiting |
livewire/waiting-on-me.blade.php |
| Enrollment | enroll |
enroll.blade.php |
| Signed out / sign-in expired | signed-out, and the callback's failure path |
signed-out.blade.php, sign-in-expired.blade.php |
All of the dashboard pages sit inside layouts/dashboard.blade.php, and every page shares partials/head.blade.php. A new surface joins this table in the change that adds it.
AAA is the target, AA is the floor. WCAG itself says full AAA conformance is not achievable for all content, so AAA is something to build toward, never a compliance claim. AA is the minimum every shipped surface must meet. When an AAA criterion genuinely cannot be met for a piece of content, meet AA and say why AAA was out of reach, in the pull request and, where the reason will outlive it, in a comment beside the markup. Never drop the bar silently.
This raises the bar the dashboard was built to. Until this rule, the standard was set ticket by ticket and aimed at AA: #197 and #229 raised dim text to the AA ratio, and the theme overrides in resources/css/dashboard.css are each justified against AA. So the existing surfaces do not meet this rule yet, and bringing them up to it is tracked per section below rather than assumed.
Three layers, WCAG first. WCAG 2.2, the published Recommendation, is the spine and the tiebreaker. Where it is thin on cognitive and low-vision needs, two W3C task-force resources extend it: the COGA techniques and the Low Vision requirements, distilled in their own sections below. Both are editor's drafts and subordinate to WCAG: apply them to go beyond WCAG, never to contradict it. If either conflicts with the latest published WCAG, WCAG wins. Their versions, and the instruction to keep them current, are under Source status.
drawer sidebar in the layout, which opens through a hidden checkbox, and any dropdown or collapse. Tracked in #400.<span> with a class, which carries none:
branch:feature/foo, an ability such as coordinator:direct, a harness or machine label): use <code>, not <var> (a placeholder) or <samp> (program output);<dfn>;<abbr title="expansion">, and spell a load-bearing one out on first use, which is COGA's expand-on-first-use;<strong>; stress emphasis: use <em>;<mark> only for content relevant to what the reader is doing right now, such as the row a filter matched. Never general emphasis, and back any meaning it carries with text, because most screen readers do not announce it by default.title= attribute alone: keyboard, touch and screen-reader users cannot reliably reach it. <abbr title> is the one sanctioned use, and even then a load-bearing expansion is also written out on first use. The dashboard has no title= today (counted 2026-09-25 across resources/views), and it should stay that way.aria-pressed, and the Queue's status select through its selected option; and must also show it visibly by more than btn-primary's color (text, a boundary, or both); the sidebar toggle says whether the menu is open. An icon-only control carries sr-only text or aria-label.wcag-contrast skill against both themes in dashboard.css, since a pair that passes in light can fail in dark. A daisyUI semantic color is visual meaning only. badge-success, badge-warning, badge-error, text-error, the theme's primary and every other semantic token reach no assistive technology, and a reader who cannot tell the hues apart gets nothing from them. So a session's status, a lock's state, a lane's condition and a task's status each read as a word, with the color as reinforcement. Tracked in #398.prefers-reduced-motion: the dashboard declares nothing for it today, and daisyUI's own transitions are short, but anything added that moves, auto-scrolls or animates must stop under it. Never override the user's text size or zoom.rem cap near 68 characters at the base size, such as max-w-xl, which is 36rem, rather than a ch measure, which varies with the element's own font size), line spacing of at least 1.5 (leading-relaxed or more, not the tighter leading that Tailwind's small-text utilities carry), never justified text, and paragraph spacing at least 1.5 times the line spacing where paragraphs stack. It applies to blocks of text, not to single-line labels, badges, table cells or button rows, whose tight leading is intentional. SC 1.4.8 frames every one of its items, including the 80-character width and resizing to 200% with no horizontal scroll, as something a mechanism must make available (1.4.8 Note 1), so a consistent reflow-safe cap is the correct reading rather than an exact character count, and the 200% resize with no page-level horizontal scroll is also SC 1.4.10's requirement, which is binding at AA.text-base). 14px (text-sm) is the floor for anything read as content, such as dense secondary text or a caption that is a sentence. 12px (text-xs) is for micro-labels only: badges, table column headers, timestamps beside a row, ids. Never use anything smaller, including an arbitrary text-[11px]. WCAG sets no minimum, because it relies on zoom, but a small default makes every low-vision user zoom. Density is never a reason to shrink content below the floor: if a surface feels crowded at 16px, loosen the layout. Watch daisyUI's size modifiers, which set their own font size: measured in the built resources/dist/dashboard.css on 2026-09-25, btn-sm sets 12px, btn-xs sets 11px and badge-xs sets 10px, so every btn-xs and badge-xs is sub-12px text whatever utility sits beside it; badge-sm sets 12px. badge-xs is the smallest text the dashboard ships today, on the change feed's coordinator badge. This guidance is what #310 applies; the scale itself is stated there, in one place.wcag-contrast skill, in both themes;Treat an accessibility regression like a failing test.
tests/Browser/DashboardAccessibilityTest.php (#403) loads every surface in a real headless Chromium with a seeded fleet, in both themes, and fails the gate on any serious or critical axe-core violation among the WCAG A and AA rules. It reports the AAA rules to build/axe-aaa.json without failing on them, and adds three checks axe cannot make: that no status is carried by color alone, that status words survive forced colors, and that every control meets its target size. Each check has a planted canary that must fail, so a clean run means the check looked. It runs as the browser job in CI and locally with composer test:browser, after npx playwright install chromium. Treat it as necessary, never sufficient. axe reliably automates only about 30 to 40 percent of WCAG (contrast, names and states, reflow), and nothing in the suite can judge plain language, focus order, or whether a state change was announced; the manual steps above stay the load-bearing pass.A keyboard, screen-reader and contrast pass never exercises pointer modality, so tap-target size, touch zoom and hover-only affordances go unchecked unless they are looked for on purpose. WCAG 2.2's Guideline 2.5, Input Modalities is the standard; apply it in the same change as the slice.
--size-field of 0.25rem: btn is 40px tall, btn-sm 32px and btn-xs 24px, and a badge is 24px, badge-sm 20px and badge-xs 16px. None of them reaches 44px, and btn-xs meets the 24px floor exactly, with no margin against it. Every control on the dashboard reaches 44px (#480). That covers the consequential ones: approving or denying an enrollment, revoking an installation or a session, approving, denying or imposing a role, parking or waiving a lane's work, exempting a seat, and signing out. It also covers the touch-primary ones #480 reclassified from dense: filters, pagers, "Show all", every form field, the navigation rows, and a link that stands alone in a table cell. A button takes btn-target; a text, number, date or time field and a select are 44px through the stylesheet with no class needed; a checkbox is reached through the label wrapped round it, which the view makes min-h-11; a lone link in a cell is min-h-11 min-w-11. Where a row of filters would wrap badly at that size, change the control rather than shrink it, as the Queue's status filter became a select. No control names an xs or sm size modifier, and DashboardThemeTest refuses one on a button, field, checkbox, radio or toggle. A control smaller than 24px ships only under one of SC 2.5.8's exceptions: Spacing (a 24px-diameter circle around it overlaps no other target or its circle), Equivalent (the same action has a qualifying control on the same page), Inline (it sits inside a sentence or block of text), User agent control (its size is the browser's, unmodified), or Essential (that presentation is essential, as in a dense data visualization). The dashboard has no case for Essential today. When a control meets only AA and could have met AAA, say why. Tracked in #399 and #480.<button>, a form submit and Livewire's wire:click all do this already, so in practice the rule is: don't bind a consequential action to pointerdown or mousedown.title= alone. A title= tooltip is unreachable by touch and keyboard. The same point is made under prose semantics, and it is repeated here because it is a pointer failure too: meaning belongs in visible text or sr-only text.The COGA techniques address memory, attention, language and executive-function load, which is where WCAG is thinnest. The dashboard is demanding here: its readers are looking at a fleet of agents described in a vocabulary of its own (seat, lane, placement, hand-back, fence, stale against gone, coordinator, directive, narration). Apply, in the same change as the slice:
<dfn> on first use, or a link to where it is explained. Never a bare term. A developer opening the dashboard for the first time must not need the README to read the Agents page. (COGA writing style and help meaning.) Tracked in #402.btn-ghost filter with no boundary at rest). Link text says where it goes, never "click here". Every icon has a text label.wire:poll, every robot-council.dashboard.poll_seconds (default 5). A refresh must never discard what a person is part-way through: a half-typed field, an open menu, a selected filter, a scroll position. Nothing has a time limit a person cannot extend, and anything long-running shows its progress and a way to stop.aria-current) and what just changed.The Low Vision requirements are mostly about not breaking the user's own customizations: zoom, enlarged text, restyled spacing, overridden colors. The readable-text bullet above (SC 1.4.8) already covers measure, line spacing, justification and 200% resize. In addition:
rem, em, %, ch), never fixed px that ignores the user's base font size; Tailwind's spacing and type scales are already rem. Content stays usable, and loses nothing, at 200% zoom, and reflows to a single column with one scroll direction at 400% (SC 1.4.10 Reflow). A wide table is the surface most at risk, and the dashboard is mostly tables: at 400% a table needs its own horizontal scroll container (overflow-x-auto around it) so that the page does not scroll sideways, or it needs to restack into rows. Tracked in #401.h-* on a container that holds text (prefer min-h-*); overflow-hidden or line-clamp-* on a block of text; and truncate or whitespace-nowrap inside a fixed-width box. No view uses truncate, line-clamp or text-ellipsis since #401, which found the three in the layout's header cut off under the text spacing; DashboardThemeTest refuses one. A long identifier wraps instead, through the stylesheet's overflow-wrap: break-word on body.partials/head.blade.php is width=device-width, initial-scale=1 and must never gain user-scalable=no or a maximum-scale. Honor prefers-reduced-motion, prefers-contrast and forced-colors (Windows High Contrast). Under forced colors every daisyUI semantic color is replaced by the system's, so a badge's status and a selected filter lose their color entirely and must still read from their text or border. That is never-color-alone, checked again in forced-colors mode.wire:poll refresh does, and when a filter changes or a menu opens, keep the user's focus and scroll position where they were. A refresh that moves focus to the top of the page, or re-announces the whole table to a screen reader, is a failure. Tracked in #401.<strong>, <em>, <code>, <cite>, headings, lists, table headers with scope) so a user stylesheet can restyle headings, links and body text separately. Don't bury meaning in undifferentiated <span> and <div> elements.Both task-force resources are W3C Editor's Drafts, not Recommendations: citable guidance, but moving targets, and subordinate to WCAG (above):
Precedence: the latest published WCAG always wins a conflict. Between an editor's draft and a more mature version of the same document, the more recent and more published one wins. Update this rule when either advances, whether to a newer editor's draft or, taking precedence, to a published version (a W3C Note, or a Candidate, Proposed or final Recommendation). When you next do accessibility work, look at both URLs; if the date or maturity has moved past what is recorded here, read the current version and reconcile this rule's guidance and citations with it. WCAG itself stays pinned to the published 2.2 Recommendation; re-pin it when a later WCAG, such as 3.0, is published.
Ported from cairnorg/cairn's accessibility rule, pinned at cairnorg/cairn@793d3ea, for #397. That rule names its own surfaces, components and tools; every one of them was replaced here with this repository's, or dropped where this repository has no counterpart. What was dropped, so that nobody goes looking for it: an in-app vocabulary page, a component-tone palette, two accessibility MCP servers, and a committed browser accessibility suite. The last is the gap #403 would close. When the source changes, compare it against the pinned commit rather than re-porting it whole.
This file is the canonical statement of the standard: WCAG 2.2 AAA, plus the COGA and Low Vision sections above. It distills the subset of each resource that applies to this dashboard; the exhaustive lists of techniques and requirements stay at the source URLs, to be cited rather than copied here. CLAUDE.md carries a one-line pointer here and does not restate the standard. Measuring a contrast ratio is the wcag-contrast skill; this rule says which ratio a surface must reach. The type scale is #310's to state in one place; this rule states only the floors it must respect. When a surface's approach turns on a design decision (an irreducible gap against AAA, a trade between accessibility and density), record what was decided, why, and what was rejected on the issue, per design-decision-forks. This rule stays the short standing directive.
How can I help you explore Laravel packages today?