Accessibility

Accessibility conformance statement

Where we actually stand against WCAG 2.2 level AA, criterion by criterion, including what is broken today and what nobody has checked yet. This page is generated from a record kept in our code, so it cannot quietly fall out of date.

What this statement covers

The public website at www.helloassembly.com, and the Assembly app that parents, carers and school staff sign in to.

We are working towards WCAG 2.2 level AA. Alongside it we track APCA, the perceptual contrast model being explored for future guidance, as a design instrument. WCAG 2.2 is the standard we are held to, and nothing on this page is a claim about APCA conformance.

Not covered here

  • Prototypes and design tools that we publish as demonstrations rather than offer as a service.

How compliant we are

This website and app are partially compliant with the Web Content Accessibility Guidelines version 2.2 AA standard, because of the non-compliances listed below. 21 of the 55 level A and AA success criteria fall short of full conformance, and they are set out below rather than summarised away.

WCAG 2.2 has 55 success criteria at levels A and AA. (Version 2.2 removed 4.1.1 Parsing, so it is not assessed and does not appear in the table.) We describe each one with the words these records normally use, so a procurement reader does not have to learn ours. One departure, stated because it matters to anyone matching this against a standard template: "Not Assessed" is our own word, not a standard one. The common templates have a status for this and reserve it for level AAA, and the words they do offer at A and AA all assert a finding we do not have. Rather than stretch a defined term or imply a pass, we added one word, kept it rare, and label it here.

Each row also records, separately from its status, what stops that criterion regressing. Only lint, test and type-check can refuse a merge on this repository, so only a criterion covered by one of those is genuinely held: 21 of the 55, and 17 of those on a criterion we record as met. Another 21 are covered by a check that runs by itself on a pull request and goes red, but that is not on the required list and so cannot stop anybody merging past it. That check is also filtered by path: a change confined to the app, or to a shared component package, does not trigger it.

Until 9 August 2026 this page counted those 21 rows together with the other 21 and said of all of them that a test held them, which was not true of the larger group. It is recorded here rather than quietly corrected, because a conformance page that revises its own claims without saying so is worth no more than one that never checks them.

Supports: 28 of 55
The website and the app meet this. Each row names what established that, and says separately whether a test holds it against regression.
Partially Supports: 20 of 55
Largely met, with a named gap: either a specific failure listed below, or a part of the criterion we cannot yet evidence.
Does Not Support: 0 of 55
A real, live failure that a person will hit today.
Not Assessed: 1 of 55
Nobody has examined this yet. Not a claim of failure and not a claim of support, and the row says what the missing work is.
Not Applicable: 6 of 55
There is nothing in the website or the app that this applies to.

Content that is not accessible

These are the 2 failures we know about today, affecting 2 of the 55 success criteria. Every one of them is something a real person can hit. Each says what you would experience, why it happens, how we found it and what we are doing about it.

Assembly app: 1 known failure

  1. NC-6, fails 4.1.3 Status Messages

    When Assembly replies in chat, a short 'Assembly has replied.' notice now announces the arrival, in a region separate from the reply itself, so it does not depend on your screen reader correctly honouring the hold that stops a streamed reply being read word by word. That hold is reliable on NVDA and handled inconsistently by VoiceOver and JAWS; the arrival notice was added on 16 August 2026 precisely so a VoiceOver or JAWS user is told the reply landed even where the hold itself goes wrong. Nobody has yet sat down with real assistive technology to confirm the notice is actually spoken.

    Why:
    A reply is written out a few words at a time. If we let the screen reader speak every update, it would talk over itself continuously while the answer was still being written, which is harder to use than silence, so we ask it to wait until the reply is complete and then read it once. Screen readers do not agree on how to honour that request: NVDA does, and VoiceOver and JAWS are inconsistent. Rather than rely on that agreement, the arrival is now also announced from a small, independent status region that does not need the hold to work at all, though a region marked up correctly is not the same fact as a region actually heard, and only real assistive technology settles that.
    How we found it:
    Code review during our own conformance mapping, 8 August 2026, which found the chat silent. Re-checked on 15 August 2026, when the live regions landed on both chat surfaces and closed that half. Narrowed again on 16 August 2026 (ASM-988), when the dedicated arrival notice landed and stopped the announcement depending on `aria-busy` being honoured.
    What we will do:
    Listen to the chat on NVDA, VoiceOver and JAWS and record what each one does with a completed reply, now specifically whether the arrival notice is spoken. That listening session is still outstanding and is the only thing left in this entry's plan.

Public website: 1 known failure

  1. NC-4, fails 1.4.10 Reflow

    Our pages fit a 320 pixel screen without sideways scrolling, and a check now holds that. An agent walked the rendered narrow layouts on 16 August 2026 and judged whether they are actually usable at that width, not just non-overflowing. Most of the site held up. Three things did not: a contrast comparison table that scrolls sideways with no visual hint that it does, three pages that carry a stretch of blank space between sections left over from a desktop layout, and a several-hundred-term glossary with no way to jump to one term short of scrolling past everything before it.

    Why:
    The overflow measurement was added to the routine scan on 15 August 2026, so that half is now watched by a check that starts itself. The rest is a judgement about a rendered layout, which no probe makes on its own: a page can pass every width measurement and still be awkward to use on a phone-sized viewport. An agent-driven walkthrough has now done that judgement pass and found three concrete rough edges, recorded in full below. None of them is severe: nothing is unreadable, unreachable, or in the wrong order, so this stays a Partial rather than a fail. What has not happened yet is the same pass with a real phone in a real hand, which is the only thing left that would close this outright. The signed-in app is outside this entry, and outside that routine scan.
    How we found it:
    Our own full accessibility sweep, 8 August 2026, which was the first time either product had been loaded at 320 pixels. Narrowed on 15 August 2026, when the overflow assertion landed and closed the measurable half. The judgement half was walked on 16 August 2026 (agent-driven), which is why this entry now names concrete rough edges instead of an unanswered question.
    What we will do:
    Fix the three named rough edges (a scroll affordance on the contrast-lab table, closing the leftover blank space on the three stepper pages, and in-page navigation on the glossary), then have a person repeat the same walkthrough with a real phone. A probe cannot make either of those two calls; an agent's read of a screenshot is closer than a probe but still short of a person's, which is the gap the plan is honest about.

Criteria nobody has assessed yet: 1 of 55

These are not claims of failure and they are not claims of support. They are work we owe, and each one says what the missing work is. Both need a person to sit down with the product and judge it, which is why no tool has closed either.

This list was 15 long until 9 August 2026. It shrank because most of those rows were not unexamined at all: they recorded a search that found the failing pattern absent, which is a finding, and they now read as met with that search as their evidence. Three moved the other way once the search found something real, including one that had claimed the opposite of what the files showed.

  • 1.3.2 Meaningful Sequence (level A). Whether content still reads in an order that keeps its meaning is a judgement, and nobody has made it. The search that would have made that judgement cheap does not come out clean: four places do set a visual order that differs from the order the content is written in, so this is not the no-op the record used to claim it was.

Reporting an accessibility problem

If you find a problem that is not listed here, or you need something on this site or in the app in a different format, email info@helloassembly.com. Tell us the page or screen, what you were trying to do and what happened. We aim to reply within 5 working days.

Accessibility reports are treated as bugs, because they are bugs. If a report shows up a failure we have not listed, it gets added to this page along with what we plan to do about it.

Enforcement procedure

Assembly is a private company, so the Public Sector Bodies (Websites and Mobile Applications) Accessibility Regulations 2018, and the Equality and Human Rights Commission route that enforces them, do not apply to us directly. We hold ourselves to the same standard those regulations set, which is why this statement follows their model.

As a provider of a service we are covered by the Equality Act 2010. If you contact us about an accessibility problem and you are not happy with our response, you can contact the Equality Advisory and Support Service (EASS) at equalityadvisoryservice.com .

How we tested, and when

This statement was prepared on , last reviewed on , and the website and app were last tested on . The testing was carried out by Assembly, in house.

What we do

  • An automated colour contrast check over every colour pair in our design system, in light and dark, which is one of the three checks that has to pass before any change can be merged. Four pairs sit in a named quarantine outside it, set out on the contrast row of the table below.
  • Automated accessibility scans (axe) over all 24 pages of the public website, in light, in dark, and at 320 pixels wide, and over 10 signed-in app screens. The website's scan starts by itself on a pull request that touches the website or the packages it renders from, and the app's runs after merge.
  • Keyboard tests that press real keys and measure the outline your browser actually draws, rather than checking the markup.
  • A full-catalogue sweep, run on demand, that turns on all 105 available rules across 284 combinations of page, colour scheme and screen width. The last run was 8 August 2026.

What we do not do

This half matters more than the half above it. A statement that lists only its strengths tells you nothing about what it missed.

  • We have not had an external accessibility audit. Nobody outside Assembly has assessed this product.
  • We have not tested the rebuilt app with a screen reader. We have a written test script for it and it has not yet been run.
  • Our page-level accessibility scans cannot stop a change being merged. Three checks are required to merge here, and none of them is a page scan; the colour-token check named above is the only accessibility check with that power. The website scan is also filtered by path, so a change confined to the app or to a shared component package never triggers it at all. It reports, we read it, and that is a weaker promise than a gate.
  • Our routine scans ran at one screen width, a desktop one, until 9 August 2026, which is how a narrow-screen keyboard failure went unseen; the website's scan now also runs at 320 pixels and the app's does not. They ran in light mode only until 8 August 2026, which is how two dark-mode contrast failures went unseen; the website's scan now runs in both themes and the app's does not.
  • No automated rule anywhere in the axe catalogue measures whether a page overflows sideways, so the one thing 320 pixels is really about is still checked by hand.
  • Automated tools cover roughly a third of the standard. They found 10 real failures across 284 page states; the most serious problem in this list was found by a person reading code, and no tool can see it.
  • No external audit has been carried out. If that changes, this line will name the auditor and the date.

Every success criterion, one by one

All 55 level A and AA success criteria in WCAG 2.2, grouped by the four principles the standard is built on, using the status words explained above. Every row names the evidence behind it, and a test in our codebase fails if a cited check is renamed, deleted or stops covering what the row says it covers, if a row says a test holds it while naming no such test, or if a row calls a check a merge gate when that check is not on the repository's required list.

Each row is a summary you can open: the SC number, its name, level, status and whether a test holds it against regression. The finding and the evidence behind it sit inside, so you can scan all 55 without reading a full page of prose, then open the ones you actually need. Printing or saving this page opens every row first, so the saved copy is the complete record.

Perceivable

