# A styleguide two models can be held to

Version 1.0.0

- Scope: web interfaces written or edited by AI coding agents, alone or beside people.
- Every rule here can be failed. Each one carries a check. A sentence nobody can fail belongs in a different document.
- Precedence: project tokens and conventions win over house defaults. Explain departures. A project convention does not waive an applicable accessibility requirement.
- Levels: MUST is required by this guide; SHOULD is a default that may be set aside with a reason. A house convention is never automatically a WCAG conformance failure. Read the Basis and the specific clause.
- Agreement means two chairs signed the same sha256 of this text. It does not mean two models were asked and both said yes.

## Working from this guide

How an agent applies the guide inside a codebase it did not write, next to another agent that reasons differently.

### use.read-first (SHOULD)

Before writing any style, read the project's tokens and the three nearest existing components.

- Check: The change adds no color, size or spacing literal that an existing token already covers.
- Why: Reading existing tokens avoids competing definitions.
- Basis: House convention.

### use.project-wins (MUST)

Where the project and this guide disagree, follow the project and say so in the change description.

- Check: The description names the rule id being set aside.
- Why: A guide that overrides a working codebase produces two styles instead of one.
- Basis: House convention.

### use.cite (SHOULD)

When a style decision is not obvious, cite the rule id in the commit or pull request.

- Check: A change that introduces a new visual pattern has a rule id in its description.
- Why: A cited rule can be argued with. An uncited preference can only be reverted.
- Basis: House convention.

### use.scope (MUST)

Do not restyle what the task did not ask about.

- Check: Every changed selector is reachable from the task's stated scope.
- Why: Drive-by restyling is how two agents end up reformatting each other's work forever.
- Basis: House convention.

### use.dispute (MUST)

When two agents disagree about a rule, neither edits the other's output; the disagreement is filed as an objection and the text stands until both sign a replacement.

- Check: No revert-and-restyle commit pair between agents on the same file within one change.
- Why: A style argument carried out in diffs has no record and no end.
- Basis: House convention.

## The checks

What an Enforced by line means, what each check reads, and what to do when the checker is not there.

- Enforced by names endpoint checks: css. ids use check_css; contrast uses check_contrast. MCP: https://hugin.studio/parley/mcp.
- Each id says what it reads. A css. check reads source: the stylesheet text as written, with no cascade, no inheritance, no markup and no rendered page. contrast reads one foreground and background color pair that you supply.
- A finding identifies a source-level departure from a rule. A warning is not a defect. One check can produce both kinds. Resolve warnings against the rule and its exceptions; unresolved means unverified. Token-dependent readings never become definite findings merely because a definition exists in the snippet. Repeated token uncertainty may be grouped by check with its declaration count and examples.
- No finding means the part of the rule that can be read from text was not broken, and nothing more.
- Enforced by identifies checks that always warn. Other checks also warn when token values or exceptions cannot be established. The kind on each result is authoritative.
- A check that could not run is not a pass. The rule is unverified, and the change description says so.
- Where the checker is not available, the rule's own Check line is the definition. Perform it by hand and record the result in the change description.
- A rule with no Enforced by line is checked by hand, at its stated level. A source scan is not an accessibility certification.

## Type

Text is most of any interface. Set it so it survives a user's own font size, a narrow screen and a long line.

### type.families (SHOULD)

Use at most two type families plus one monospace.

- Check: Count the distinct first-choice families across all font-family declarations.
- Why: Every added family is a download, a rhythm and a decision the next contributor has to guess at.
- Basis: House convention.
- Enforced by: css.families (source)

### type.body-size (SHOULD)

Use a computed body and form-control text size of at least 16px at default settings; verify the actual size rather than assuming 1rem always equals 16px.

- Check: Computed font-size of running text and of input, select and textarea is 16px or more at default settings.
- Why: Smaller body text is the commonest reason a page is zoomed, and iOS zooms the viewport on focus for controls under 16px.
- Basis: House convention, not a WCAG minimum font size.
- Enforced by: css.body-size (source, warns), css.input-size (source, warns)

### type.rem (SHOULD)

Set font sizes in rem, or a clamp() built on rem, never in px. The root element keeps the reader's default: its font-size is 100 percent or unset, never a px or percentage value that rescales it.

