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 (✓, ○, ⚠, ←, →, —) 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:
- 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. - 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"(oraria-live="assertive"for error toasts). Theincident-cost-calculatoralready does this on#icc-report-output; carry the pattern forward. - Focus indicators rely on browser default. The dark theme overrides input borders on
:focus(setsborder-color:#64ffda) but does not set a CSSoutline. Some browsers suppress the default outline onceborder-colorchanges; 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¶
#64ffdaaccent on#16213ecard background: 12.76:1 -- PASS AAA.#e0e0e0body text on#16213ecard: 12.04:1 -- PASS AAA.#a78bfaviolet on#16213ecard (used for.output-row .label,<h3>): 5.84:1 -- PASS AA.#8892b0muted (used for<label>and.subtitle) on#16213e: 5.14:1 -- PASS AA.#cbd5e0output-box text on#0a0a1ainput/output background: 13.19:1 -- PASS AAA.#64ffdacopy-button text on#1e3a5fbutton background: 9.23:1 -- PASS AAA.#fcd34dwarning text on#78350f(compositing alpha to opaque): 6.29:1 -- PASS AA.#fca5a5error text on#7f1d1d: 5.28:1 -- PASS AA.- FAIL (gradient):
button.secondaryuseslinear-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 everybutton.secondaryin the toolkit (RSA decrypt, encoding decode buttons, random preset buttons).
Keyboard navigation¶
- Tab order is natural DOM order; no positive
tabindexvalues. 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 onborder-color:#64ffdaon: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 norole="tab", noaria-selected, noaria-controls, no parentrole="tablist". Tab panels (.tab-content) lackrole="tabpanel"andaria-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 viafor/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: addfor="hash-input"to each label and ensure the input has matchingid.- Output boxes (
<div class="output-box" id="hash-hex">) update via.textContent =after each click. They have noaria-live-- AT users do not hear results. Severity: HIGH -- breaks the core function for blind users. Fix: addaria-live="polite"to each output container, or wrap pairs of outputs (hex + base64) in a singlearia-liveregion. - Status divs (
#aes-status,#rsa-status) hold success/error feedback that updates on click. Noaria-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
requiredattribute and no visual asterisk. The JS silentlyreturns 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 addrequiredand 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
errorclass. Oncearia-liveis 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-2grid. Tabs areflex-wrap:wrapso they reflow. PASS 1.4.10 reflow at 320px. - Touch target sizes (WCAG 2.5.8 minimum 24x24 CSS px):
.tabispadding:10px 18pxplusfont-size:0.86rem-- height ~38px, width depends on label. PASS. Main buttons arepadding:10px 20px-- ~38px height. PASS.button.copyispadding:5px 11px font-size:0.78rem-- height ~24-26px. Marginal PASS / borderline FAIL depending on browser font-rendering. Severity: MEDIUM -- bump copy buttons topadding:6px 12pxto add safety margin.
WCAG 2.2 new criteria¶
- 2.4.11 Focus Not Obscured: header is
position:relativenotsticky, 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#64ffdaon#16213e: 12.76:1 -- PASS AAA..match-row .id#a78bfaon#0f1a2e: 6.39:1 -- PASS AA..match-row .preview#cbd5e0on#0f1a2e: 11.70:1 -- PASS AAA..match-row .offset#8892b0on#0f1a2e: 5.62:1 -- PASS AA..verdict.match#fca5a5on#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#86efacon#14532d: 6.49:1 -- PASS AA..example-pill#64ffdaon#1e3a5f: 9.23:1 -- PASS AAA..syntax-help code#64ffdaon#0a0a1a: 15.74:1 -- PASS AAA.
Keyboard navigation¶
- Same focus-indicator gap as crypto-toolkit (no
:focus-visibleoutline on buttons). HIGH. - The
.example-pillelements (lines 91-94) are<span>withcursor:pointerand a click handler (line 232). They are not keyboard-accessible.<span>is not focusable, has no Enter/Space handler, has norole="button", and notabindex="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 addrole="button" tabindex="0"plus a keydown handler for Enter/Space. - Textareas are
min-height:280pxandresize: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:2applied). 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 viaout.innerHTML = ...after the scan. Noaria-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
←in back-link is fine for AT; but the verdict text (✓ MATCHline 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) oraria-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 viafor/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¶
.splitcollapses at 980px (line 29). At 320px the textareas become full-width and stack. Themin-height:280pxon each textarea means a phone user has ~580px of textarea before they reach the Scan button. Acceptable. PASS 1.4.10..example-pillpadding: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#e0e0e0on#1a2640: PASS AAA..flashcard .topic#a78bfaon#1a2640: 5.53:1 -- PASS AA..flashcard .answer#cbd5e0on#0a0a1a: 13.19:1 -- PASS AAA..rating-btn#cbd5e0on#0f1a2e: 11.70:1 -- PASS AAA..rating-btn[data-rating="1"]:hover#fca5a5on#7f1d1d(alpha-blended; close enough to opaque): ~5.3:1 -- PASS AA..kpi-box .v#64ffdaon#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.tagNameto 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-visibleoutline on the rating buttons. The rating buttons are large (padding 14px 10px) and have a colouredborder-colorthat already varies by rating, but on:focusthe 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) withoutaria-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), butaria-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 withstyle="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 withrole="dialog"would be more polished, not required for AA. - Import errors land in
#data-status(line 469) withoutaria-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#a78bfaon#0f1a2e: 6.39:1 -- PASS AA..concept-item .label#cbd5e0on#0f1a2e: 11.70:1 -- PASS AAA..concept-item.masteredbackground is#14532d22(alpha) over#0f1a2e~= effective#152436-ish dark. Text#cbd5e0on 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 addingaria-label="not mastered"/aria-label="mastered"to the row, or visible text..kpi-box .v#64ffdaon#0f1a2e: PASS AAA.
Keyboard navigation¶
- CRITICAL:
.concept-item(line 40) is a<div>withcursor:pointerand a click handler (line 247). It is not focusable, has norole="button", notabindex, 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 addrole="button" tabindex="0"plus akeydownhandler 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-rownumeric KPIs update on every mastery toggle withoutaria-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-itemis 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 viafor/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-2collapses 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 is1frso 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#cbd5e0on#16213e: 10.70:1 -- PASS AAA..card h3#a78bfaon#16213e: 5.84:1 -- PASS AA..compare th#64ffdaon#0a0a1a: 15.74:1 -- PASS AAA..kv-table td:first-child#a78bfaon the inherited card bg: 5.84:1 -- PASS AA..pill.ok#86efacon#14532d: 6.49:1 -- PASS AA..pill.bad#fca5a5on#7f1d1d: 5.28:1 -- PASS AA.- FAIL (gradient):
button.dangerislinear-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 asbutton.secondaryin crypto-toolkit. Fix: change start of gradient to#dc2626(~4.6:1) or remove gradient and use solid#b91c1c. - FAIL (gradient):
button.secondarylinear-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.textContentor.innerHTMLwith noaria-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-claimslist (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. Addingaria-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 anaria-label="Claim key"/aria-label="Claim value". - Date inputs:
<label>Valid from</label>(line 232) is not linked viafor/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-2collapses at 760px. The.tabscontainer hasoverflow-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:relativenot sticky. PASS. - 2.5.7: no drag. PASS.
- 2.5.8: copy buttons have
padding:5px 11pxagain -- borderline 24x24. The remove-claim✕button has no explicit padding override -- inherits button base ofpadding:10px 20pxinitially, then JS overrides topadding: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
#8892b0on#16213e: 5.14:1 -- PASS AA. - Tab active
#64ffdaon#0f0f23: 15.15:1 -- PASS AAA. - Form labels
#64ffdaon#1a1a2e(input bg): PASS AAA. - Input text
#e0e0e0on#1a1a2e: PASS AAA. .icc-checkbox-item label#ccd6f6on#1a1a2e: 11.48:1 -- PASS AAA..icc-cost-value#64ffda: PASS AAA..icc-confidence-label.high#ff6b6bon#0f0f23: 6.04:1 -- PASS AA..icc-confidence-label.mid#ffd93don#0f0f23: 12.81:1 -- PASS AAA..icc-cost-line-label#8892b0on#1a1a2e: 5.51:1 -- PASS AA..icc-fines-table td#b0b8d0on#1a1a2e: 8.62:1 -- PASS AAA.- FAIL (small label, normal-text threshold):
.icc-benchmark-scale span#6a7b9aon#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#7a8aaato clear (~4.6:1) or to#8892b0to clear (5.51:1). - Same
#6a7b9ais 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
#ccd6f6on#16213eis fine (10.99:1), but tooltip is triggered by:hoveronly -- 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>witharia-describedbyand a hidden tooltip element, OR addtabindex="0"and a:focusrule 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-valuerows. 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 viaoninput-- 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 updatearia-valuetextin JS, ORaria-describedbyto 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.
:focuson form inputs setsborder-color:#64ffdaANDbox-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:focusrule -- relies on browser default. HIGH (systemic).
ARIA + semantics¶
- Tab UI: same lack of
role="tablist"etc. HIGH. Inlineonclick(lines 771-775) instead ofaddEventListener-- works but a CSP that disallows inline handlers would break this. Project uses CSP per CLAUDE.md; verify the CSP allowsunsafe-inlinefor 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. Noaria-liveon the cost grid container. The report-output container (#icc-report-output, line 1189) DOES havearia-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 viafor/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 -- aftericcCopyReport()add a status announcement.
Mobile / responsive¶
.icc-form-rowand.icc-form-row-3collapse at 768px (line 139)..icc-cost-gridcollapses at 768px (line 348). PASS 1.4.10..icc-checkbox-gridisrepeat(auto-fill,minmax(160px,1fr))-- responds well. PASS.- The
<canvas>charts usewidth="700"height="300"HTML attributes pluswidth:100%; max-height:350pxCSS -- they scale down. PASS. - Touch targets: checkbox items have padding
8px 12pxcontaining 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), — (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.
Recommended remediation roadmap¶
Phase 1 -- CRITICAL + HIGH (~1 week, ~6-8 hours)¶
- 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. - Add
:focus-visibleoutline rule to every button across all tools. One CSS edit per file (or one shared stylesheet). 15 minutes total. - Wrap result containers in
aria-live="polite"regions (the 15+ instances enumerated under S2). 1-2 hours. - Link every
<label>to its<input>viafor/id. Mechanical sweep. 2-3 hours. - Refactor tab UI to ARIA tab pattern -- shared
docs/javascripts/tabs.jshelper, thendata-tablistwiring on each tool. 2-3 hours.
Phase 2 -- MEDIUM (~2 weeks, can parallel-track)¶
- Fix gradient button text contrast (S6). Either darken gradient starts or switch text color. 30 minutes.
- Bump small-button target sizes (S7). 30 minutes.
- Add
aria-valuetextto every<input type="range">(incident-cost-calculator). 1 hour. - Add
<canvas role="img" aria-label="...">to chart canvases (incident-cost-calculator). 30 minutes. - Convert info-icon
?from<span>to<button aria-label="...">with:focustooltip mirror. 1 hour. - Replace placeholder-as-label on dynamic claim rows (did-web). Add
aria-label. 30 minutes. - Add
<caption>to data tables (did-web kv-table, pattern-hunter syntax help). 15 minutes. - Add explicit error-identification and
requiredattributes to forms that have functionally-required inputs. 1-2 hours.
Phase 3 -- LOW + best-practice (~ongoing)¶
aria-hidden="true"sweep on decorative glyphs (S5). 1-2 hours.- Add
aria-label="(opens in new tab)"to external links across the corpus. 30 minutes. - Manual NVDA/VoiceOver pass on the three highest-traffic tools (separate task -- needs human tester).
- Lighthouse / axe-core CI check -- add to existing CI workflows once Actions quota resets (2026-05-01). Wire
npx @axe-core/cliagainst the built site. - Add a
prefers-reduced-motionmedia query -- the.icc-tab-contentuses a fadeIn animation (line 748), and the benchmark bar hastransition: 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 inincident-cost-calculator.html(lines 771-775, 791, 804, 811, etc.) requirescript-src 'unsafe-inline'or per-handler hashes. The project's_headersfile 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¶
- WCAG 2.2 specification: https://www.w3.org/TR/WCAG22/
- WCAG 2.2 What's New: https://www.w3.org/WAI/standards-guidelines/wcag/new-in-22/
- ARIA Authoring Practices Guide (tab pattern): https://www.w3.org/WAI/ARIA/apg/patterns/tabs/
- ARIA Authoring Practices Guide (button pattern): https://www.w3.org/WAI/ARIA/apg/patterns/button/
- WebAIM contrast checker: https://webaim.org/resources/contrastchecker/
- MDN: Using the
aria-liveattribute: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Live_Regions - WCAG 2.2 SC 2.5.8 Target Size (Minimum) understanding: https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html
- WCAG 2.2 SC 2.4.11 Focus Not Obscured (Minimum): https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html
- WCAG 2.2 SC 2.5.7 Dragging Movements: https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html
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.