You can take the information in, whatever you use to read a screen. 20 success criteria. Each one opens to show the finding and its evidence.

  • 1.1.1 Non-text Content Level A Partially Supports Advisory only

    Every photograph carries alt text, decorative artwork is hidden from screen readers, and every icon-only button has a name. Automated checks run on both the website and the app. Since 16 August 2026 every decorative icon on the website carries the attribute that hides it from screen readers, so no shape without a meaning is read out. What automation cannot do is judge whether alt text is any good, and the app has not had that same icon pass yet.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Runs the WCAG 2.2 A and AA rule set over 24 public routes and requires zero violations. That set carries image-alt, button-name and link-name. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same rules over 10 signed-in screens, but on the post-merge tier, not the pull request tier. apps/parent/e2e/a11y-nav.spec.ts
    • Code review, 2026-08-16: Every icon the website renders through its icon component now carries aria-hidden. Counted on the built site rather than the source, because the component is what emits the tag: after the build, no rendered icon is left without it. The only other drawings in the pages are the two logos, which are labelled on purpose, one tick already inside a hidden wrapper, and the example-profile progress rings, which each carry their own title.
    • Not verified: Alt text quality has never been reviewed by a person. And the app has not had the pass the website had on 16 August 2026: the icon library it uses does not hide icons from screen readers by default, and nobody has been through the app's icons to say which of them should be hidden.
  • 1.2.1 Audio-only and Video-only (Prerecorded) Level A Not Applicable

    We publish no video and no audio anywhere in the website or the app, so there is nothing this criterion applies to.

    Evidence

    • Code review, 2026-08-08: Two independent sweeps for video, audio, media embeds, media file types and autoplay found nothing. No media files exist in either app's assets.
  • 1.2.2 Captions (Prerecorded) Level A Not Applicable

    No prerecorded video or audio. See 1.2.1.

    Evidence

    • Code review, 2026-08-08: As 1.2.1: no time-based media exists in scope.
  • 1.2.3 Audio Description or Media Alternative (Prerecorded) Level A Not Applicable

    No prerecorded video or audio. See 1.2.1.

    Evidence

    • Code review, 2026-08-08: As 1.2.1: no time-based media exists in scope.
  • 1.2.4 Captions (Live) Level AA Not Applicable

    Nothing on the website or in the app streams live.

    Evidence

    • Code review, 2026-08-08: As 1.2.1: no time-based media exists in scope.
  • 1.2.5 Audio Description (Prerecorded) Level AA Not Applicable

    No prerecorded video. See 1.2.1.

    Evidence

    • Code review, 2026-08-08: As 1.2.1: no time-based media exists in scope.
  • 1.3.1 Info and Relationships Level A Supports Held

    Structure is built in rather than added afterwards: every group of choices is a real fieldset with a legend, every field has a real label, and the report tabs use proper tab semantics. The neurodivergence checkboxes were the last group missing that, closed on 16 August 2026: they now sit inside a fieldset whose legend carries the page's own heading text, held by a test that resolves the group by that name. Two others were fixed on 15 August 2026, the chat box on the report page having no label at all, now held by a test on all three of the app's chat boxes, and the school staff screens having no headings, now held by a test that reads each screen's outline word for word.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Resolves the checkbox group by its accessible name (the route's own heading text) and asserts every preset checkbox sits inside it, and that the unrelated 'Other' text row does not. apps/parent/src/routes/children/neurodivergence.component.test.tsx (required check: test)
    • Automated check, advisory only: axe structural rules over 24 public routes. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same rules over 10 signed-in screens and all five school staff screens, post-merge only. Each school staff scan also asserts the page's heading outline, which no axe rule in the gated set does. apps/parent/e2e/a11y-nav.spec.ts
    • Code review, 2026-08-15: The five school staff screens were read one by one and given a descending heading outline whose section headings name their section rather than repeat the page title, and the roster's styled paragraph became a real heading. Each screen's outline, words included, is pinned by apps/parent/src/routes/org/heading-outline.test.tsx, which runs with the unit tests rather than on demand.
  • 1.3.2 Meaningful Sequence Level A Not Assessed Not held

    Whether content still reads in an order that keeps its meaning is a judgement, and nobody has made it. The search that would have made that judgement cheap does not come out clean: four places do set a visual order that differs from the order the content is written in, so this is not the no-op the record used to claim it was.

    No test holds this

    Evidence

    • Code review, 2026-08-09: This entry corrects the one it replaces, which said no CSS reordering existed. Re-run on 2026-08-09, four places reorder: the website's alternating how-it-works step rows, a report dimension row, a dialog's footer buttons and a button's icon placement. None is known to break the reading order and none has been looked at. No positive tabindex value exists anywhere in either product, which is the other usual cause and really is absent.
    • Not verified: Nobody has read either product in document order and judged whether it still makes sense. The four reordering sites above are where such a pass should start.
  • 1.3.3 Sensory Characteristics Level A Partially Supports Not held

    This is a rule about wording: an instruction must not point at something by shape, position or sound alone. One instance is live, on the page about deleting your data, where a link is identified only as 'the link below'. The near miss in the app gets it right: the profiler's help text names the Get help button as well as saying where it sits.

    No test holds this

    Evidence

    • Code review, 2026-08-09: A phrase search across the website's pages and components, the app's routes and the interface message catalogue, for instructions naming a control by position, shape or colour alone. One hit that relies on position alone: 'The link below pre-fills the subject line' on the delete-your-data page. The other hit names its button as well as its position, so it does not rely on the position.
    • Not verified: A phrase search finds the patterns we thought to search for. No editorial pass has been run over the whole product against this criterion, and no tool can run one.
  • 1.3.4 Orientation Level AA Supports Advisory only

    Both products work whichever way you hold the device. Nothing anywhere locks the screen to portrait or landscape, and the on-demand sweep that can see this found no violation across 284 page states. Since 16 August 2026 a probe also renders every public route, and all sixteen signed-in app screens, at a real landscape phone size and asserts the main landmark is there and nothing runs off the side: it found nothing wrong on either product.

    A check watches this, but cannot block a merge

    Evidence

    • Code review, 2026-08-09: No orientation lock of any kind exists: no CSS orientation media query used to restrict content, no call to lock the screen orientation, and no web app manifest setting one. Searched across both apps and the shared styles. The axe rule for this is tagged experimental, so it does not run under our normal tag filter, which is why the sweep below rather than the gate is what measured it.
    • Automated check, run on demand: The full-catalogue sweep enables every rule including the experimental ones, and returned no violation of this rule across 284 page states on 2026-08-08. It gates nothing. apps/marketing/e2e/a11y-full-audit.spec.ts
    • Automated check, advisory only: Renders all 24 scanned public routes at 844x390, a real landscape phone size, alongside the existing 320x800 portrait-shaped coverage in a11y.spec.ts's chromium-narrow project. Asserts the main landmark is visible and the document does not overflow sideways. Clean everywhere on 2026-08-16. apps/marketing/e2e/resize-spacing-orientation.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: The same landscape check over all sixteen of the app's scanned screens, widened from eight on 16 August 2026 (ASM-1130). Not a hand-kept list any more: the spec declares which registry route each case stands for and a parity test in the same file fails if that set is not exactly the registry's scanned parent set, so a screen cannot join the product without joining this probe. Clean on every screen on 2026-08-16. On the post-merge tier (`.github/workflows/parent-e2e.yml` does not run on the pull request path, ASM-793), so it reports rather than gates. apps/parent/e2e/resize-spacing-orientation.spec.ts
  • 1.3.5 Identify Input Purpose Level AA Supports Held

    Every field that asks for information about you carries the standard autofill hint, so your browser or password manager can fill it in: your name, your organisation, your role and your email address. Six fields, six correct. The hidden fields that catch spam opt out on purpose, which is also correct. A test now holds all six, so a refactor that drops one fails the required checks rather than shipping unnoticed.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Asserts the four website enquiry fields (name/organisation/role/email) carry their exact autocomplete tokens from the shared enquiryFields() source, and that the early-access email field's markup keeps name="email" paired with autocomplete="email" (source-pinned: no rendering harness exists for .astro in this repo). apps/marketing/tests/form-errors.test.ts (required check: test)
    • Automated gate, required to merge: Asserts the sign-in email field keeps autoComplete="email" on the rendered DOM. The sixth field, the sign-in code, is outside this criterion (a one-time value, not information about the signer-in), but its autoComplete="one-time-code" is pinned in the same file for the separate SC 3.3.8/3.3.9 claim. apps/parent/src/routes/login.render.test.tsx (required check: test)
    • Code review, 2026-08-09: Six of six fields collecting the user's own information carry a correct autocomplete token: four on the website's enquiry form (name, organisation, role, email), one on the early access field (email) and one on the app's sign-in (email). The two honeypot fields opt out. The one remaining email box, where a member of school staff types a parent's address to invite them, collects information about somebody else, which is outside what this criterion asks for. The sign-in code field is outside it too, being a one-time value rather than information about you, and it still carries the one-time-code hint so a device can fill it.
  • 1.4.1 Use of Color Level A Supports Not held

    Colour is never the only thing carrying a message. Links in body text are underlined, the four result bands always show their name in words beside the colour, and the report's per-area indicator changes shape as well as colour: a smile, a flat line or a frown.

    No test holds this

    Evidence

    • Code review, 2026-08-09: Read rather than assumed, in three places. The app underlines links inside paragraphs and inside alert and status regions through a zero-specificity base rule. The website underlines links in article prose through the same kind of rule, and a search for anchors on the website carrying neither that rule nor an explicit underline returned only the skip links, which are not body text. The band chip renders the band name as text and hides its coloured dot from screen readers. The report indicator draws a different curve per state as well as a different colour, and carries an accessible name. Neither product draws a chart.
  • 1.4.2 Audio Control Level A Not Applicable

    Nothing plays audio. See 1.2.1.

    Evidence

    • Code review, 2026-08-08: As 1.2.1: no audio exists in scope.
  • 1.4.3 Contrast (Minimum) Level AA Partially Supports Held

    Text contrast is checked by two automated checks, and they do not have the same force. The check over the colour pairs in our design system, in light and dark, is one of the checks that must pass before a change can be merged. The check over every page of the website, also in light and dark, runs by itself on any pull request that touches the website, and goes red, but it is not on the required list, so it reports rather than refuses. Two places on the home page did fail in dark mode, because they used hardcoded colours sitting outside the design system; both were fixed on 8 August 2026 and the page-level check now covers the theme they failed in. Four colour pairs are quarantined from the design-system check while we finish splitting one colour into a fill version and an ink version, and one element is excluded from the page-level check by name, which is the accessibility page's own demonstration of a failing button. All five are set out below, because an exclusion nobody can see is worth less than no exclusion at all. There was a second exclusion until 15 August 2026, and it hid a real failure: a miniature Download PDF pill in an illustration, disclosed as a live failure while it stood. Its colour was deepened until it passes, and the exclusion was removed in the same change, so nothing on the site is now excused from this check except the demonstration. One thing about that pill is worth writing down. This criterion does not reach text that is part of a picture carrying significant other visual content, and the pill is a drawing of a button inside a drawing of the app, so it was arguably never in scope at all. We treated it as a failure and fixed it anyway, and the deeper colour stays: reopening something that now passes, to argue it never had to, would leave the record worse than it is. The exemption is recorded here so the next reader does not spend the effort we did on text drawn inside an illustration.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Asserts the WCAG ratio and the APCA perceptual score for every semantic colour pair, in both themes, at the size and weight each pair is used at. Since 15 August 2026 that includes the deepened peach used for drawings of our call-to-action button, gated at the 4.5:1 normal-text bar rather than the 3:1 large-text bar the real button relies on, which is what stops an illustration quietly reverting to a colour that only passes at button size. Four pairs sit in a named quarantine register rather than in the check, each one recorded with its measurement, the files it renders in and the fix we intend. Where they can actually be seen, honestly: three of the four are dark theme only, and the app applies no dark theme at all, so those three cannot be reached in it. That is why they cannot be reached, and it is worth being exact about, because one of them does render through a component the app really uses, the styling of a destructive menu item such as Sign out. The website has a dark theme but does not use that shared component library at all. The fourth pair is light theme only and does not reach the app or the website: it renders in our internal content-review tool, in a presenter demo that has no deployment path in this repository, and in the school-facing prototype, which is published as a funder demo rather than offered as a service. packages/brand/src/contrast.test.ts (required check: test)
    • Automated check, advisory only: axe color-contrast over 24 public routes, requiring zero violations, with exactly one named exclusion: the demonstration further down the accessibility page itself, which deliberately renders our old failing button so its numbers can be measured in front of you. Scoring a deliberate exhibit of a violation as a violation would be a false positive, and it hides nothing, because showing the numbers is the whole point of that section. There were two exclusions until 15 August 2026. The other was the miniature Download PDF pill in the product illustration on the how it works page, white bold text at 13 pixels measuring 3.17:1 against a 4.5:1 bar, which was a real failure and was disclosed as one for as long as it was excluded. Its background was deepened until white measures 4.96:1 there, and the exclusion was deleted in the same change, so the pill is now measured on every run. The remaining exclusion is written into the suite by name and this manifest names it back, so removing it without saying so breaks this claim. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, advisory only: That scan runs three times over the same build: light and dark at desktop width, and light at 320 pixels. Until 8 August 2026 it ran in light only, which is how two dark-mode failures on the home page stayed invisible to it; the narrow run was added on 9 August 2026. Contrast is measured in all three. apps/marketing/playwright.config.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: The on-demand sweep that found those two failures on 2026-08-08, by measuring the dark theme for the first time. Reporting only, gates nothing. apps/marketing/e2e/a11y-full-audit.spec.ts
    • Not verified: Two residual gaps keep this short of a full claim. Four colour pairs sit outside the design-system check, in the quarantine described above, and one of them renders through a component the app really uses. And nothing about the page-level check can refuse a merge, so a contrast regression on the website goes red without stopping anybody. There was a third gap until 15 August 2026, a live failing element excluded from the page-level check by name; it was fixed and the exclusion removed together, and it is the reason this row still reads as short of full support rather than as never having had a failure.
  • 1.4.4 Resize Text Level AA Partially Supports Advisory only

    You can zoom to 200% and beyond: we never block zoom, and the type scale is set in relative units. Since 16 August 2026 a probe measures the exposure the old row could only describe: it sets the ROOT font-size to 200%, which is what a browser or assistive tool's 'increase text size only' setting does, as distinct from full page zoom (measured elsewhere, and clean). Writing it found one bug that reached every page, a footer column with no floor under its grid track width, fixed the same day. It also found something the type scale being relative does not protect against: most of the site's SPACING (padding, gaps) is set in the same relative unit as the text, `rem`, so a text-only zoom that changes the root font-size grows the gaps around a heading right along with the heading itself, and on seven routes plus the home page's testimonial column that combination leaves too little room for one line to wrap. Those are named, dated and excluded from the probe rather than hidden, tracked under ASM-993: closing them is a typography decision (fixed-px gaps at this size, a narrower label style, or something else), not a mechanical fix this ticket should make unasked. Widening the app's half of that probe from eight screens to all sixteen on 16 August 2026 found a real failure in the app too, of the same shape as the footer one: the answer slider capped each of its middle labels at a fixed pixel width while sizing the label's own text in relative units, so at 200% the single word 'Somewhat' ran past its own edge with nowhere to wrap and was cut off. The cap is relative now, and every question card is measured.

    A check watches this, but cannot block a merge

    Evidence

    • Code review, 2026-08-08: Neither app blocks zoom. The shared type scale is fully relative. Roughly sixty fixed pixel sizes bypass it, including a primary button and the website header.
    • Automated check, run on demand: Loading at 640 CSS pixels, the 200% zoom equivalent of a 1280 desktop, produced no horizontal overflow in either app on 2026-08-08. A full PAGE zoom measurement, not a text-only one: everything scales together at 640, which is why it could not have found the padding/gap exposure below. apps/marketing/e2e/a11y-full-audit.spec.ts
    • Automated check, advisory only: Sets html { font-size: 200% }, then asserts no horizontal document overflow and no text-bearing element clips its own text. Found and fixed on 2026-08-16: the footer's link columns used a literal `1.5fr_1fr_1fr_1fr_1fr` grid-cols with no minmax(0, ...) floor, so at 200% every column's own min-content width could force the whole grid, and with it the page body, wider than the viewport on all 24 routes; `min-w-0` on each column plus `break-words` on the one heading a doubled letter-spacing still overran is the fix. A second, different-shaped exposure remains, disclosed rather than fixed: seven routes and the home page's testimonial column pair fixed-pixel-equivalent rem PADDING (which also doubles under a root font-size override) with rem TEXT, and on those specific elements there isn't room left to wrap. `KNOWN_ZOOM_CLIP_DEBT` and `KNOWN_ZOOM_DOCUMENT_OVERFLOW_PX` name each one, dated, with the exact text or amount measured, so a copy change or a real fix makes the entry stop matching rather than silently keep passing. apps/marketing/e2e/resize-spacing-orientation.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: The same font-size-only zoom check, over all sixteen of the app's scanned screens since 16 August 2026 (ASM-1130) rather than the eight it started with, held against the surface registry by a parity test in the spec. Widening it found one real failure the narrower set never reached: the answer slider's middle labels were capped at a fixed 140 pixels while their text was sized in rem, so at 200% the word 'Somewhat' overran its own box by 22 pixels and was clipped. Fixed the same day by making the cap relative as well (packages/profiler-components/src/components/RadioSlider.components.tsx). Clean on all sixteen after that, with no equivalent of the marketing footer or padding exposure anywhere in the app. apps/parent/e2e/resize-spacing-orientation.spec.ts
    • Not verified: No real browser or assistive-technology 'increase text size only' feature has driven this, only a root font-size override that approximates it. And neither probe has looked at whether two adjacent, independently-sized text runs can visually OVERLAP without either one's own box registering as clipped, which is not something a scrollWidth/clientWidth comparison can see.
  • 1.4.5 Images of Text Level AA Supports Held

    The two articles that carried a whole table as a picture now carry the table itself, transcribed cell by cell into markup you can select, zoom and read with a screen reader, and the pictures are deleted. The three images left with words in them are labelled diagrams, where the words label parts of an illustration, which the standard allows. A required check fails if either table becomes a picture again, or if any article embeds an image whose filename says it is a table.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Asserts both transcribed tables are still markup, with a caption, the expected first row header and the expected number of rows, and that no resource article embeds an image whose filename reads as a table, infographic, chart, matrix or comparison. Where it stops, precisely: it matches filenames and reads the article source rather than the rendered pixels, so a screenshot of text called something innocent would pass it. No check anywhere measures whether an image contains text, and the standing exposure is a person importing an image nobody opens. That is the exposure this row previously carried as an unverified line, kept here because it is a limit of the check rather than a gap in the finding. apps/marketing/tests/no-images-of-text.test.ts (required check: test)
    • Code review, 2026-08-15: Both images were opened, read and transcribed row by row into HTML tables in the MDX, then the built pages were re-read to confirm the markup survived MDX: 6 row headers and 38 list items in the classroom table, 12 row headers and 36 cells in the therapy table, each with a caption. Both routes were then loaded at 320 CSS pixels: no horizontal overflow on the document, sideways scrolling confined to the table's own region, and axe clean at wcag2a, wcag2aa, wcag21a, wcag21aa and wcag22aa. One transcription departs from the picture by a single character: a bullet reading 'Strong reactions to "no' in the screenshot, where the closing quotation mark had been lost, is written with the quotation closed.
    • Code review, 2026-08-15: The three images resource articles still embed were re-opened in this change and are labelled diagrams: a linear autism spectrum arrow, an autism spectrum wheel chart, and a low self-esteem and withdrawal loop. Each is an illustration whose words label its parts, which is the exception this criterion grants. This is a re-check of those three only. The claim that everything else the site renders is a photograph, decorative shape, branded icon or logo rests on the sweep of 9 August 2026 recorded below, not on a fresh pass.
    • Code review, 2026-08-09: Every image the live website renders was traced from the page that references it and then opened and read. This sweep is why the row can say anything at all about images nobody has re-opened since: it replaced a claim that the only text-bearing images were logotypes and award badges, which was wrong and had never been checked against the images themselves. Two images were photographs of a data table, both in resource articles, and those two are what NC-14 disclosed and this change fixes. Three were labelled diagrams. The rest are photographs, decorative shapes, branded icons and logos. The comparison tables and outcome infographics elsewhere in the asset folder are left over from the previous website and no page references them. The app renders no bitmap images at all: its illustrations are drawn as inline vectors.
  • 1.4.10 Reflow Level AA Partially Supports Advisory only

    Since 15 August 2026 the routine scan of the website measures, on every page and at the 320 pixel width this criterion names, whether the page is wider than the screen it is being read on. It runs by itself on any change that touches the site and goes red, though it is not on the short list of checks that can refuse a merge. When it fails it names the elements sticking out, because a reflow fault is usually one stubborn element rather than a broken page. An agent-driven walkthrough judged the website's narrow layouts on 16 August 2026 and found three rough edges, none severe enough to call the layouts unusable: a comparison table that scrolls without any visual cue that it does, three pages that carry leftover blank space from a collapsed desktop layout, and a long reference page with no way to jump to a term. A person has not yet sat with a real phone and done the same pass. The signed-in app is measured only by a sweep somebody starts by hand, and this walkthrough did not cover it.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Compares the document's scroll width against the viewport on all 24 public routes, in every project the suite runs, including the 320 pixel one. Added on 15 August 2026 because no rule in the axe catalogue measures this at any width, which is why running the scan at 320 since 9 August 2026 had not closed it: the suite was at the right width with nothing that could fail there. It reports the widest offending elements by tag, id and class, and it is a soft assertion paired with the axe one so that a contrast failure on the same page cannot stop the reflow result being reported. Confirmed to fail on a deliberately overflowing element before it was reverted. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Zero horizontal overflow across 284 page states at 320, 640, 768 and 1280 pixels, in light and dark. Wider than the routine check in viewports and themes, and reporting only, on no CI path. apps/marketing/e2e/a11y-full-audit.spec.ts
    • Automated check, run on demand: The same probe over the signed-in app screens, comparing the document's scroll width against the viewport at 320 pixels. Reporting only, on no CI path, and the app has no routine narrow check of any kind. apps/parent/e2e/a11y-full-audit.spec.ts
    • Recorded audit, 2026-08-16: The judgement pass this row was missing: 23 public routes walked at 320x812 and read for reading order, buried content, table/figure legibility and control reachability (the scan set grew to 24 the same day; /early-access is the one scanned route not yet walked, named in the record). Agent-driven, not a person. 18 of 23 routes were clean. Three concrete rough edges: the /accessibility contrast-lab table scrolls with no visual cue that it does; /how-it-works, /nhs and /schools carry leftover blank space between numbered steps from a collapsed desktop layout; /glossary has no in-page way to jump to a term across its 61,000px scroll. None makes a layout unusable, so this stays a Partial rather than closing outright. agents-docs/design-system/audits/2026-08-16-320px-walkthrough.md
    • Not verified: The judgement pass above was agent-driven and covered light mode only. Nobody has done the same walkthrough with a real phone, in dark mode, or with the FAQ disclosures and tabbed panels opened rather than left at their default state. The signed-in app has no equivalent judgement pass at all, only the on-demand overflow sweep.
  • 1.4.11 Non-text Contrast Level AA Supports Held

    The things that are not text but still tell you something, the focus ring around whatever you have selected and the border around a form field, are held to the 3:1 minimum in both light and dark by a check over our colour tokens, and that one really is on the list of checks a change has to pass before it can be merged.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Asserts 3:1 for the focus ring against page, card and muted surfaces, and for the input border against card and page, in both themes. This gate caught and fixed a real failure where the default focus outline rendered at half opacity and measured 2.08:1. packages/brand/src/contrast.test.ts (required check: test)
    • Automated check, run on demand: Turns on every rule axe knows about, read from axe's own catalogue rather than from a list we maintain, across 284 page states on 2026-08-08, and reported nothing here. Named for what it does not add as much as for what it does: no rule in that catalogue measures the contrast of a focus ring or a field border, so this sweep corroborates nothing on this row. The check over our colour tokens is the whole automated basis for it. apps/marketing/e2e/a11y-full-audit.spec.ts
  • 1.4.12 Text Spacing Level AA Supports Advisory only

    If you increase line height, letter spacing and word spacing with your own stylesheet, nothing is cut off. Two different kinds of evidence sit behind that, and neither is enough on its own. The first is a probe: since 16 August 2026 it injects the criterion's own values (1.5 line height, 2em paragraph spacing, 0.12em letter spacing, 0.16em word spacing) on every public route and on all sixteen scanned app screens, and asserts that no text-bearing element clips its own box, horizontally or vertically. Every route and every screen is clean, and it runs by itself on any pull request that touches the website, advisory rather than required. What it cannot see is two runs of text overlapping while each stays inside its own box, which is still loss of content under this criterion: measuring a box against its own contents can never detect that. The second is a person-equivalent look, because that is the only thing that can. On 16 August 2026 an agent applied the same override and read the rendered screens as images, going first to the places where text is positioned rather than flowed, which is the only place two runs can meet: the profiler's answer slider above all, then the dense tables and the badges and pills on both products. Nothing overlapped anywhere it looked. The slider, the likeliest offender in either product, keeps 43 pixels of clear space between its closest pair of labels at the narrowest layout that shows them. That look was a sample, not a sweep: eight of the app's sixteen screens and twelve of the website's twenty-four routes, light mode, English, one state per screen. It was an agent reading screenshots, not a person using the product and not a screen-reader session. The full method, the surfaces it skipped and why, and two measurement traps that looked like failures and were not, are in the record below.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, run on demand: Injects the standard text-spacing override into every page state and compares against a baseline. Zero new clipping in either app, at all four widths, in every colour scheme. apps/marketing/e2e/a11y-full-audit.spec.ts
    • Automated check, advisory only: Injects the SC 1.4.12 override values as a style tag on all 24 scanned routes and asserts no text-bearing element's scrollWidth or scrollHeight exceeds its own client box, deliberately excluding elements that already truncate on purpose (overflow hidden, nowrap, ellipsis) since the override did not create that choice. Clean on every route on 2026-08-16. Where it stops: a box that clips its own text is what this measures, and two text runs that overlap while each stays inside its own box is not something a scrollWidth against clientWidth comparison can see. Nothing we have found detects that, and it is the reason the remark above says clipped rather than obscured. apps/marketing/e2e/resize-spacing-orientation.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: The same override over all sixteen of the app's scanned screens, widened from eight on 16 August 2026 (ASM-1130) and held against the surface registry by a parity test in the spec, so a screen cannot join the product without joining this probe. Clean on all sixteen, including the report, the review index, the done card and the four school-staff screens the narrower set never reached. The same limit as the marketing probe applies: it sees clipping, not overlap. apps/parent/e2e/resize-spacing-orientation.spec.ts
    • Recorded audit, 2026-08-16: The overlap half, which no probe on this row can reach. The same override applied by hand and the rendered screens read as images: 8 of the app's 16 scanned screens and 12 of the website's 24 scanned routes, at 768 to 1280 pixels wide, prioritised by where text is positioned or transformed rather than flowed, since text in normal flow cannot overlap a sibling. No overlap found anywhere. The profiler's answer slider, whose middle labels are absolutely positioned next to each other on one track and are the likeliest place in either product for this to happen, keeps 43 pixels between its closest pair at 768 pixels wide, growing with the viewport from there. Agent-driven and honest about it: an agent reading captured screenshots, not a person using the product, not a screen reader, light mode and English only, one state per screen, and 30 of the 194 captured frames read as images rather than all of them. The record names every surface it skipped. It also names two ways of asking a browser where an element is that each manufactured a convincing false finding, which is why this stayed a written walkthrough rather than becoming another automated check. agents-docs/design-system/audits/2026-08-16-text-spacing-overlap-walkthrough.md
  • 1.4.13 Content on Hover or Focus Level AA Supports Held

    Tooltips behave properly: you can dismiss them, move your pointer onto them, and they stay put. The one exception, a speech bubble on the app's Get help button that appeared on hover only and could not be hovered or dismissed, was removed on 15 August 2026 rather than rebuilt, because its text repeated the button's own label and nothing was lost with it. What holds this now is a test that the bubble has not come back; a hover tooltip built somewhere new would be caught by review rather than by that test.

    A required check holds this

    Evidence

    • Code review, 2026-08-08: Shared tooltips are dismissable, hoverable and persistent, including a deliberate wrapper that makes a disabled button's tooltip reachable. A read of one date: nothing re-reads them since.
    • Automated gate, required to merge: Fails if the removed hover bubble comes back, or if anything else inside that button gains a hover or focus reveal class. It checks an ABSENCE on purpose, and that is where it stops: a CSS reveal cannot be exercised in jsdom, so no unit test anywhere can prove that a tooltip which DOES exist behaves. Only that one does not. packages/profiler-components/src/components/__tests__/ProfilerChatWidget.test.tsx (required check: test)