- Check: No font-size declaration in px, and the root font-size is 100 percent or unset.
- Why: A px size ignores the default text size a reader has set in their browser. Page zoom does scale px text, so this is a rule of this guide, not a conformance failure.
- Basis: Convention. WCAG 2.2 SC 1.4.4 Resize Text (AA) is met by page zoom and does not require relative units, so do not report a px font-size as a WCAG defect.
- Enforced by: css.font-px (source), css.root-size (source)

### type.scale (SHOULD)

Take every size from one scale of at most eight steps.

- Check: The stylesheet declares eight or fewer distinct font-size values.
- Why: A ninth size is almost always a mistake nobody wanted to look up.
- Basis: House convention.
- Enforced by: css.size-count (source)

### type.measure (SHOULD)

Hold running text to between 45 and 75 characters a line.

- Check: Prose containers set max-width between 45ch and 75ch.
- Why: Past about 80 characters the eye loses the start of the next line.
- Basis: WCAG 2.2 SC 1.4.8 Visual Presentation (AAA) caps width at 80 characters; 45 to 75 is the older typographic convention

### type.leading (SHOULD)

Line-height is unitless: 1.5 to 1.7 for running text, 1.05 to 1.3 for headings.

- Check: No line-height carries a length unit, and running text computes to at least 1.5.
- Why: A line-height with a unit does not scale when the font size does, and lines collide.
- Basis: WCAG 2.2 SC 1.4.8 Visual Presentation (AAA) asks for line spacing of at least space-and-a-half within paragraphs. Not SC 1.4.12, which governs the reader's own overrides: see type.spacing-override.
- Enforced by: css.leading-units (source)

### type.spacing-override (MUST)

Text survives the reader's own spacing: no text is clipped when line height is set to 1.5 times the font size, paragraph spacing to 2 times, letter spacing to 0.12em and word spacing to 0.16em.

- Check: Apply all four user spacing overrides and confirm no text or functionality is lost.
- Check: Fixed block sizes with overflow hidden/clip warrant inspection; source cannot prove actual clipping. Scrolling overflow and growing min-height containers may work. For intentionally truncated text, verify that its full form remains available through an accessible action.
- Why: A box fitted to default typography may clip text when a reader increases spacing.
- Basis: WCAG 2.2 SC 1.4.12 Text Spacing (AA). The allowance for truncated text whose full form is available is from the W3C's Understanding document for this criterion.
- Enforced by: css.clipped-text (source, warns)

### type.no-justify (SHOULD)

Never justify text.

- Check: No text-align: justify.
- Why: Browsers justify without hyphenation control, which opens rivers of white through the paragraph.
- Basis: WCAG 2.2 SC 1.4.8 Visual Presentation (AAA)
- Enforced by: css.justify (source)

### type.caps (SHOULD)

Keep capitals for labels of three words or fewer, tracked out by at least 0.06em.

- Check: Every rule that sets text-transform: uppercase also sets letter-spacing of 0.06em or more.
- Why: Untracked capitals set tight and read as shouting; a sentence in capitals loses its word shapes.
- Basis: House convention.
- Enforced by: css.caps-tracking (source)

### type.numerals (SHOULD)

Numbers that change, or that align in a column, use tabular figures.

- Check: Tables, counters and timers set font-variant-numeric: tabular-nums.
- Why: Proportional figures make a counting number jitter and a column of prices wander.
- Basis: House convention.

### type.wrap (SHOULD)

Balance headings with text-wrap: balance and never break one by hand.

- Check: No <br> inside a heading.
- Why: A hand-placed break is correct at exactly one width.
- Basis: House convention.

## Space and layout

One unit, one owner for every gap, and a page that works on the narrowest screen first.

### space.grid (SHOULD)

Every margin, padding and gap is a multiple of 4px, or of 0.25rem, taken from a named scale.

- Check: No spacing declaration is a px value that 4 does not divide, or a rem value that 0.25 does not divide, hairlines of 1px and 2px excepted. Values in em, percentages and calc() are not judged.
- Why: Off-grid values are how a layout ends up with eleven slightly different gaps.
- Basis: House convention.
- Enforced by: css.grid-4 (source)

### space.owner (SHOULD)

The parent owns the space between children: use gap, and give components no outer margin.

- Check: A component's root selector declares no margin.
- Why: A component that carries its own margin is wrong in every context but the one it was drawn in.
- Basis: House convention.

### layout.mobile-first (SHOULD)

Write base styles for the narrowest screen and add min-width queries upward.

- Check: Media queries use min-width; a max-width query carries a comment saying why.
- Why: Desktop-first styles ship every override to the device least able to afford them.
- Basis: House convention.
- Enforced by: css.max-width-query (source)

