Skip to content

WCAG 2.2 AA Accessibility Audit -- April 2026

Auditor: Nexus SecOps quality work, session s44 (Revolution 2 adaptive-learning sub-track 6/13) Scope: 6 standalone HTML tools shipped from docs/tools/ between sessions s46 and s49 Standard: WCAG 2.2 Level AA, plus the three new 2.2 success criteria (2.4.11, 2.5.7, 2.5.8) Audit type: Static source review (HTML/CSS/JS) plus contrast computation. Not a screen-reader test.

Executive summary

The Nexus SecOps tool surface ships with a structurally sound foundation but a thin accessibility layer. The dark-first palette is well chosen for contrast: the dominant body/text combinations (#e0e0e0 on #0f0f23 body, #cbd5e0 on #0a0a1a inputs, #64ffda accent on dark surfaces) all clear WCAG AAA at 12:1+. Buttons are real <button> elements (not <div onclick>), form inputs are styled by selector rather than synthesized from divs, and most outputs use semantic HTML. The voice-rewrite era's discipline carries over to markup choices: tools are quick to read, quick to keyboard-tab through, and free of the worst anti-patterns (placeholder-as-label, mouse-only drag, fixed-pixel font sizing).

Where they fall short is assistive-technology semantics and dynamic-content announcement. Across all six tools audited, none of the multi-tab UIs use role="tablist" / role="tab" / role="tabpanel" / aria-selected / aria-controls. None of the regions that update results in place (hash output, HMAC output, scan verdict, FSRS card, mastery counts, cost breakdown) carry aria-live. Several form inputs (the incident-cost-calculator checkboxes wrap an <input> next to a <label for> inside a clickable .icc-checkbox-item div, but the click handler lives on the input only) work with a screen reader by accident rather than by design. Decorative glyphs (, , , , , &mdash;) leak into the accessible name of buttons because nothing is hidden with aria-hidden="true". The result for a sighted keyboard user is fine; the result for a NVDA/VoiceOver user is choppy and verbose.

Top three systemic issues, in remediation order:

  1. Tab UIs lack ARIA. Six of the six audited tools use class-only tabs. A screen-reader user does not know they are tabs, cannot use arrow-key navigation between them, and has to Tab through every panel's contents to discover the next tab control. Fix is mechanical: add role="tablist" / role="tab" / aria-selected / aria-controls / role="tabpanel" plus arrow-key handling. One shared helper covers all six.
  2. Result regions are not live. Hash output, HMAC output, scan verdict, FSRS card transitions, mastery progress KPIs, and cost-breakdown values all update silently. Wrap each result container with aria-live="polite" (or aria-live="assertive" for error toasts). The incident-cost-calculator already does this on #icc-report-output; carry the pattern forward.
  3. Focus indicators rely on browser default. The dark theme overrides input borders on :focus (sets border-color:#64ffda) but does not set a CSS outline. Some browsers suppress the default outline once border-color changes; the result is an unreliable focus halo on a dark background. Standardize a :focus-visible { outline: 2px solid #64ffda; outline-offset: 2px; } shared rule.

If this trio is fixed, the surface jumps from "WCAG AA with caveats" to "WCAG AA conformant for sighted keyboard users and substantially conformant for screen-reader users." A second wave (target-size sweep, gradient-button text contrast, did-web danger button) closes the remaining technical gaps.

Methodology

  • Static review of the six HTML files listed in scope. No runtime test, no automated tool (axe-core, Lighthouse, Pa11y).
  • Contrast computation: WCAG-formula sRGB-to-linear conversion, then (L_lighter + 0.05) / (L_darker + 0.05). Ratios cited in this report were computed; they are not approximations.
  • Criteria checked: 1.3.1 (Info and relationships), 1.4.3 (Contrast minimum), 1.4.10 (Reflow), 1.4.11 (Non-text contrast), 2.1.1 (Keyboard), 2.1.2 (No keyboard trap), 2.4.3 (Focus order), 2.4.7 (Focus visible), 2.4.11 (Focus not obscured -- new in 2.2), 2.5.5 (Target size enhanced -- AAA, noted), 2.5.7 (Dragging -- new in 2.2), 2.5.8 (Target size minimum -- new in 2.2), 3.3.1 (Error identification), 3.3.2 (Labels), 4.1.2 (Name role value).
  • Sampling caveat: this is a 6-tool sample of a ~30-tool surface. Patterns observed will likely repeat in unaudited tools because the boilerplate (header, tab CSS, copy buttons, output boxes) is copied between files. Findings are generalizable; specific code citations are not.

Severity rubric

  • CRITICAL -- blocks users with assistive tech from completing the core function the tool advertises.
  • HIGH -- significantly degrades AT user experience, causes confusion or extra effort even with workarounds.
  • MEDIUM -- a clear technical non-conformance that does not block the core task but should be fixed.
  • LOW -- best-practice gap or minor friction; does not block AA conformance.

Findings by tool

1. crypto-toolkit.html (s46) -- 458 lines

Tool intent: Browser-side cryptography (hash, HMAC, AES-GCM, RSA-OAEP, encoding, secure random) using Web Crypto API. Six tabs.

Color contrast

  • #64ffda accent on #16213e card background: 12.76:1 -- PASS AAA.
  • #e0e0e0 body text on #16213e card: 12.04:1 -- PASS AAA.
  • #a78bfa violet on #16213e card (used for .output-row .label, <h3>): 5.84:1 -- PASS AA.
  • #8892b0 muted (used for <label> and .subtitle) on #16213e: 5.14:1 -- PASS AA.
  • #cbd5e0 output-box text on #0a0a1a input/output background: 13.19:1 -- PASS AAA.
  • #64ffda copy-button text on #1e3a5f button background: 9.23:1 -- PASS AAA.
  • #fcd34d warning text on #78350f (compositing alpha to opaque): 6.29:1 -- PASS AA.
  • #fca5a5 error text on #7f1d1d: 5.28:1 -- PASS AA.
  • FAIL (gradient): button.secondary uses linear-gradient(135deg,#a78bfa,#7c3aed) with white text. White on #a78bfa (the lighter end of the gradient) is 2.72:1, below the 4.5:1 minimum for normal text. Where the text physically sits on the gradient depends on button width; on narrow buttons text is over the lighter half. The right end (#7c3aed) gives 5.70:1 which passes. Severity: MEDIUM -- mitigate by darkening the gradient start to e.g. #8b5cf6 (would push the start to ~3.5:1 with white) or by switching the text to #0a0a1a (which gives 7.96:1 on #a78bfa). Affects every button.secondary in the toolkit (RSA decrypt, encoding decode buttons, random preset buttons).

Keyboard navigation

  • Tab order is natural DOM order; no positive tabindex values. PASS 2.4.3.
  • All interactive elements (<button>, <input>, <textarea>, <select>) are reachable via Tab. PASS 2.1.1.
  • Tabs are real <button class="tab"> -- activate on Enter and Space. PASS 2.1.1.
  • No keyboard traps observed; textareas have native tab-out. PASS 2.1.2.
  • Focus visibility: inputs override outline:none (line 29) and rely on border-color:#64ffda on :focus. Buttons have no focus styling at all -- they will use whatever the browser default is, which on Chrome/Edge is a thin auto outline that can be invisible against the dark gradient buttons. Severity: HIGH for buttons, MEDIUM for inputs. Fix: add :focus-visible{outline:2px solid #64ffda;outline-offset:2px} rule.

ARIA + semantics

  • Tabs (lines 57-64) use <button class="tab" data-tab="hash"> with no role="tab", no aria-selected, no aria-controls, no parent role="tablist". Tab panels (.tab-content) lack role="tabpanel" and aria-labelledby. Severity: HIGH -- screen reader announces these as plain buttons; the relationship between a tab and its panel is invisible to AT.
  • <label> elements (e.g. line 70, 72) are not associated with their inputs via for/id. They are visually adjacent and the input follows immediately, but a screen reader will not announce the label when focusing the input. Severity: HIGH -- breaks 1.3.1 and 3.3.2. Fix: add for="hash-input" to each label and ensure the input has matching id.
  • Output boxes (<div class="output-box" id="hash-hex">) update via .textContent = after each click. They have no aria-live -- AT users do not hear results. Severity: HIGH -- breaks the core function for blind users. Fix: add aria-live="polite" to each output container, or wrap pairs of outputs (hex + base64) in a single aria-live region.
  • Status divs (#aes-status, #rsa-status) hold success/error feedback that updates on click. No aria-live. Severity: HIGH.
  • The glyph in .back-link (line 50) and the <select> option labels with em dash separators are part of the accessible name; this is fine but worth noting that the is read aloud as "left-pointing arrow" by some screen readers.

Forms

  • Required fields are not marked. The Hash tab's "Input text" is functionally required but has no required attribute and no visual asterisk. The JS silently returns on empty input (line 299) instead of telling the user. Severity: MEDIUM -- breaks 3.3.1 (errors not identified). Fix: either add inline validation messaging on click ("Enter text to hash") into a live region, or add required and let native browser validation fire.
  • No placeholder-as-label anti-pattern observed -- every input has a real <label> above it. PASS.
  • Validation errors for AES (line 341, 343) throw to the status div with an error class. Once aria-live is added to #aes-status (see above) this becomes conformant. Currently silent for AT.

Mobile / responsive

  • Single media query at 760px (line 41) collapses the .row-2 grid. Tabs are flex-wrap:wrap so they reflow. PASS 1.4.10 reflow at 320px.
  • Touch target sizes (WCAG 2.5.8 minimum 24x24 CSS px): .tab is padding:10px 18px plus font-size:0.86rem -- height ~38px, width depends on label. PASS. Main buttons are padding:10px 20px -- ~38px height. PASS. button.copy is padding:5px 11px font-size:0.78rem -- height ~24-26px. Marginal PASS / borderline FAIL depending on browser font-rendering. Severity: MEDIUM -- bump copy buttons to padding:6px 12px to add safety margin.

WCAG 2.2 new criteria

  • 2.4.11 Focus Not Obscured: header is position:relative not sticky, so no obscuration risk. PASS.
  • 2.5.7 Dragging Movements: no drag interactions in this tool. PASS.
  • 2.5.8 Target Size: copy buttons borderline (above). MEDIUM.

Tool 1 severity tally: HIGH 4 (focus indicators on buttons, label associations, tab ARIA, live regions), MEDIUM 3 (gradient button text, copy-button target size, error identification), LOW 1 (decorative glyph reads).


2. pattern-hunter.html (s48) -- 545 lines

Tool intent: YARA-style multi-pattern matcher. Two large textareas (rule + input), example pills, scan button, results card, syntax help table.

Color contrast

  • .card h2 #64ffda on #16213e: 12.76:1 -- PASS AAA.
  • .match-row .id #a78bfa on #0f1a2e: 6.39:1 -- PASS AA.
  • .match-row .preview #cbd5e0 on #0f1a2e: 11.70:1 -- PASS AAA.
  • .match-row .offset #8892b0 on #0f1a2e: 5.62:1 -- PASS AA.
  • .verdict.match #fca5a5 on #7f1d1d: 5.28:1 -- PASS AA. (Note: the match verdict uses red, the no-match uses green -- this is semantically inverted from what most users expect for "match found". Not a contrast issue but a UX issue worth flagging.)
  • .verdict.nomatch #86efac on #14532d: 6.49:1 -- PASS AA.
  • .example-pill #64ffda on #1e3a5f: 9.23:1 -- PASS AAA.
  • .syntax-help code #64ffda on #0a0a1a: 15.74:1 -- PASS AAA.

Keyboard navigation

  • Same focus-indicator gap as crypto-toolkit (no :focus-visible outline on buttons). HIGH.
  • The .example-pill elements (lines 91-94) are <span> with cursor:pointer and a click handler (line 232). They are not keyboard-accessible. <span> is not focusable, has no Enter/Space handler, has no role="button", and no tabindex="0". Severity: CRITICAL -- breaks 2.1.1 entirely; a keyboard-only user cannot load the example rules. Fix: convert <span class="example-pill"> to <button class="example-pill"> (and reset button browser styling), or add role="button" tabindex="0" plus a keydown handler for Enter/Space.
  • Textareas are min-height:280px and resize:vertical -- can be tabbed in and tabbed out. PASS 2.1.2.
  • The Tab key inside the textarea inserts a tab character (default browser behavior; CSS tab-size:2 applied). This means Tab does NOT move focus out of the textarea by the first press in browsers that capture Tab for textareas -- actually, browsers do not do that; default textarea behavior moves focus. PASS.

ARIA + semantics

  • The result card (#output, line 112) is built dynamically via out.innerHTML = ... after the scan. No aria-live -- AT users get no announcement that the scan completed or what the verdict was. Severity: HIGH -- breaks core function. Fix: <div id="output" aria-live="polite" aria-atomic="true">.
  • The <table> in .syntax-help (line 117) lacks a <caption>. Has proper <th> row. PASS for the most part; adding <caption>Supported YARA-subset syntax</caption> would help.
  • The example-pills lack a programmatic group label; they are just adjacent spans. Wrap in <div role="group" aria-label="Load example"> to clarify.
  • Decorative glyph &larr; in back-link is fine for AT; but the verdict text (✓ MATCH line 510) uses non-standard glyphs whose announcement varies by AT.
  • The <textarea id="rule"> and <textarea id="input"> have no <label> element AT ALL -- the visible heading is <h2>Rule</h2> inside a .card (line 65-66). The <h2> is not programmatically associated with the textarea. Severity: HIGH -- breaks 1.3.1, 3.3.2. Fix: add <label for="rule">Rule</label> (visually hidden) or aria-labelledby="rule-h" referencing the <h2 id="rule-h">.

Forms

  • The <select id="mode"> has a <label>Input mode</label> immediately above (line 82-83) but they are not linked via for/id. Same fix as above. Severity: MEDIUM.
  • Empty/invalid rule produces an error card (line 495); not announced to AT until the live region is added.

Mobile / responsive

  • .split collapses at 980px (line 29). At 320px the textareas become full-width and stack. The min-height:280px on each textarea means a phone user has ~580px of textarea before they reach the Scan button. Acceptable. PASS 1.4.10.
  • .example-pill padding:5px 12px font-size:0.75rem -- height ~24px. Borderline FAIL 2.5.8 (24x24 minimum). MEDIUM.

WCAG 2.2 new criteria

  • 2.4.11: no sticky header. PASS.
  • 2.5.7: no drag interactions. PASS.
  • 2.5.8: example-pill height ~24px is at the floor; bump padding to 6-7px vertical for safety. MEDIUM.

Tool 2 severity tally: CRITICAL 1 (example pills not keyboard-reachable), HIGH 3 (focus indicator, textarea labels, output live region), MEDIUM 3 (select label association, pill target size, gradient button text inherited from same theme).


3. daily-practice.html (s49) -- 500 lines

Tool intent: FSRS-5 spaced-repetition flashcards. Four tabs (Practice, Stats, History, Settings). Card flow: question -> reveal answer -> rate Again/Hard/Good/Easy.

Color contrast

  • Flashcard background linear-gradient(135deg,#16213e 0%,#1a2640 100%) -- mid value #1a2640. Text #e0e0e0: ~11:1 -- PASS AAA.
  • .flashcard .question #e0e0e0 on #1a2640: PASS AAA.
  • .flashcard .topic #a78bfa on #1a2640: 5.53:1 -- PASS AA.
  • .flashcard .answer #cbd5e0 on #0a0a1a: 13.19:1 -- PASS AAA.
  • .rating-btn #cbd5e0 on #0f1a2e: 11.70:1 -- PASS AAA.
  • .rating-btn[data-rating="1"]:hover #fca5a5 on #7f1d1d (alpha-blended; close enough to opaque): ~5.3:1 -- PASS AA.
  • .kpi-box .v #64ffda on #0f1a2e: PASS AAA.

Keyboard navigation -- POSITIVE EXAMPLE

  • Lines 433-444 implement explicit keyboard shortcuts: Space reveals the answer, 1/2/3/4 rate the card. The handler correctly checks e.target.tagName to avoid hijacking keystrokes inside form inputs. This is the model other tools should follow. PASS 2.1.1, exceeds.
  • Show-answer button has visible "(space)" hint text (line 306), giving sighted keyboard users the discoverability without hunting in docs. PASS 2.4.6.
  • Same focus-indicator caveat as the others -- no :focus-visible outline on the rating buttons. The rating buttons are large (padding 14px 10px) and have a coloured border-color that already varies by rating, but on :focus the border color does not change visibly. Severity: HIGH.
  • Tabs (lines 84-87) are <button class="tab"> -- keyboard-activatable. PASS 2.1.1.

ARIA + semantics

  • Tab UI: same pattern as crypto-toolkit -- no role="tablist", role="tab", aria-selected, aria-controls. Severity: HIGH.
  • The flashcard region (#flashcard-container, line 99) updates dramatically (question -> answer -> different card) without aria-live. A screen-reader user reveals the answer with Space and hears nothing -- they have to manually navigate back to the answer block. Severity: HIGH -- breaks the core flashcard loop for blind users. Fix: <div id="flashcard-container" aria-live="polite" aria-atomic="true">.
  • The KPI boxes (#queue-kpis) update silently after each rating. Less critical (the user can re-navigate), but aria-live="polite" would be polite. Severity: LOW.
  • The Settings tab's <label> elements wrap the input (line 124-127) -- this is the implicit-label pattern, which works for AT. PASS.
  • The <input type="checkbox" id="setting-show-topic"> (line 134) is wrapped by <label> -- PASS.
  • The <input type="file" id="import-file"> is hidden with style="display:none" and triggered by the Import button (line 455). Display-none means it is NOT in the accessible tree -- a screen-reader user clicking Import will trigger the file picker correctly via the JS path. PASS.

Forms

  • The reset confirm uses confirm() (line 475). confirm() is announced by AT and accepts keyboard. PASS but old-school -- a custom dialog with role="dialog" would be more polished, not required for AA.
  • Import errors land in #data-status (line 469) without aria-live. MEDIUM.

Mobile / responsive

  • KPI grid is repeat(auto-fit,minmax(120px,1fr)) -- collapses gracefully on narrow screens. PASS 1.4.10.
  • Rating buttons in a 4-column grid (grid-template-columns:repeat(4,1fr)) at any width. On 320px each button is ~70px wide -- clickable but cramped. PASS 2.5.8 (much larger than 24x24).

WCAG 2.2 new criteria

  • 2.4.11: no sticky header. PASS.
  • 2.5.7: no drag. PASS.
  • 2.5.8: all interactive elements above 24x24. PASS.

Tool 3 severity tally: HIGH 3 (focus indicator, tab ARIA, flashcard live region), MEDIUM 1 (data-status not live), LOW 1 (kpi update silent). POSITIVE: keyboard shortcuts implemented properly.


4. skill-mastery-map.html (s48) -- 420 lines

Tool intent: Mastery tracker over the 543-concept knowledge graph. Five tabs (Overview, Domains, Next, All, Data). List interaction (click concept to toggle mastered).

Color contrast

  • .group-card .name #a78bfa on #0f1a2e: 6.39:1 -- PASS AA.
  • .concept-item .label #cbd5e0 on #0f1a2e: 11.70:1 -- PASS AAA.
  • .concept-item.mastered background is #14532d22 (alpha) over #0f1a2e ~= effective #152436-ish dark. Text #cbd5e0 on that: ~10:1 -- PASS AAA.
  • .concept-item .check #86efac (mastered): >10:1 PASS AAA.
  • .concept-item .check #2a3a5f (unmastered, line 44) on #0f1a2e: ~1.4:1 -- this is decorative (it's the empty circle indicator) but it is visually invisible against background. Severity: LOW -- the check icon's "off" state is barely perceptible, which is intentional dim-state design but should be paired with the text label that says the state. Currently the only state indicator is the icon. Suggest adding aria-label="not mastered" / aria-label="mastered" to the row, or visible text.
  • .kpi-box .v #64ffda on #0f1a2e: PASS AAA.

Keyboard navigation

  • CRITICAL: .concept-item (line 40) is a <div> with cursor:pointer and a click handler (line 247). It is not focusable, has no role="button", no tabindex, no Enter/Space handler. Hundreds of concept items in this tool are not keyboard-accessible. A keyboard-only user cannot mark anything mastered. Severity: CRITICAL -- this is the worst single finding in the audit because the core function of the tool is "click a concept to mark mastered". Fix: convert <div class="concept-item"> to <button class="concept-item" type="button"> (and override default button styles to keep the grid layout), or add role="button" tabindex="0" plus a keydown handler for Enter/Space. Same for .group-card (line 34).
  • Same focus-indicator gap on real buttons. HIGH.
  • Tab key flows through the filter inputs (group select, name search, unmastered checkbox) in DOM order. PASS 2.4.3.

ARIA + semantics

  • Tab UI: same lack of ARIA as the others. HIGH.
  • The .kpi-row numeric KPIs update on every mastery toggle without aria-live. MEDIUM (the user just clicked, so they expect the change; not as critical as Daily Practice).
  • The "Next to Study" list (#next-list) and "All Concepts" list (#all-list) update silently after filter changes. MEDIUM.
  • The <a href="..." onclick="event.stopPropagation()">open →</a> (line 241) inside the concept item works for keyboard users only if the parent .concept-item is also focusable -- otherwise the inner anchor is reachable but the parent's mastery toggle is not. Currently the anchor IS reachable (anchors are natively focusable). So a keyboard user can navigate to "open →" but cannot toggle mastery. Confusing.
  • The and / glyphs leak into the accessible name. aria-hidden="true" on the <span class="check"> would clean this up.

Forms

  • Filter <input type="text" id="filter-name"> has a <label>Search by name</label> (line 138-139) NOT linked via for/id. Same systemic issue. HIGH.
  • The unmastered-only checkbox uses an implicit <label> wrapper (line 143) -- works for AT.
  • Confirm dialog for reset (line 400) -- same as Daily Practice, confirm() is fine.

Mobile / responsive

  • .row-2 collapses at 760px (line 60). Concept items have a 4-column grid that may overflow on 320px (grid-template-columns:auto 1fr auto auto); the second column is 1fr so it shrinks but the badge column may push out. Acceptable.
  • Group cards are minmax(280px,1fr) -- on 320px each card is 280-296px and stacks one per row. PASS 1.4.10.
  • Concept items have generous padding (10px 14px) -- target size PASS.

WCAG 2.2 new criteria

  • 2.4.11: no sticky header. PASS.
  • 2.5.7: no drag. PASS.
  • 2.5.8: all targets pass.

Tool 4 severity tally: CRITICAL 1 (concept items not keyboard-reachable), HIGH 3 (group cards same issue, filter labels, tab ARIA), MEDIUM 2 (live regions on lists, focus on real buttons), LOW 1 (off-state icon contrast).


5. did-web-vc-demo.html (s48) -- 845 lines

Tool intent: W3C did:web + Verifiable Credentials 2.0 demo. Five tabs (Intro, DID Doc, Issue, Verify, Portfolio). Multi-step flow with cross-tab state (key generated in tab 2 used in tab 3).

Color contrast

  • .card p #cbd5e0 on #16213e: 10.70:1 -- PASS AAA.
  • .card h3 #a78bfa on #16213e: 5.84:1 -- PASS AA.
  • .compare th #64ffda on #0a0a1a: 15.74:1 -- PASS AAA.
  • .kv-table td:first-child #a78bfa on the inherited card bg: 5.84:1 -- PASS AA.
  • .pill.ok #86efac on #14532d: 6.49:1 -- PASS AA.
  • .pill.bad #fca5a5 on #7f1d1d: 5.28:1 -- PASS AA.
  • FAIL (gradient): button.danger is linear-gradient(135deg,#ef4444,#b91c1c) with white text. White on #ef4444: 3.76:1 -- FAILS 4.5:1 normal-text minimum. White on #b91c1c: 6.47:1 PASS. Severity: MEDIUM -- text sits over the gradient; on short buttons most text is over the lighter end. Same structural issue as button.secondary in crypto-toolkit. Fix: change start of gradient to #dc2626 (~4.6:1) or remove gradient and use solid #b91c1c.
  • FAIL (gradient): button.secondary linear-gradient(135deg,#a78bfa,#7c3aed) with white -- same 2.72:1 issue. MEDIUM.

Keyboard navigation

  • Tab UI buttons (lines 84-89) are real <button> -- keyboard-activatable. PASS 2.1.1.
  • target="_blank" rel="noopener" links (e.g. line 95-96) are correctly marked. PASS.
  • The remove-claim button <button class="danger">✕</button> (line 569) is keyboard-reachable and has the visible glyph as accessible name -- which gets read aloud as "x" or "multiplication sign" by AT. Severity: MEDIUM -- replace with <button aria-label="Remove claim">×</button> (or use a visible word).
  • Same focus-indicator caveat. HIGH.

ARIA + semantics

  • Tab UI: no ARIA tab pattern. HIGH -- this tool is hardest hit by the missing ARIA because the workflow REQUIRES jumping between tabs (generate key in tab 2, then issue in tab 3, then verify in tab 4). A screen-reader user has no programmatic awareness of the multi-tab structure.
  • Output boxes (#dd-doc, #dd-jwk, #iss-signed, #ver-result) update via .textContent or .innerHTML with no aria-live. HIGH -- the verifier outcome (VALID / INVALID, line 750-757) is the entire point of this tab and is not announced.
  • <table class="kv-table" id="ver-table"> has rows added via JS without a <caption> or <thead>. MEDIUM -- add <caption>Credential inspection details</caption> and a <thead> row.
  • The iss-claims list (line 238) has dynamically-added rows. The remove button has no programmatic association with the row it removes.
  • <input type="date"> (lines 232, 234) is keyboard-accessible by default but the date-picker UI varies wildly by browser; this is a known browser shortcoming, not a code issue.
  • <a href="..." target="_blank" rel="noopener"> -- some screen readers do not announce that links open in a new tab. Adding aria-label="W3C DID Core 1.0 (opens in new tab)" is a small improvement; LOW.

Forms

  • The "Generate Keypair" button changes its own text to "Generating..." and back (line 486, 507). This is fine visually but invisible to AT after the first read. Adding aria-busy="true" while generating, plus a status announcement to a live region, would announce progress.
  • Claim rows: the <input type="text"> has placeholder 'claim key' and 'claim value' (line 567-568) but no <label>. Severity: MEDIUM -- placeholder-as-label anti-pattern. Each dynamically-added input should get an aria-label="Claim key" / aria-label="Claim value".
  • Date inputs: <label>Valid from</label> (line 232) is not linked via for/id. HIGH (systemic).
  • "Credential subject DID" input has value="did:web:learner.example.com" as a default. The label is above; not linked. HIGH (systemic).

Mobile / responsive

  • .row-2 collapses at 760px. The .tabs container has overflow-x:auto (line 19) which means at 320px the tabs become horizontally scrollable. PASS for reflow (no content lost), but mobile users have to swipe to find later tabs (Verify, Portfolio). Adding visible scroll affordance (gradient fade on right edge) would be friendlier. LOW.

WCAG 2.2 new criteria

  • 2.4.11: header is position:relative not sticky. PASS.
  • 2.5.7: no drag. PASS.
  • 2.5.8: copy buttons have padding:5px 11px again -- borderline 24x24. The remove-claim button has no explicit padding override -- inherits button base of padding:10px 20px initially, then JS overrides to padding:6px 10px font-size:0.78rem (line 569). Height ~24-26px. Borderline. MEDIUM.

Tool 5 severity tally: HIGH 4 (tab ARIA, output live regions, label associations across all forms, focus indicator), MEDIUM 4 (gradient buttons text, claim inputs placeholder-as-label, kv-table caption, target-size of small buttons), LOW 2 (link new-tab announcement, mobile tab scrolling affordance).


6. incident-cost-calculator.html (s49 extracted) -- 1930 lines

Tool intent: Five-tab calculator (Org Profile, Incident Params, Cost Breakdown, Benchmarking, Report Export) with sliders, checkboxes, dropdowns, two <canvas> charts.

Color contrast

  • Tab idle #8892b0 on #16213e: 5.14:1 -- PASS AA.
  • Tab active #64ffda on #0f0f23: 15.15:1 -- PASS AAA.
  • Form labels #64ffda on #1a1a2e (input bg): PASS AAA.
  • Input text #e0e0e0 on #1a1a2e: PASS AAA.
  • .icc-checkbox-item label #ccd6f6 on #1a1a2e: 11.48:1 -- PASS AAA.
  • .icc-cost-value #64ffda: PASS AAA.
  • .icc-confidence-label.high #ff6b6b on #0f0f23: 6.04:1 -- PASS AA.
  • .icc-confidence-label.mid #ffd93d on #0f0f23: 12.81:1 -- PASS AAA.
  • .icc-cost-line-label #8892b0 on #1a1a2e: 5.51:1 -- PASS AA.
  • .icc-fines-table td #b0b8d0 on #1a1a2e: 8.62:1 -- PASS AAA.
  • FAIL (small label, normal-text threshold): .icc-benchmark-scale span #6a7b9a on #1a1a2e: 3.99:1 -- FAILS 4.5:1 normal-text minimum (passes 3:1 UI minimum, so PASSES 1.4.11 but fails 1.4.3 because the scale labels are text). Severity: MEDIUM -- bump to #7a8aaa to clear (~4.6:1) or to #8892b0 to clear (5.51:1).
  • Same #6a7b9a is used in .icc-cost-sublabel (line 388) and .icc-per-record-label (line 645). All borderline. MEDIUM -- one CSS variable update fixes them all.
  • FAIL: tooltip #ccd6f6 on #16213e is fine (10.99:1), but tooltip is triggered by :hover only -- keyboard users cannot see it. Severity: HIGH -- breaks 1.4.13 (Content on Hover or Focus). Fix: also trigger on :focus, and the trigger element (<span class="icc-info" data-tip="...">?</span>) is not focusable. Convert to <button> with aria-describedby and a hidden tooltip element, OR add tabindex="0" and a :focus rule that mirrors :hover.
  • The chart <canvas> elements (#icc-cost-chart, #icc-trend-chart) are decorative-aside-from-the-text-summary -- the bar values are rendered as text in .icc-cost-line-value rows. PASS for non-text contrast (data also available in text). However the chart itself has no text alternative on the canvas; add <canvas role="img" aria-label="Cost distribution: direct $X, indirect $Y, recovery $Z, fines $W">. MEDIUM.

Keyboard navigation

  • Tab buttons (.icc-tab-btn, lines 771-775) are real <button> -- PASS 2.1.1.
  • All form inputs (<select>, <input type="number">, <input type="range">, <input type="checkbox">) are native and keyboard-accessible. PASS.
  • Slider keyboard support: <input type="range"> natively supports arrow keys / Page Up/Down / Home/End. PASS 2.1.1. The displayed value (#icc-employees-val, etc.) updates via oninput -- live update for sighted users. For AT users, however, the live value is in a separate <span> not associated with the slider. Severity: MEDIUM -- use <input type="range" aria-valuetext="5,000 employees"> and update aria-valuetext in JS, OR aria-describedby to point at the value span.
  • The <span class="icc-info" data-tip="...">?</span> tooltip triggers (lines 790, 803, 809, 827, 885, 896, 904, 912) are NOT focusable. Severity: HIGH -- breaks 2.1.1 for any user trying to learn what the ? icon means. Fix: convert to <button type="button" class="icc-info" aria-label="...">?</button> so it is naturally focusable and announces.
  • Tab navigation flow: tab buttons -> form inputs -> Next button. Logical. PASS 2.4.3.
  • :focus on form inputs sets border-color:#64ffda AND box-shadow:0 0 0 2px rgba(100, 255, 218, 0.15) (line 124) -- this is the only audited tool with a real focus halo. POSITIVE.
  • Buttons (.icc-btn) have no :focus rule -- relies on browser default. HIGH (systemic).

ARIA + semantics

  • Tab UI: same lack of role="tablist" etc. HIGH. Inline onclick (lines 771-775) instead of addEventListener -- works but a CSP that disallows inline handlers would break this. Project uses CSP per CLAUDE.md; verify the CSP allows unsafe-inline for handlers (checking later) -- if it does not, this tool may already be broken in production. CRITICAL contingent on CSP -- needs verification.
  • The "Total Estimated Incident Cost" output (#icc-total-cost) updates on every input change. No aria-live on the cost grid container. The report-output container (#icc-report-output, line 1189) DOES have aria-live="polite" -- so the team knows the pattern. POSITIVE on the report output, MEDIUM on the cost values -- carry the live region to #icc-panel-costs.
  • Checkbox grids (Regulatory Frameworks line 826-853, Compromised Data Types line 911-934) use <input type="checkbox" id="icc-reg-hipaa"> with <label for="icc-reg-hipaa">HIPAA</label> -- properly associated. POSITIVE.
  • The <canvas> elements lack alt text / aria-label.
  • Sliders lack aria-valuetext (above).
  • The tooltip-trigger ? icon is announced as "?" by AT -- meaningless.

Forms

  • Required fields not marked. Revenue is <input type="number" id="icc-revenue" value="50000000"> -- has a default so functionally not required. PASS.
  • Number input has min="0" step="1000000" -- HTML5 validation will fire for negatives. PASS 3.3.1 partially.
  • Checkboxes properly linked via for/id. PASS.
  • Selects properly inside <div class="icc-form-group"> with <label> -- not linked via for/id. HIGH (systemic).
  • The "Generate Report" / "Copy to Clipboard" buttons (line 1186-1187) -- the report region is aria-live="polite", so report generation announces. Copy success is silent. MEDIUM -- after iccCopyReport() add a status announcement.

Mobile / responsive

  • .icc-form-row and .icc-form-row-3 collapse at 768px (line 139). .icc-cost-grid collapses at 768px (line 348). PASS 1.4.10.
  • .icc-checkbox-grid is repeat(auto-fill,minmax(160px,1fr)) -- responds well. PASS.
  • The <canvas> charts use width="700" height="300" HTML attributes plus width:100%; max-height:350px CSS -- they scale down. PASS.
  • Touch targets: checkbox items have padding 8px 12px containing a 16x16 checkbox -- the whole row is the click target via the <label> -- effective height ~36px. PASS 2.5.8.
  • Sliders: thumb is width:18px height:18px (lines 180-181). FAILS 2.5.8 minimum 24x24 if measured strictly on the thumb. However WCAG 2.5.8 has an exception for "essential" target sizes and standard slider thumbs are widely understood to be exempt because the target is the entire slider track. PASS by exception but worth noting. To be safe, expand thumb to 24x24.

WCAG 2.2 new criteria

  • 2.4.11: no sticky header. PASS.
  • 2.5.7: sliders are pointer-and-keyboard, no drag-only. PASS.
  • 2.5.8: slider thumb borderline (above); checkbox-item ? info-icon is 16x16 (line 681) -- FAILS 24x24. MEDIUM.

Tool 6 severity tally: HIGH 4 (tab ARIA, focus on buttons, label associations, tooltip-icon not focusable), MEDIUM 6 (cost-grid live region, scale text contrast, slider aria-valuetext, canvas alt text, copy-success announcement, info-icon target size), LOW 1 (slider thumb size below 24x24 by spec but exempt). POSITIVE: input focus halo, checkbox label association, report aria-live.

Systemic findings (cross-tool patterns)

These appear in 4-or-more of the 6 tools and represent the highest-leverage fixes -- one fix sweeps the codebase.

S1. Tab UIs lack ARIA (6/6)

Every audited tool uses class-only tabs (<button class="tab" data-tab="..."> + <div class="tab-content">). None implement role="tablist" / role="tab" / role="tabpanel" / aria-selected="true" / aria-controls="..." / arrow-key navigation. Systemic HIGH. A single shared script (docs/javascripts/tabs.js) plus a one-line CSS rule could refactor all six. The pattern is documented in the WAI-ARIA Authoring Practices Guide -- tab pattern with manual activation is recommended.

S2. Result regions are not announced (6/6)

Every tool has at least one region whose content updates dynamically and is not wrapped in aria-live. Specific instances: hash output (crypto), HMAC output (crypto), AES status (crypto), RSA output (crypto), encoding output (crypto), random output (crypto), pattern hunter scan verdict (pattern), flashcard container (daily-practice), KPI grids (skill-mastery, daily-practice), DID-doc output (did-web), credential signed/unsigned (did-web), verify result (did-web), kv-table inspection (did-web), incident cost grid (icc), benchmark comparison (icc). Systemic HIGH. incident-cost-calculator already does aria-live="polite" on #icc-report-output -- carry this forward.

S3. Focus indicator gap on buttons (6/6)

Inputs override outline:none and rely on border-color:#64ffda for :focus. Buttons (the most-used interactive element) have no focus styling at all -- they inherit whatever the user-agent default outline is, which on dark gradient buttons is unreliable. Systemic HIGH. One CSS rule fixes everything:

button:focus-visible, [role="button"]:focus-visible, .tab:focus-visible {
  outline: 2px solid #64ffda;
  outline-offset: 2px;
}

S4. <label> not linked to input via for/id (5/6 -- daily-practice uses implicit-wrap correctly)

Pattern: <label>Field name</label><input id="field-x">. Visual association OK, programmatic association absent. Screen reader does not announce the label when focusing the input. Systemic HIGH. Mechanical fix -- add for= matching every id=. Roughly 40-60 instances across the audited tools.

S5. Decorative glyphs leak into accessible names (5/6)

Glyphs like (back link), (open chapter), / (mastery), ? (info), (remove), &mdash; (separator), and emoji ( in empty states) are part of the textContent and read aloud by screen readers as "left-pointing arrow", "check mark", etc. Systemic LOW. Wrap in <span aria-hidden="true"> or convert to CSS pseudo-elements.

S6. Gradient buttons fail contrast at the lighter end (3/6)

button.secondary (linear-gradient(135deg, #a78bfa, #7c3aed)) with white text gives 2.72:1 at the start of the gradient. This appears in crypto-toolkit, did-web-vc-demo, and likely every tool that uses the shared button styles. button.danger in did-web (linear-gradient(135deg, #ef4444, #b91c1c)) gives 3.76:1 at the start. Systemic MEDIUM. Two-token fix: change gradient start colors to darker shades, or remove gradient and use solid color.

S7. Small icon buttons borderline 24x24 (4/6)

button.copy is padding:5px 11px font-size:0.78rem -- height ~24-26px. .example-pill is padding:5px 12px font-size:0.75rem -- height ~24px. Info icon .icc-info is 16x16 (FAIL). Systemic MEDIUM. Bump vertical padding by 2-3px globally on these classes.

Phase 1 -- CRITICAL + HIGH (~1 week, ~6-8 hours)

  1. Convert non-button click targets to real buttons (skill-mastery .concept-item + .group-card, pattern-hunter .example-pill, incident-cost-calculator .icc-info). 30 minutes per tool, verify nothing breaks visually.
  2. Add :focus-visible outline rule to every button across all tools. One CSS edit per file (or one shared stylesheet). 15 minutes total.
  3. Wrap result containers in aria-live="polite" regions (the 15+ instances enumerated under S2). 1-2 hours.
  4. Link every <label> to its <input> via for/id. Mechanical sweep. 2-3 hours.
  5. Refactor tab UI to ARIA tab pattern -- shared docs/javascripts/tabs.js helper, then data-tablist wiring on each tool. 2-3 hours.

Phase 2 -- MEDIUM (~2 weeks, can parallel-track)

  1. Fix gradient button text contrast (S6). Either darken gradient starts or switch text color. 30 minutes.
  2. Bump small-button target sizes (S7). 30 minutes.
  3. Add aria-valuetext to every <input type="range"> (incident-cost-calculator). 1 hour.
  4. Add <canvas role="img" aria-label="..."> to chart canvases (incident-cost-calculator). 30 minutes.
  5. Convert info-icon ? from <span> to <button aria-label="..."> with :focus tooltip mirror. 1 hour.
  6. Replace placeholder-as-label on dynamic claim rows (did-web). Add aria-label. 30 minutes.
  7. Add <caption> to data tables (did-web kv-table, pattern-hunter syntax help). 15 minutes.
  8. Add explicit error-identification and required attributes to forms that have functionally-required inputs. 1-2 hours.

Phase 3 -- LOW + best-practice (~ongoing)

  1. aria-hidden="true" sweep on decorative glyphs (S5). 1-2 hours.
  2. Add aria-label="(opens in new tab)" to external links across the corpus. 30 minutes.
  3. Manual NVDA/VoiceOver pass on the three highest-traffic tools (separate task -- needs human tester).
  4. Lighthouse / axe-core CI check -- add to existing CI workflows once Actions quota resets (2026-05-01). Wire npx @axe-core/cli against the built site.
  5. Add a prefers-reduced-motion media query -- the .icc-tab-content uses a fadeIn animation (line 748), and the benchmark bar has transition: width 0.5s ease. Wrap in @media (prefers-reduced-motion: no-preference) { ... }.

Tools NOT yet audited

This audit covers 6 of approximately 30 standalone HTML tools in docs/tools/. The following are out of scope for this report and need separate audits before the surface can be claimed conformant:

Other standalone tools (~24 files): - ai-jailbreak-tester.html - attck-coverage-heatmap.html - detection-browser.html - detection-query-optimizer.html - incident-post-mortem-generator.html - ir-tabletop-generator.html - packet-parser.html - query-translator.html - red-team-report-generator.html - regex-tester.html - security-budget-calculator.html - security-metrics-dashboard.html - skill-portfolio.html - soar-playbook-designer.html - soc-digital-twin.html - threat-actor-playbook-generator.html - threat-model-canvas.html

Pattern-prediction: because they share boilerplate (header, tab CSS, copy buttons, label/input pairs), they will exhibit the same systemic findings (S1-S7). Specific findings will vary by tool functionality (e.g., soar-playbook-designer.html has drag-and-drop -- WCAG 2.5.7 risk; soc-digital-twin.html likely has a force-directed graph with drag interactions -- same).

Other surfaces: - 58 chapter markdown files (docs/chapters/ch01.md through ch58.md) -- rendered through MkDocs Material. Theme-handled mostly; needs a separate audit pass for code-block contrast in the rendered HTML. - 33 lab files (docs/labs/) -- markdown, mostly text content. - 47 microsim HTML files (docs/microsims/) -- standalone interactive simulations; same risk profile as the audited tools. - 108 scenario files (docs/scenarios/). - 248 purple-team exercise files. - 18 blog posts. - Knowledge-graph 3D visualization (if shipped) and any D3-based viz. - The MkDocs Material theme defaults (chapter chrome, search box, navigation drawer, footer) -- mostly handled by Material upstream but should be spot-checked.

What this audit does NOT cover

  • Screen-reader manual testing with NVDA, JAWS, VoiceOver, TalkBack. This is a static-source audit; actual AT behavior may surface issues this audit misses (and may also be more forgiving than this audit predicts in some cases).
  • Cognitive accessibility heuristics (clear language, consistent navigation, error tolerance, memory load). The voice-rewrite phase addressed clarity; cognitive load on the tool surface is its own audit.
  • Internationalization / localization considerations (text expansion, RTL languages, character set handling).
  • Performance accessibility -- low-bandwidth and assistive-tech-on-low-end-hardware behavior.
  • The 540+ chapter / lab / scenario / blog markdown files -- separate audit needed.
  • The MkDocs Material theme defaults -- separate audit; mostly handled by Material upstream.
  • Automated tool comparison -- this report does not run axe-core, Lighthouse, Pa11y, or WAVE; cross-checking against those would surface additional findings (and probably trigger some false positives).
  • CSP compliance verification -- the inline onclick="..." handlers in incident-cost-calculator.html (lines 771-775, 791, 804, 811, etc.) require script-src 'unsafe-inline' or per-handler hashes. The project's _headers file should be cross-checked.
  • Touch target precision testing -- pixel measurements are based on CSS values + font rendering; actual rendered pixel sizes vary by browser, zoom level, and font.

References


Audit date: 2026-04-23 Audit scope confirmed: 6 of approximately 30 standalone HTML tools in docs/tools/ Audit method: Static source review + WCAG-formula contrast computation; no runtime testing Next recommended action: Phase 1 remediation (CRITICAL + HIGH) -- estimated 6-8 hours, eliminates the two CRITICAL keyboard-trap findings and the four most-impactful systemic HIGH issues.