Operable

You can drive it, with a keyboard, a mouse, a finger or a voice. 20 success criteria. Each one opens to show the finding and its evidence.

  • 2.1.1 Keyboard Level A Partially Supports Advisory only

    We build with plain HTML controls wherever we can, so keyboard support comes for free: the answer control is five real radio buttons, the review list is a native disclosure, and each profiler step is a real page. The gap that mattered here has closed. Until 9 August 2026 our routine scan of the website only ever loaded pages at desktop width, where nothing scrolls, so a scrolling area that appears only on a narrow screen and that a keyboard cannot reach was something the scan was structurally unable to see. It now loads all 21 pages at 320 pixels as well, which is the width where that condition exists, so the rule that catches it finally has something to fire on. What is left: that scan reports rather than blocks, it does not run on a change that touches only the app, and there is still no keyboard walkthrough test for the report tabs, the chat or the school staff screens.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Drives the skip links with real key presses and reads the computed focus outline rather than the markup. apps/marketing/e2e/keyboard-focus.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, advisory only: Added 9 August 2026. A third pass over the same build at 320 CSS pixels, covering all 24 public routes. The axe rule that catches an unreachable scrolling area only fires where a container actually overflows, which at desktop width none does, so this is what turned that rule from passing by absence into passing on a measurement. It found the two tables on our own accessibility pages already fixed, and no other route failing. apps/marketing/playwright.config.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: One keyboard contract asserted end to end in the app, on the post-merge tier. apps/parent/e2e/a11y-nav.spec.ts
    • Code review, 2026-08-09: Traced from the route rather than taken on trust, because our internal map had this wrong. The live 88-question route renders the shared answer control, which is a radio group of five real radio stops, each named with the option it stands for. The similarly named control in the app's own folder is reachable only from an internal component gallery and is not what anybody answers questions with. Neither this row nor any other in this file cited that one.
    • Not verified: No keyboard walkthrough exists for the 88 question flow, the report tabs, the chat or the school staff screens.
  • 2.1.2 No Keyboard Trap Level A Partially Supports Advisory only

    Escape closes the app's menu and returns you to the button that opened it, and that is asserted by a test. A behavioural probe (ASM-984) now tabs all the way through every scanned public marketing route and a representative set of app routes, bounded by twice each route's own focusable-element count, and confirms focus never sticks on one control and Shift+Tab can always retreat from wherever the walk stops. Two places remain genuinely untested: the shared dialog's own trap behaviour, and the get-help chat panel's OPEN state (its CLOSED state is walked and does not trap Tab, though it is not painted where you can see it, see the SC 2.4.11 row).

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, run on demand: Asserts Escape closes the navigation sheet and returns focus to its trigger. Post-merge tier. apps/parent/e2e/a11y-nav.spec.ts
    • Automated check, advisory only: Tabs through every scanned public route until the tab order laps its own first stop or runs out, bounded by twice the route's own focusable-element count, and asserts no element holds focus across two consecutive Tab presses and that Shift+Tab can always move back from wherever the walk stops. Clean across all 24 scanned routes. apps/marketing/e2e/keyboard-focus.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same walk over home, privacy, create-child, a profiler overview card, a profiler slider card and the SENCO org roster. No stall on any of the six. Where it stops: those six screens, so the shared dialog and the report/chat surfaces are not walked, and it runs on request and on the push to main rather than on every pull request. apps/parent/e2e/keyboard-focus.spec.ts
    • Not verified: The hand-written focus trap in the shared modal, and the get-help chat panel's OPEN state, carry no assertion.
  • 2.1.4 Character Key Shortcuts Level A Supports Advisory only

    No single-character keyboard shortcut exists in either product, which is the thing this criterion is about. Two bindings need saying rather than glossing. A number key jumps straight to an answer position, and it works only while the answer control already has focus, which the criterion allows explicitly. A Ctrl or Command plus B combination collapses a sidebar, and the criterion covers shortcuts made of a character alone, not ones that need a modifier held down. Nothing else listens for a character key. A behavioural probe (ASM-984) now samples a set of printable keys with nothing focused and confirms none of them move the page, on both products.

    A check watches this, but cannot block a merge

    Evidence

    • Code review, 2026-08-09: Every keyboard listener in both apps and the shared component packages was read, and re-counted on 2026-08-09 because the previous count here was wrong. Listeners attached to the document, the window or the global object: six, not three. Four are Escape only, on the website's mobile menu, the app's help bubble, the app's chat panel and the comments sheet. One, in the shared modal, handles Escape and also Tab, to keep focus inside the dialog. The sixth is the sidebar's Ctrl or Command plus B toggle, which lives in the shared component library and is rendered only by the school-facing prototype and our component gallery. Everything else is bound to the control that has focus and uses arrow keys, Home, End, or the digits 1 to 5 on the answer control. Neither Escape nor Tab is a character key, and a modifier combination is outside what this criterion covers, so the verdict does not turn on the corrected count.
    • Automated check, advisory only: With focus on the page body, presses a sample of ten printable keys on two routes and asserts the URL and any open dialog stay unchanged. A sample, not a proof: it cannot rule out a shortcut bound to a character this list does not try. apps/marketing/e2e/keyboard-focus.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same sample on the app's home screen. Post-merge tier, so a regression here is reported rather than blocked. apps/parent/e2e/keyboard-focus.spec.ts
  • 2.2.1 Timing Adjustable Level A Supports Not held

    Nothing in either product puts you under a clock you have to beat. Your session lasts 7 days and extends as you use it, which is well past the point this criterion starts to apply. The three things we email that do expire, the sign-in link at 15 minutes, the sign-in code at 10 minutes and a school's invitation link at 14 days, are all covered by the standard's own exceptions: two are security limits, the third is longer than 20 hours. If a sign-in link or code runs out you simply ask for another one.

    No test holds this

    Evidence

    • Code review, 2026-08-15: Read from the auth configuration and the invitation code: session 7 days with a rolling extension after a day's use, magic link 15 minutes, sign-in code 10 minutes, parent invitation 14 days. No countdown, no idle timeout, no auto-submitting form and no timed redirect exists anywhere in either app. What we have not walked is the screen you land on after an expired link, which is a question about how good the recovery is rather than about the time limit.
  • 2.2.2 Pause, Stop, Hide Level A Partially Supports Not held

    There are no carousels, tickers or autoplaying anything, and every looping animation stops if you have asked your device to reduce motion. One demonstration in the app's welcome tour changes its own text on a loop with no pause button.

    No test holds this

    Evidence

    • Code review, 2026-08-08: No carousels, marquees, tickers, timers or autoplay exist in scope. Every infinite animation sits behind a reduced-motion guard. The welcome tour demo auto-cycles visible text every 3.9 seconds with no in-page control.
    • Not verified: The public website has no site-wide reduced-motion guard, only four per-component ones, so a new animation is unguarded by default.
  • 2.3.1 Three Flashes or Below Threshold Level A Supports Not held

    Nothing flashes. The looping animations that do exist take 900 milliseconds or longer to complete a cycle, which is comfortably under three flashes a second even if you count every cycle as a flash, and they move or fade rather than switching brightness.

    No test holds this

    Evidence

    • Code review, 2026-08-09: Every endlessly looping animation in both products was read: a scroll cue bobbing on a 2.2 second cycle, a form field pulse at 900 milliseconds, two background motifs at 3 and 5 seconds, and the loading placeholder's 2 second fade. The fastest of those is around 1 hertz against a threshold of 3. No blink, no strobe, no rapid colour inversion and no video exists in scope. One-shot transitions run 120 to 220 milliseconds and do not repeat.
  • 2.4.1 Bypass Blocks Level A Partially Supports Advisory only

    Every page on the website starts with skip links, and a test drives them with real key presses. The app has skip links on every signed-in screen but no test for them, and the error screen you see after a wrong link has none at all.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Asserts both skip links behaviourally on the public website. apps/marketing/e2e/keyboard-focus.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Not verified: No app test asserts the skip link. The app's error screen renders no skip link and no landmark id. Three internal design-lab pages carry a skip link whose target does not exist.
  • 2.4.2 Page Titled Level A Supports Held

    Every page of the public website has a title, through a single shared component. The app has a default title at its root, so no screen can emit none, and every screen that a parent or a member of school staff can reach names itself: the last two that did not, an individual chat conversation sharing the chat list's title and the report titling itself in English regardless of the language you chose, were fixed on 15 August 2026. A test walks the app's real route list and fails if a screen ships without a title of its own, which is the part that was missing before: until then a route could ship untitled and nothing would notice.

    A required check holds this

    Evidence

    • Automated check, advisory only: Every public route passes through one title component. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated gate, required to merge: Reads the app's real route graph and fails if a route that renders a screen has no title of its own, if the chat list and a conversation converge on one title again, or if a title is written as an English literal rather than a translatable message. It is a source scan, so it checks that a title EXISTS and cannot judge whether the words are any good. apps/parent/src/routes.titled.test.ts (required check: test)
    • Automated gate, required to merge: Holds the root default, including the two error screens, so the floor under every route cannot be removed silently. apps/parent/src/root.meta.test.ts (required check: test)
  • 2.4.3 Focus Order Level A Partially Supports Held

    In the profiler, the card moves on by itself about half a second after your first answer. Your keyboard position now moves with it, onto the new question's heading, and the new question is announced. That was a real failure until 8 August 2026, when a keyboard or screen reader user was dropped back to the top of the page on every one of the 88 questions; it is now fixed and held by tests. Focus order elsewhere in the product, the report tabs, the chat and the school staff screens, has no walkthrough test.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Asserts that when the card advances, focus lands on the new card's heading wrapper rather than falling to the page body, that going back gets the same treatment, and that a direct or bookmarked load does not steal focus. apps/parent/src/routes/profiler/dimension-card.component.test.tsx (required check: test)
    • Automated check, run on demand: The same contract driven end to end with real key presses: tab to the answer control, select with the space bar, then assert focus sits inside the new card. Post-merge tier, so it reports rather than gates. apps/parent/e2e/a11y-nav.spec.ts
    • Not verified: No automated rule in the axe catalogue can see a focus-order problem, so nothing sweeps for the next one. There is no keyboard walkthrough for the report tabs, the chat or the school staff screens.
  • 2.4.4 Link Purpose (In Context) Level A Supports Advisory only

    Every link has a name. That is checked automatically on the website whenever a change touches it, and on the app's screens after merge, though neither check is one of the three that can actually stop a merge. A search across both products for the vague link text this criterion exists to stop, 'click here', 'read more', 'learn more', 'more', 'here', returned nothing. We know of no link whose purpose is unclear from where it sits.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: The WCAG 2.2 A and AA rule set, which carries link-name, over 24 public routes. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: The same rule set, which is what carries link-name, over 10 signed-in screens. Post-merge tier, so it reports rather than gates. apps/parent/e2e/a11y-nav.spec.ts
    • Code review, 2026-08-09: A search for vague link text across the website, the app and the report components, re-run on 2026-08-09, returned nothing. This row was published as 'Partially Supports' until that date purely because the old rule in this file required a gate for anything else, which it had. The gate was never the issue.
  • 2.4.5 Multiple Ways Level AA Partially Supports Advisory only

    The public website gives you a header menu, a four-column footer, breadcrumbs and a sitemap. The profiler is a step-by-step process, which the standard exempts. The school staff area has only one way to get anywhere.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Crawls the built site's own nav, footer and hub-page content (the /trust, /glossary and /accessibility index pages linking their own children, plus the sitewide header CTA) and fetches the real generated sitemap, then asserts every registered marketing route is reachable by at least two of those mechanisms. The school staff area is app-side and out of this probe's scope; its single-mechanism gap is the code-review finding below. Runs on the Marketing a11y (axe) workflow, which is plain `playwright test` with no project filter, so it picks this spec up whenever a pull request touches the site. It goes red on a regression and cannot refuse a merge, being no part of lint, test or type-check. apps/marketing/e2e/consistency.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Code review, 2026-08-08: Website: header nav, footer, breadcrumbs and sitemap. App school staff area: one navigation affordance and no second route to anything.
  • 2.4.6 Headings and Labels Level AA Partially Supports Advisory only

    Labels are in good shape throughout. The school staff screens were the failure here, and were fixed on 15 August 2026: each of the five now has one top-level heading and named sections, pinned by a test that reads each screen's outline word for word. Whether a heading elsewhere in the app or on the website is well worded is a judgement no tool makes, and nobody has made it.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Structural checks over 24 public routes. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: All five school staff screens are scanned and their heading outline asserted: one top-level heading, first, and no level skipped. Three are scanned in both render branches; the roster's empty state and the assisted session's code step are held by a unit test instead, because no browser run can reach them. Post-merge tier. apps/parent/e2e/a11y-nav.spec.ts
    • Code review, 2026-08-15: The wording of the new section headings was chosen against the screen they head rather than the page title, which is the part of this criterion no rule measures. The same review is what found that the disclosure it retired had miscounted the screens and missed that all of them already had a top-level heading.
    • Not verified: No one has read the headings on the parent journey or the public website and judged whether each describes what follows it.
  • 2.4.7 Focus Visible Level AA Supports Advisory only

    Every focusable thing on the public website draws a visible outline, and since 15 August 2026 so does everything in the app. Before that the app's site-wide rule was only a default: 56 controls switched it off, and most of them looked from their code as though they had not, because the ring they switched to was painting fully transparent. All 56 were fixed, and a test now walks the entire keyboard path of eight screens in the app, reads back the outline the browser really drew on each stop, and measures it at 3 to 1 against the surface behind it. It runs on request and on the push to main rather than on every pull request, so it reports a regression within a day rather than the moment one is written.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Reads computed styles, so a rule that exists but does not render would still fail. Covers the website only. apps/marketing/e2e/keyboard-focus.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Tabs through the whole keyboard path of home, the privacy page, the create-child form, the neurodivergence step, a profiler overview card, a profiler slider card, the review index and the SENCO roster, and for every stop requires an indicator that focus itself added, at 3 to 1 against its backdrop. Each control is read again unfocused, so an existing drop shadow cannot pass as a focus style. Where it stops: those eight screens in Chromium, so the report reading surface, the meeting sheet, chat and the welcome tour are not walked, and it runs on request and on the push to main rather than on every pull request. apps/parent/e2e/keyboard-focus.spec.ts
    • Automated check, run on demand: Asserts that following the app's skip link lands focus somewhere that draws a real outline, read from the rendered style. Covers that one target rather than the app generally. apps/parent/e2e/a11y-nav.spec.ts
  • 2.4.11 Focus Not Obscured (Minimum) Level AA Partially Supports Advisory only

    New in WCAG 2.2. No axe rule checks it, so a behavioural probe (ASM-984) now does: for every stop in a route's tab order it resolves the element's own centre point through the browser (elementFromPoint) and requires it to land on the element itself, a descendant, an ancestor or its label, rather than under a fixed header, footer or overlay. The public website is clean across all 24 scanned routes. The app has one disclosed, live gap: on every real dimension question card, the get-help chat panel stays in the keyboard tab order while closed. It slides fully off-screen with a CSS transform instead of going inert, so its text input and its Close and Clear buttons are three real Tab stops that land nowhere a sighted keyboard user can see. Not yet fixed.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: For every stop in every scanned route's tab order, resolves the element's own bounding-box centre point through elementFromPoint and requires it to land on the element itself, a descendant, an ancestor or its label. Clean across all 24 scanned routes. apps/marketing/e2e/keyboard-focus.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same check over home, privacy, create-child, a profiler overview card, a profiler slider card and the SENCO org roster. Found the get-help chat panel's three closed-state controls obscured on the slider card, the route where the panel actually renders; nothing else obscured across the six screens. The finding is disclosed through a named, narrow carve-out (KNOWN_OFFSCREEN_PANEL_SELECTOR in that file, keyed on a testid added for this probe) rather than silently dropped, so a NEW obscuring failure elsewhere still fails the test. Where it stops: those six screens, so the report's sticky tab strip, the mobile report bar, chat threads, the welcome tour and the org meeting/invite/assisted screens are not walked, and it runs on request and on the push to main rather than on every pull request. apps/parent/e2e/keyboard-focus.spec.ts
    • Not verified: No axe rule exists for this criterion, verified against the pinned axe-core 4.12.1. The probe above covers 24 marketing routes and 6 app screens; the report's sticky tab strip, the mobile report bar, chat threads, the welcome tour and the org meeting/invite/assisted screens are not yet walked by it.
  • 2.5.1 Pointer Gestures Level A Supports Held

    Nothing in either product needs a pinch, a swipe along a path or more than one finger. Every control works with a single tap or click, and that is now a running check rather than a dated read of the source: packages/brand's test script absence-gates the multipoint/path-gesture symbols and the known gesture library names (ASM-985).

    A required check holds this

    Evidence

    • Code review, 2026-08-09: No multipoint or path-based gesture handler exists anywhere in scope: no pointer-move or touch-move listener, no draggable element, no range input and no gesture library, across both apps and the shared component packages. The single wheel listener is a passive scroll shadow, not a control. Superseded by the automated-gate row below; kept as history.
    • Automated gate, required to merge: The absence gate fails on any use of a second simultaneous touch point (touches[1]) or coalesced-event path reconstruction (getCoalescedEvents), and on any known multipoint/path-gesture library (hammerjs, @use-gesture/react, interactjs and near neighbours) becoming a dependency of apps/marketing, apps/parent or the shared component packages. It rides packages/brand's test script (ASM-923 wiring), which turbo's required test job runs unconditionally, so a regression here cannot merge. scripts/a11y-absence-gate.mjs (required check: test)
  • 2.5.2 Pointer Cancellation Level A Supports Advisory only

    Everything happens on click, or on submit, never the instant you press down, so you can always slide off a control to change your mind. Held behaviourally now on the header CTA and the app's create-child submit: press down, drag off, release, and nothing fires (ASM-985).

    A check watches this, but cannot block a merge

    Evidence

    • Code review, 2026-08-09: No down-event handler performs an action anywhere in scope. Two press-down listeners exist and neither acts: one notes that a form has been touched, for spam detection, and the other is a comment recording that a redundant binding was deliberately dropped in favour of click alone. Kept as history; the rows below hold the claim behaviourally.
    • Automated check, advisory only: Mousedown on the header CTA, drag off, mouseup elsewhere: asserts the page never navigates. Runs on pull requests that touch the site, on the same path-filtered advisory workflow as SC 2.5.3 and 2.5.8. apps/marketing/e2e/pointer-cancellation.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same probe against the app's create-child submit button: mousedown, drag off, mouseup, asserts no POST fires and the page never leaves /children/new. On-demand/post-merge tier, so it reports rather than gates. apps/parent/e2e/pointer-cancellation.spec.ts
  • 2.5.3 Label in Name Level A Partially Supports Advisory only

    Where a control has a visible label, its spoken name should contain that label, so speech control works. The website's scan now checks this by itself on any change that touches the site, though it reports rather than blocking. The tool marks this rule experimental and our normal filter skips experimental rules even when they carry a WCAG tag, so we turn it on by name rather than relying on the filter. The app's chat screens run the same rule after merge; the rest of the app's screens do not yet.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: The rule is turned on by name over 24 public routes, which is the only way to run it: the tag filter drops it for being experimental. Zero violations. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: The same rule, turned on the same way, over the app's chat screens. Post-merge tier, so it reports rather than gates. apps/parent/e2e/chat.spec.ts
    • Automated check, run on demand: The full-catalogue sweep enables every rule. Zero violations across 284 page states on 2026-08-08. Reporting only, gates nothing. apps/marketing/e2e/a11y-full-audit.spec.ts
    • Not verified: The app's main screen sweep does not turn this rule on yet, so 10 signed-in screens are unchecked for it. One known risk there: the answer control takes its spoken name from an attribute while the visible label is hidden at some screen sizes. They match today, and nothing keeps them matching.
  • 2.5.4 Motion Actuation Level A Supports Held

    Nothing responds to tilting, shaking or moving your device, so there is nothing you need to be able to turn off. That rests on a running check now rather than a dated read of the source: packages/brand's test script absence-gates the device-motion and device-orientation API names (ASM-985).

    A required check holds this

    Evidence

    • Code review, 2026-08-09: Zero uses of the device motion, device orientation, accelerometer and gyroscope APIs, across both apps and the shared component packages. Superseded by the automated-gate row below; kept as history.
    • Automated gate, required to merge: The absence gate fails on the device motion and device orientation event names, the DeviceMotionEvent/DeviceOrientationEvent constructors and the Generic Sensor API's Accelerometer/Gyroscope classes appearing anywhere in apps/marketing, apps/parent or the shared component packages. Rides packages/brand's test script (ASM-923 wiring), which turbo's required test job runs unconditionally, so a regression here cannot merge. scripts/a11y-absence-gate.mjs (required check: test)
  • 2.5.7 Dragging Movements Level AA Supports Held

    Nothing has to be dragged. The answer control is where this would normally go wrong, and it is built on plain radio buttons: one click, or one key press. Both the source symbols and the behaviour are held now (ASM-985): the absence gate fails on drag/pointer-path symbols, and a browser probe presses one radio, drags across the row and confirms release on a different stop changes nothing. The slider drag affordance question is deliberately out of scope for this wave (ASM-991); if it is answered yes, this row's gate and probe are the ones that have to change first.

    A required check holds this

    Evidence

    • Code review, 2026-08-09: No drag path exists in scope: no range input, no draggable attribute, no drag-and-drop library and no pointer-move or touch-move listener, in either app or the shared component packages. Superseded by the automated-gate row below; kept as history.
    • Automated gate, required to merge: The absence gate fails on a range input, draggable/ondragstart in either casing, a drag-and-drop library, or a pointermove/setPointerCapture listener (the API a hand-rolled drag handler needs to keep tracking the pointer once it leaves the element) anywhere in apps/marketing, apps/parent or the shared component packages. Rides packages/brand's test script (ASM-923 wiring), which turbo's required test job runs unconditionally, so a regression here cannot merge. scripts/a11y-absence-gate.mjs (required check: test)
    • Automated check, run on demand: Answers a dimension question, presses down on that stop, drags the pointer across the row and releases on a different one: asserts the original stop is still the only one checked and no save POST fired. On-demand/post-merge tier, so it reports rather than gates. Documents the ASM-991 flip condition inline so the probe itself is where a future drag-affordance change has to reckon with SC 2.5.7's actual requirement. apps/parent/e2e/pointer-dragging.spec.ts
  • 2.5.8 Target Size (Minimum) Level AA Supports Advisory only

    Every button and link is either at least 24 by 24 pixels, or small enough to need the standard's spacing exception and far enough from its neighbours to get it. This is the one new WCAG 2.2 criterion a machine can check, and both the website and the app run that check with zero violations. That check is real but narrow: it covers the routes the two suites visit, it judges size and spacing only, and the standard's other exceptions, for targets inside a sentence and for targets that are essential, still need a person. The app's smallest targets were measured by hand on 2026-08-09 rather than left to the tool.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: The WCAG 2.2 AA tag carries the target-size rule. Zero violations over 24 public routes. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same rule over the app screens, at 1280 and at 320 pixels wide, on the post-merge and on-demand tier rather than the pull request tier. The home scan asserts the multi-child pill row is actually present before it measures anything, so trimming the dev roster can no longer turn this into a green scan of a screen that lost the surface. apps/parent/e2e/a11y-nav.spec.ts
    • Code review, 2026-08-09: Hand measurement of the app's smallest targets. The smallest is the 'Change an answer' link on the profiler done screen at 118 by 21 CSS pixels: under 24, so it needs the spacing exception, and it has room to spare, because a 24 pixel circle centred on it reaches 1.5 pixels past its own edge and the nearest other control is 12 pixels away. The home screen's multi-child pill row, the case a tightly wrapping row would be expected to break, cannot fail: an explicit line height fixes each pill at 35 pixels tall and 12 pixels of padding on each side put a 26 pixel floor under its width, whatever the child is called. Checked at 5, 8, 12, 16, 22 and 67 children, at 1280, 640, 480, 390 and 320 pixels wide, with no failure at any combination.