### layout.reflow (MUST)

The page works at 320 CSS pixels wide with no horizontal scrolling and nothing cut off.

- Check: At a 320px viewport, document.documentElement.scrollWidth is 320 or less, and no container hides its overflow to get there.
- Exceptions: Content that needs two dimensions to be understood, such as a data table, a map or a diagram, may scroll inside its own region.
- Why: 320px is a phone in portrait and also a desktop window zoomed to 400 percent.
- Basis: WCAG 2.2 SC 1.4.10 Reflow (AA)

### layout.logical (SHOULD)

Use logical properties such as margin-inline and padding-block in place of left and right.

- Check: No margin-left, margin-right, padding-left or padding-right in new code.
- Why: Physical sides are wrong the day the interface is translated into a right-to-left language.
- Basis: House convention.
- Enforced by: css.physical-props (source)

### layout.targets (MUST)

Pointer targets are at least 24 by 24 CSS pixels, or are spaced so that a 24px circle centred on each does not touch another target's. Primary and touch-first controls SHOULD reach 44 by 44.

- Check: Measure authored pointer targets outside the exceptions. Under 24px in either axis fails unless the criterion's 24px-circle spacing test passes. Record primary or touch-first targets below 44px as SHOULD misses, not AA failures.
- Exceptions: The criterion exempts inline or line-height-constrained targets, equivalent controls on the same page, unmodified user-agent targets, and essential or legally required presentation.
- Why: Small or crowded targets are difficult to operate precisely; inline text needs different treatment.
- Basis: WCAG 2.2 SC 2.5.8 Target Size (Minimum) (AA, 24px, five exceptions). SC 2.5.5 Target Size (Enhanced) (AAA, 44px) is the SHOULD, not the MUST.

### layout.z (SHOULD)

z-index values come from a named scale of at most six layers.

- Check: Every z-index is a token read with var(), except 0, 1 and -1 used for local stacking inside one component, and the stylesheet reads six distinct layer tokens or fewer.
- Why: z-index: 9999 is an argument somebody lost with a stacking context.
- Basis: House convention.
- Enforced by: css.z-index (source)

### layout.box (MUST)

Every image, video, iframe and embed declares its intrinsic box: width and height attributes, or an aspect-ratio, or a container that reserves the space.

- Check: Reserve each img, video, iframe and embed's space before loading: matching width/height attributes, aspect-ratio on the element, or an ancestor with aspect-ratio or fixed block size that the element fills. Missing reservations or a declared ratio that mismatches the file fail.
- Why: Missing or incorrect reservations shift the content below a loading element.
- Basis: House convention.

### layout.shift (SHOULD)

Late content never moves what is being read: no banner, consent bar, ad slot, toast or injected notice is inserted above content that is already painted.

- Check: Load the page on a throttled connection and watch above the fold; nothing already painted changes position. Where the project measures it in the field, Cumulative Layout Shift at the 75th percentile is 0.1 or less.
- Why: A page that jumps under the reader's thumb turns a tap into the wrong tap.
- Basis: Core Web Vitals: good CLS is at most 0.1 at the field's 75th percentile. A product target, not a source-code verdict.

## Color

Color is a small set of named jobs, measured for contrast, and never the only thing carrying a meaning.

### color.tokens (SHOULD)

Use semantic tokens for component colors and keep color literals in palette definitions.

- Check: Outside custom-property definitions, prefer var(), currentColor, transparent or inheritance keywords. Other CSS color literals, including named colors and color functions, depart from this default. System colors are allowed in forced-colors and prefers-contrast rules. Judge all color-bearing properties, including borders, shadows, gradients, SVG fills and strokes.
- Why: Central definitions make theme changes and contrast reviews easier.
- Basis: House convention.
- Enforced by: css.color-literal (source)

### color.semantic (SHOULD)

Name tokens for their job, such as surface, ink, accent and danger, not for their hue.

- Check: No token consumed by a component has a hue word or a palette step in its name.
- Why: A token called blue-500 is a lie the first time dark mode makes it a different blue.
- Basis: House convention.
- Enforced by: css.token-hue-names (source)

### color.contrast-text (MUST)

Text meets 4.5 to 1 against its background; text of 24px, or 18.66px bold, and larger meets 3 to 1.

- Check: Measure actual foreground/background pairs used by text in every relevant rendered state. Unused token combinations are not conformance failures.
- Check: For imagery, gradients and translucent layers, judge the minimum composited contrast beneath the text. For a fixed opaque foreground, text luminance outside a known background-luminance range plus a passing nearer extreme is sufficient, not necessary. Over unpredictable imagery, use a backing with verified worst-case contrast; it may be translucent if every possible composite passes. A few video samples do not establish exhaustive coverage.
- Exceptions: SC 1.4.3 exempts text in inactive controls, pure decoration, text invisible to everyone, text within a picture containing significant other visual content, and logos or brand names. Overlaid interface text is not incidental picture text.
- Why: Below 4.5 to 1, body text disappears for readers with low vision and for everyone in sunlight. A headline over a photograph is the one that changes ratio with every image the CMS loads.
- Basis: WCAG 2.2 SC 1.4.3 Contrast Minimum (AA)
- Enforced by: contrast (a color pair you supply), css.block-contrast (source, warns)

### color.contrast-ui (MUST)

Input borders, focus rings, icons and chart marks that carry meaning meet 3 to 1 against what they touch.

- Check: Measure against adjacent colors the visual information needed to identify controls, states and graphics. Do not demand a boundary when sufficiently contrasting visible content already identifies the control.
- Exceptions: Inactive controls; unmodified user-agent appearance; graphics whose particular presentation is essential to their information.
- Why: A form whose fields cannot be seen is a form that cannot be filled in.
- Basis: WCAG 2.2 SC 1.4.11 Non-text Contrast (AA)
- Enforced by: contrast (a color pair you supply)

### color.not-only (MUST)

Color is never the only signal: pair it with text, an icon or a shape.

- Check: View the interface in grayscale; every state and status is still distinguishable.
- Why: A red and a green of similar lightness are one color to a reader with red-green color blindness.
- Basis: WCAG 2.2 SC 1.4.1 Use of Color (A)

### color.one-accent (SHOULD)

Use one accent hue per product, and keep success, warning and danger colors for status alone.

- Check: Interactive emphasis uses a single accent token; status tokens appear only on status.
- Why: When everything is highlighted the highlight carries no information.
- Basis: House convention.

### color.no-pure (SHOULD)

Do not set pure black on pure white; soften at least one end.

- Check: No rule pairs #000 text with a #fff background, or the reverse.
- Why: This is a convention, not a standard: the harshest pair a screen can show buys nothing, since near-black on off-white still clears 15 to 1.
- Basis: Convention
- Enforced by: css.pure-bw (source)

### color.dark (SHOULD)

Dark mode is the same tokens with different values, switched by prefers-color-scheme, with color-scheme declared. A switch the reader can set by hand is a SHOULD, and a product decision.

- Check: No component selector sets a literal color under a dark-mode parent; the root declares color-scheme. A site that follows prefers-color-scheme and offers no toggle is not defective.
- Why: A second stylesheet for dark mode is a second interface to keep correct.
- Basis: House convention.

## Motion

Motion explains a change of state. It is short, cheap to render, and optional for anyone who asks.

### motion.duration (SHOULD)

Nothing a person waits on runs past 500ms. The working range for interface transitions is 120 to 320ms.

- Check: No transition-duration or animation-duration above 500ms outside a progress indicator.
- Why: Past half a second an animation stops explaining and starts being waited for.
- Basis: House convention.
- Enforced by: css.duration (source, warns)

### motion.properties (SHOULD)

Prefer transform and opacity. Color, shadow, filter and clip-path are allowed. Never animate a layout property, and never write transition: all.

- Check: No transition or animation, including any @keyframes block, names all, or any of width, height, inline-size, block-size, top, right, bottom, left, inset, margin, padding, border-width, font-size, or a grid or flex sizing property. Transitions and animations of color, background-color, border-color, box-shadow, filter, opacity, transform and clip-path are allowed.
- Why: Layout animation can repeat layout work; transition: all also animates future untested properties. This performance default allows color and shadow feedback.
- Basis: House convention.
- Enforced by: css.transition-all (source), css.layout-anim (source)

### motion.reduced (MUST)

Every animation has a prefers-reduced-motion: reduce fallback.