Understandable

It behaves predictably and says what it means. 13 success criteria. Each one opens to show the finding and its evidence.

  • 3.1.1 Language of Page Level A Supports Held

    Every page declares its language, checked automatically on both products. Assembly is delivered in English (United Kingdom) only, so the language a page declares is always the language it is written in.

    A required check holds this

    Evidence

    • Automated check, advisory only: axe html-has-lang over 24 public routes. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated gate, required to merge: This row used to disclose a real defect: a page could declare Polish while its questionnaire, report and one heading rendered English. It is closed by removal rather than by translation. A second locale is now offered only on an allowlist of internal deployment tiers (development, pull-request previews, and a stakeholder showcase tier that is not currently provisioned), and never on staging, production or beta. In every tier outside that allowlist a locale-prefixed address redirects to the unprefixed route, so no page of the live service this statement covers declares a language it does not render. The test asserts that redirect directly, including that a stored preference for the withdrawn locale is ignored rather than acted on. It also covers the case a review of this change found open: a form submission is not a page request, cannot be redirected without discarding what was typed, and so is refused outright rather than answered in the withdrawn language. apps/parent/src/lib/locale-continuity.descope.test.ts (required check: test)
    • Automated gate, required to merge: Iterates the closed deployment-tier enum and asserts staging and production are English-only, so a tier added later fails this test until somebody decides which side of the line it belongs on rather than silently inheriting the permissive answer. apps/parent/src/i18n/multilingual.test.ts (required check: test)
  • 3.1.2 Language of Parts Level AA Supports Held

    The Assembly interface on the live service is written in English (United Kingdom) throughout, so no passage of the interface is in a different language for a screen reader to mispronounce. This covers the interface we write. It is not a claim about what a parent types into a free-text field: their own words are stored and shown back to them as they wrote them, and we do not label the language of anything a person enters.

    A required check holds this

    Evidence

    • Automated gate, required to merge: This row used to disclose the language switcher showing the word 'Polski' inside an English page without marking it as Polish. Both halves are now held. Where more than one language is offered, each option carries the language its own label is written in, so a screen reader pronounces it with the right engine. Where only one is offered, which is staging, production and beta, the control does not render at all. apps/parent/src/components/locale-switcher.test.tsx (required check: test)
    • Code review, 2026-08-20: Superseded evidence, kept for the trail: the earlier code review dated 8 August 2026 said the language attribute appeared exactly twice in the codebase and that the switcher button carried none. That stopped being true when the attribute was added to each option, and the assertion above now holds it.
  • 3.2.1 On Focus Level A Supports Advisory only

    Tabbing onto something never changes the page around you. Nothing opens, submits or navigates because focus arrived. A behavioural probe (ASM-984) now tabs through every scanned public marketing route and a representative set of app routes and confirms the URL never moves and no dialog appears from focus alone.

    A check watches this, but cannot block a merge

    Evidence

    • Code review, 2026-08-09: One focus handler exists in scope and it selects the text of a read-only link field so it can be copied, which is not a change of context. Two fields set autofocus when their page loads, which places focus rather than changing anything once you are there.
    • Automated check, advisory only: Tabs through every scanned route and asserts the URL stays the same and no [role=dialog] appears from focus alone. Clean across all 24 scanned routes. apps/marketing/e2e/keyboard-focus.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same walk over home, privacy, create-child, a profiler overview card, a profiler slider card and the SENCO org roster. Clean on all six. Where it stops: those six screens, and it runs on request and on the push to main rather than on every pull request. apps/parent/e2e/keyboard-focus.spec.ts
  • 3.2.2 On Input Level A Supports Held

    In the profiler, choosing an answer still moves you to the next question by itself about half a second later. Since 15 August 2026 we tell you so first, in the words "When you choose an answer, we move you on to the next question. You can go back and change any answer later." It appears on the welcome screen at the start, and again on the introduction to each of the eight topics, so a parent picking the journey up days later is told too rather than only on their first day. Your keyboard position moves with the card, which is a separate criterion and is also fixed.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Asserts the notice on both intro screens, and that it carries real copy rather than an empty element the layout happens to render. What it cannot check is that the wording is clear enough to do its job, which is a human read. packages/profiler-components/src/components/cards/__tests__/IntroCard.test.tsx (required check: test)
    • Code review, 2026-08-15: The auto-advance itself is unchanged and deliberate: it keeps 88 questions moving, and it is already suppressed on edits and revisits. Nothing else in either product changes context on input; that search is recorded on the SC 3.2.1 row above.
  • 3.2.3 Consistent Navigation Level AA Partially Supports Advisory only

    On the website, one shared source feeds the desktop menu, the mobile menu and the footer, so the order never diverges. In the app the menu is fixed and consistent, but the school staff area uses a different shell with no menu, and the sign-in page has none.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Reads the primary nav, mobile nav and footer columns on every registered marketing route and asserts an identical ordered link list on all of them, plus that the primary/mobile nav really are the nav.ts source arrays rather than a drifted copy. Runs on the Marketing a11y (axe) workflow, which is plain `playwright test` with no project filter, so it picks this spec up whenever a pull request touches the site. It goes red on a regression and cannot refuse a merge, being no part of lint, test or type-check. apps/marketing/e2e/consistency.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Opens the shell nav sheet on every route it renders (home, privacy, help, create-child, a freshly created child's first profiler card, the neurodivergence step, an overview card, the welcome tour) and asserts an identical ordered item list, then asserts the SENCO surface renders no such menu at all on any of its four routes. Post-merge tier, so it reports rather than gates. apps/parent/e2e/consistency.spec.ts
    • Code review, 2026-08-08: The website has one navigation source with the current-page marker computed once and applied identically to both modes. The app menu is four entries in a fixed order and position.
  • 3.2.4 Consistent Identification Level AA Partially Supports Not held

    One real conflict: 'Get help' names two different things in the app, a menu entry that opens a help page and a floating button that opens a chat.

    No test holds this

    Evidence

    • Automated check, run on demand: Pins the conflict as it stands: the nav entry's visible label and the chat FAB's visible label are both exactly "Get help", their accessible names and destinations differ (the nav entry opens /help, the FAB opens the per-question chat), and, in a tier that offers more than one language, the language switcher's group name and per-option labels are asserted identical wherever that control recurs (home, an overview card). That control does not render at all where only one language is offered, which is staging, production and beta. The probe fails on a NEW inconsistency, not on this one, which stays open. Post-merge tier, so it reports rather than gates. apps/parent/e2e/consistency.spec.ts
    • Code review, 2026-08-08: The intent is recorded in the code (the help page explains the in-flow pattern rather than duplicating it), which is a sound design story and still a same-label-different-function situation.
  • 3.2.6 Consistent Help Level A Partially Supports Advisory only

    New in WCAG 2.2, and no tool can check it. On the website, Contact is always last in the mobile menu and the Support column is always third in the footer, though there is no help link in the desktop header at all. In the app the Get help entry is always second in the menu but is absent on sign-in, on the school staff screens and on the error screen.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: Asserts the desktop primary nav stays free of any help/contact/support entry, Contact stays last in the mobile panel and Support stays the third of the footer's four columns, on every route checked. Runs on the Marketing a11y (axe) workflow, which is plain `playwright test` with no project filter, so it picks this spec up whenever a pull request touches the site. It goes red on a regression and cannot refuse a merge, being no part of lint, test or type-check. apps/marketing/e2e/consistency.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Asserts 'Get help' sits second in the nav's primary section wherever the nav renders, and that it is absent, with the FAB also absent, on the SENCO surface and on the root error boundary (reached via a hide-existence 404). The two states this probe cannot reach live (pre-auth /login, unauthenticated /privacy) are held instead by apps/parent/src/routes/privacy.test.tsx's own unauthenticated-frame assertion, named in a comment rather than faked here. Post-merge tier, so it reports rather than gates. apps/parent/e2e/consistency.spec.ts
    • Code review, 2026-08-08: Consistently thin on the website, which technically satisfies the criterion. In the app the entry is also removable by partner configuration.
  • 3.3.1 Error Identification Level A Supports Held

    On the website this is met and checked: both of its forms mark the failing field as invalid, attach the message to that field so a screen reader reads the two together, and the longer form opens with a summary of what went wrong that takes your keyboard position with it. The app caught up on 16 August 2026 (ASM-990): every route form that renders an inline error now attaches it to the field it belongs to (aria-describedby) and marks that field aria-invalid. That was a wider fix than the two forms first disclosed here (sign-in and the parent's own add-a-child form): the same sweep also found the SENCO-assisted sign-in and the invite-a-parent form doing the same thing, and fixed both. The one place an error still renders unbound to a single field is the neurodivergence disclosure step, and it stays that way on purpose: a rejected condition code is a whole-submission parse failure, not one erroring field among several, so there is no single item for aria-describedby to name.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Holds the website's error text: every message must name the field it belongs to. A generic message fails this on the required check, which is where the old wording would have been caught. apps/marketing/tests/form-errors.test.ts (required check: test)
    • Automated check, advisory only: Submits both real forms in a browser and asserts the accessible description each field ends up with, plus aria-invalid and the focus move into the summary. Attribute-level assertions would pass on an id pointing nowhere, so it reads the accessibility tree instead. Advisory and path filtered. apps/marketing/e2e/form-errors.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated gate, required to merge: Renders sign-in with a mocked error result on each step (the magic-link email step and the code step) and asserts the field's accessible description resolves to the alert text, plus aria-invalid. Sign-in's forms are plain document POSTs by design, so this mounts the component with the actionData a submit would have produced rather than driving a click through jsdom's unimplemented form navigation. apps/parent/src/routes/login.render.test.tsx (required check: test)
    • Automated gate, required to merge: Submits the parent's add-a-child form with a blank name and asserts the field's accessible description resolves to the rendered alert text, plus aria-invalid. apps/parent/src/routes/children/new.component.test.tsx (required check: test)
    • Automated gate, required to merge: Same check across both steps of the SENCO-assisted sign-in screen (the parent-email step and the code step), found unbound during the ASM-990 sweep and fixed alongside sign-in and add-a-child. apps/parent/src/routes/org/assisted.render.test.tsx (required check: test)
    • Automated gate, required to merge: Adds the same binding check to the invite-a-parent screen, alongside its existing live-region announcement tests, found unbound during the ASM-990 sweep and fixed alongside sign-in and add-a-child. apps/parent/src/routes/org/invite.render.test.tsx (required check: test)
    • Code review, 2026-08-15: The website half was fixed on this date and NC-13 retired with it. The app was re-read rather than assumed: apps/parent/src/routes/org/child-new.tsx binds its error and marks the field invalid, and apps/parent/src/routes/login.tsx and children/new.tsx render an error block bound to nothing. Neither of the checks above visits the app, so nothing holds that half either way.
    • Code review, 2026-08-16: ASM-990: every `role="alert"` error block under apps/parent/src/routes was found and read (login.tsx x2, children/new.tsx, org/child-new.tsx already bound, org/assisted.tsx x2, org/invite.tsx). All now attach via aria-describedby and set aria-invalid, error copy unchanged. Two renders were read and left as-is because there is no single field to bind: children/pilot-consent.tsx and claim.tsx render a write-failure alert next to two decision BUTTONS, not a text field, and children/neurodivergence.tsx's alert is the whole-submission condition-disclosure failure described above. Scope was apps/parent/src/routes; components outside that directory (e.g. packages/parent-components) were not swept.
  • 3.3.2 Labels or Instructions Level A Supports Held

    Labelling is strong throughout. The one exception, the chat box on the report page having placeholder text only and no real label, was fixed on 15 August 2026: all three of the app's chat boxes now carry a real label, hidden visually and read by screen readers, and a test resolves each of them by the name a screen reader would announce.

    A required check holds this

    Evidence

    • Code review, 2026-08-08: Real labels tied to their inputs throughout both apps. Found one placeholder-only field, live on the report page, which is the exception now closed.
    • Automated gate, required to merge: Resolves the report page's chat box by its accessible name, which a placeholder alone cannot supply, including its unnamed-child fallback. The app's two other chat boxes are held the same way by apps/parent/src/components/chat/chat-conversation.test.tsx and apps/parent/src/components/profiler/help-chat-panel.test.tsx. packages/report-components/src/ProfileChatInput.test.tsx (required check: test)
  • 3.3.3 Error Suggestion Level AA Partially Supports Held

    The website's messages now name the field and say what to do with it, and an address that is the wrong shape gets an example of a right one. The app's are still the older kind, which report that something is wrong without saying which thing or what would fix it. That is a wording gap, separate from whether the message reaches the field at all (SC 3.3.1, fixed in the app on 16 August 2026): ASM-990 wired every app error up to the field it describes, but left every message's WORDS exactly as they were, so this row's gap survives that fix untouched.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Asserts the shape of the sentence, not just that a sentence exists: it must name the field as that page labels it, say what to do, and carry an example address where the address is what went wrong. This is the check the old 'Please fill in the fields marked above' would have failed. apps/marketing/tests/form-errors.test.ts (required check: test)
    • Code review, 2026-08-15: The website wording was rewritten on this date and moved to apps/marketing/src/lib/form-errors.ts so a test can hold it. The app was not touched and its messages were not re-read line by line, so this row stays short of support on the app's account alone.
    • Code review, 2026-08-16: ASM-990 fixed how the app's errors reach the field (SC 3.3.1), scoped deliberately to the binding and not the copy: every message cited on the SC 3.3.1 row above is the same Paraglide string it was before, unchanged. This row's gap, that those messages still don't name the field or suggest a fix in their own words, was read and confirmed to remain exactly as described above.
  • 3.3.4 Error Prevention (Legal, Financial, Data) Level AA Supports Held

    There are no payments and no legal commitments in the product, and answers stay editable for a period after you give them, so most of this criterion has nothing to bite on. The one place it did bite was the create-child form: going back from the neurodivergence step and submitting again created a second child record rather than editing the first. Since 15 August 2026 it edits the child you came back from, and a test that has to pass before any change can be merged holds it there. If you already have two records from before that, ask us to remove one, the same way the privacy page tells you to ask for a copy or a deletion.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Drives the real create-child action against a real database. Asserts that a resubmit carrying a corrected value amends the child the form was seeded from, leaving one record and not two, and that an unchanged resubmit writes nothing and simply resumes. The domain function underneath is held by packages/domain/src/children/children.test.ts on the same check, and apps/parent/e2e/back-nav.spec.ts walks the whole path in a browser. Where it stops: this covers the one flow where a mistake could cost you data. Nothing automated watches the rest of the criterion, because the rest of the criterion has nothing in this product to watch. apps/parent/src/routes/children/new.test.ts (required check: test)
    • Code review, 2026-08-15: A thin surface by design: nothing in scope is a financial transaction, a legal commitment or a deletion of data you control. The editable grace window makes profiler answers reversible. Re-checked on the day the duplicate-child path was fixed, which is what moved this row off a disclosed live failure.
  • 3.3.7 Redundant Entry Level A Supports Held

    New in WCAG 2.2, and largely designed for: a parent invited by their school never re-enters what the school already gave us, and the neurodivergence step remembers earlier answers. Going back from that step used to give you an empty form and make you retype your child's name, age, gender and pronouns. Since 8 August 2026 it gives you back what you typed, and since 15 August 2026 changing something there corrects that child rather than starting a second one. A test that has to pass before any change can be merged holds both.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Asserts that arriving back at the create-child form from the neurodivergence step re-seeds every field from the child already created, and that resubmitting it, changed or unchanged, leaves one child record rather than two. apps/parent/e2e/back-nav.spec.ts walks the same path in a browser, back link and all. Where it stops: this holds the one multi-step form in the product where a value could have to be entered twice. No automated check sweeps the product for a new one, and no axe rule can, because none exists for this criterion. apps/parent/src/routes/children/new.test.ts (required check: test)
    • Code review, 2026-08-08: The claim path deliberately skips already-known details, and email is never asked twice: the magic link carries the session, so there is no second password or code to re-enter.
  • 3.3.8 Accessible Authentication (Minimum) Level AA Supports Held

    There is nothing to remember. You sign in by clicking a link we email you, or, if you would rather type something, with a short code we email instead: no password, no puzzle and no CAPTCHA anywhere in either product. The code goes in one ordinary field you can paste into, or let your device fill, so there is nothing to copy out by hand either. A test that has to pass before any change can be merged holds it.

    A required check holds this

    Evidence

    • Automated gate, required to merge: Written for the stricter AAA claim (SC 3.3.9, agents-docs/design-system/WCAG-2.2-AAA-ADOPTION.md), which this AA criterion sits inside: asserts the resolved Better Auth config registers exactly magic-link and email-otp and nothing password/passkey/2FA/social-shaped, that emailOTP keeps disableSignUp so the code can never create an account, and that the rendered code field keeps autoComplete="one-time-code" with no maxLength, pattern or paste handler that would turn it into a transcription task. What it cannot see is CAPTCHA, which is a repo-wide absence claim held separately: scripts/a11y-absence-gate.mjs greps for turnstile/hcaptcha/recaptcha/password fields, chained into packages/brand's own "test" script (turbo.json), so it runs on the same required check. apps/parent/src/lib/auth.server.test.ts (required check: test)
    • Code review, 2026-08-15: The auth configuration registers two sign-in methods, both passwordless and both delivered to the same inbox: an emailed magic link (primary, always offered) and an emailed one-time code, which is sign-in only and cannot create an account. Email and password is off, no social provider is configured, and no two-factor, passkey or username plugin is present; the account table's password column is documented as null for us by design. The code field is a single text input with autocomplete set to one-time-code, no length or pattern restriction, no per-character boxes and nothing blocking paste, so entering it needs no transcription. A search of both apps for CAPTCHA, reCAPTCHA, Turnstile and hCaptcha returns one comment, explaining why there is none.

Robust

It works with assistive technology now and as that technology changes. 2 success criteria. Each one opens to show the finding and its evidence.

  • 4.1.2 Name, Role, Value Level A Partially Supports Advisory only

    We use plain HTML controls wherever possible, so their name, role and state come for free, and automated checks cover the rest on both products. Two known gaps: a progress bar has no name, and on the answer control a position you set by clicking the bar rather than choosing one of the five options is announced as a percentage, 'Custom position at 63%', which tells you nothing about what you have said. A third, the report page chat box having no name, was fixed on 15 August 2026 and is now held by a test.

    A check watches this, but cannot block a merge

    Evidence

    • Automated check, advisory only: The axe name, role and value rule family over 24 public routes. apps/marketing/e2e/a11y.spec.ts (advisory check, cannot block a merge: Marketing a11y (axe))
    • Automated check, run on demand: Same family over 10 signed-in screens and all five school staff screens, post-merge tier. 27 test files assert controls by role. apps/parent/e2e/a11y-nav.spec.ts
    • Code review, 2026-08-09: The shipped answer control was read from the live route down. Its bones are right: a radio group of five real radio stops, each named with the option it stands for, which is the path a keyboard or screen reader user takes. Two things sit alongside it. The invisible full-width button over the bar, which lets a pointer set a position between the stops, is named 'Click to set custom position', an instruction rather than a purpose. When that button is used, the extra stop it creates is named by percentage rather than in the words of the question.
    • Not verified: No scan visits sign-in, the invitation claim page or the help page. Nothing asserts the answer control's names either: the rule that would catch a meaningless one is a judgement, not a rule any tool has.
  • 4.1.3 Status Messages Level AA Partially Supports Held

    Failures are announced, success messages on both website forms are announced politely, and the invitation screen re-announces correctly on a repeat. The chat used to say nothing at all when a reply arrived; since 15 August 2026 both chat surfaces mark the message list as a log that announces politely and the thinking indicator as a status region, and since 16 August 2026 a completed reply also announces its own arrival from a dedicated status region that does not depend on the log's `aria-busy` hold. What is not settled is whether either announcement is actually heard: the hold is followed properly by some screen readers and not others, and the arrival notice, while marked up correctly, has not yet been confirmed on real assistive technology either.

    A required check holds this

    Evidence

    • Code review, 2026-08-08: Considered work, not accidental: live regions are pre-rendered and cleared so a repeat re-announces, and progress uses a native output element. Found the chat message list carrying no live region and the thinking indicator a plain paragraph, which is the gap now closed.
    • Automated gate, required to merge: Pins the log role, the polite setting, the hold that stops a streaming reply being re-announced on every token, and the rule that the thinking and error regions are never nested inside the log. The persistent chat is held identically by apps/parent/src/components/chat/chat-conversation.test.tsx. apps/parent/src/components/profiler/help-chat-panel.test.tsx (required check: test)
    • Automated gate, required to merge: Pins the arrival notice added for ASM-988: it renders into its own role="status" region, sibling to the log and never nested inside it, only once a reply has settled (never while submitted or streaming), and clears the moment the next send starts so a repeat reply re-announces. The persistent chat is held identically by apps/parent/src/components/chat/chat-conversation.test.tsx. apps/parent/src/components/profiler/help-chat-panel.test.tsx (required check: test)
    • Not verified: Nobody has confirmed on real assistive technology that either announcement is actually heard: not the log's hold, reliable on NVDA and handled inconsistently by VoiceOver and JAWS, and not the newer arrival notice, whose markup is standard status-region semantics but has not itself been listened to. The tests assert the markup, which is not the same as hearing it.

Beyond AA: what we borrow from AAA

WCAG 2.2 has 36 success criteria at its strictest level, AAA, and the standard itself advises against claiming all of them: some content genuinely cannot meet every one at once. So we do not claim AAA. Instead we look at each AAA criterion on its own and ask whether it matters for what we build. Where it does, we adopt it, with a reason recorded below. Where it does not, we decline it, with the reason recorded too, so the question does not come round again unanswered. A specific claim like this is stronger than a tier claim: it cannot be found wanting by the one criterion nobody checked.

This is not a conformance claim, and it never becomes one on this page. A row below reads as met only where a check already holds it against regression, 3 of the 36. A further 11 carry a check that holds part of the work, or holds the fact that we publish nothing the criterion would apply to. Everywhere else, even a criterion we genuinely meet today is shown with the same honesty this page already uses for a criterion with no regression gate, because a met claim that one unreviewed change could quietly break is exactly the kind of overclaim we are trying not to make.

We adopt this: 26 of 36
Committed on the merits, whatever state it is in today.
We adopt the cheap part now: 4 of 36
We take the cheap, useful part now, and hold off claiming the rest until something measures it.
We decline this: 6 of 36
Declined with a reason on the record, not left unspoken.

Where we land today: 3 met and held by a test, 17 partly met, 10 not yet met, 1 not assessed, and 5 that do not apply to anything we publish. A verdict of adopt does not mean met: it means we have decided the criterion is worth building towards, on a build order we are working through.

Perceivable

WCAG 2.2 level AAA Perceivable success criteria, with what Assembly decided and why.
Criterion Our decision Where it stands Why
1.2.6 Sign Language (Prerecorded) Decline Not applicable

We publish no video, and promising a British Sign Language interpreter for every future video is a commitment that never gets cheap. We caption everything instead, and we would revisit this if an NHS contract asked for it by name.

1.2.7 Extended Audio Description Decline Not applicable

We publish no video. If we ever do, our production rule is to write the narration so it already says what the screen shows, which is the cheaper and more reliable answer to the same problem.

1.2.8 Media Alternative (Prerecorded) Adopt Not applicable

We have no video today, but we are adopting this ahead of time: any future video gets a written transcript from day one, because the script already exists and a transcript costs almost nothing on top of it.

1.2.9 Audio-only (Live) Decline Not applicable

Nothing on our site streams live audio, and nothing is planned. A page nobody maintains reads as coverage while holding nothing, so we are not adding one.

1.3.6 Identify Purpose Decline Not met

The technique this criterion asks for depends on a machine-readable format that no browser or assistive technology actually reads today, so claiming it would not help anyone using our product.

1.4.6 Contrast (Enhanced) Adopt the cheap part Not met

We are raising our body text to the stronger contrast bar where it is a simple colour step, but not across every coloured panel: that would undo contrast decisions we already tuned carefully, including a signed-off welcome colour, for a gain that is mostly theoretical there.

1.4.7 Low or No Background Audio Adopt Not applicable

We have no audio today, and we are adopting the rule now: no music behind the voice in any future video, because a music bed makes speech harder to follow for people with auditory processing differences, who are common among our users.

1.4.8(a) Visual Presentation: user-selectable colours Decline Not met

Rather than build our own colour picker, we would rather make the product properly respect the contrast and colour settings you have already set on your own device.

1.4.8(b) Visual Presentation: 80-character measure Adopt Partly met

Our website and our printed report already keep lines of text short and easy to follow. The gap is inside the app, where some cards and the report screen still run wider than we would like.

1.4.8(c) Visual Presentation: no justified text Adopt Partly met, and a test holds what's built

We keep text ragged rather than justified everywhere we can, on screen and in the printed report, because justified text creates uneven gaps between words that are harder for dyslexic readers to follow, and dyslexic parents are exactly who this criterion is for.

1.4.8(d) Visual Presentation: line and paragraph spacing Adopt Partly met

The space between paragraphs used to be too tight across our website, our printed report and the app. We widened it on all three in August 2026, setting it against each surface's own line height rather than one number everywhere. It is partly met rather than met because only the website version is measured automatically, and that check is one we watch rather than one that can block a change.

1.4.8(e) Visual Presentation: 200% without horizontal scroll Adopt Partly met

Zooming in to the 200 percent equivalent causes no page to scroll sideways. In August 2026 we made that an automatic check rather than a one-off measurement, and included the report page and a question page for the first time. It stays partly met because the check is one we watch rather than one that can block a change.

1.4.9 Images of Text (No Exception) Adopt Not met

Two of our articles show a whole information table as a picture rather than real text. We are committed to rebuilding both as text you can select, resize and have read aloud.

Operable

WCAG 2.2 level AAA Operable success criteria, with what Assembly decided and why.
Criterion Our decision Where it stands Why
2.1.3 Keyboard (No Exception) Adopt Partly met, and a test holds what's built

Every control in our product already works from a keyboard alone, with nothing that needs a drag or a mouse-only gesture, so meeting this in full costs us nothing extra, and we now block any change that would reintroduce one.

2.2.3 No Timing Adopt Not met

The app itself has no time limit anywhere. Our website's waitlist form has a hidden minimum submit time that can silently drop a genuine, fast signup behind a success message, and we are fixing that.

2.2.4 Interruptions Adopt the cheap part Partly met

We already avoid pop-ups and anything that interrupts you unasked. The one gap is a demo animation in the app's welcome tour that loops with no pause control, which is worth a small fix rather than a whole project.

2.2.5 Re-authenticating Adopt Partly met

If your sign-in link expires, we already send you back to exactly where you were with your saved answers untouched. What we have not confirmed is what happens to anything you were mid-way through typing at that exact moment.

2.2.6 Timeouts Adopt Partly met, and a test holds what's built

Your answers are saved as you go, one question at a time, and your session lasts well over a day even if you walk away, and we now test that the session length can never quietly shrink.

2.3.2 Three Flashes Adopt Met, and a test holds it

Nothing on our site or in our app flashes fast enough to be a seizure risk, a real concern for children more than adults, and we check every animation stays well under that speed.

2.3.3 Animation from Interactions Adopt Partly met

The app already respects your device's reduce-motion setting everywhere. Our marketing site only does it in some places today, and we are closing that gap so it applies everywhere by default.

2.4.8 Location Adopt Partly met

Most of our product already tells you where you are, with breadcrumbs and a progress bar. The app's own menu does not yet mark your current page, and children's profile tabs need a clearer label than colour alone.

2.4.9 Link Purpose (Link Only) Adopt Partly met

Our printed report already names every link clearly on its own. On screen, a repeated 'Read the full definition' link does not say which term it is about, and we are fixing that.

2.4.10 Section Headings Adopt Partly met

A parent or a school reading the report by its headings hits a few tabs where we deliberately hid the section heading. We are putting those headings back.

2.4.12 Focus Not Obscured (Enhanced) Adopt the cheap part Not met

We are fixing the underlying problem, a fixed header or footer sometimes covering the very thing you have just tabbed to, because it is worth doing on its own. We will only claim the stricter, enhanced version once something actually measures that nothing is ever covered.

2.4.13 Focus Appearance Adopt Not met

Our default keyboard focus ring is already the right size and colour everywhere in both apps, and we removed the handful of thinner overrides that fell short of it, then added a check that blocks any new one shipping.

2.5.5 Target Size (Enhanced) Adopt Not met

The tap targets on our 88-question profiler are smaller than we would like today. Getting this right matters more to us than to most products, because motor skills are one of the very things our profiler assesses.

2.5.6 Concurrent Input Mechanisms Adopt Met, and a test holds it

Nothing in our product works with touch only, or with a mouse only, and we block any change that would restrict how you interact with it.

Understandable

WCAG 2.2 level AAA Understandable success criteria, with what Assembly decided and why.
Criterion Our decision Where it stands Why
3.1.3 Unusual Words Adopt Partly met, and a test holds what's built

This is the criterion our whole product exists to satisfy: a plain-English glossary for clinical and school language, tapped open wherever it appears. The mechanism is built and protected from regressing; we have barely started filling it with real terms rather than our own product words.

3.1.4 Abbreviations Adopt Not met

Initials like ADHD appear throughout our content with nothing yet spelling them out on first use. Our glossary already has a field ready to hold the full name; nothing has been written into it yet.

3.1.5 Reading Level Adopt Partly met, and a test holds what's built

The wording inside the product is measured against the reading age we aim at, and a change that pushes a screen's text over it fails before it can ship. Each long-form article on the public site is measured against its own, deliberately older target, and fails if it drifts from the figure we publish or reads too old. Both checks are narrow on purpose, and PARTIAL is the honest status because neither reaches the two things that matter most: the finished pages as a parent actually sees them, and the report, which is written fresh for each child and so needs a different kind of check. We are not promising a separate easy-read version of the whole site, which we could not keep maintained.

3.1.6 Pronunciation Decline Not assessed

We have not found any word in our content whose pronunciation is genuinely ambiguous, so there is nothing yet for this criterion to apply to. We have not done the content review that would settle it either way.

3.2.5 Change on Request Adopt the cheap part Not met

The profiler moves you to the next question automatically, which is what makes 88 questions manageable. A way to switch that off already exists for anyone revisiting an earlier answer; we just need to offer it as a real setting rather than a side effect.

3.2.6 Consistent Help Adopt Partly met

Where help exists, it sits in the same place every time. A handful of screens, including the safety questions, offer no help at all today, and that is the gap we are closing.

3.3.5 Help Adopt Partly met

Help is genuinely good where we have built it: real guidance first, before anything automated. It is missing on exactly the most frightening steps, the screener and the safety questions, which is where it matters most.

3.3.6 Error Prevention (All) Adopt Partly met

You can already go back and change an answer during a grace window before it is final. A few specific slips, like accidentally creating a duplicate child record, still cannot be undone without emailing us, and we are closing those one by one.

3.3.9 Accessible Authentication (Enhanced) Adopt Met, and a test holds it

You sign in with a link we email you, or, if you would rather type something, a short code we email instead. Neither asks you to remember or work anything out: the code is one field you can paste into or let your device fill, and there is still no password and no CAPTCHA anywhere in the product. We test that none of those can quietly sneak back in.

Where this page comes from

Every status, count, date and disclosed failure above is read from a single record kept in our source code, next to the tests it cites. An automated check fails if that record and the codebase disagree, so the statement cannot drift away from the product the way a hand written one does. The live contrast working behind our colour decisions sits on the accessibility page.