- Check: For each declared animation, or transition longer than 120ms, that moves, scales, parallaxes or rotates an element, a prefers-reduced-motion: reduce block later in the cascade sets that animation to none, to a non-moving equivalent, or to a duration under 20ms. An empty reduced-motion block, or one that does not reach the animation in question, does not count.
- Check: Motion driven from JavaScript reads window.matchMedia for prefers-reduced-motion: reduce before it starts and subscribes to its change event. A motion library is configured from that value at the root, once.
- Why: For people with vestibular disorders, parallax and zoom are nausea, not polish. A reduced-motion block that answers for no animation in particular is a comment, not a fallback.
- Basis: House policy covers all animation. WCAG 2.2 SC 2.3.3 (AAA) covers interaction-triggered motion and permits essential motion. Do not call an autoplay fallback omission a 2.3.3 failure; see motion.no-loop for autoplay.
- Enforced by: css.reduced-motion (source, warns)

### motion.easing (SHOULD)

Enter with ease-out, leave with ease-in, and keep linear for progress.

- Check: No linear timing on an element that enters or leaves.
- Why: Things in the world decelerate into place; linear motion reads as mechanical.
- Basis: House convention.

### motion.no-loop (MUST)

Automatically moving content that lasts more than five seconds alongside other content can be paused, stopped or hidden unless the movement is essential.

- Check: Observe each auto-starting loop alongside other content. If it lasts more than five seconds, verify a pause, stop or hide mechanism, or document why movement is essential. A progress indicator is not exempt merely because its class name says progress.
- Why: Perpetual motion pulls the eye from the task the page exists for.
- Basis: WCAG 2.2 SC 2.2.2 Pause, Stop, Hide (A); evaluate its essential-activity exception in the actual context.
- Enforced by: css.infinite (source, warns)

### motion.purpose (SHOULD)

If removing an animation loses no information, remove it.

- Check: Delete the animation; if a user can still tell what changed, the deletion stands.
- Why: Decoration that moves is paid for on every visit by every visitor.
- Basis: House convention.

## Components and states

A component is its states. The ones nobody drew are the ones users meet on a bad day.

### state.five (SHOULD)

Design each control's supported states: default, hover, focus, active and disabled where applicable.

- Check: Inspect supported states using pointer and keyboard. Native browser states can satisfy this requirement. Do not invent a disabled state for a link or control that never becomes disabled; visible keyboard focus remains required by state.focus.
- Why: A missing state is a moment where the interface stops answering.
- Basis: House convention.

### state.focus (MUST)

Keep keyboard focus visible; prefer a 2px or equivalent high-contrast indicator.

- Check: Tab through controls. Each shows a visible indicator. This guide additionally prefers an indicator as large as a 2px perimeter and 3:1 change between focused and unfocused pixels (AAA). Verify 3:1 against adjacent colors where SC 1.4.11 applies. An outline removed without a usable replacement fails; a noninteractive programmatic target may use different focus treatment.
- Why: Keyboard users navigate by the focus ring; removing it removes the cursor.
- Basis: WCAG 2.2 SC 2.4.7 Focus Visible (AA). The 2px and the focused-versus-unfocused 3 to 1 are SC 2.4.13 Focus Appearance (AAA); the indicator-against-background 3 to 1 is SC 1.4.11 Non-text Contrast (AA).
- Enforced by: css.outline-none (source, warns)

### state.focus-visible (SHOULD)

Draw focus rings on :focus-visible, not :focus.

- Check: Ring styles are declared on :focus-visible.
- Why: Rings that appear on mouse click are why designers asked for outline: none in the first place.
- Basis: House convention.
- Enforced by: css.focus-not-visible (source, warns)

### comp.native (MUST)

Prefer native controls for actions and navigation, and give every interactive control an accessible name.

- Check: Use button for an action and a with href for navigation. A custom control must provide the equivalent role, keyboard behavior, focus and state. Every interactive form control has an accessible name; comp.forms owns its visible-label policy. Hidden inputs need no name. Button-like inputs use their native naming mechanism; image inputs need appropriate alt.
- Why: Native elements bring keyboard, focus and screen-reader behavior that a div has to fake and usually gets wrong.
- Basis: WCAG 2.2 SC 2.1.1 Keyboard (A) and SC 4.1.2 Name, Role, Value (A). Native-element preference is house policy; a conforming custom control is not automatically a WCAG failure.

### comp.one-primary (SHOULD)

One primary action per view.

- Check: Count elements styled as the primary button in any single view; the answer is one.
- Why: Two primary buttons is a decision handed back to the user.
- Basis: House convention.

### comp.forms (MUST)

Give user-editable fields a persistent visible label and an accessible name; a placeholder is never the only label.

- Check: For input other than hidden, submit, reset, button and image, and for select and textarea, use a wrapping label or matching for/id. An exception may use aria-labelledby referring to persistent visible text for a search field beside a Search button or a table field whose row and column context identifies its purpose. State the exception in the change description and verify its computed name. A fieldset legend names a group, not every field; each control still needs its own identifying label.
- Check: For a field collecting information about the user whose purpose is in the WCAG input-purpose list, supply the matching HTML autocomplete token. Do not apply this criterion indiscriminately to every application field.
- Why: Placeholders vanish during entry. Detached labels do not reliably name controls for assistive technology.
- Basis: WCAG 2.2 SC 3.3.2 Labels or Instructions (A), SC 1.3.1 Info and Relationships (A), SC 1.3.5 Identify Input Purpose (AA). Persistent labels and the limited aria-labelledby exceptions are house policy stricter than those standards.

### comp.errors (MUST)

An error says what happened and what to do, sits next to the field, and is announced to assistive technology.

- Check: Each error is tied to its field with aria-describedby and contains an instruction, not only a verdict.
- Why: Invalid input tells a person they failed. Enter a date after today tells them how to succeed.
- Basis: WCAG 2.2 SC 3.3.1 Error Identification (A) and SC 3.3.3 Error Suggestion (AA)

### comp.states-designed (MUST)

Every view of data has a designed empty state, loading state and error state.

- Check: Each data component renders three named non-happy states, and each is reachable in a test.
- Why: The empty state is the first thing every new user sees.
- Basis: House convention.

### comp.dialog (MUST)

Match focus behavior to modality: modal surfaces contain focus and block the background; non-modal surfaces leave the page reachable.

- Check: Open a modal from the keyboard. Place focus at a meaningful starting point, contain Tab and Shift+Tab, make the background inert and provide an accessible close action including Escape. On close, return focus to the invoker, or a logical next step if it disappeared or the workflow moved on. Prefer dialog.showModal(), then verify naming, initial focus and return focus.
- Check: A non-modal drawer or panel does not trap focus or make the background inert. Move focus when the task continues inside it; leave it on the trigger for an ordinary disclosure. Follow the applicable widget keyboard pattern. A command palette is modal only if its behavior blocks the page.
- Why: A modal that leaves focus on the page behind it is a modal only for people who can see it, and a hand-built focus loop with no exit is a keyboard trap.
- Basis: WCAG 2.2 SC 2.1.2 No Keyboard Trap (A), SC 2.4.3 Focus Order (A), and the WAI-ARIA APG modal-dialog pattern. APG is implementation guidance, not an additional WCAG success criterion.

### comp.status (MUST)

A message reporting the result of an action, such as saved, copied, deleted or four results found, is announced without moving focus.

- Check: Each such message renders into a container that was already in the DOM, carrying a role of status or aria-live set to polite; the container is not created at the moment the message arrives. A message that stops the user takes a role of alert.
- Why: A toast is silent to a screen reader unless something tells it to speak, and a live region added at the same instant as its text usually says nothing at all.
- Basis: WCAG 2.2 SC 4.1.3 Status Messages (AA)

### comp.hover-content (MUST)

Content that appears on hover or focus is dismissible, hoverable and persistent.

- Check: With the tooltip or popover open: Escape dismisses it without moving pointer or focus; the pointer can travel onto the content without it closing; it stays until pointer or focus leaves, the user dismisses it, or its information stops being valid. It never closes on a timer.
- Exceptions: Taken from the criterion: content that communicates an input error, or that does not obscure or replace other content, need not be dismissible; and content whose presentation the user agent controls and the author has not modified, such as a native title tooltip, is outside the rule.
- Why: A tooltip that vanishes when you move toward it cannot be read by anyone using magnification.
- Basis: WCAG 2.2 SC 1.4.13 Content on Hover or Focus (AA)

### comp.drag-alternative (MUST)

Anything operated by dragging also works with single clicks or taps, without dragging.

- Check: For each non-exempt drag interaction, complete the same task using a single pointer without dragging. Offer clickable move controls, a slider track or numeric input, or a file picker. Test keyboard operability separately under a11y.keyboard; arrow keys alone do not satisfy this rule.
- Exceptions: Dragging that is essential, such as freehand drawing, and unmodified user-agent functionality are exempt. Map panning can use clickable direction controls or search and is not inherently exempt.
- Why: Dragging needs a sustained, accurate press that many people cannot make, and it is the interaction that fails first on a trackpad.
- Basis: WCAG 2.2 SC 2.5.7 Dragging Movements (AA)

### comp.auth (MUST)

Signing in does not depend on an unsupported cognitive test.

- Check: Credential fields accept paste and password-manager autofill. Use current-password, new-password and one-time-code as appropriate; verify the actual flow.
- Check: A cognitive test has a non-cognitive alternative, an assisting mechanism, an object-recognition-only test or identification of the user's own provided content, as SC 3.3.8 permits. A cross-device code must have a supported route such as paste or autofill.
- Why: Remembering and retyping credentials excludes people who depend on assistive mechanisms.
- Basis: WCAG 2.2 SC 3.3.8 Accessible Authentication (Minimum) (AA). Object recognition and personal content satisfy this level; they do not satisfy SC 3.3.9 (AAA).

## Words

Interface text is read by someone in the middle of doing something else.

### copy.case (SHOULD)

Sentence case everywhere: headings, buttons, labels and menus.

- Check: No Title Case string in interface copy apart from proper nouns.
- Why: Sentence case reads faster and ends the argument about which words to capitalize.
- Basis: House convention.

### copy.buttons (SHOULD)

A button is a verb and its object, such as Save draft, and never OK, Submit or Click here.

- Check: Every button label begins with a verb and names what it acts on.
- Why: A button should be understandable with the rest of the dialog covered up.
- Basis: House convention.

### copy.links (MUST)

Link text names its destination and makes sense read alone.

- Check: No link reads here, this, more or read more without its object.
- Why: Screen-reader users pull up a list of links; ten of them saying here is no list.
- Basis: House convention requiring standalone link purpose, stricter than WCAG 2.2 SC 2.4.4 (A), which allows programmatically determined context. Standalone purpose is SC 2.4.9 (AAA), subject to its exceptions. Do not report a contextual link as a Level A failure.

### copy.numbers (MUST)

Make consequential quantities and dates unambiguous in their visible context.

- Check: For prices, measurements, deadlines and event times, verify that units, currency, full dates and relevant timezone can be determined without guessing. Spell the month or use an unambiguous date format when the locale is not established. A number may inherit its unit from an associated table header or label.
- Why: 03/04 is two different days depending on which side of an ocean it is read from.
- Basis: House functional requirement: a reader must identify the same quantity or instant the product means.

### copy.no-filler (SHOULD)

No lorem ipsum, no Welcome to, and no exclamation marks in system text.

- Check: Search shipped strings for lorem, Welcome to and an exclamation mark.
- Why: Placeholder text that ships says nobody read the screen before release.
- Basis: House convention.

### copy.length (SHOULD)

Interface sentences stay under 20 words and lead with the point.

- Check: Count words per sentence in any text longer than a label.
- Why: Nobody reads an interface; they scan it for the next thing to do.
- Basis: House convention.

## Structure and access

The document underneath the pixels is the interface for a large number of people and for every machine.

### a11y.headings (MUST)

One h1 per page, and heading levels never skip.

- Check: First, every string presented as a heading is a heading element: a div, p or span styled large or bold in that role is a defect. Then list the heading elements in document order: there is exactly one h1, and no level is jumped on the way down.
- Why: Headings are the table of contents a screen reader navigates by, and a heading that is only bold text is not in it.
- Basis: WCAG 2.2 SC 1.3.1 Info and Relationships (A) requires that a visual heading be marked up as one. The single h1 and the unskipped levels are this guide's convention, not Level A, so do not report them as WCAG failures.

### a11y.landmarks (MUST)

Use header, nav, main and footer, with exactly one main.

- Check: The page has one main element and its navigation is inside nav.
- Why: Landmarks let a keyboard user skip the menu they have already heard forty times.
- Basis: House convention.

### a11y.alt (MUST)

Every image has an alt attribute, and a decorative image has an empty one.

- Check: No img without alt; decorative images carry an empty alt.
- Why: A missing alt makes a screen reader read out the file name.
- Basis: WCAG 2.2 SC 1.1.1 Non-text Content (A)

### a11y.keyboard (MUST)

Everything works from the keyboard in visual order, with no positive tabindex.

- Check: Tab through the page without a mouse; search the markup for tabindex values above 0.
- Why: A positive tabindex reorders the page for keyboard users and for nobody else.
- Basis: WCAG 2.2 SC 2.1.1 Keyboard (A) and SC 2.4.3 Focus Order (A)

### a11y.focus-not-covered (MUST)

No sticky header, footer, toolbar or floating panel hides the focused element entirely. It SHOULD not cover any part of it.

- Check: Tab through the page at 320px and at desktop width with every sticky region present. At each stop, some part of the focused element and its focus ring is visible: none visible is a defect, partly covered is a SHOULD miss. Where a sticky header exists, the scroll container sets scroll-padding-block-start to at least its height, which satisfies both.
- Why: A focus ring behind a sticky header is the same as no focus ring, and it is the failure a keyboard user meets first.
- Basis: WCAG 2.2 SC 2.4.11 Focus Not Obscured (Minimum) (AA) is the MUST: not entirely hidden. SC 2.4.12 Focus Not Obscured (Enhanced) (AAA), no part of the component hidden, is the SHOULD.

### a11y.names (MUST)

An icon-only control has an accessible name.

- Check: Verify each icon-only button or link's computed accessible name. Valid sources include aria-label, aria-labelledby, an image's alt, SVG naming and visually hidden text; an attribute alone does not prove the computed name.
- Why: An unnamed icon button is announced as button and nothing else.
- Basis: WCAG 2.2 SC 4.1.2 Name, Role, Value (A)

### a11y.lang (MUST)

The html element declares its language.

- Check: The root element has a lang attribute.
- Why: Without it a screen reader pronounces the page in whatever voice was used last.
- Basis: WCAG 2.2 SC 3.1.1 Language of Page (A)

### a11y.zoom (MUST)

Text resizes to 200 percent without loss, and the viewport never disables zoom.

- Check: Resize text to 200 percent and verify content and functionality remain available. Check that viewport settings allow zoom; absence of restrictive metadata alone does not prove the rendered result.
- Why: Disabling pinch-zoom takes away the one accommodation every phone ships with.
- Basis: WCAG 2.2 SC 1.4.4 Resize Text (AA)

## Front-end code

Style code that the next agent, or the next person, can change without fear.

### code.custom-props (SHOULD)

Define shared design tokens as CSS custom properties at the scope where they apply.

- Check: Shared tokens have a clear definition site; theme and component overrides are intentional and documented. A scoped override is not automatically a defect.
- Why: A token needs an identifiable owner; CSS inheritance and local overrides are useful parts of that design.
- Basis: House convention.

### code.no-important (SHOULD)

Avoid !important unless it implements an accessibility override, a required third-party override or an intentional utility.

- Check: Outside reduced-motion and forced-colors overrides, put a comment beside the declaration naming code.no-important and its reason. Review that reason against the actual cascade.
- Why: Escalating specificity makes later changes harder; some overrides nevertheless need to win.
- Basis: House convention.
- Enforced by: css.important (source)

### code.specificity (SHOULD)

Keep selectors to two classes of specificity and never style by id.

- Check: No id selector in a stylesheet, and no selector chained more than three deep.
- Why: High specificity is debt that is paid back with !important.
- Basis: House convention.
- Enforced by: css.id-selector (source)

### code.no-inline (SHOULD)

No inline style attribute except for a value computed at runtime.

- Check: Search the markup for style= and confirm each one carries a computed value.
- Why: Inline styles cannot be themed, overridden or found.
- Basis: House convention.

### code.breakpoints (SHOULD)

Breakpoints are named, number four or fewer, and live in one place.

- Check: Count the distinct widths used across media queries.
- Why: A fifth breakpoint is usually a component that should have been flexible.
- Basis: House convention.
- Enforced by: css.breakpoint-count (source)

### code.dead (SHOULD)

Delete styles with the markup they served, and leave no commented-out CSS behind.

- Check: No comment contains a CSS declaration.
- Why: Commented-out code is a question nobody will ever answer.
- Basis: House convention.
- Enforced by: css.commented-code (source)

### code.fonts (SHOULD)

Prefer self-hosted fonts, font-display: swap or optional, and no more than four font files on first paint.

- Check: Inspect every @font-face font-display value. Missing, auto, block or fallback departs from this house default. A comment naming code.fonts and a specific reason, such as an icon font, produces an unverified warning requiring review; the comment is not proof. Verify hosting and the first-paint file count in the network panel; the CSS check reads only font-display.
- Why: A font that blocks rendering hides the text from the people with the slowest connections.
- Basis: House performance convention; font-display choice is not a WCAG verdict.
- Enforced by: css.font-display (source)
