@wcag-checkr/ci 1.0.0-rc.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -0
- package/dist/assets/ErrorBoundary-CBJAOIP1.js +544 -0
- package/dist/assets/_commonjsHelpers-Cpj98o6Y.js +1 -0
- package/dist/assets/ai-usage-log-Dj9Ub_DT.js +1 -0
- package/dist/assets/content-script.ts-loader-BjJqq5Xq.js +13 -0
- package/dist/assets/content-script.ts-vGsvD70K.js +181 -0
- package/dist/assets/copy-ai-fixer-prompt-CZvVr028.js +19 -0
- package/dist/assets/crash-reporter-Dc5lvxvY.js +4 -0
- package/dist/assets/custom-rules-CAe0nbES.js +1 -0
- package/dist/assets/devtools-panel-KS9iXH8M.js +1 -0
- package/dist/assets/devtools.html-DQBohI9U.js +1 -0
- package/dist/assets/diff-DIBMr3fQ.js +1 -0
- package/dist/assets/dom-criterion-analyzers-C2gMp3xS.js +1 -0
- package/dist/assets/forensic-log-z_FcTaq6.js +129 -0
- package/dist/assets/modulepreload-polyfill-B5Qt9EMX.js +1 -0
- package/dist/assets/options-Byz5A0JD.js +6 -0
- package/dist/assets/parallel-tab-flow-CGFwqQRo.js +1 -0
- package/dist/assets/preload-helper-D7HrI6pR.js +1 -0
- package/dist/assets/reflow-analyzer-DNgBX8N_.js +1 -0
- package/dist/assets/service-worker.ts-DnDnrbM5.js +985 -0
- package/dist/assets/side-panel-CpytrH5X.js +1 -0
- package/dist/assets/site-report-renderer-fqGucmuR.js +147 -0
- package/dist/assets/state-BIsozXtc.js +1 -0
- package/dist/assets/styles-Bh3rgVbO.css +1 -0
- package/dist/assets/styles-DhnON7zr.js +84 -0
- package/dist/axe.min.js +12 -0
- package/dist/devtools/devtools.html +11 -0
- package/dist/devtools/panel.html +20 -0
- package/dist/fonts/mona-sans-variable.woff2 +0 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-32.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/manifest.json +76 -0
- package/dist/options/options.html +20 -0
- package/dist/service-worker-loader.js +1 -0
- package/dist/side-panel/App.tsx +193 -0
- package/dist/side-panel/README.md +57 -0
- package/dist/side-panel/audit-launcher.test.ts +56 -0
- package/dist/side-panel/audit-launcher.ts +65 -0
- package/dist/side-panel/format-component-id.test.ts +89 -0
- package/dist/side-panel/format-component-id.ts +40 -0
- package/dist/side-panel/github-issue.test.ts +102 -0
- package/dist/side-panel/github-issue.ts +66 -0
- package/dist/side-panel/jira-issue.ts +64 -0
- package/dist/side-panel/main.tsx +19 -0
- package/dist/side-panel/side-panel.html +21 -0
- package/dist/side-panel/store.ts +289 -0
- package/dist/side-panel/styles.css +16 -0
- package/dist/side-panel/wire-messaging.test.ts +202 -0
- package/dist/side-panel/wire-messaging.ts +288 -0
- package/package.json +44 -0
- package/wcagcheckr-ci.mjs +559 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/ErrorBoundary-CBJAOIP1.js","assets/styles-DhnON7zr.js","assets/_commonjsHelpers-Cpj98o6Y.js","assets/crash-reporter-Dc5lvxvY.js","assets/state-BIsozXtc.js","assets/styles-Bh3rgVbO.css","assets/preload-helper-D7HrI6pR.js","assets/forensic-log-z_FcTaq6.js"])))=>i.map(i=>d[i]);
|
|
2
|
+
import{_ as b}from"./preload-helper-D7HrI6pR.js";import{r as y}from"./crash-reporter-Dc5lvxvY.js";async function h(t){var l,u;if(t.results.length===0)return;let i;if(t.componentId){const e=await chrome.storage.local.get("igtRuns"),r=((e==null?void 0:e.igtRuns)??{})[t.componentId]??{};i=Object.values(r)}let s,c;const o=((l=t.results[0])==null?void 0:l.pageUrl)??((u=t.results[0])==null?void 0:u.scope)??"";if(o){const{getDismissalsForUrl:e}=await b(async()=>{const{getDismissalsForUrl:n}=await import("./ErrorBoundary-CBJAOIP1.js").then(a=>a.d);return{getDismissalsForUrl:n}},__vite__mapDeps([0,1,2,3,4,5,6,7])),m=await e(o);s=Object.keys(m);const{getResolutionsForPage:r}=await b(async()=>{const{getResolutionsForPage:n}=await import("./forensic-log-z_FcTaq6.js").then(a=>a.Y);return{getResolutionsForPage:n}},__vite__mapDeps([7,3,4]));c=await r(o)}const p=await y({type:"EXPORT_REQUEST",format:"ai-prompt",results:t.results,delta:t.delta??void 0,manualRuns:i,siteCrawlReport:t.siteCrawlReport??void 0,dismissedKeys:s,incompleteResolutions:c});try{await navigator.clipboard.writeText(p.content)}catch{}const g=`<!doctype html><html><head><meta charset="utf-8"><title>AI fix prompt</title>
|
|
3
|
+
<style>body{font:14px/1.5 ui-monospace,Menlo,monospace;max-width:920px;margin:24px auto;padding:0 16px;color:#0f172a;}
|
|
4
|
+
.banner{background:#ecfdf5;border:1px solid #6ee7b7;padding:12px 14px;border-radius:6px;margin-bottom:16px;font-family:system-ui;}
|
|
5
|
+
pre{white-space:pre-wrap;word-break:break-word;background:#f8fafc;border:1px solid #e2e8f0;padding:14px;border-radius:6px;font-size:13px;}
|
|
6
|
+
button{font:13px system-ui;padding:6px 12px;border:1px solid #cbd5e1;border-radius:4px;background:white;cursor:pointer;}
|
|
7
|
+
button:hover{background:#f1f5f9;}</style></head>
|
|
8
|
+
<body>
|
|
9
|
+
<div class="banner">
|
|
10
|
+
<strong>Already copied to your clipboard.</strong> Paste it into Claude, Cursor, Copilot Chat, or any LLM with codebase access.
|
|
11
|
+
<button id="copy" style="margin-left:12px;">Copy again</button>
|
|
12
|
+
</div>
|
|
13
|
+
<pre id="content">${p.content.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">")}</pre>
|
|
14
|
+
<script>
|
|
15
|
+
document.getElementById('copy').addEventListener('click', () => {
|
|
16
|
+
navigator.clipboard.writeText(document.getElementById('content').textContent || '');
|
|
17
|
+
});
|
|
18
|
+
<\/script>
|
|
19
|
+
</body></html>`,x=new Blob([g],{type:"text/html"}),d=URL.createObjectURL(x);window.open(d,"_blank","noopener,noreferrer"),setTimeout(()=>URL.revokeObjectURL(d),6e4)}export{h as copyAiFixerPrompt};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const u=[];function M(e){u.push(e),u.length>50&&u.shift()}function S(){return[...u]}const l={debug:0,info:1,warn:2,error:3,silent:4};let $="info";function y(e){return{debug:(n,...r)=>f("debug",e,n,r),info:(n,...r)=>f("info",e,n,r),warn:(n,...r)=>f("warn",e,n,r),error:(n,...r)=>f("error",e,n,r)}}function f(e,n,r,t){if(M(b(e,n,r,t)),l[e]<l[$])return;const o=`[${n}]`,s=e==="error"?console.error:e==="warn"?console.warn:console.log;t.length>0?s(o,r,...t):s(o,r)}function b(e,n,r,t){const o=new Date().toISOString(),s=t.length>0?` ${t.map(O).join(" ")}`:"";return`${o} ${e.toUpperCase()} [${n}] ${r}${s}`}function O(e){if(e instanceof Error)return e.message;if(typeof e=="string")return e;try{return JSON.stringify(e)}catch{return String(e)}}const _=y("messaging");function j(e){return chrome.runtime.sendMessage(e)}function q(e,n,r){const t=r!==void 0?{frameId:r}:void 0;return chrome.tabs.sendMessage(e,n,t)}async function C(e){const n=await chrome.runtime.sendMessage(e);if(!d(n))throw new Error(`messaging.request: response was not a Message (sent ${e.type}, got ${typeof n})`);return n}const h=45e3;async function U(e,n,r){const t=r!==void 0?{frameId:r}:void 0,o=await Promise.race([chrome.tabs.sendMessage(e,n,t),new Promise((s,a)=>setTimeout(()=>a(new Error(`messaging.requestFromTab: ${n.type} timed out after ${h/1e3}s`)),h))]);if(!d(o))throw new Error(`messaging.requestFromTab: response was not a Message (sent ${n.type}, got ${typeof o})`);return o}function x(e,n){const r=(t,o,s)=>{if(!d(t)||t.type!==e)return!1;let a;try{a=n(t,o)}catch(c){return p(e,c),s(void 0),!1}return a instanceof Promise?(a.then(c=>s(c??void 0)).catch(c=>{p(e,c),s(void 0)}),!0):(s(a??void 0),!1)};return chrome.runtime.onMessage.addListener(r),()=>chrome.runtime.onMessage.removeListener(r)}function D(e){chrome.runtime.sendMessage(e).catch(()=>{})}function d(e){return typeof e=="object"&&e!==null&&typeof e.type=="string"}function p(e,n){_.error(`handler for ${e} failed`,n)}const I=/https?:\/\/[^\s)]+/g,N=/[\w.+-]+@[\w-]+\.[\w.-]+/g,R=/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;function k(e){return e.replace(I,"[URL]").replace(N,"[EMAIL]").replace(R,"[TOKEN]")}function L(e){return e.map(k)}const A=y("crash-reporter"),F="https://api.wcagcheckr.com/v1/products/wcagcheckr/crash",g="crashReporting:enabled",w=10;let T="unknown",m=!1;const i=new Set;function G(e){m||(m=!0,T=e,typeof window<"u"&&(window.addEventListener("error",n=>{E(n.error??new Error(n.message))}),window.addEventListener("unhandledrejection",n=>{const r=n.reason,t=r instanceof Error?r:new Error(String(r));E(t)})))}async function E(e){if(!await P())return;const n=v(e);if(i.has(n))return;if(i.add(n),i.size>w){const t=[...i].slice(-w);i.clear(),t.forEach(o=>i.add(o))}const r={productSlug:"wcagcheckr",context:T,fingerprint:n,message:e.message,stack:X(e.stack??""),extensionVersion:chrome.runtime.getManifest().version,occurredAt:new Date().toISOString(),logTail:L(S())};try{await fetch(F,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r)})}catch(t){A.warn("crash report failed to send",t)}}async function P(){try{return(await chrome.storage.local.get(g))[g]===!0}catch{return!1}}async function K(e){await chrome.storage.local.set({[g]:e})}function v(e){var r;const n=((r=(e.stack??"").split(`
|
|
2
|
+
`)[1])==null?void 0:r.trim())??"";return`${e.name}:${e.message}::${n}`.slice(0,200)}function X(e){return L(e.split(`
|
|
3
|
+
`)).join(`
|
|
4
|
+
`)}export{g as O,K as a,q as b,y as c,U as d,D as e,L as f,S as g,E as h,G as i,x as o,C as r,j as s};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o="customRules",l=/^[a-z0-9-]+$/,m=["minor","moderate","serious","critical"];function a(s,r=255){return typeof s=="string"&&s.length>0&&s.length<=r}function n(s,r){if(typeof s!="object"||s===null)return{error:`Rule ${r+1}: not an object.`};const t=s;if(!a(t.id,64)||!l.test(t.id))return{error:`Rule ${r+1}: id must be lowercase letters, digits, hyphens (max 64).`};if(!a(t.description))return{error:`Rule ${r+1}: description required (max 255 chars).`};if(typeof t.impact!="string"||!m.includes(t.impact))return{error:`Rule ${r+1}: impact must be one of minor / moderate / serious / critical.`};if(!a(t.selector,500))return{error:`Rule ${r+1}: selector required (max 500 chars).`};if(typeof t.check!="object"||t.check===null)return{error:`Rule ${r+1}: check object required.`};const e=t.check;let i;if(e.type==="fail-on-match"){if(!a(e.message))return{error:`Rule ${r+1}: check.message required.`};i={type:"fail-on-match",message:e.message}}else if(e.type==="attribute-required"){if(!a(e.attribute,64)||!a(e.message))return{error:`Rule ${r+1}: check.attribute + check.message required.`};i={type:"attribute-required",attribute:e.attribute,message:e.message}}else if(e.type==="attribute-equals"){if(!a(e.attribute,64)||typeof e.value!="string"||!a(e.message))return{error:`Rule ${r+1}: check.attribute + check.value + check.message required.`};i={type:"attribute-equals",attribute:e.attribute,value:e.value,message:e.message}}else return{error:`Rule ${r+1}: check.type must be fail-on-match / attribute-required / attribute-equals.`};const u=typeof t.enabled=="boolean"?t.enabled:!0;return{rule:{id:t.id,description:t.description,impact:t.impact,selector:t.selector,check:i,enabled:u}}}async function f(){const r=(await chrome.storage.local.get(o))[o];if(!Array.isArray(r))return[];const t=[];for(let e=0;e<r.length;e++){const{rule:i}=n(r[e],e);i&&t.push(i)}return t}async function p(s){await chrome.storage.local.set({[o]:s})}function d(s){const r=[],t=[];if(!Array.isArray(s))return{valid:[],errors:["Expected an array of rules."]};const e=new Set;for(let i=0;i<s.length;i++){const{rule:u,error:c}=n(s[i],i);if(!u){c&&r.push(c);continue}if(e.has(u.id)){r.push(`Rule ${i+1}: duplicate id "${u.id}"`);continue}e.add(u.id),t.push(u)}return{valid:t,errors:r}}export{f as l,p as s,d as v};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./modulepreload-polyfill-B5Qt9EMX.js";import{c as t,j as o,R as e}from"./styles-DhnON7zr.js";import{E as s,A as i}from"./ErrorBoundary-CBJAOIP1.js";import{i as m}from"./crash-reporter-Dc5lvxvY.js";import"./_commonjsHelpers-Cpj98o6Y.js";import"./state-BIsozXtc.js";import"./preload-helper-D7HrI6pR.js";import"./forensic-log-z_FcTaq6.js";m("devtools-panel");const r=document.getElementById("root");if(!r)throw new Error("devtools panel: #root not found");t(r).render(o.jsx(e.StrictMode,{children:o.jsx(s,{children:o.jsx(i,{})})}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import"./modulepreload-polyfill-B5Qt9EMX.js";chrome.devtools.panels.create("A11y","icons/icon-32.png","devtools/panel.html",()=>{});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var K=Object.defineProperty;var E=(t,e,s)=>e in t?K(t,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[e]=s;var h=(t,e,s)=>E(t,typeof e!="symbol"?e+"":e,s);function j(t,e,s=!1,r){return{code:t,message:e,recoverable:s,details:r}}class $ extends Error{constructor(s){super(s.message);h(this,"payload");this.name="TypedError",this.payload=s}}function k(t){return t instanceof $}async function T(t){const e=i(t.currentState),s=[t.target.tagName,t.target.role??"",t.target.accessibleName??"",t.target.textSnippet??"",t.target.attrId??"",t.target.attrTestid??""].join("|");return A(`${t.ruleId}|${t.componentId}|${e}|${s}`)}function F(t,e,s,r,c){const u=new Map(t.map(n=>[n.matchKey,n])),w=new Map(e.map(n=>[n.matchKey,n])),o=c??new Set,y=e.filter(n=>!u.has(n.matchKey)&&!o.has(n.matchKey)),f=e.filter(n=>u.has(n.matchKey)&&!o.has(n.matchKey)),m=t.filter(n=>!w.has(n.matchKey)&&!o.has(n.matchKey)),p=e.filter(n=>o.has(n.matchKey));let g,d;if(r){if(r.currentAnnouncements){const n=new Set((r.baselineAnnouncements??[]).map(l));g=r.currentAnnouncements.filter(a=>!n.has(l(a)))}if(r.currentFocusEvents){const n=new Set((r.baselineFocusEvents??[]).map(S));d=r.currentFocusEvents.filter(a=>!n.has(S(a)))}}return{new:y,persistent:f,fixed:m,newCount:y.length,persistentCount:f.length,fixedCount:m.length,baselineSnapshotMeta:s,comparedAt:new Date().toISOString(),newAnnouncements:g,newFocusEvents:d,acknowledged:p,acknowledgedCount:p.length}}function l(t){return`${t.selector}::${t.text}::${t.politeness}`}function S(t){return`${t.toSelector}::${t.isFocusReset}`}function i(t){if(t===null||typeof t!="object")return JSON.stringify(t);if(Array.isArray(t))return"["+t.map(i).join(",")+"]";const e=t;return"{"+Object.keys(e).sort().map(c=>`${JSON.stringify(c)}:${i(e[c])}`).join(",")+"}"}async function A(t){const e=new TextEncoder().encode(t),s=await crypto.subtle.digest("SHA-1",e);return Array.from(new Uint8Array(s)).map(r=>r.toString(16).padStart(2,"0")).join("")}export{$ as T,i as a,T as c,F as d,k as i,j as m};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const c="https://www.w3.org/WAI/WCAG21/Understanding/";function f(e=document){const t=[],r=e.querySelectorAll('meta[http-equiv="refresh" i]');for(const n of Array.from(r)){const i=n.getAttribute("content")??"",o=parseInt(i.split(";")[0]??"",10);Number.isFinite(o)&&o>0&&t.push({ruleId:"wcc-meta-refresh",wcagCriterion:"2.2.1",wcagLevel:"A",impact:"serious",description:`Page uses <meta http-equiv="refresh"> to auto-refresh after ${o}s. WCAG 2.2.1 requires that users can turn off, adjust, or extend any time limit. Auto-refresh without user control fails this criterion.`,helpUrl:`${c}timing-adjustable.html`,element:n,failureSummary:`Auto-refresh interval: ${o}s. No user control mechanism on the page.`})}const a=e.querySelectorAll("script:not([src])");for(const n of Array.from(a)){const i=n.textContent??"";i.length===0||!/\b(setTimeout|setInterval)\s*\(/.test(i)||!/\b(innerHTML|textContent|replaceChildren|window\.location|document\.location|location\.href|location\.replace)\b/.test(i)||t.push({ruleId:"wcc-timer-mutates-content",wcagCriterion:"2.2.1",wcagLevel:"A",impact:"moderate",description:"Inline script uses setTimeout/setInterval to mutate page content. WCAG 2.2.1 requires that users can turn off, adjust, or extend any time-limited content change. Verify a pause / extend / disable mechanism exists in the UI.",helpUrl:`${c}timing-adjustable.html`,element:n,failureSummary:"Script combines timer + DOM mutation. Without a user control to pause/extend/disable, fails 2.2.1."})}return t}function g(e=document){const t=[],r=e.querySelectorAll("marquee, blink");for(const o of Array.from(r))t.push({ruleId:"wcc-marquee-blink",wcagCriterion:"2.2.2",wcagLevel:"A",impact:"serious",description:`<${o.tagName.toLowerCase()}> creates continuous motion with no built-in pause control. WCAG 2.2.2 requires a way to pause, stop, or hide auto-moving content.`,helpUrl:`${c}pause-stop-hide.html`,element:o,failureSummary:`Deprecated <${o.tagName.toLowerCase()}> element auto-animates without pause control.`});const a=e.querySelectorAll("video[autoplay]");for(const o of Array.from(a))!o.hasAttribute("controls")&&!o.muted&&t.push({ruleId:"wcc-autoplay-no-controls",wcagCriterion:"2.2.2",wcagLevel:"A",impact:"serious",description:"<video autoplay> without controls attribute (or muted) creates moving content the user cannot pause. WCAG 2.2.2 requires a pause / stop / hide mechanism.",helpUrl:`${c}pause-stop-hide.html`,element:o,failureSummary:"Autoplay video lacks user-accessible pause control."});const n=e.querySelectorAll("*");let i=0;for(const o of Array.from(n)){if(i>1500)break;i++;const s=window.getComputedStyle(o);if(s.animationName==="none"||!s.animationName)continue;const l=s.animationDuration.split(",").map(u=>parseFloat(u)),m=s.animationIterationCount.split(",").map(u=>u.trim());for(let u=0;u<l.length;u++){const d=l[u]??0,h=(m[u]??"1")==="infinite";h&&d>0&&d<.34&&t.push({ruleId:"wcc-flash-risk",wcagCriterion:"2.3.1",wcagLevel:"A",impact:"critical",description:`Element animates with duration ${d.toFixed(2)}s on infinite repeat — that's > 3 cycles/sec, which can trigger photosensitive seizures. WCAG 2.3.1 prohibits content that flashes more than 3 times per second unless the flashing area is below threshold.`,helpUrl:`${c}three-flashes-or-below-threshold.html`,element:o,failureSummary:`Infinite CSS animation @ ${d.toFixed(2)}s/cycle = ${(1/d).toFixed(1)} flashes/sec.`}),h&&d>=5&&t.push({ruleId:"wcc-long-infinite-animation",wcagCriterion:"2.2.2",wcagLevel:"A",impact:"moderate",description:`Element runs an infinite CSS animation (${d.toFixed(1)}s/cycle). WCAG 2.2.2 requires a way to pause, stop, or hide animation longer than 5 seconds. Verify a pause mechanism exists.`,helpUrl:`${c}pause-stop-hide.html`,element:o,failureSummary:`Infinite animation @ ${d.toFixed(1)}s/cycle. Confirm user can pause.`})}}return t}function y(e=document){const t=[],r=new Set,a=e.querySelectorAll("[ontouchstart], [ontouchmove], [ongesturestart], [ongesturechange]");for(const i of Array.from(a)){const o=p(i);r.has(o)||(r.add(o),t.push({ruleId:"wcc-gesture-no-alternative",wcagCriterion:"2.5.1",wcagLevel:"A",impact:"serious",description:"Element handles multi-point or path-based gestures (touch/gesture events). WCAG 2.5.1 requires that all functionality operable with a path-based or multi-point gesture is ALSO operable with a single-pointer activation. Verify a button / link / single-tap alternative exists.",helpUrl:`${c}pointer-gestures.html`,element:i,failureSummary:"Touch/gesture handler on this element. Confirm single-pointer alternative."}))}const n=e.querySelectorAll('[draggable="true"], [ondragstart]');for(const i of Array.from(n)){const o=p(i);r.has(o)||(r.add(o),t.push({ruleId:"wcc-drag-no-alternative",wcagCriterion:"2.5.7",wcagLevel:"AA",impact:"serious",description:'Element uses HTML5 drag-and-drop (`draggable="true"` or `ondragstart`). WCAG 2.5.7 requires that any function achieved via a dragging movement is ALSO operable by a single-pointer action (click, button, arrow keys). Confirm an alternative exists.',helpUrl:`${c}dragging-movements.html`,element:i,failureSummary:"Drag-and-drop interaction. Confirm non-drag alternative for keyboard / touch users."}))}return t}function A(e=document){const t=[],r=e.querySelectorAll("video");for(const n of Array.from(r)){const i=Array.from(n.querySelectorAll("track")),o=i.some(l=>(l.kind??"").toLowerCase()==="captions"||(l.kind??"").toLowerCase()==="subtitles"),s=i.some(l=>(l.kind??"").toLowerCase()==="descriptions");o||t.push({ruleId:"wcc-video-no-captions",wcagCriterion:"1.2.2",wcagLevel:"A",impact:"serious",description:'<video> element without `<track kind="captions">` (or `subtitles`). WCAG 1.2.2 requires captions for all prerecorded audio content in synchronized media. If this video has no audio, mark this finding as not-applicable.',helpUrl:`${c}captions-prerecorded.html`,element:n,failureSummary:'No <track kind="captions"> child. Captions required unless video is silent.'}),s||t.push({ruleId:"wcc-video-no-descriptions",wcagCriterion:"1.2.5",wcagLevel:"AA",impact:"moderate",description:'<video> element without `<track kind="descriptions">`. WCAG 1.2.5 (AA) requires audio description of important visual content in prerecorded video. If all video content is conveyed by the audio track alone, mark this finding as not-applicable.',helpUrl:`${c}audio-description-prerecorded.html`,element:n,failureSummary:'No <track kind="descriptions"> child. Audio description required when visuals carry info not in audio.'})}const a=e.querySelectorAll("audio");for(const n of Array.from(a))t.push({ruleId:"wcc-audio-needs-transcript",wcagCriterion:"1.2.1",wcagLevel:"A",impact:"moderate",description:"<audio> element present. WCAG 1.2.1 requires a text alternative (transcript) for prerecorded audio-only content. Confirm a transcript is linked nearby.",helpUrl:`${c}audio-only-and-video-only-prerecorded.html`,element:n,failureSummary:"Audio element detected. Verify nearby transcript link."});return t}function w(e=document){const t=[],r=/\b(window\.location|document\.location|location\.href|location\.replace|window\.open|\.submit\s*\()/,a=e.querySelectorAll("[onfocus]");for(const i of Array.from(a)){const o=i.getAttribute("onfocus")??"";r.test(o)&&t.push({ruleId:"wcc-onfocus-context-change",wcagCriterion:"3.2.1",wcagLevel:"A",impact:"serious",description:"Element's `onfocus` handler causes a context change (navigation, submit, popup). WCAG 3.2.1 forbids context changes triggered solely by focus — they disorient keyboard and screen-reader users who land on the element while scanning.",helpUrl:`${c}on-focus.html`,element:i,failureSummary:"Focus handler navigates / submits / opens window. Move trigger to explicit click/keypress."})}const n=e.querySelectorAll("select[onchange], input[onchange], textarea[onchange]");for(const i of Array.from(n)){const o=i.getAttribute("onchange")??"";r.test(o)&&t.push({ruleId:"wcc-oninput-context-change",wcagCriterion:"3.2.2",wcagLevel:"A",impact:"serious",description:"Form control's `onchange` handler causes a context change without explicit user request. WCAG 3.2.2 requires that changing a setting in a form control does NOT automatically navigate or submit — the user must explicitly confirm via a button.",helpUrl:`${c}on-input.html`,element:i,failureSummary:"Change handler navigates / submits. Replace with a separate submit button."})}return t}function v(e=document){const t=[],r=[".g-recaptcha","[data-sitekey]",".h-captcha",".cf-turnstile",".frc-captcha",'iframe[src*="recaptcha"]','iframe[src*="hcaptcha"]','iframe[src*="turnstile"]'].join(", "),a=e.querySelectorAll(r),n=new Set;for(const i of Array.from(a)){if(n.has(i))continue;n.add(i);const o=i.closest("form"),s=((o==null?void 0:o.textContent)??"").toLowerCase(),l=/audio captcha|alternative|sign in with|continue with|use email|magic link/.test(s);t.push({ruleId:"wcc-cognitive-auth-challenge",wcagCriterion:"3.3.8",wcagLevel:"AA",impact:l?"moderate":"serious",description:l?"CAPTCHA / cognitive-function challenge detected. An alternative authentication mechanism appears available — verify it actually bypasses the CAPTCHA. WCAG 3.3.8 requires that the alternative not also require a cognitive function test.":'CAPTCHA / cognitive-function challenge detected with no apparent alternative. WCAG 3.3.8 requires an alternative authentication mechanism that does NOT depend on a cognitive function test (recognizing distorted text, solving puzzles, identifying objects in images, etc.). Add a "Sign in with [SSO]" or magic-link option.',helpUrl:`${c}accessible-authentication-minimum.html`,element:i,failureSummary:l?"CAPTCHA detected; alternative may exist — verify it skips the cognitive challenge.":"CAPTCHA detected without an alternative authentication path."})}return t}function b(e=document){const t=[],r=e.querySelectorAll('meta[name="viewport" i]');for(const n of Array.from(r)){const i=n.getAttribute("content")??"";(/user-scalable\s*=\s*(no|0)\b/i.test(i)||/maximum-scale\s*=\s*1(\.0+)?\b/i.test(i))&&t.push({ruleId:"wcc-viewport-locks-zoom",wcagCriterion:"1.4.4",wcagLevel:"AA",impact:"serious",description:'<meta name="viewport"> disables user zoom (`user-scalable=no` or `maximum-scale=1`). WCAG 1.4.4 requires that users can scale text up to 200% without loss of content or functionality. Remove the zoom-disabling tokens.',helpUrl:`${c}resize-text.html`,element:n,failureSummary:"Viewport meta disables zoom. Remove user-scalable=no and maximum-scale<2."})}let a=0;for(const n of Array.from(e.styleSheets)){if(a>40)break;a++;try{const i=n.cssRules;if(!i)continue;for(const o of Array.from(i)){if(!(o instanceof CSSMediaRule))continue;const s=o.media.mediaText.toLowerCase(),l=/\(\s*orientation\s*:\s*portrait\s*\)/.test(s),m=/\(\s*orientation\s*:\s*landscape\s*\)/.test(s);if(!(!l&&!m)){for(const u of Array.from(o.cssRules))if(u instanceof CSSStyleRule&&/display\s*:\s*none/i.test(u.style.cssText)){t.push({ruleId:"wcc-orientation-locked-content",wcagCriterion:"1.3.4",wcagLevel:"AA",impact:"serious",description:`Stylesheet hides content via "display: none" inside an @media (orientation: ${l?"portrait":"landscape"}) block. WCAG 1.3.4 requires content not be restricted to a single display orientation unless that orientation is essential. Provide content in both orientations or document the essential-orientation exception.`,helpUrl:`${c}orientation.html`,element:e.documentElement,failureSummary:`Orientation-locked display:none on selector "${u.selectorText}".`});break}}}}catch{}}return t}function C(e=document){const t=[];let r=0;const a=e.querySelectorAll("[title]");for(const n of Array.from(a)){if(r>200)break;r++;const i=n.getAttribute("title");!i||i.trim().length<8||n.hasAttribute("aria-describedby")||n.tagName==="A"&&(n.textContent??"").includes(i)||t.push({ruleId:"wcc-title-tooltip-no-persistence",wcagCriterion:"1.4.13",wcagLevel:"AA",impact:"moderate",description:'Element uses the `title` attribute for a tooltip-style description. Browser title tooltips auto-dismiss when the pointer moves, violating WCAG 1.4.13\'s "hoverable" and "persistent" requirements (content triggered on hover must remain visible until the user dismisses it). Use a proper aria-describedby pattern with a hoverable, dismissible tooltip element.',helpUrl:`${c}content-on-hover-or-focus.html`,element:n,failureSummary:"title-attribute tooltip is not dismissible / hoverable."})}return t}function S(e=document){const t=[],r=typeof window<"u"?window.location.pathname:"/";if(r==="/"||r===""||/\/(search|results)\b/.test(r))return t;const a=e.querySelector('nav, [role="navigation"]')!==null,n=e.querySelector('input[type="search"], [role="search"]')!==null||Array.from(e.querySelectorAll('input[type="text"]')).some(l=>/search/i.test(l.name)||/search/i.test(l.placeholder??"")),i=e.querySelector('a[href*="sitemap" i]')!==null,o=Array.from(e.querySelectorAll('a[href^="#"]')).length>=3,s=[a,n,i,o].filter(Boolean).length;return s<2&&t.push({ruleId:"wcc-only-one-way-to-find",wcagCriterion:"2.4.5",wcagLevel:"AA",impact:"moderate",description:`Page provides only ${s} navigation aid${s===1?"":"s"}. WCAG 2.4.5 requires at least TWO ways to locate a page within a set: typically navigation menu + site search, OR navigation + sitemap link. Home pages and search results are exempt.`,helpUrl:`${c}multiple-ways.html`,element:e.documentElement,failureSummary:`Found ${s} of 4 affordances (nav=${a}, search=${n}, sitemap=${i}, toc=${o}).`}),t}function q(e=document){const t=[],r=/\b(window\.location|location\.href|\.submit\s*\(|window\.open|fetch\s*\(|XMLHttpRequest)\b/,a=e.querySelectorAll("[onmousedown], [onpointerdown]");for(const n of Array.from(a)){const i=(n.getAttribute("onmousedown")??"")+";"+(n.getAttribute("onpointerdown")??"");r.test(i)&&t.push({ruleId:"wcc-down-event-fires-action",wcagCriterion:"2.5.2",wcagLevel:"A",impact:"serious",description:"Element's mousedown/pointerdown handler fires a navigation, submit, or network action. WCAG 2.5.2 requires that single-pointer actions execute on UP-EVENT (mouseup, click), not down-event, so users can abort by dragging away. Move the action to the click handler or onmouseup.",helpUrl:`${c}pointer-cancellation.html`,element:n,failureSummary:"Action fires on pointer-down; user cannot abort by dragging away before release."})}return t}function k(e=document){const t=[],r=e.querySelectorAll("script:not([src])");for(const a of Array.from(r)){const n=a.textContent??"";if(/\b(devicemotion|deviceorientation)\b/.test(n)&&/\baddEventListener\s*\(\s*['"`](devicemotion|deviceorientation)/.test(n)){t.push({ruleId:"wcc-motion-actuation-no-alt",wcagCriterion:"2.5.4",wcagLevel:"A",impact:"serious",description:"Page registers a `devicemotion` or `deviceorientation` listener (shake / tilt to act). WCAG 2.5.4 requires that any function triggered by device motion is ALSO operable via standard UI controls AND can be disabled to prevent accidental actuation. Add an on-screen button equivalent.",helpUrl:`${c}motion-actuation.html`,element:a,failureSummary:"Device-motion listener present. Confirm UI-control alternative + disable mechanism."});break}}return t}function x(e=document){const t=[],r=e.querySelectorAll("form");for(const a of Array.from(r)){const n=a.querySelectorAll("input[required], select[required], textarea[required]");if(n.length===0||Array.from(n).some(s=>s.hasAttribute("aria-describedby")||s.hasAttribute("aria-invalid")||s.hasAttribute("aria-errormessage")))continue;const o=/\b(required|please|must|\*)\b/i.test(a.textContent??"");t.push({ruleId:"wcc-form-no-aria-error-pattern",wcagCriterion:"3.3.1",wcagLevel:"A",impact:o?"moderate":"serious",description:`Form has ${n.length} required field${n.length===1?"":"s"} but no fields use ARIA error patterns (\`aria-invalid\`, \`aria-describedby\`, \`aria-errormessage\`). WCAG 3.3.1 requires that input errors be identified in text the user can perceive. Browser-default HTML5 validation bubbles aren't reliably announced by all screen readers; wire each required field to a visible error region via aria-describedby and toggle aria-invalid when validation fails.`,helpUrl:`${c}error-identification.html`,element:a,failureSummary:`${n.length} required input(s) without ARIA error pattern.`})}return t}function p(e){if(e.id)return`#${e.id}`;const t=e.classList[0];return`${e.tagName.toLowerCase()}${t?"."+t:""}`}function $(e=document){return[...f(e),...g(e),...y(e),...A(e),...w(e),...v(e),...b(e),...C(e),...S(e),...q(e),...k(e),...x(e)]}const I=["2.2.1","2.2.2","2.3.1","2.5.1","2.5.7","1.2.1","1.2.2","1.2.5","3.2.1","3.2.2","3.3.8","1.3.4","1.4.4","1.4.13","2.4.5","2.5.2","2.5.4","3.3.1"];export{I as DOM_ANALYZER_COVERED_CRITERIA,g as analyzeAnimation,v as analyzeCognitiveAuth,C as analyzeContentOnHover,w as analyzeContextChangeOnFocusInput,x as analyzeErrorIdentification,A as analyzeMedia,k as analyzeMotionActuation,S as analyzeMultipleWays,b as analyzeOrientation,q as analyzePointerCancellation,y as analyzePointerGestures,f as analyzeTimingAdjustable,$ as runAllDomCriterionAnalyzers};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import{o as E,e as P,c as Re}from"./crash-reporter-Dc5lvxvY.js";import{c as re,T as qe,d as me}from"./state-BIsozXtc.js";async function Bt(){const e=await chrome.tabs.query({currentWindow:!0}),t=e.find(o=>o.active);if(t!=null&&t.id&&se(t.url))return t.id;const n=e.find(o=>o.id&&se(o.url));return(n==null?void 0:n.id)??null}function se(e){return typeof e=="string"&&/^https?:\/\//.test(e)}const K="acknowledgedFindings";async function ne(){var n;if(typeof chrome>"u"||!((n=chrome.storage)!=null&&n.local))return[];const t=(await chrome.storage.local.get([K]))[K];return Array.isArray(t)?t:[]}async function jt(e){const t=await ne();return new Set(t.filter(n=>n.componentId===e).map(n=>n.matchKey))}async function $t(e){const n=(await ne()).filter(o=>!(o.componentId===e.componentId&&o.matchKey===e.matchKey));n.push({...e,acknowledgedAt:new Date().toISOString()}),await ge(n)}async function Yt(e,t){const o=(await ne()).filter(r=>!(r.componentId===e&&r.matchKey===t));await ge(o)}async function ge(e){var t;typeof chrome>"u"||!((t=chrome.storage)!=null&&t.local)||await chrome.storage.local.set({[K]:e})}const q="license:cache",Oe=60*60*1e3,Me=24*60*60*1e3,We=7*24*60*60*1e3;async function M(){return(await chrome.storage.local.get(q))[q]??null}async function C(e){await chrome.storage.local.set({[q]:e})}async function R(){await chrome.storage.local.remove(q)}function Le(e,t=Date.now()){const n=t-e.validatedAt;return n<Oe?"fresh":n<Me?"stale":n<We?"grace":"expired"}const w=Re("license-gate"),Fe="https://api.wcagcheckr.com",fe="wcagcheckr",Pe=`${Fe}/v1/products/${fe}/license/validate`,_e=1e4,Ne=`https://${fe}.com/upgrade`,_="trialStartedAt",H="licenseToken";async function ve(){let t=(await chrome.storage.local.get(_))[_];return t||(t=Date.now(),await chrome.storage.local.set({[_]:t})),t}function Ue(e,t=Date.now()){return t-e>me*864e5}async function B(){return(await chrome.storage.local.get(H))[H]??null}async function Ge(e){await chrome.storage.local.set({[H]:e})}async function N(e){const t=await fetch(Pe,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({token:e}),signal:AbortSignal.timeout(_e)});if(!t.ok)throw new Error(`validate http ${t.status}`);return{type:"LICENSE_VALIDATE_RESPONSE",...await t.json()}}async function T(e){const t=await M();if(t&&t.token===e){const o=Le(t);if(o==="fresh")return t.response;if(o==="stale")return N(e).then(r=>C({token:e,validatedAt:Date.now(),response:r})).catch(r=>w.warn("background refresh failed",r)),t.response;if(o==="grace")try{const r=await N(e);return await C({token:e,validatedAt:Date.now(),response:r}),r}catch(r){return w.warn("grace-period revalidation failed; using last-known-good",r),t.response}}const n=await N(e);return await C({token:e,validatedAt:Date.now(),response:n}),n}function Ve(e){return e==="solo"||e==="solo-yearly"||e==="solo-single-month"?"solo":e==="team"||e==="team-15"||e==="team-yearly"||e==="team-15-yearly"?"team":"free"}async function D(){const e=await B();if(e)try{const n=await T(e);if(n.valid&&n.plan)return Ve(n.plan)}catch(n){w.warn("validation failed, falling back",n)}const t=await ve();return Ue(t)?"free":"trial"}async function ze(){const e=await M(),t=e==null?void 0:e.response.trialEndsAt;if(t){const r=new Date(t).getTime()-Date.now();return Math.max(0,Math.ceil(r/864e5))}const n=await ve(),o=me*864e5-(Date.now()-n);return Math.max(0,Math.ceil(o/864e5))}async function Ke(){const e=await M(),t=e==null?void 0:e.response.features;if(!t||typeof t!="object")return{};const n=t.seatsUsed,o=t.seatsTotal;return{seatsUsed:typeof n=="number"?n:void 0,seatsTotal:typeof o=="number"?o:void 0}}function He(e,t){const n=qe[e];switch(t){case"audit:multi-component":return n.maxComponents!==1;case"audit:state-matrix":return n.stateMatrix!=="default-only";case"storybook:auto-iterate":return n.storybookAutoIterate;case"export:json":return n.exportJson;case"export:sarif":return n.exportSarif;case"export:junit":return n.exportJunit;case"cloud-sync":return n.cloudSync}}function Jt(){const e=[];e.push(E("TIER_GET",async r=>{r.forceRefresh&&await R();const s=await D(),i={type:"TIER_GET_RESPONSE",tier:s};if(s==="trial")i.trialDaysRemaining=await ze();else if(s==="team"){const{seatsUsed:l,seatsTotal:a}=await Ke();l!==void 0&&(i.seatsUsed=l),a!==void 0&&(i.seatsTotal=a)}if(s!=="trial"){const l=await M();if(l!=null&&l.response.valid&&l.response.plan){if(i.planCode=l.response.plan,l.response.plan==="solo-single-month"&&l.response.expiresAt){const a=new Date(l.response.expiresAt).getTime()-Date.now();i.licenseDaysRemaining=Math.max(0,Math.ceil(a/864e5))}l.response.pastDueSince&&(i.pastDue=!0)}}return i})),e.push(E("LICENSE_CHECK_REQUEST",async r=>{const s=await D(),i=He(s,r.feature);return{type:"LICENSE_CHECK_RESPONSE",allowed:i,tier:s,reason:i?void 0:`${r.feature} requires a higher tier`,upgradeUrl:i?void 0:Ne}})),e.push(E("LICENSE_VALIDATE_REQUEST",async r=>{try{if(r.forceRefresh){await R();const s=await T(r.token);return P({type:"LICENSE_CHANGED_EVENT",tier:await D()}),s}return await T(r.token)}catch(s){return w.error("validate failed",s),{type:"LICENSE_VALIDATE_RESPONSE",valid:!1,plan:null,email:null,status:null,expiresAt:null}}})),e.push(E("LICENSE_SET_REQUEST",async r=>{await Ge(r.token),await R();let s=!1,i={ok:!1,reason:"not-attempted"};try{const a=await T(r.token);if(await C({token:r.token,validatedAt:Date.now(),response:a}),s=a.valid===!0,s){const c=await re(r.token);c.ok?i={ok:!0,seatsUsed:c.seatsUsed,seatsTotal:c.seatsTotal,overCapacity:c.overCapacity}:i=c}}catch(a){w.warn("validation after set failed",a)}const l=await D();return P({type:"LICENSE_CHANGED_EVENT",tier:l}),{type:"LICENSE_SET_RESPONSE",validated:s,seatClaim:i}})),(async()=>{try{const r=await B();r&&await re(r)}catch(r){w.debug("boot-time seat claim failed",r)}})();const t="license:periodic-refresh",n=60;chrome.alarms.get(t,r=>{r||chrome.alarms.create(t,{periodInMinutes:n})});const o=async r=>{if(r.name===t)try{const s=await B();if(!s)return;await R();const i=await T(s);await C({token:s,validatedAt:Date.now(),response:i}),P({type:"LICENSE_CHANGED_EVENT",tier:await D()}),w.debug("periodic license refresh complete")}catch(s){w.warn("periodic license refresh failed",s)}};return chrome.alarms.onAlarm.addListener(o),e.push(()=>chrome.alarms.onAlarm.removeListener(o)),w.info("handlers registered"),()=>e.forEach(r=>r())}const Be={"ai-alt-misleading":{summary:`The AI vision model judged this image's alt text is empty, misleading, or generic when the image is actually informative or functional. Use the AI assessment (shown above this recipe) to understand what the image depicts, then write descriptive alt text conveying its meaning to a screen-reader user. Use alt="" only when the image is genuinely decorative (a divider, a flourish, an icon next to text that already labels the action). Decorative is rare — when in doubt, describe.`,snippetLang:"html",snippet:`<!-- Failing — empty alt on an informational logo -->
|
|
2
|
+
<img src="/assets/logo.png" alt="">
|
|
3
|
+
|
|
4
|
+
<!-- Passing — descriptive alt -->
|
|
5
|
+
<img src="/assets/logo.png" alt="Acme Corp logo">
|
|
6
|
+
|
|
7
|
+
<!-- Decorative (rare) — empty alt + aria-hidden -->
|
|
8
|
+
<img src="/divider.svg" alt="" aria-hidden="true">`},"ai-alt-uncertain":{summary:"The AI could not verify whether the existing alt text accurately matches the image (typically: a person's name on a portrait, a product SKU on a packshot — plausible but not provable from pixels). Surface this finding to a human content owner who knows the actual subject. Confirm or correct the alt. No code change needed if the alt is already accurate.",snippetLang:"html",snippet:`<!-- Example: AI sees a person, can't confirm identity -->
|
|
9
|
+
<img src="/team/cliff.jpg" alt="Cliff C. - Founder of Locustware">
|
|
10
|
+
<!-- Action: verify the named person matches the image. If yes, leave it. -->`},"ai-heading-not-descriptive":{summary:'WCAG 2.4.6 — Headings and labels must describe the topic or purpose of the section they introduce. The AI judged this heading is too generic ("More info", "Section 1", "Welcome") to convey what its section actually covers. Rewrite the heading to summarise the section content in a handful of words. Screen-reader users navigate by heading; vague headings make the page unscannable.',snippetLang:"html",snippet:`<!-- Failing -->
|
|
11
|
+
<h2>More info</h2>
|
|
12
|
+
<p>Our 2024 financial results show ...</p>
|
|
13
|
+
|
|
14
|
+
<!-- Passing -->
|
|
15
|
+
<h2>2024 financial results</h2>
|
|
16
|
+
<p>Our 2024 financial results show ...</p>`},"ai-sensory-instruction":{summary:'WCAG 1.3.3 — Instructions must not rely solely on sensory characteristics (shape, size, colour, position, sound). "Click the round green button" excludes users who cannot perceive colour or shape. Add a programmatic identifier — visible text label, accessible name, or labelled reference — that does not depend on visual or auditory perception.',snippetLang:"html",snippet:`<!-- Failing -->
|
|
17
|
+
<p>Click the green button on the right to proceed.</p>
|
|
18
|
+
|
|
19
|
+
<!-- Passing -->
|
|
20
|
+
<p>Click the <strong>"Proceed"</strong> button to continue.</p>`},"ai-aria-misuse":{summary:`WCAG 4.1.2 — ARIA role must match the element's actual behaviour. The AI judged this element uses an ARIA role that does not fit its DOM structure or behaviour. First rule of ARIA: don't use ARIA when a native element works. Prefer <button> over <div role="button">, <nav> over <div role="navigation">, etc. If the role IS appropriate, verify all required ARIA attributes (aria-expanded, aria-controls, aria-label, aria-selected, etc.) are present and accurate.`,snippetLang:"html",snippet:`<!-- Failing — div claims to be a button -->
|
|
21
|
+
<div role="button" onclick="...">Submit</div>
|
|
22
|
+
|
|
23
|
+
<!-- Passing — use the native element -->
|
|
24
|
+
<button type="button" onclick="...">Submit</button>`},"ai-image-of-text":{summary:"WCAG 1.4.5 — Text rendered into a raster image (banner copy, infographic labels, social-share text, quote graphics) does not scale, does not respect user font preferences, can't reflow, and is only reachable via alt text. Replace with real HTML text styled to match the previous design. Exceptions: logos, brand wordmarks, and product photography where text is incidental.",snippetLang:"html",snippet:`<!-- Failing — text baked into image -->
|
|
25
|
+
<img src="/banners/big-quote.png" alt="Customer testimonial: 'Best service ever.'">
|
|
26
|
+
|
|
27
|
+
<!-- Passing — real text -->
|
|
28
|
+
<figure class="testimonial">
|
|
29
|
+
<blockquote>Best service ever.</blockquote>
|
|
30
|
+
<figcaption>— a happy customer</figcaption>
|
|
31
|
+
</figure>`},"ai-color-only-meaning":{summary:"WCAG 1.4.1 — Information conveyed by colour alone is invisible to users with colour-vision deficiency, monochrome displays, or low-contrast environments. Pair every colour signal with a redundant text label, icon, shape, or pattern. Status indicators, form validation, chart series, table-row highlights — wherever colour carries meaning, add a secondary cue.",snippetLang:"html",snippet:`<!-- Failing — status conveyed only by background colour -->
|
|
32
|
+
<tr style="background:#fee">Failed run</tr>
|
|
33
|
+
<tr style="background:#efe">Passed run</tr>
|
|
34
|
+
|
|
35
|
+
<!-- Passing — colour + icon + text -->
|
|
36
|
+
<tr><td>❌ Failed</td>...</tr>
|
|
37
|
+
<tr><td>✅ Passed</td>...</tr>`},"ai-generic-link-text":{summary:'WCAG 2.4.4 — Link purpose must be clear from the link text alone OR the link text plus its immediate surrounding context. "Click here", "Learn more", "Read this", and similarly vague text fail when a screen-reader user navigates a list of links out of context. Rewrite the link to describe its destination, ensure the surrounding sentence makes the destination unambiguous, or use aria-label / aria-labelledby to attach a descriptive name.',snippetLang:"html",snippet:`<!-- Failing — link text alone is meaningless -->
|
|
38
|
+
<p>Our pricing has changed. <a href="/pricing">Click here</a>.</p>
|
|
39
|
+
|
|
40
|
+
<!-- Passing — link text is self-describing -->
|
|
41
|
+
<p>Our pricing has changed. <a href="/pricing">See our 2025 pricing tiers</a>.</p>`},"ai-wall-of-text":{summary:"WCAG 3.1.5 — Long unbroken prose with no scannable structure is exhausting to read and inaccessible to users with cognitive disabilities. Break the block into shorter paragraphs (aim for 50-150 words each), add a subheading every 2-4 paragraphs, convert lists-in-prose into actual <ul> / <ol>, and use plain language where possible.",snippetLang:"html",snippet:`<!-- Failing — one giant paragraph -->
|
|
42
|
+
<p>Our process is straightforward we start by gathering requirements then we design the solution then we implement it then we test it then we deploy it then we support it ongoing ...</p>
|
|
43
|
+
|
|
44
|
+
<!-- Passing — chunked with subheadings -->
|
|
45
|
+
<h3>1. Requirements</h3>
|
|
46
|
+
<p>We start by gathering your specifics.</p>
|
|
47
|
+
<h3>2. Design</h3>
|
|
48
|
+
<p>We map out the solution before writing code.</p>`},"ai-language-mismatch":{summary:`WCAG 3.1.1 / 3.1.2 — The page or passage's declared language attribute does not match the actual content language. Screen readers use the lang attribute to choose pronunciation rules; mismatch produces unintelligible speech. Either correct the <html lang=""> declaration to match the content, or add lang="" to any sub-passage that is in a different language than the page default.`,snippetLang:"html",snippet:`<!-- Failing — content is English but lang says French -->
|
|
49
|
+
<html lang="fr"><body>...English content...</body></html>
|
|
50
|
+
|
|
51
|
+
<!-- Passing — correct page-level declaration -->
|
|
52
|
+
<html lang="en"><body>...English content...</body></html>
|
|
53
|
+
|
|
54
|
+
<!-- Passing — passage-level override for non-default language -->
|
|
55
|
+
<p>The German term is <span lang="de">Schadenfreude</span>.</p>`},"wcagcheckr-reflow":{summary:"WCAG 1.4.10 Reflow requires content to be presentable at 320 CSS pixels wide without requiring horizontal scrolling, except for data tables, code blocks, maps, and other intrinsically wide content. The usual culprits: fixed widths in px, hard-coded layout containers, white-space: nowrap on long text, large images without max-width, and horizontal scroll containers that exceed the viewport.",snippetLang:"css",snippet:`/* Failing: fixed-width container forces horizontal scroll at 320px */
|
|
56
|
+
.container { width: 1024px; }
|
|
57
|
+
|
|
58
|
+
/* Passing: use max-width and let content reflow */
|
|
59
|
+
.container { max-width: 1024px; width: 100%; }
|
|
60
|
+
|
|
61
|
+
/* Prevent images / media from forcing overflow */
|
|
62
|
+
img, video, iframe { max-width: 100%; height: auto; }
|
|
63
|
+
|
|
64
|
+
/* Avoid hard nowrap on long inline content */
|
|
65
|
+
.long-string { overflow-wrap: anywhere; }`},"color-contrast":{summary:"Ensure foreground and background colors meet WCAG 2 AA contrast ratios: 4.5:1 for normal text, 3:1 for large text (18pt+ regular or 14pt+ bold). Check both default and interactive states (:hover, :focus) — a hover-only contrast failure is the most common regression.",snippetLang:"css",snippet:`/* Failing: light gray on white = 1.85:1 */
|
|
66
|
+
.muted { color: #d3d3d3; }
|
|
67
|
+
|
|
68
|
+
/* Passing: dark slate on white = 12.6:1 */
|
|
69
|
+
.muted { color: #475569; }`},"image-alt":{summary:'Every <img> must have an alt attribute. Use empty alt="" for decorative images (the browser then announces nothing); use descriptive text for informative images.',snippetLang:"html",snippet:`<!-- Decorative -->
|
|
70
|
+
<img src="divider.svg" alt="" />
|
|
71
|
+
|
|
72
|
+
<!-- Informative -->
|
|
73
|
+
<img src="chart.png" alt="Q3 revenue rose 42%" />`},label:{summary:'Every form input needs a programmatic label. Either wrap the input in a <label>, or use <label for="..."> matching the input id, or use aria-label / aria-labelledby. Placeholder text alone is NOT a label.',snippetLang:"html",snippet:`<!-- ✅ Wrapping label -->
|
|
74
|
+
<label>Email <input type="email" name="email" /></label>
|
|
75
|
+
|
|
76
|
+
<!-- ✅ for/id pair -->
|
|
77
|
+
<label for="email">Email</label>
|
|
78
|
+
<input id="email" type="email" name="email" />
|
|
79
|
+
|
|
80
|
+
<!-- ✅ aria-label when visual label isn't possible -->
|
|
81
|
+
<input type="search" aria-label="Search products" />`},"button-name":{summary:"Every <button> needs an accessible name. Provide visible text, an aria-label, or aria-labelledby. Icon-only buttons especially need aria-label so screen reader users hear what the button does.",snippetLang:"html",snippet:`<!-- ✅ Visible text -->
|
|
82
|
+
<button>Save changes</button>
|
|
83
|
+
|
|
84
|
+
<!-- ✅ Icon-only with aria-label -->
|
|
85
|
+
<button aria-label="Close dialog">
|
|
86
|
+
<svg aria-hidden="true">...</svg>
|
|
87
|
+
</button>`},"link-name":{summary:'Links must have discernible text. Avoid empty <a> tags or links containing only an icon without an aria-label. "Read more" links should reference what they read more about via aria-label or surrounding context.',snippetLang:"html",snippet:`<!-- ❌ Generic -->
|
|
88
|
+
<a href="/post/42">Read more</a>
|
|
89
|
+
|
|
90
|
+
<!-- ✅ Specific -->
|
|
91
|
+
<a href="/post/42">Read more about Q3 revenue</a>
|
|
92
|
+
|
|
93
|
+
<!-- ✅ Or with aria-label -->
|
|
94
|
+
<a href="/post/42" aria-label="Read more about Q3 revenue">Read more</a>`},"heading-order":{summary:"Don't skip heading levels. After an <h2>, the next heading should be <h2> or <h3> — never <h4>. Screen reader users navigate by heading level, and skipped levels imply missing structure.",snippetLang:"html",snippet:`<!-- ❌ -->
|
|
95
|
+
<h1>Page title</h1>
|
|
96
|
+
<h3>Section</h3>
|
|
97
|
+
|
|
98
|
+
<!-- ✅ -->
|
|
99
|
+
<h1>Page title</h1>
|
|
100
|
+
<h2>Section</h2>`},region:{summary:"Wrap top-level content in a landmark element so screen reader users can navigate by region. Use <main> for primary content, <nav> for navigation, <aside> for complementary content, <footer> for the page footer.",snippetLang:"html",snippet:`<header>...</header>
|
|
101
|
+
<nav aria-label="Primary">...</nav>
|
|
102
|
+
<main>
|
|
103
|
+
<h1>Page title</h1>
|
|
104
|
+
...
|
|
105
|
+
</main>
|
|
106
|
+
<footer>...</footer>`},"landmark-one-main":{summary:`Every page should have exactly one <main> element (or one element with role="main"). It marks the start of the page's primary content for screen reader users.`,snippetLang:"html",snippet:`<body>
|
|
107
|
+
<header>...</header>
|
|
108
|
+
<main>
|
|
109
|
+
<!-- primary content -->
|
|
110
|
+
</main>
|
|
111
|
+
<footer>...</footer>
|
|
112
|
+
</body>`},"landmark-no-duplicate-banner":{summary:'A page should have at most one <header> at the top level (or one [role="banner"]). Multiple top-level banners confuse landmark navigation.'},"landmark-no-duplicate-contentinfo":{summary:'A page should have at most one <footer> at the top level (or one [role="contentinfo"]). Multiple top-level footers confuse landmark navigation.'},"aria-required-attr":{summary:'When using an ARIA role, you must include all required attributes for that role. For example, role="checkbox" requires aria-checked; role="combobox" requires aria-expanded.',snippetLang:"html",snippet:`<!-- ❌ Missing aria-checked -->
|
|
113
|
+
<div role="checkbox">Subscribe</div>
|
|
114
|
+
|
|
115
|
+
<!-- ✅ -->
|
|
116
|
+
<div role="checkbox" aria-checked="false" tabindex="0">Subscribe</div>`},"aria-allowed-attr":{summary:"Some ARIA attributes only apply to specific roles. For example, aria-required is only valid on form widgets; putting it on a <div> with no role is wrong. Either remove the attribute or add the appropriate role."},"aria-roles":{summary:'The role attribute value must be a valid ARIA role from the WAI-ARIA spec. Common typos: "buton", "tab-list", "menuitem-radio". Check the WAI-ARIA spec for the exact role name.'},"aria-valid-attr":{summary:"Use ARIA attribute names exactly as specified in WAI-ARIA. Common typos: aria-labeledby (should be aria-labelledby with two L's), aria-describeby (should be aria-describedby)."},"duplicate-id":{summary:"IDs must be unique within a document. Duplicate IDs break label association, fragment navigation, and aria-labelledby/aria-describedby references."},"duplicate-id-aria":{summary:"IDs referenced by ARIA attributes (aria-labelledby, aria-describedby, aria-controls) must be unique. Duplicate IDs cause assistive tech to associate the wrong element."},"empty-button":{summary:"A button with no text and no aria-label has no name for screen readers. Add visible text or an aria-label describing what the button does."},"empty-heading":{summary:"Heading elements should contain text. An empty heading provides no information and confuses screen reader heading navigation. Either add text or remove the heading."},"empty-link":{summary:'A link with no text and no aria-label has no name. Screen reader users hear "link" with no destination cue. Add link text or aria-label.'},"form-field-multiple-labels":{summary:'A form field should have one label, not multiple. Multiple labels (e.g., wrapping <label> + separate <label for="">) confuse assistive tech about which is the canonical name.'},"html-has-lang":{summary:"The root <html> element needs a lang attribute so screen readers use the correct pronunciation rules.",snippetLang:"html",snippet:'<html lang="en">'},"html-lang-valid":{summary:`The lang attribute value must be a valid IANA language tag. Use "en" for English, "es" for Spanish, "fr-CA" for Canadian French, etc. Don't invent codes.`},"input-button-name":{summary:'Input elements with type="button", type="submit", or type="reset" must have an accessible name via the value attribute or aria-label.',snippetLang:"html",snippet:'<input type="submit" value="Save changes" />'},"input-image-alt":{summary:"Image-type inputs must have alt text describing the button's action.",snippetLang:"html",snippet:'<input type="image" src="search.png" alt="Search" />'},"meta-viewport":{summary:"Don't set user-scalable=no or maximum-scale less than 5.0 on the viewport meta. That blocks low-vision users from zooming.",snippetLang:"html",snippet:`<!-- ✅ Allows zoom -->
|
|
117
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
118
|
+
|
|
119
|
+
<!-- ❌ Blocks zoom -->
|
|
120
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />`},"nested-interactive":{summary:"Don't put interactive elements inside other interactive elements. A button inside a link, or a link inside a button, breaks keyboard activation and screen reader announcements."},"presentation-role-conflict":{summary:`role="presentation" or role="none" removes an element's semantics. Don't apply it to elements that have important interactive properties (focusable, named) — those will conflict and be ignored.`},"select-name":{summary:'Every <select> needs a programmatic label, just like <input> elements. Use <label for=""> or aria-label.'},"target-size":{summary:"Interactive targets must be at least 24×24 CSS px (WCAG 2.5.8 AA) or 44×44 (2.5.5 AAA). Small touch targets are hard to hit accurately, especially on mobile and for users with motor impairments. If the target itself can't be larger, ensure 24px of clear spacing between adjacent targets.",snippetLang:"css",snippet:`/* Pad the hit area without changing visual size */
|
|
121
|
+
.icon-button {
|
|
122
|
+
width: 24px;
|
|
123
|
+
height: 24px;
|
|
124
|
+
/* Ensures clickable area is 44×44 */
|
|
125
|
+
padding: 10px;
|
|
126
|
+
background-clip: content-box;
|
|
127
|
+
}`},"valid-lang":{summary:'lang attribute values on individual elements (e.g., <span lang="es">) must be valid IANA language tags, just like the root html lang.'},"autocomplete-valid":{summary:'autocomplete attribute values must come from the WHATWG list. Common valid values: "name", "email", "tel", "current-password", "new-password", "street-address", "postal-code". Browsers and password managers rely on these to suggest the right values.'},bypass:{summary:'Provide a way for keyboard users to skip past repetitive page content. Standard pattern: a "Skip to main content" link as the first focusable element, hidden offscreen but visible on focus.',snippetLang:"html",snippet:`<a href="#main" class="skip-link">Skip to main content</a>
|
|
128
|
+
<!-- ... -->
|
|
129
|
+
<main id="main">...</main>`}};function Qt(e){return Be[e]??null}const j="aiColorSuggestions";async function W(){try{const t=(await chrome.storage.local.get(j))[j];return t&&typeof t=="object"?t:{}}catch{return{}}}async function ye(e){await chrome.storage.local.set({[j]:e})}async function Xt(e){const t=await W();t[e.matchKey]=e,await ye(t)}async function Zt(e){if(e.length===0)return;const t=await W();for(const n of e)t[n.matchKey]=n;await ye(t)}async function en(e){return(await W())[e]??null}async function tn(){return await W()}const je=/:nth-(?:last-)?(?:child|of-type)\([^)]+\)/g,$e=/\[id="[^"]*"\]/g,Ye=/\[data-testid="[^"]*"\]/g,Je=new Set(["region","landmark-one-main","page-has-heading-one","landmark-no-duplicate-banner","landmark-no-duplicate-contentinfo","landmark-no-duplicate-main","landmark-main-is-top-level","landmark-banner-is-top-level","landmark-contentinfo-is-top-level","landmark-complementary-is-top-level","landmark-unique","bypass","document-title","html-has-lang","meta-viewport"]);function Qe(e){return e&&e.replace(je,"").replace($e,"").replace(Ye,"").replace(/\s+/g," ").trim()}function Xe(e,t){return Je.has(e)?`${e}::__structural__`:`${e}::${Qe(t)}`}const le={minor:0,moderate:1,serious:2,critical:3};function Ze(e){const t=new Map;for(const n of e){const o=Xe(n.ruleId,n.target.selector),r=t.get(o);if(!r){t.set(o,n);continue}(le[n.impact]??0)>(le[r.impact]??0)&&t.set(o,n)}return Array.from(t.values())}const $="interactiveAuditResults";function ae(e,t){return`${e}::${t}`}async function L(){try{const t=(await chrome.storage.local.get($))[$];return t&&typeof t=="object"?t:{}}catch{return{}}}async function be(e){await chrome.storage.local.set({[$]:e})}async function nn(e){const t=await L(),n=JSON.stringify(e).length;let o=e;n>2*1024*1024&&(o={...e,steps:e.steps.map(r=>({...r,screenshot:void 0,axContext:void 0}))}),t[ae(e.componentId,e.criterionId)]=o,await be(t)}async function an(e,t){return(await L())[ae(e,t)]??null}async function et(){const e=await L();return Object.values(e)}async function on(e){return(await et()).filter(n=>n.componentId===e)}async function rn(e,t){const n=await L();delete n[ae(e,t)],await be(n)}const tt={id:"keyboard",name:"Keyboard navigation",blurb:"Verify everyone who relies on the keyboard can reach, operate, and escape every interactive element. WCAG 2.1.1, 2.1.2, 2.4.3, 2.4.7.",steps:[{id:"tab-reaches-all",prompt:"Tab through the component from start to finish. Compare against the Tab-order overlay.",question:"Did Tab reach every interactive element (button, link, form field, custom widget)?",wcag:"2.1.1",visualizer:"tab-order",severity:"required"},{id:"tab-order-logical",prompt:"Watch the focus indicator as you Tab. Compare visual order vs the Tab-order numbered overlay.",question:"Does focus move in a logical order matching the visual layout (left-to-right, top-to-bottom)? Red badges indicate visual mismatches.",wcag:"2.4.3",visualizer:"tab-order",severity:"required",autoDismissActivity:"tab-order"},{id:"focus-visible",prompt:"On each focused element, confirm a visible focus indicator (outline, ring, color change).",question:"Is the focus indicator clearly visible on every interactive element?",wcag:"2.4.7",relatedAxeRule:"focus-order-semantics",severity:"required"},{id:"no-trap",prompt:"Tab forward and Shift+Tab backward through any modal, dropdown, or custom widget.",question:"Can you escape the widget with Tab/Shift+Tab/Esc — no trapping?",wcag:"2.1.2",severity:"required"},{id:"enter-activates",prompt:"Focus a button or link and press Enter (and Space for buttons).",question:"Did Enter/Space activate the control as expected?",wcag:"2.1.1",severity:"required"},{id:"esc-closes",prompt:"Open any modal, dropdown, or popover and press Escape.",question:"Did Escape close the overlay and return focus to the trigger?",wcag:"2.1.2",severity:"required"},{id:"arrow-keys-grouped",prompt:"For grouped widgets (radio, tabs, listbox, menu): focus into the group, then use arrow keys.",question:"Do arrow keys move within the group as expected (and Tab moves out of it)?",wcag:"2.1.1",severity:"advisory"},{id:"character-key-shortcuts",prompt:'Identify any single-character keyboard shortcuts (e.g., "S" to open search, "/" to focus the search box, J/K for next/prev). If none exist, mark N/A.',question:"For every single-character shortcut: can it be turned off, remapped to a modifier-key combo, OR is it only active when the relevant control has focus? (Prevents accidental activation by speech-input users.)",wcag:"2.1.4",severity:"required"}]},nt={id:"screen-reader",name:"Screen reader readiness",blurb:"Verify the component is operable and meaningful via a screen reader. Use NVDA (Windows), VoiceOver (macOS/iOS), or TalkBack (Android). WCAG 1.3.1, 4.1.2.",steps:[{id:"sr-announces-on-focus",prompt:"Activate your screen reader and Tab to each interactive element.",question:"Does the SR announce a meaningful name + role + state for every focused element?",wcag:"4.1.2",relatedAxeRule:"aria-required-attr",severity:"required"},{id:"sr-headings-navigate",prompt:"Use the SR's heading-navigation shortcut (NVDA: H, VoiceOver: VO+Cmd+H). Compare against the Outline overlay.",question:"Are all logical sections reachable via heading navigation, in correct hierarchy order?",wcag:"1.3.1",relatedAxeRule:"heading-order",visualizer:"outline",severity:"required"},{id:"sr-reading-order",prompt:"Read the page top-to-bottom using the SR's read-all command (NVDA: NumPad+Down, VoiceOver: VO+A). Compare against the Reading-order numbered overlay.",question:"Does the SR read content in the meaningful order implied by the visual layout (1.3.2 Meaningful Sequence)? Red badges in the Reading-order overlay show DOM-vs-visual divergences.",wcag:"1.3.2",visualizer:"reading-order",severity:"required",autoDismissActivity:"reading-order"},{id:"sr-landmarks",prompt:"Use the SR's landmark-navigation shortcut (NVDA: D, VoiceOver: VO+U → Landmarks). Compare against the Outline overlay.",question:"Can the user jump to main, nav, banner, contentinfo via landmark navigation?",wcag:"1.3.1",relatedAxeRule:"region",visualizer:"outline",severity:"required"},{id:"sr-live-region",prompt:"Trigger any dynamic update (form submission, status change, toast).",question:'Does the SR announce the change automatically (via aria-live or role="status"/role="alert")?',wcag:"4.1.3",severity:"required"},{id:"sr-form-errors",prompt:"Submit a form with an invalid field.",question:"Does the SR announce the error and which field it relates to?",wcag:"3.3.1",severity:"required"},{id:"sr-images-meaningful",prompt:"Navigate over each image (NVDA: G, VoiceOver: VO+Cmd+G).",question:"Does each meaningful image have descriptive alt text? Are decorative images silent?",wcag:"1.1.1",relatedAxeRule:"image-alt",severity:"required"}]},at={id:"focus-management",name:"Focus management",blurb:"Verify focus moves and restores correctly across overlays, navigations, and dynamic content. WCAG 2.4.3, 2.4.7, 3.2.2.",steps:[{id:"modal-initial-focus",prompt:"Open a modal/dialog.",question:"Did focus move into the modal automatically (typically to the first focusable element or a heading)?",severity:"required"},{id:"modal-restored-on-close",prompt:"Close a modal/dialog (via close button or Esc).",question:"Was focus restored to the element that opened it?",severity:"required"},{id:"modal-trap-while-open",prompt:"While a modal is open, Tab/Shift+Tab through it.",question:"Is focus trapped inside the modal until it closes?",severity:"required"},{id:"route-change-focus",prompt:"Trigger a client-side route change.",question:"Did focus move sensibly (to the new page heading, or main landmark) — not to <body>?",severity:"required"},{id:"expanded-focus",prompt:"Open an expandable section (accordion, dropdown, collapsible).",question:"Is focus moved into or near the newly-revealed content (or kept on the trigger appropriately)?",severity:"advisory"},{id:"no-unexpected-focus-move",prompt:"Type or click in form fields without explicitly tabbing.",question:"Does focus stay where you put it (no auto-jump on input)?",wcag:"3.2.2",severity:"required"},{id:"no-context-change-on-focus",prompt:"Tab into each interactive element on the page.",question:"Does merely RECEIVING focus on an element NOT trigger a context change (auto-submit, open modal, navigate elsewhere, change focus to a different field)? (Context change on focus violates 3.2.1; activating a control via click/Enter is fine.)",wcag:"3.2.1",severity:"required"}]},it={id:"forms",name:"Forms",blurb:"Verify form fields are properly labeled, errors are clear, and the experience works for assistive technology. WCAG 1.3.1, 3.3.1, 3.3.2, 4.1.2.",steps:[{id:"visible-labels",prompt:"Look at every form field.",question:"Does every field have a visible label (not just placeholder text)?",wcag:"3.3.2",relatedAxeRule:"label",severity:"required"},{id:"label-association",prompt:"Click each visible label.",question:"Did clicking the label focus the associated input?",wcag:"1.3.1",relatedAxeRule:"label",severity:"required"},{id:"required-marked",prompt:"Identify required fields.",question:"Are required fields marked both visually AND programmatically (aria-required or required attribute)?",wcag:"3.3.2",severity:"required"},{id:"autocomplete-set",prompt:"For personal-info fields (name, email, phone, address): inspect the autocomplete attribute.",question:'Is autocomplete set with a valid HTML5 token (e.g., "email", "given-name", "tel")?',wcag:"1.3.5",relatedAxeRule:"autocomplete-valid",severity:"required"},{id:"error-text",prompt:"Submit with invalid input.",question:'Are error messages specific (not just "invalid"), shown near the field, and programmatically associated (aria-describedby)?',wcag:"3.3.1",severity:"required"},{id:"no-color-only-error",prompt:"Look at how errors are signaled.",question:"Is the error indicated by more than just color (also icon, text, or border style)?",wcag:"1.4.1",severity:"required"},{id:"success-announced",prompt:"Submit a form successfully.",question:'Is the success state announced to screen readers (via aria-live or role="status")?',wcag:"4.1.3",severity:"advisory"},{id:"error-suggestion",prompt:"Submit a form with an obvious invalid input (wrong email format, password too short, missing required field).",question:'Does the error message INCLUDE a specific suggestion for fixing it (not just "invalid")?',wcag:"3.3.3",severity:"required"},{id:"redundant-entry",prompt:"Identify any multi-step or multi-form flow where the user re-enters previously provided information (shipping = billing, profile data repeated in another form).",question:"Is previously-entered information auto-populated, selectable from a list, or otherwise NOT manually re-entered?",wcag:"3.3.7",severity:"required"},{id:"accessible-authentication",prompt:"Identify the site's authentication flow (login, password reset, multi-factor).",question:'Does auth NOT require a cognitive function test (e.g., remembering / transcribing characters), OR is an accessible alternative offered (passkey, magic link, password-manager autofill, "show password" toggle)?',wcag:"3.3.8",severity:"required"}]},ot={id:"page-structure",name:"Page structure",blurb:"Verify the page declares its language, has a meaningful title, and exposes a sensible heading + landmark hierarchy. WCAG 2.4.2, 3.1.1, 3.1.2, 1.3.1, 2.4.6.",steps:[{id:"page-title",prompt:"Look at the browser tab — is the page title set and meaningful?",question:'Does the title describe the page topic / purpose (not just "Untitled" or the site name)?',wcag:"2.4.2",relatedAxeRule:"document-title",severity:"required"},{id:"page-lang",prompt:"Inspect the <html> element's lang attribute.",question:'Is lang set to a valid language code (e.g., "en", "es-MX")?',wcag:"3.1.1",relatedAxeRule:"html-has-lang",severity:"required"},{id:"lang-of-parts",prompt:"For any text not in the page's primary language (foreign quote, brand name in another language), inspect the parent element.",question:"Is lang attribute set on those elements to indicate the language change?",wcag:"3.1.2",severity:"advisory"},{id:"h1-present",prompt:"Use the Outline visualizer or SR heading shortcut to find the h1.",question:"Is there exactly one h1, and does it describe the main content of the page?",wcag:"1.3.1",relatedAxeRule:"page-has-heading-one",visualizer:"outline",severity:"required"},{id:"descriptive-headings",prompt:"Read each heading. Imagine someone listening only to the heading text.",question:"Does each heading describe its section meaningfully on its own?",wcag:"2.4.6",severity:"required"},{id:"main-landmark",prompt:"Use the Outline visualizer or SR landmark shortcut.",question:"Is there exactly one <main> landmark wrapping the page's primary content?",wcag:"1.3.1",relatedAxeRule:"landmark-one-main",visualizer:"outline",severity:"required"},{id:"parsing-well-formed",prompt:"Open DevTools → Console. Reload the page and check for HTML parse errors. Or run W3C validator on the URL.",question:"Is the page's markup well-formed (no duplicate IDs in scope, no invalid nesting, no missing required attributes)? WCAG 4.1.1 was obsoleted in 2.2 but still applies to 2.1 conformance claims.",wcag:"4.1.1",severity:"required"}]},rt={id:"visual-indicators",name:"Visual indicators",blurb:"Verify nothing relies on color/shape/position alone, and non-text contrast meets thresholds for UI components and focus rings. WCAG 1.3.3, 1.4.1, 1.4.11.",steps:[{id:"no-color-only-info",prompt:"Scan instructions, error messages, charts, status indicators.",question:"Is information conveyed by something OTHER than color alone (text, icon, pattern, label)?",wcag:"1.4.1",severity:"required"},{id:"no-sensory-only",prompt:'Look for instructions referencing shape ("the round button"), location ("on the left"), sound ("the chime"), or visual cues alone.',question:"Are those instructions backed up by a programmatic identifier (label, role, accessible name)?",wcag:"1.3.3",severity:"required"},{id:"non-text-contrast",prompt:"For UI components (form-field borders, focus rings, icons-as-buttons), verify contrast vs adjacent colors. Use a contrast checker if needed.",question:"Do non-text UI components meet ≥3:1 contrast against adjacent colors?",wcag:"1.4.11",severity:"required"},{id:"focus-ring-contrast",prompt:"Tab through; for each focused element, verify the focus ring is visible against both the element and the page background.",question:"Does the focus ring meet ≥3:1 contrast on every focusable surface?",wcag:"1.4.11",visualizer:"tab-order",severity:"required"},{id:"state-changes-visible",prompt:"For toggleable controls (checkboxes, switches, expanded states): change them and observe.",question:"Is the state change conveyed visibly AND programmatically (aria-pressed/expanded/checked)?",wcag:"4.1.2",severity:"required"}]},st={id:"reflow",name:"Reflow & text spacing",blurb:"Verify the layout survives narrow viewports, browser zoom, and user-overridden text spacing. WCAG 1.4.4, 1.4.10, 1.4.12.",steps:[{id:"resize-320",prompt:"Resize the browser to 320px wide (or set DevTools device toolbar to 320×256).",question:"Does content reflow without two-dimensional scrolling? (Vertical scroll OK; horizontal scroll within content blocks NOT OK.)",wcag:"1.4.10",relatedAxeRule:"wcagcheckr-reflow",severity:"required"},{id:"zoom-200",prompt:"In a desktop viewport, zoom the browser to 200%.",question:"Is all content + functionality available, with no text clipping or unusable controls?",wcag:"1.4.4",severity:"required"},{id:"text-spacing-override",prompt:"Apply text-spacing override (line-height: 1.5, paragraph spacing 2x font-size, letter-spacing 0.12em, word-spacing 0.16em) via a bookmarklet or DevTools.",question:"Does content stay readable with no overlap or clipping?",wcag:"1.4.12",severity:"required"},{id:"no-fixed-pixel-text",prompt:"Inspect text styles for fixed px-based font sizes, line-heights.",question:"Are text dimensions set in relative units (rem/em/%) so they scale with user settings?",severity:"advisory"},{id:"orientation-not-locked",prompt:"Rotate a mobile device (or use DevTools device toolbar to rotate) between portrait and landscape.",question:"Does the layout adapt to both orientations? (Content not locked to one orientation unless essential.)",wcag:"1.3.4",severity:"required"},{id:"images-of-text",prompt:"Look for marketing banners, hero images, or any image that contains substantial text content as part of the raster.",question:"Is real HTML text used in preference to images-of-text, except for logos / decorative purposes? (Images-of-text fail to scale with zoom, can't be selected, and don't respect user text settings.)",wcag:"1.4.5",severity:"required"}]},lt={id:"hover-focus-content",name:"Content on hover or focus",blurb:"Tooltips, dropdown previews, and any content revealed on hover or focus must be dismissable, hoverable, and persistent. WCAG 1.4.13.",steps:[{id:"dismissable",prompt:"Hover or focus to reveal a tooltip / popover. Press Escape.",question:"Did Escape dismiss the revealed content without moving pointer or focus?",wcag:"1.4.13",severity:"required"},{id:"hoverable",prompt:"Hover to reveal a tooltip. Move the pointer over the tooltip itself (without losing the trigger's hover).",question:"Does the revealed content stay visible when the pointer enters it?",wcag:"1.4.13",severity:"required"},{id:"persistent",prompt:"Reveal a tooltip and don't do anything else.",question:"Does the content remain visible until the user dismisses it, moves focus/pointer away, or the trigger info is no longer valid?",wcag:"1.4.13",severity:"required"}]},ct={id:"pointer",name:"Pointer interactions",blurb:"Verify gestures, target sizes, and pointer cancellation respect users with motor limitations. WCAG 2.5.1, 2.5.2, 2.5.5/2.5.8.",steps:[{id:"no-multi-point-gesture",prompt:"Identify any feature that requires a multi-point gesture (pinch, two-finger swipe) or path-based gesture (drag along a line).",question:"Is there a single-tap / single-click alternative for that feature?",wcag:"2.5.1",severity:"required"},{id:"pointer-cancellation",prompt:"Mouse-down on a button, drag the pointer OFF the button, then release.",question:"Was the action canceled (the button did NOT activate)? Activation should fire on up-event over the target, not down-event.",wcag:"2.5.2",severity:"required"},{id:"target-size-aa",prompt:"Toggle the Targets visualizer. Look for any red overlays (≥24×24 fail).",question:"Are all interactive targets ≥24×24 CSS px (or have sufficient spacing per WCAG 2.5.8)?",wcag:"2.5.8",relatedAxeRule:"target-size",visualizer:"targets",severity:"required"},{id:"label-in-name",prompt:"For every visually-labeled control (button, link), inspect its accessible name (use the SR or DevTools).",question:"Does the accessible name START with — or fully contain — the visible label text?",wcag:"2.5.3",relatedAxeRule:"label-content-name-mismatch",severity:"required"},{id:"motion-actuation-alt",prompt:"Identify any feature that triggers on device motion (shake to undo, tilt to scroll, step counter).",question:"Is there a non-motion alternative (button / control) for every motion-triggered function, AND can motion be disabled?",wcag:"2.5.4",severity:"required"},{id:"dragging-alternative",prompt:"Identify any feature that requires a dragging movement (slider thumb, drag-to-rearrange, swipe-to-delete).",question:"Is there a single-tap / single-click alternative for every dragging action (e.g., +/- buttons next to a slider, up/down buttons next to draggable list items)?",wcag:"2.5.7",severity:"required"}]},dt={id:"time-based",name:"Time-based behaviors",blurb:"Verify time limits, auto-updating content, and motion can be controlled by users. WCAG 2.2.1, 2.2.2, 2.3.1.",steps:[{id:"time-limits-adjustable",prompt:"Identify any time-limited interaction (session timeout, form auto-submit, OTP expiration).",question:"Can the user turn off, adjust, or extend the time limit before it expires (warning + control before the deadline)?",wcag:"2.2.1",severity:"required"},{id:"pause-moving-content",prompt:"Look for content that moves, blinks, scrolls, or auto-updates for >5 seconds (carousels, animated banners, news tickers, auto-refreshing feeds).",question:"Is there a control to pause / stop / hide the moving content?",wcag:"2.2.2",severity:"required"},{id:"no-flash",prompt:"Identify any flashing or blinking content (>3 flashes per second).",question:"Is flashing absent, OR limited below the general/red flash thresholds?",wcag:"2.3.1",severity:"required"},{id:"reduced-motion-honored",prompt:'Enable system "reduce motion" preference. Reload the page.',question:"Does the site honor prefers-reduced-motion (parallax / animations / spinners minimized or disabled)?",wcag:"2.3.3",severity:"advisory"}]},ut={id:"error-prevention",name:"Error prevention",blurb:"Verify destructive, financial, or legal actions are reversible, checked, or confirmed. WCAG 3.3.4, 3.3.6.",steps:[{id:"destructive-confirm",prompt:"Identify destructive actions (delete account, cancel subscription, destroy data, send irreversible message).",question:'Is each destructive action reversible (undo), or checked (validation), or confirmed (explicit "Are you sure?" dialog)?',wcag:"3.3.4",severity:"required"},{id:"financial-confirm",prompt:"Identify financial / legal commitments (purchase, contract sign, transfer).",question:"Before submission, does the user see a review screen and an explicit confirmation step?",wcag:"3.3.4",severity:"required"},{id:"undo-available",prompt:'Test "destructive" actions on user content (delete a note, archive an item).',question:'Is there an undo / restore mechanism, even briefly (toast with "undo" button)?',wcag:"3.3.6",severity:"advisory"}]},ht={id:"consistency",name:"Consistency",blurb:"Verify navigation, identification, and user paths are consistent across pages. WCAG 2.4.5, 3.2.3, 3.2.4.",steps:[{id:"consistent-nav",prompt:"Compare the primary navigation across 3+ pages.",question:"Does the same navigation appear in the same order on every page?",wcag:"3.2.3",severity:"required"},{id:"consistent-id",prompt:'Compare the same UI element appearing on multiple pages (e.g., a search icon, a "Help" link).',question:"Does the same component have the same accessible name everywhere it appears?",wcag:"3.2.4",severity:"required"},{id:"multiple-ways",prompt:"Beyond the primary navigation, is there a second way to find a page (search, sitemap, related links, breadcrumbs)?",question:"Are there at least 2 ways to locate any page (single-process pages exempt)?",wcag:"2.4.5",severity:"required"},{id:"skip-link",prompt:"Tab once when the page loads.",question:'Does the first focusable element offer a "Skip to main content" link (or equivalent bypass)?',wcag:"2.4.1",relatedAxeRule:"bypass",severity:"required"},{id:"consistent-help",prompt:"Identify pages that surface a help mechanism (contact info, help link, FAQ link, chatbot trigger). Compare position + order across pages.",question:"When the same help mechanism appears on multiple pages, does it appear in the same relative order (e.g., always last in the header)?",wcag:"3.2.6",severity:"required"}]},pt={id:"time-based-media",name:"Time-based media",blurb:'Verify audio + video have the alternatives required by WCAG. If the site has no time-based media, mark every step "Not applicable." WCAG 1.2.1, 1.2.2, 1.2.3, 1.2.4, 1.2.5, 1.4.2.',steps:[{id:"prerecorded-audio-only-alt",prompt:"Identify any prerecorded audio-only content (podcast embed, audio quote, voice message).",question:"Does each prerecorded audio-only file have a text transcript readable on the page (or linked from it)?",wcag:"1.2.1",severity:"required"},{id:"prerecorded-video-only-alt",prompt:"Identify any prerecorded silent video (animated GIF with content, silent screencast, animated explainer).",question:"Does each prerecorded video-only file have an equivalent text or audio description?",wcag:"1.2.1",severity:"required"},{id:"prerecorded-captions",prompt:"Identify any prerecorded video that includes audio (interviews, tutorials, marketing videos).",question:"Does each video provide synchronized captions for ALL spoken content + meaningful non-speech sound?",wcag:"1.2.2",severity:"required"},{id:"prerecorded-alt-for-video",prompt:"For each prerecorded video with audio, check if there's a full text transcript on the page (covering both audio dialog AND visual action) OR if audio description is provided.",question:"Does each prerecorded video have either a full text alternative OR an audio-description track?",wcag:"1.2.3",severity:"required"},{id:"prerecorded-audio-description",prompt:"Watch each prerecorded video without looking at it. Listen only.",question:"Is all visually-conveyed information also conveyed in the audio (either built into the narration OR via a separate audio-description track)?",wcag:"1.2.5",severity:"required"},{id:"live-captions",prompt:"Identify any LIVE audio/video stream (live shopping, webinar, conference broadcast).",question:"Does each live broadcast provide real-time captions?",wcag:"1.2.4",severity:"required"},{id:"audio-control",prompt:"Reload the page. Listen for audio that starts automatically and plays for >3 seconds (background music, autoplay video with sound).",question:"Is there a mechanism to pause / stop the audio, OR can the volume be controlled INDEPENDENTLY from the system volume?",wcag:"1.4.2",severity:"required"}]},we=[tt,nt,at,it,ot,rt,st,lt,ct,dt,pt,ut,ht];function sn(e,t){let n=0,o=0,r=0,s=0,i=0;for(const l of t.steps){const a=e.steps[l.id];if(!a){i++;continue}a.status==="pass"?n++:a.status==="fail"?o++:a.status==="not-applicable"?s++:a.status==="skip"?r++:i++}return{passed:n,failed:o,skipped:r,notApplicable:s,unanswered:i,total:t.steps.length,fullyAnswered:i===0}}const mt=[{id:"1.1.1",title:"Non-text Content",level:"A",addedIn:"2.0",understandingSlug:"non-text-content",shortDescription:"All non-text content has a text alternative."},{id:"1.2.1",title:"Audio-only and Video-only (Prerecorded)",level:"A",addedIn:"2.0",understandingSlug:"audio-only-and-video-only-prerecorded",shortDescription:"Alternatives for prerecorded audio-only or video-only."},{id:"1.2.2",title:"Captions (Prerecorded)",level:"A",addedIn:"2.0",understandingSlug:"captions-prerecorded",shortDescription:"Captions for prerecorded audio in synchronized media."},{id:"1.2.3",title:"Audio Description or Media Alternative (Prerecorded)",level:"A",addedIn:"2.0",understandingSlug:"audio-description-or-media-alternative-prerecorded",shortDescription:"Alternative for time-based media (prerecorded)."},{id:"1.2.4",title:"Captions (Live)",level:"AA",addedIn:"2.0",understandingSlug:"captions-live",shortDescription:"Captions for live synchronized media."},{id:"1.2.5",title:"Audio Description (Prerecorded)",level:"AA",addedIn:"2.0",understandingSlug:"audio-description-prerecorded",shortDescription:"Audio description for prerecorded video in synchronized media."},{id:"1.2.6",title:"Sign Language (Prerecorded)",level:"AAA",addedIn:"2.0",understandingSlug:"sign-language-prerecorded",shortDescription:"Sign-language interpretation for prerecorded audio."},{id:"1.2.7",title:"Extended Audio Description (Prerecorded)",level:"AAA",addedIn:"2.0",understandingSlug:"extended-audio-description-prerecorded",shortDescription:"Extended audio description for prerecorded video."},{id:"1.2.8",title:"Media Alternative (Prerecorded)",level:"AAA",addedIn:"2.0",understandingSlug:"media-alternative-prerecorded",shortDescription:"Alternative for time-based media (prerecorded)."},{id:"1.2.9",title:"Audio-only (Live)",level:"AAA",addedIn:"2.0",understandingSlug:"audio-only-live",shortDescription:"Alternative for live audio-only content."},{id:"1.3.1",title:"Info and Relationships",level:"A",addedIn:"2.0",understandingSlug:"info-and-relationships",shortDescription:"Information and structural relationships are programmatically determinable."},{id:"1.3.2",title:"Meaningful Sequence",level:"A",addedIn:"2.0",understandingSlug:"meaningful-sequence",shortDescription:"Reading sequence is programmatically determinable."},{id:"1.3.3",title:"Sensory Characteristics",level:"A",addedIn:"2.0",understandingSlug:"sensory-characteristics",shortDescription:"Instructions don't rely solely on shape, color, size, or sound."},{id:"1.3.4",title:"Orientation",level:"AA",addedIn:"2.1",understandingSlug:"orientation",shortDescription:"Content doesn't restrict view to portrait or landscape."},{id:"1.3.5",title:"Identify Input Purpose",level:"AA",addedIn:"2.1",understandingSlug:"identify-input-purpose",shortDescription:"Purpose of input fields is programmatically identifiable."},{id:"1.3.6",title:"Identify Purpose",level:"AAA",addedIn:"2.1",understandingSlug:"identify-purpose",shortDescription:"Purpose of UI components, icons, and regions is identifiable."},{id:"1.4.1",title:"Use of Color",level:"A",addedIn:"2.0",understandingSlug:"use-of-color",shortDescription:"Color is not the only means of conveying information."},{id:"1.4.2",title:"Audio Control",level:"A",addedIn:"2.0",understandingSlug:"audio-control",shortDescription:"Mechanism to pause/stop auto-playing audio over 3s."},{id:"1.4.3",title:"Contrast (Minimum)",level:"AA",addedIn:"2.0",understandingSlug:"contrast-minimum",shortDescription:"Text has 4.5:1 contrast (3:1 for large text)."},{id:"1.4.4",title:"Resize Text",level:"AA",addedIn:"2.0",understandingSlug:"resize-text",shortDescription:"Text resizable to 200% without loss of content/function."},{id:"1.4.5",title:"Images of Text",level:"AA",addedIn:"2.0",understandingSlug:"images-of-text",shortDescription:"Text used in preference to images of text where possible."},{id:"1.4.6",title:"Contrast (Enhanced)",level:"AAA",addedIn:"2.0",understandingSlug:"contrast-enhanced",shortDescription:"Text has 7:1 contrast (4.5:1 for large text)."},{id:"1.4.7",title:"Low or No Background Audio",level:"AAA",addedIn:"2.0",understandingSlug:"low-or-no-background-audio",shortDescription:"Background audio at least 20dB lower than speech."},{id:"1.4.8",title:"Visual Presentation",level:"AAA",addedIn:"2.0",understandingSlug:"visual-presentation",shortDescription:"Configurable visual presentation for blocks of text."},{id:"1.4.9",title:"Images of Text (No Exception)",level:"AAA",addedIn:"2.0",understandingSlug:"images-of-text-no-exception",shortDescription:"Images of text used only for decoration / essential."},{id:"1.4.10",title:"Reflow",level:"AA",addedIn:"2.1",understandingSlug:"reflow",shortDescription:"Content reflows at 320 CSS px without 2D scrolling."},{id:"1.4.11",title:"Non-text Contrast",level:"AA",addedIn:"2.1",understandingSlug:"non-text-contrast",shortDescription:"UI components and graphical objects have 3:1 contrast."},{id:"1.4.12",title:"Text Spacing",level:"AA",addedIn:"2.1",understandingSlug:"text-spacing",shortDescription:"Text spacing overrides don't cause content loss."},{id:"1.4.13",title:"Content on Hover or Focus",level:"AA",addedIn:"2.1",understandingSlug:"content-on-hover-or-focus",shortDescription:"Hover/focus tooltips are dismissible, hoverable, persistent."},{id:"2.1.1",title:"Keyboard",level:"A",addedIn:"2.0",understandingSlug:"keyboard",shortDescription:"All functionality available from the keyboard."},{id:"2.1.2",title:"No Keyboard Trap",level:"A",addedIn:"2.0",understandingSlug:"no-keyboard-trap",shortDescription:"Keyboard focus can move away from any component."},{id:"2.1.3",title:"Keyboard (No Exception)",level:"AAA",addedIn:"2.0",understandingSlug:"keyboard-no-exception",shortDescription:"All functionality is keyboard-operable without timing."},{id:"2.1.4",title:"Character Key Shortcuts",level:"A",addedIn:"2.1",understandingSlug:"character-key-shortcuts",shortDescription:"Single-key shortcuts can be turned off or remapped."},{id:"2.2.1",title:"Timing Adjustable",level:"A",addedIn:"2.0",understandingSlug:"timing-adjustable",shortDescription:"User can turn off, adjust, or extend time limits."},{id:"2.2.2",title:"Pause, Stop, Hide",level:"A",addedIn:"2.0",understandingSlug:"pause-stop-hide",shortDescription:"Moving/blinking/scrolling content can be paused."},{id:"2.2.3",title:"No Timing",level:"AAA",addedIn:"2.0",understandingSlug:"no-timing",shortDescription:"No timing essential to content/function (with exceptions)."},{id:"2.2.4",title:"Interruptions",level:"AAA",addedIn:"2.0",understandingSlug:"interruptions",shortDescription:"User can postpone/suppress non-emergency interruptions."},{id:"2.2.5",title:"Re-authenticating",level:"AAA",addedIn:"2.0",understandingSlug:"re-authenticating",shortDescription:"Authenticated session can be resumed without data loss."},{id:"2.2.6",title:"Timeouts",level:"AAA",addedIn:"2.1",understandingSlug:"timeouts",shortDescription:"Inactivity timeouts warned in advance."},{id:"2.3.1",title:"Three Flashes or Below Threshold",level:"A",addedIn:"2.0",understandingSlug:"three-flashes-or-below-threshold",shortDescription:"No content flashes more than 3 times per second."},{id:"2.3.2",title:"Three Flashes",level:"AAA",addedIn:"2.0",understandingSlug:"three-flashes",shortDescription:"No content flashes more than 3 times per second (no exceptions)."},{id:"2.3.3",title:"Animation from Interactions",level:"AAA",addedIn:"2.1",understandingSlug:"animation-from-interactions",shortDescription:"Motion animation from interactions can be disabled."},{id:"2.4.1",title:"Bypass Blocks",level:"A",addedIn:"2.0",understandingSlug:"bypass-blocks",shortDescription:"Mechanism to skip blocks of repeated content."},{id:"2.4.2",title:"Page Titled",level:"A",addedIn:"2.0",understandingSlug:"page-titled",shortDescription:"Pages have titles that describe topic/purpose."},{id:"2.4.3",title:"Focus Order",level:"A",addedIn:"2.0",understandingSlug:"focus-order",shortDescription:"Components receive focus in a meaningful order."},{id:"2.4.4",title:"Link Purpose (In Context)",level:"A",addedIn:"2.0",understandingSlug:"link-purpose-in-context",shortDescription:"Link purpose is clear from the link text + context."},{id:"2.4.5",title:"Multiple Ways",level:"AA",addedIn:"2.0",understandingSlug:"multiple-ways",shortDescription:"Two or more ways to locate a page within a set."},{id:"2.4.6",title:"Headings and Labels",level:"AA",addedIn:"2.0",understandingSlug:"headings-and-labels",shortDescription:"Headings and labels describe topic or purpose."},{id:"2.4.7",title:"Focus Visible",level:"AA",addedIn:"2.0",understandingSlug:"focus-visible",shortDescription:"Keyboard focus indicator is visible."},{id:"2.4.8",title:"Location",level:"AAA",addedIn:"2.0",understandingSlug:"location",shortDescription:"Information about user's location within a set is available."},{id:"2.4.9",title:"Link Purpose (Link Only)",level:"AAA",addedIn:"2.0",understandingSlug:"link-purpose-link-only",shortDescription:"Link purpose clear from the link text alone."},{id:"2.4.10",title:"Section Headings",level:"AAA",addedIn:"2.0",understandingSlug:"section-headings",shortDescription:"Section headings are used to organize content."},{id:"2.4.11",title:"Focus Not Obscured (Minimum)",level:"AA",addedIn:"2.2",understandingSlug:"focus-not-obscured-minimum",shortDescription:"Focused component not entirely hidden by other content."},{id:"2.4.12",title:"Focus Not Obscured (Enhanced)",level:"AAA",addedIn:"2.2",understandingSlug:"focus-not-obscured-enhanced",shortDescription:"Focused component fully visible."},{id:"2.4.13",title:"Focus Appearance",level:"AAA",addedIn:"2.2",understandingSlug:"focus-appearance",shortDescription:"Focus indicator meets specific size/contrast requirements."},{id:"2.5.1",title:"Pointer Gestures",level:"A",addedIn:"2.1",understandingSlug:"pointer-gestures",shortDescription:"Multi-point or path-based gestures have single-pointer alternatives."},{id:"2.5.2",title:"Pointer Cancellation",level:"A",addedIn:"2.1",understandingSlug:"pointer-cancellation",shortDescription:"Pointer actions can be aborted or undone."},{id:"2.5.3",title:"Label in Name",level:"A",addedIn:"2.1",understandingSlug:"label-in-name",shortDescription:"Accessible name includes the visible label text."},{id:"2.5.4",title:"Motion Actuation",level:"A",addedIn:"2.1",understandingSlug:"motion-actuation",shortDescription:"Motion-triggered functions have UI alternatives + can be disabled."},{id:"2.5.5",title:"Target Size (Enhanced)",level:"AAA",addedIn:"2.1",understandingSlug:"target-size-enhanced",shortDescription:"Pointer target size at least 44×44 CSS px."},{id:"2.5.6",title:"Concurrent Input Mechanisms",level:"AAA",addedIn:"2.1",understandingSlug:"concurrent-input-mechanisms",shortDescription:"Content doesn't restrict input modality."},{id:"2.5.7",title:"Dragging Movements",level:"AA",addedIn:"2.2",understandingSlug:"dragging-movements",shortDescription:"Dragging actions have single-pointer alternatives."},{id:"2.5.8",title:"Target Size (Minimum)",level:"AA",addedIn:"2.2",understandingSlug:"target-size-minimum",shortDescription:"Pointer target size at least 24×24 CSS px."},{id:"3.1.1",title:"Language of Page",level:"A",addedIn:"2.0",understandingSlug:"language-of-page",shortDescription:"Default human language of the page is programmatically set."},{id:"3.1.2",title:"Language of Parts",level:"AA",addedIn:"2.0",understandingSlug:"language-of-parts",shortDescription:"Language of each passage is programmatically set."},{id:"3.1.3",title:"Unusual Words",level:"AAA",addedIn:"2.0",understandingSlug:"unusual-words",shortDescription:"Definitions provided for jargon, idioms, etc."},{id:"3.1.4",title:"Abbreviations",level:"AAA",addedIn:"2.0",understandingSlug:"abbreviations",shortDescription:"Expanded form or meaning of abbreviations is available."},{id:"3.1.5",title:"Reading Level",level:"AAA",addedIn:"2.0",understandingSlug:"reading-level",shortDescription:"Supplemental content provided for advanced reading levels."},{id:"3.1.6",title:"Pronunciation",level:"AAA",addedIn:"2.0",understandingSlug:"pronunciation",shortDescription:"Mechanism for pronunciation of words where meaning is ambiguous."},{id:"3.2.1",title:"On Focus",level:"A",addedIn:"2.0",understandingSlug:"on-focus",shortDescription:"Focus does not trigger an unexpected context change."},{id:"3.2.2",title:"On Input",level:"A",addedIn:"2.0",understandingSlug:"on-input",shortDescription:"Input does not trigger an unexpected context change."},{id:"3.2.3",title:"Consistent Navigation",level:"AA",addedIn:"2.0",understandingSlug:"consistent-navigation",shortDescription:"Navigation is consistent across a set of pages."},{id:"3.2.4",title:"Consistent Identification",level:"AA",addedIn:"2.0",understandingSlug:"consistent-identification",shortDescription:"Components with the same function are identified consistently."},{id:"3.2.5",title:"Change on Request",level:"AAA",addedIn:"2.0",understandingSlug:"change-on-request",shortDescription:"Changes of context only on user request."},{id:"3.2.6",title:"Consistent Help",level:"A",addedIn:"2.2",understandingSlug:"consistent-help",shortDescription:"Help mechanisms appear in consistent order across pages."},{id:"3.3.1",title:"Error Identification",level:"A",addedIn:"2.0",understandingSlug:"error-identification",shortDescription:"Form errors are identified and described in text."},{id:"3.3.2",title:"Labels or Instructions",level:"A",addedIn:"2.0",understandingSlug:"labels-or-instructions",shortDescription:"Form fields have labels or instructions."},{id:"3.3.3",title:"Error Suggestion",level:"AA",addedIn:"2.0",understandingSlug:"error-suggestion",shortDescription:"Error suggestions provided when known."},{id:"3.3.4",title:"Error Prevention (Legal, Financial, Data)",level:"AA",addedIn:"2.0",understandingSlug:"error-prevention-legal-financial-data",shortDescription:"Submissions are reversible, checked, or confirmed."},{id:"3.3.5",title:"Help",level:"AAA",addedIn:"2.0",understandingSlug:"help",shortDescription:"Context-sensitive help is available."},{id:"3.3.6",title:"Error Prevention (All)",level:"AAA",addedIn:"2.0",understandingSlug:"error-prevention-all",shortDescription:"All submissions reversible, checked, or confirmed."},{id:"3.3.7",title:"Redundant Entry",level:"A",addedIn:"2.2",understandingSlug:"redundant-entry",shortDescription:"Previously-entered information is auto-populated or selectable."},{id:"3.3.8",title:"Accessible Authentication (Minimum)",level:"AA",addedIn:"2.2",understandingSlug:"accessible-authentication-minimum",shortDescription:"Auth doesn't require cognitive function test."},{id:"3.3.9",title:"Accessible Authentication (Enhanced)",level:"AAA",addedIn:"2.2",understandingSlug:"accessible-authentication-enhanced",shortDescription:"Auth doesn't require cognitive function test (no exceptions)."},{id:"4.1.1",title:"Parsing",level:"A",addedIn:"2.0",removedIn:"2.2",understandingSlug:"parsing",shortDescription:"Markup is well-formed (obsoleted in WCAG 2.2)."},{id:"4.1.2",title:"Name, Role, Value",level:"A",addedIn:"2.0",understandingSlug:"name-role-value",shortDescription:"UI components have programmatically determinable name + role + state."},{id:"4.1.3",title:"Status Messages",level:"AA",addedIn:"2.1",understandingSlug:"status-messages",shortDescription:"Status messages programmatically announced without focus change."}];function gt(e,t){const n={A:1,AA:2,AAA:3},o={"2.0":1,"2.1":2,"2.2":3};return mt.filter(r=>o[r.addedIn]>o[e]||r.removedIn&&o[r.removedIn]<=o[e]?!1:n[r.level]<=n[t])}function ln(e,t){return`https://www.w3.org/WAI/WCAG${t.replace(".","")}/Understanding/${e.understandingSlug}`}const Ae="2.1",Ie="AA";function ft(e){const t=gt(Ae,Ie),n=new Map;for(const s of t)n.set(s.id,{criterionId:s.id,level:s.level,title:s.title,state:"not-evaluated",evidence:[]});const o=new Map;function r(s,i){let l=o.get(s);return l||(l=new Set,o.set(s,l)),l.has(i)?!0:(l.add(i),!1)}for(const s of e.audits){const i=s.pageUrl??s.scope;for(const a of s.violations){if(!a.wcagCriterion)continue;const c=n.get(a.wcagCriterion);if(!c)continue;const d=a.ruleId.startsWith("ai-"),p=a.target.selector;if(e.acknowledgedMatchKeys.has(a.matchKey)){const f=`ack::${a.ruleId}::${i}::${p}`;if(r(a.wcagCriterion,f))continue;c.evidence.push({source:d?"ai":"axe",ruleOrStepId:a.ruleId,outcome:"ack",pageUrl:i,selector:p});continue}if(a.needsReview){const f=`needsReview::${a.ruleId}::${i}::${p}`;if(r(a.wcagCriterion,f))continue;c.evidence.push({source:"ai",ruleOrStepId:a.ruleId,outcome:"incomplete",pageUrl:i,selector:p,resolutionHint:"Acknowledge in the Matrix view if not an issue, or fix the underlying alt text."});continue}const u=`fail::${a.ruleId}::${i}::${p}`;r(a.wcagCriterion,u)||c.evidence.push({source:d?"ai":"axe",ruleOrStepId:a.ruleId,outcome:"fail",pageUrl:i,selector:p,resolutionHint:`Fix ${a.ruleId} on ${p}.`})}const l=s.axeRulesEvaluated;if(l){for(const a of l.passed){const c=n.get(a.wcagCriterion);if(!c)continue;const d=`axe::${a.ruleId}::${i}::pass`;r(a.wcagCriterion,d)||c.evidence.push({source:"axe",ruleOrStepId:a.ruleId,outcome:"pass",pageUrl:i})}for(const a of l.inapplicable){const c=n.get(a.wcagCriterion);if(!c)continue;const d=`axe::${a.ruleId}::${i}::inapplicable`;r(a.wcagCriterion,d)||c.evidence.push({source:"axe",ruleOrStepId:a.ruleId,outcome:"inapplicable",pageUrl:i})}for(const a of l.incomplete){const c=n.get(a.wcagCriterion);if(!c)continue;const d=`axe::${a.ruleId}::${i}::incomplete`,p=c.evidence.findIndex(u=>u.source==="axe"&&u.outcome==="incomplete"&&u.ruleOrStepId===a.ruleId&&u.pageUrl===i);if(p>=0){if(a.elements&&a.elements.length>0){const u=c.evidence[p],f=new Set((u.elements??[]).map(y=>y.selector));for(const y of a.elements)f.has(y.selector)||((u.elements??(u.elements=[])).push(y),f.add(y.selector))}continue}r(a.wcagCriterion,d)||c.evidence.push({source:"axe",ruleOrStepId:a.ruleId,outcome:"incomplete",pageUrl:i,elements:a.elements?[...a.elements]:void 0,resolutionHint:"axe-core couldn't fully determine. Common causes: text against an image/gradient background, or aria-role on a hidden element. Review the page manually."})}}}if(e.incompleteResolutions&&e.incompleteResolutions.length>0){const s=new Map;for(const i of e.incompleteResolutions)s.set(`${i.pageUrl}::${i.ruleId}::${i.selector}`,i);for(const i of n.values()){const l=[];for(const a of i.evidence){if(a.source!=="axe"||a.outcome!=="incomplete"||!a.elements||a.elements.length===0){l.push(a);continue}const c=[];for(const d of a.elements){const p=`${a.pageUrl}::${a.ruleOrStepId}::${d.selector}`,u=s.get(p);if(!u){c.push(d);continue}u.verdict==="pass"?l.push({source:"ai",ruleOrStepId:`ai-resolved::${a.ruleOrStepId}`,outcome:"pass",pageUrl:a.pageUrl,selector:d.selector,notes:u.reasoning}):u.verdict==="fail"?l.push({source:"ai",ruleOrStepId:`ai-resolved::${a.ruleOrStepId}`,outcome:"fail",pageUrl:a.pageUrl,selector:d.selector,notes:u.reasoning,resolutionHint:`AI determined ${a.ruleOrStepId} fails on this element. Review and fix or override.`}):c.push(d)}c.length>0&&l.push({...a,elements:c})}i.evidence=l}}if(e.interactiveAuditResults&&e.interactiveAuditResults.length>0)for(const s of e.interactiveAuditResults){const i=n.get(s.criterionId);i&&i.evidence.push({source:"ai",ruleOrStepId:s.ruleId,outcome:s.verdict==="pass"?"pass":s.verdict==="fail"?"fail":"incomplete",pageUrl:s.pageUrl,notes:s.reasoning,resolutionHint:s.verdict==="fail"?"Review the AI walkthrough in the Compliance tab; fix the underlying focus-order issue and re-run.":s.verdict==="uncertain"?"Re-run the interactive audit, or manually verify via Guided Tests.":void 0})}for(const s of e.workflows)for(const i of s.steps){if(!i.wcag)continue;const l=n.get(i.wcag);if(l)for(const a of e.igtRuns){if(a.workflowId!==s.id)continue;const c=a.steps[i.id],d=`${s.id}.${i.id}`;if(!c){l.evidence.push({source:"igt",ruleOrStepId:d,outcome:"skip",resolutionHint:`Open Guided Tests → ${s.name} → answer "${i.id}" as Pass / Fail / N/A.`});continue}c.status==="pass"?l.evidence.push({source:"igt",ruleOrStepId:d,outcome:"pass",notes:c.notes}):c.status==="fail"?l.evidence.push({source:"igt",ruleOrStepId:d,outcome:"fail",notes:c.notes,resolutionHint:`Address the failing manual check: ${i.question}`}):c.status==="not-applicable"?l.evidence.push({source:"igt",ruleOrStepId:d,outcome:"n/a",notes:c.notes}):c.status==="skip"&&l.evidence.push({source:"igt",ruleOrStepId:d,outcome:"skip",notes:c.notes,resolutionHint:`Open Guided Tests → ${s.name} → re-answer "${i.id}" as Pass / Fail / N/A.`})}}for(const s of n.values())s.state=vt(s.evidence);return n}function vt(e){return e.length===0?"not-evaluated":e.some(n=>n.outcome==="fail")?"fail":e.some(n=>n.outcome==="incomplete"||n.outcome==="skip")?"inconclusive":e.some(n=>n.outcome==="pass"||n.outcome==="inapplicable"||n.outcome==="ack")?"pass":"not-applicable"}function yt(e){const t={pass:[],fail:[],inconclusive:[],notApplicable:[],notEvaluated:[]};for(const o of e.values())t[bt(o.state)].push(o.criterionId);const n=e.size;return{pass:t.pass.length,fail:t.fail.length,inconclusive:t.inconclusive.length,notApplicable:t.notApplicable.length,notEvaluated:t.notEvaluated.length,totalApplicable:n,canClaimConformance:t.fail.length===0&&t.inconclusive.length===0&&t.notEvaluated.length===0,byState:t}}function bt(e){return e==="not-applicable"?"notApplicable":e==="not-evaluated"?"notEvaluated":e}function cn(e,t=we){const n=new Set,o=new Set,r=new Set;for(const i of e.values())for(const l of i.evidence)l.source==="axe"?n.add(i.criterionId):l.source==="ai"?o.add(i.criterionId):l.source==="igt"&&r.add(i.criterionId);for(const i of t)for(const l of i.steps)l.wcag&&e.has(l.wcag)&&r.add(l.wcag);function s(i,l){const a=Array.from(l).sort();let c=0;const d=[],p=[];for(const u of a){const f=e.get(u);if(!f)continue;const y=f.evidence.filter(v=>i==="automation"&&v.source==="axe"||i==="ai"&&v.source==="ai"||i==="human"&&v.source==="igt");if(y.length===0){p.push(u);continue}const g=y.some(v=>v.outcome==="fail"),b=y.some(v=>v.outcome==="incomplete"||v.outcome==="skip"),h=y.some(v=>v.outcome==="pass"||v.outcome==="inapplicable"||v.outcome==="ack"||v.outcome==="n/a");g||b?d.push(u):h?c++:p.push(u)}return{layer:i,coveredCriteria:a,cleared:c,blocked:d,unevaluated:p}}return{automation:s("automation",n),ai:s("ai",o),human:s("human",r)}}const U=[{id:"contrast",label:"Color contrast",rules:["color-contrast","color-contrast-enhanced","link-in-text-block"],needsManualCheck:!1},{id:"alt",label:"Image alt text",rules:["image-alt","input-image-alt","area-alt","svg-img-alt","object-alt","role-img-alt"],needsManualCheck:!1},{id:"labels",label:"Form labels",rules:["label","select-name","aria-input-field-name","aria-toggle-field-name","form-field-multiple-labels","label-content-name-mismatch","autocomplete-valid"],needsManualCheck:!1},{id:"names",label:"Buttons / links named",rules:["button-name","input-button-name","link-name","empty-button","empty-heading","empty-link"],needsManualCheck:!1},{id:"keyboard",label:"Keyboard operability",rules:["nested-interactive","tabindex","frame-focusable-content","no-focusable-content"],needsManualCheck:!0},{id:"structure",label:"Landmarks + heading structure",rules:["heading-order","page-has-heading-one","landmark-one-main","landmark-no-duplicate-banner","landmark-no-duplicate-contentinfo","landmark-no-duplicate-main","region","bypass"],needsManualCheck:!1}];function wt(e){const t=new Map;for(const n of e){const o=`${n.ruleId}::${n.target.selector}`;t.has(o)||t.set(o,n)}return Array.from(t.values())}function At(e,t,n="2.1",o="AA",r,s,i){const l=ft({audits:e??[],igtRuns:(t==null?void 0:t.runs)??[],workflows:(t==null?void 0:t.workflows)??we,acknowledgedMatchKeys:r??new Set,incompleteResolutions:s,interactiveAuditResults:i}),a={},c={},d=[],p=[],u=[];let f=0;for(const g of l.values()){if(g.state==="not-evaluated"){d.push(g.criterionId);continue}if(f++,g.state==="fail"){u.push(g.criterionId);const b=[];for(const h of g.evidence)h.outcome==="fail"&&b.push({source:h.source,ruleOrStepId:h.ruleOrStepId,selector:h.selector,notes:h.notes,pageUrl:h.pageUrl});b.length>0&&(c[g.criterionId]=b)}if(g.state==="inconclusive"){p.push(g.criterionId);const b=[];for(const h of g.evidence)if(h.outcome==="incomplete"){const v=h.source==="ai"?"ai-uncertain":"axe-incomplete";b.push({source:v,ruleOrStepId:h.ruleOrStepId,resolutionHint:h.resolutionHint,elements:h.elements,pageUrl:h.pageUrl})}else h.outcome==="skip"&&b.push({source:"igt-skip",ruleOrStepId:h.ruleOrStepId,resolutionHint:h.resolutionHint});b.length>0&&(a[g.criterionId]=b)}}const y=yt(l);return{targetVersion:Ae,targetLevel:Ie,totalApplicable:l.size,evaluated:f,untestedCriteria:d,inconclusiveCriteria:p,failingCriteria:u,inconclusiveReasons:a,failingReasons:c,canClaimConformance:y.canClaimConformance}}function dn(e,t,n,o,r){const s=wt(e),i=s.filter(m=>m.needsReview),l=s.filter(m=>!m.needsReview),a={critical:0,serious:0,moderate:0,minor:0,unique:l.length};for(const m of l)a[m.impact]++;let c;t&&(c=It(t.runs,t.workflows),a.serious+=c.failedRequired,a.moderate+=c.failedAdvisory,a.unique+=c.failedRequired+c.failedAdvisory);const d=new Map;for(const m of U)for(const A of m.rules)d.set(A,m);const p=new Map;for(const m of U)p.set(m.id,{count:0,rules:new Set});for(const m of Ze(l)){const A=d.get(m.ruleId);if(!A)continue;const k=p.get(A.id);k.count++,k.rules.add(m.ruleId)}const u=U.map(m=>{const A=p.get(m.id);let k="pass";return A.count>0?k="fail":m.needsManualCheck&&(k="unchecked"),{id:m.id,label:m.label,status:k,violationCount:A.count,rules:Array.from(A.rules),needsManualCheck:m.needsManualCheck}}),f=a.critical>0,y=a.serious>0,g=n?At(n,t,"2.1","AA",o,void 0,r):void 0,b=g?!g.canClaimConformance:!1;let h;a.critical>=5?h="F":a.critical>=1?h="D":a.serious>=5||a.serious>=1?h="C":a.moderate>=3||a.moderate>=1?h="B":(a.minor>=1,h="A"),b&&h==="A"&&(h="B");const v=u.filter(m=>m.status==="fail").length;let S;return a.critical>0||v>=4?S="critical":a.serious>=3||v>=2?S="high":a.serious>=1||a.moderate>=3?S="moderate":S="low",{letter:h,risk:S,caps:{cappedAtC:f,cappedAtB:y,cappedAtBByCoverage:b},totals:a,categories:u,coverage:g,manual:c,reviewQueue:{count:i.length,rules:Array.from(new Set(i.map(m=>m.ruleId)))}}}function It(e,t){new Map(t.map(l=>[l.id,l]));const n=new Map(e.map(l=>[l.workflowId,l]));let o=0,r=0,s=0;const i=t.map(l=>{const a=n.get(l.id);let c=0,d=0,p=0,u=0,f=0;for(const y of l.steps){const g=a==null?void 0:a.steps[y.id];g&&(f++,g.status==="pass"?c++:g.status==="skip"?u++:g.status==="fail"&&(y.severity==="required"?d++:p++))}return f===l.steps.length&&s++,o+=d,r+=p,{workflowId:l.id,name:l.name,passed:c,failedRequired:d,failedAdvisory:p,skipped:u,answered:f,total:l.steps.length}});return{workflowsCompleted:s,workflowsTotal:t.length,failedRequired:o,failedAdvisory:r,perWorkflow:i}}const un={low:"Low exposure — no critical or serious violations detected. Manual checks still recommended; automation catches ~30–50% of WCAG issues.",moderate:"Moderate exposure — some serious or multiple moderate violations. Should be addressed but not lawsuit-emergency level.",high:"High exposure — multiple serious violations OR multiple lawsuit-magnet categories failing. ADA Title III suits target sites at this level.",critical:"Critical exposure — at least one critical-impact violation OR most lawsuit-magnet categories failing. Sites in this state are demand-letter targets."},hn={low:"✓ Off the radar",moderate:"⚡ Some lawsuit exposure",high:"⚠️ High target risk",critical:"🚨 Lawsuit magnet"},pn={low:"Your site clears the automated checks ADA-targeting tools use to identify lawsuit victims. Targeting bots cover roughly half of WCAG — switch to Developer mode for the Deep Scan to verify full compliance and stay ahead of human plaintiffs.",moderate:"Some failures. Not the easiest target, but the issues here are exactly what automated targeting tools look for. Catch these plus the ~50% of WCAG issues automation alone misses by running the Deep Scan in Developer mode.",high:"Multiple serious failures. Automated lawsuit-screening tools will flag this site. Real risk of being targeted with a demand letter — Developer mode has the Deep Scan with the full issue list and fix-it walkthroughs.",critical:"Your site fails the automated WCAG checks demand-letter mills run on thousands of sites to identify lawsuit targets. Without remediation, expect to be flagged. Developer mode has the Deep Scan that lists every issue plus fix-it guidance for each."},mn={A:"No critical, serious, or moderate violations detected. Note: automation catches ~30–50% of WCAG; run the Guided Tests workflows to fully validate.",B:"Only moderate-impact violations detected. No serious or critical findings.",C:"Has at least one serious-impact violation. Automatically capped here regardless of how few.",D:"Has at least one critical-impact violation. Automatically capped here regardless of how few.",F:"Multiple critical-impact violations detected. Lawsuit-magnet profile."},Y="incompleteResolutions";function ke(e,t,n){return`${e}::${t}::${n}`}async function ie(){try{const t=(await chrome.storage.local.get(Y))[Y];return t&&typeof t=="object"?t:{}}catch{return{}}}async function xe(e){await chrome.storage.local.set({[Y]:e})}async function kt(e){const t=await ie();t[ke(e.pageUrl,e.ruleId,e.selector)]=e,await xe(t)}async function xt(e){if(e.length===0)return;const t=await ie();for(const n of e)t[ke(n.pageUrl,n.ruleId,n.selector)]=n;await xe(t)}async function Se(){const e=await ie();return Object.values(e)}async function St(e){return(await Se()).filter(n=>n.pageUrl===e)}const gn=Object.freeze(Object.defineProperty({__proto__:null,getAllResolutions:Se,getResolutionsForPage:St,saveResolution:kt,saveResolutions:xt},Symbol.toStringTag,{value:"Module"})),Dt={"color-contrast":{whatsWrong:"Some text on this page is too light to read against its background.",whyItMatters:"People with low vision, older eyes, or anyone reading on a sunny phone screen can't make out faint text. Color blindness affects about 1 in 12 men and 1 in 200 women. Insufficient contrast is the #1 most-common cause of accessibility lawsuits — every Domino's-style ADA suit cites it.",howToFix:"If you use a website builder (Squarespace, Wix, Shopify, WordPress with a theme), open your site styles or theme color settings and pick darker text colors or lighter backgrounds. Aim for very dark text on white, or very light text on dark. Free check at webaim.org/resources/contrastchecker. If you have a developer, ask them to ensure all text meets WCAG AA contrast (4.5:1 for normal text, 3:1 for large text)."},"color-contrast-enhanced":{whatsWrong:"Text on this page doesn't meet the higher (AAA) contrast level — important for people with significant low vision.",whyItMatters:"AAA is a stricter standard than the legal-minimum AA. If your audience includes older adults, people with cataracts or macular degeneration, or government-related contracts that require AAA conformance, this matters.",howToFix:"Same as basic contrast — go darker on text or lighter on backgrounds. AAA requires 7:1 for normal text and 4.5:1 for large text."},"link-in-text-block":{whatsWrong:"Links inside paragraphs are distinguishable from the surrounding text by color alone.",whyItMatters:"If a user is colorblind or sees in grayscale, they can't tell which words are clickable. WCAG requires another visual cue (underline, bold, icon) in addition to color.",howToFix:"Add an underline to links inside paragraphs. In most theme editors there's a 'link style' setting — turn underlines back on. The underline is the standard convention; it's also more discoverable for everyone."},"image-alt":{whatsWrong:"Some images on this page have no description for screen readers.",whyItMatters:"Blind and low-vision users rely on screen readers to read pages aloud. When an image has no description, the screen reader either skips it entirely or reads out the file name — useless. Missing alt text is the #2 lawsuit-magnet category.",howToFix:"In your site editor, click each image and look for an 'Alt text' or 'Image description' field. Write a short sentence describing what the image shows or what it links to. For purely decorative images (dividers, background flourishes), enter empty alt text or check a 'decorative' box if the platform offers one."},"input-image-alt":{whatsWrong:"Image-based form buttons (submit buttons that are images) have no description.",whyItMatters:"Without a description, screen-reader users can't tell what the button does — they hear 'button, submit.png' or just 'graphic'.",howToFix:"Replace image submit buttons with regular text buttons (most platforms let you do this in form settings), OR if you must use an image, ensure it has alt text describing the action ('Subscribe', 'Send message', 'Submit form')."},"area-alt":{whatsWrong:"An image map (clickable regions inside a single image) has unlabeled regions.",whyItMatters:"Each clickable area is a link — without a description, screen-reader users don't know what it points to.",howToFix:"Image maps are uncommon today. If you need one, your developer should add an alt attribute to each clickable area. The simpler modern approach: use multiple separate linked images or buttons instead."},"svg-img-alt":{whatsWrong:"Some SVG icons that act as images don't have a description.",whyItMatters:"SVG icons used decoratively are fine to leave silent, but icons that convey meaning (status indicators, social-media icons that link out) need a description so screen readers know what they represent.",howToFix:"In most platforms this requires a developer. Tell them: 'meaningful SVG icons need a title or aria-label; decorative SVGs need aria-hidden=true'. Examples: an envelope icon linking to 'Contact us' should be labeled 'Contact us', not silent."},"object-alt":{whatsWrong:"Embedded content (PDFs, videos, embedded apps) has no fallback description.",whyItMatters:"If the embed fails to load — or the user's screen reader can't navigate it — they need a text description of what the embed is.",howToFix:"Replace embedded objects with native modern alternatives where possible (HTML5 video instead of Flash, linked PDF with a description). If you must use an <object> embed, ask your developer to add fallback content describing what it is."},"role-img-alt":{whatsWrong:"An element marked as an image is missing its description.",whyItMatters:"Same as missing image alt — screen-reader users get nothing where they should hear a description.",howToFix:"Ask your developer: 'elements with role=img need aria-label or aria-labelledby'."},"image-redundant-alt":{whatsWrong:"Some images have alt text that duplicates nearby visible text.",whyItMatters:"Screen-reader users hear the same content twice — the surrounding text and then the alt text. Annoying, not blocking. Lower-priority fix.",howToFix:"Find images where the caption or nearby paragraph already says what the image shows, and either change the alt text to add new info or set the alt to empty (so the screen reader skips it as decorative)."},label:{whatsWrong:"Some form fields have no label that screen readers can announce.",whyItMatters:"Visually you can see what each field is for (next to a 'Name:' or 'Email:' text), but screen-reader users hear only 'edit text' if there's no programmatic label connecting the text to the field. The result: blind users can't tell what to type where.",howToFix:"In a website builder, every form field has a 'Label' field or similar. Make sure every input has one filled in. Placeholder text (the gray hint inside the field) does NOT count as a label — it has to be a real label."},"select-name":{whatsWrong:"A dropdown menu has no description.",whyItMatters:"Screen-reader users hear 'combo box, has popup' but not what the dropdown is for. They can't fill out forms reliably.",howToFix:"In your form editor, check that each dropdown has a visible label above or beside it. If your platform has separate label/field settings, both must be filled."},"aria-input-field-name":{whatsWrong:"A custom input control (search box, range slider, custom date picker) has no name.",whyItMatters:"Custom controls are common in modern site builders — they look fancy but break for screen-reader users when not labeled.",howToFix:"This is a developer fix. Ask: 'custom inputs with role=textbox / searchbox / slider / spinbutton need an aria-label or aria-labelledby'."},"aria-toggle-field-name":{whatsWrong:"A toggle switch (on/off control) has no description.",whyItMatters:"User can see the switch but can't tell what it controls without a label.",howToFix:"Developer fix: 'switches and checkboxes with role=switch need an accessible name'."},"form-field-multiple-labels":{whatsWrong:"A form field has multiple labels, which confuses screen readers.",whyItMatters:"Screen readers will read all labels — sometimes saying conflicting things — leaving users unsure what to enter.",howToFix:"Developer fix: 'this field has multiple <label> associations; pick one and remove the others'."},"label-content-name-mismatch":{whatsWrong:"A button's visible text doesn't match the description it announces to screen readers.",whyItMatters:"A user dictating 'click Submit' to voice-control software may see 'Submit' on screen but the announce-name is something else, so the voice command fails.",howToFix:"Developer fix: 'aria-label should start with or fully contain the visible button text'."},"autocomplete-valid":{whatsWrong:"A field that should auto-fill (name, email, phone, address) is missing the right autocomplete hint.",whyItMatters:"Browsers use these hints to auto-fill personal info, helping users with motor disabilities and saving everyone time. Older adults appreciate this most.",howToFix:"In your form builder, look for an 'Autocomplete' or 'Field type' setting per field. Set them to 'email', 'name', 'tel', 'street-address' etc. as appropriate."},"button-name":{whatsWrong:"Some buttons have no description that screen readers can announce.",whyItMatters:"Visual users see an icon (a heart, a trash can, an X), but screen-reader users hear only 'button' — they don't know if it'll favorite a post, delete it, or close a dialog. This is a critical accessibility blocker and a top lawsuit driver.",howToFix:"Every button needs a text label. If you can't put text inside the button (icon-only design), in your platform's button settings look for an 'Aria label' or 'Screen reader text' field — fill it with what the button does ('Close', 'Add to cart', 'Open menu')."},"input-button-name":{whatsWrong:"An <input type=submit> or similar button has no description.",whyItMatters:"Same as button-name — screen-reader users don't know what the button does.",howToFix:"Set the button's 'value' or 'label' to text describing the action ('Submit', 'Subscribe', 'Send')."},"link-name":{whatsWrong:"Some links have no readable text — usually icon-only links.",whyItMatters:"Screen-reader users hear 'link' but not where the link goes. Users navigate by listening to a list of links — useless if every entry is just 'link, link, link'.",howToFix:"In your site editor, for icon-only links (social-media icons, navigation arrows), set an 'Aria label' or 'Title' field describing where it goes ('Follow us on Twitter', 'Next page')."},"empty-button":{whatsWrong:"A button is completely empty — no text, no icon, no description.",whyItMatters:"Both visual and screen-reader users can't tell what an empty button does.",howToFix:"Find empty buttons in your editor (often a leftover from a deleted icon) and either delete them or add text/an icon + description."},"empty-link":{whatsWrong:"A link has no readable text or description.",whyItMatters:"Screen-reader users hear nothing useful; visual users see an empty space they can't tell is clickable.",howToFix:"Same as link-name — add link text or an aria-label describing the destination."},"empty-heading":{whatsWrong:"A heading on this page has no text.",whyItMatters:"Screen-reader users navigate pages by jumping between headings. An empty heading is dead air — they don't know what the section is about.",howToFix:"Find the empty heading in your editor (often a section title that was deleted but the heading element remained) and either delete it or add real heading text."},"document-title":{whatsWrong:"This page's title (in the browser tab) is missing or generic.",whyItMatters:"Tab titles help users who have many tabs open know which one is which. Screen readers also announce the page title when the page loads.",howToFix:"In your platform's page settings or SEO panel, find the 'Page title' field and set it to something descriptive ('Contact Us — YourBusiness', not 'Untitled' or just your business name)."},"html-has-lang":{whatsWrong:"The page doesn't declare what language it's in.",whyItMatters:"Screen readers use this to pick the right voice and pronunciation rules. Without it, English content might be read with a French accent (or vice versa).",howToFix:"In your platform's site settings, find a 'Site language' option and set it (English = en, Spanish = es, etc.). Most modern platforms do this automatically — if it's missing, ask your developer to add lang='en' to the html element."},"html-lang-valid":{whatsWrong:"The page declares a language code that isn't valid.",whyItMatters:"Same as missing language — screen readers can't pick the right voice.",howToFix:"Use a standard ISO language code: en, es, fr, de, ja, zh, pt, etc. Avoid full strings like 'english' — use the two-letter code."},"valid-lang":{whatsWrong:"Text marked as a different language uses an invalid code.",whyItMatters:"Same as above — screen readers can't switch voices for the foreign-language passage.",howToFix:"Use ISO codes (en, fr, es) for the lang attribute on individual elements. This typically requires a developer."},"page-has-heading-one":{whatsWrong:"This page is missing a main heading (H1).",whyItMatters:"Screen-reader users use the H1 to confirm they landed on the right page. The H1 is also a strong SEO signal — Google uses it to understand what the page is about.",howToFix:"Add a clear, descriptive H1 to the top of the page. In website builders, this is usually the 'Title' or 'Page heading' setting. Each page should have exactly one H1."},"heading-order":{whatsWrong:"Heading levels skip — for example, an H2 jumps directly to an H4 with no H3 between them.",whyItMatters:"Screen-reader users navigate by heading levels. Skipped levels suggest missing content and break the page's outline. Confusing for everyone.",howToFix:"In your editor, find the headings that skip levels and either bump them up to the right level (H4 → H3 if no H2 exists between) or insert the missing level. Headings should follow a logical outline like a document outline."},"landmark-one-main":{whatsWrong:"This page has no 'main content' region marked.",whyItMatters:"Screen-reader users press a single shortcut to jump to the main content, skipping menus and headers. Without a marked main region, they have to tab through everything.",howToFix:"Most modern platforms add this automatically. If yours doesn't, ask your developer to wrap the main content in a <main> element."},"landmark-no-duplicate-banner":{whatsWrong:"There are multiple top banners (headers) on the page when there should be one.",whyItMatters:"Screen-reader users get confused by duplicate landmarks — they don't know which is the 'real' header.",howToFix:"Developer fix: 'only one element should have role=banner or be a top-level <header>; remove duplicates'."},"landmark-no-duplicate-contentinfo":{whatsWrong:"There are multiple footer regions on the page when there should be one.",whyItMatters:"Same as duplicate banner — confusing landmark navigation.",howToFix:"Developer fix: 'only one element should have role=contentinfo or be a top-level <footer>; remove duplicates'."},region:{whatsWrong:"Some content on the page isn't inside a recognized region (header, nav, main, footer, aside).",whyItMatters:"Screen-reader users navigate by these regions. Content outside any region can be missed when skimming.",howToFix:"Developer fix: 'wrap orphaned page content in semantic landmark elements (<main>, <aside>, <nav>) so it's reachable via landmark nav'."},bypass:{whatsWrong:"There's no 'skip to main content' link at the top of the page.",whyItMatters:"Keyboard-only users (and some screen-reader users) have to tab through every menu item on every page before reaching the main content. A skip link gives them a single keystroke to jump past the menu.",howToFix:"Most modern themes include a skip link automatically — it's only visible when keyboard-focused. If yours doesn't, ask your developer to add one as the first focusable element on every page."},"aria-required-attr":{whatsWrong:"An element uses a screen-reader role but is missing required information for that role.",whyItMatters:"Custom controls (sliders, tabs, dialogs) need specific extra info to work properly with screen readers. Missing pieces = broken behavior.",howToFix:"Developer fix: 'this role requires additional aria-* attributes; check axe-core docs for the specific role'."},"aria-allowed-attr":{whatsWrong:"An element has a screen-reader hint that doesn't apply to its role.",whyItMatters:"Inappropriate hints can confuse or break screen-reader announcements.",howToFix:"Developer fix: 'this aria-* attribute isn't allowed on this role; remove it or change the role'."},"aria-roles":{whatsWrong:"An element claims to be a type that isn't valid (typo or misuse of a role).",whyItMatters:"Screen readers ignore invalid roles, leaving the element unannounced.",howToFix:"Developer fix: 'this role value isn't recognized; check spelling or use a valid role'."},"aria-valid-attr":{whatsWrong:"An element uses a screen-reader hint that doesn't exist (typo).",whyItMatters:"Same as invalid roles — typos are silently ignored.",howToFix:"Developer fix: 'this aria-* attribute name isn't recognized; check spelling'."},"presentation-role-conflict":{whatsWrong:"An element is marked as decorative but is also focusable or has a description — contradictory.",whyItMatters:"Screen readers handle this inconsistently; some announce it anyway, some don't, leaving users with mixed experiences.",howToFix:"Developer fix: 'role=presentation/none cannot coexist with focusability or aria-label'."},"duplicate-id":{whatsWrong:"Two or more elements on the page share the same internal ID.",whyItMatters:"Some screen reader and skip-link behaviors rely on unique IDs. Duplicates can break label associations and skip-link targets.",howToFix:"Developer fix: 'rename duplicate IDs so each is unique'. Often arises when copying components."},"duplicate-id-aria":{whatsWrong:"An ID referenced by a screen-reader hint exists multiple times.",whyItMatters:"Screen readers may pick the wrong one and announce wrong information.",howToFix:"Developer fix: 'IDs used in aria-labelledby / aria-describedby must be unique'."},"meta-viewport":{whatsWrong:"The page tells mobile browsers users can't zoom in.",whyItMatters:"People with low vision rely on pinch-zoom to read small text. Disabling zoom is a real WCAG failure and a common lawsuit target.",howToFix:"In platform site settings, look for a 'Mobile zoom' or 'Pinch zoom' option and enable it. If your developer added user-scalable=no in a viewport meta tag, ask them to remove it."},"nested-interactive":{whatsWrong:"An interactive element (link, button) is inside another interactive element.",whyItMatters:"Keyboard and screen-reader users can't reliably reach the inner element. Common in 'card-as-link' designs where a button sits inside a clickable card.",howToFix:"Developer fix: 'avoid putting buttons inside links or links inside buttons; use a single interactive element with internal helpers'."},tabindex:{whatsWrong:"Some elements have positive tabindex values, forcing a custom focus order.",whyItMatters:"Custom focus orders are nearly always confusing — focus jumps around unexpectedly. Best practice is to never use positive tabindex.",howToFix:"Developer fix: 'replace tabindex=1/2/3... with tabindex=0 (or remove entirely); use DOM order to control focus order'."},"frame-focusable-content":{whatsWrong:"A frame on the page can't be navigated by keyboard.",whyItMatters:"Keyboard users get stuck — they can't reach interactive content inside the frame.",howToFix:"Developer fix: 'iframes containing interactive content shouldn't have tabindex=-1 on their parent or content'."},"no-focusable-content":{whatsWrong:"An element looks interactive but can't actually receive keyboard focus.",whyItMatters:"Keyboard-only users can't activate the element.",howToFix:"Developer fix: 'replace divs/spans-with-onClick with a real <button>, or add tabindex=0'."},"target-size":{whatsWrong:"Some buttons or links are too small to tap reliably on touch screens.",whyItMatters:"WCAG 2.2 requires touch targets to be at least 24×24 pixels. Small targets are a problem for users with hand tremors, large fingers, or motor disabilities. They're also frustrating for everyone.",howToFix:"In your editor, increase padding around small icon buttons. If your developer is involved, ask them to ensure all interactive elements have minimum 24×24 CSS pixels of clickable area."},"frame-title":{whatsWrong:"An iframe (embedded content) has no title.",whyItMatters:"Screen readers announce frame titles when entering them. Without one, users hear 'frame' with no context.",howToFix:"If using a third-party embed (YouTube, Vimeo, Stripe), most platforms set the title automatically. If yours doesn't, look for a 'Title' or 'Description' field on the embed block."}};function Tt(e){const t=Dt[e];return t||{whatsWrong:"An accessibility rule failed on this page.",whyItMatters:"Some users — particularly those using screen readers, keyboard-only navigation, or who have low vision or motor disabilities — may have trouble using this part of the page.",howToFix:"Share the technical rule ID with your developer. They can look up the full fix at https://dequeuniversity.com/rules/axe."}}const Ct={whatsWrong:"This element appears to have a contrast issue, but it's actually transparent — its parent (or itself) has an opacity below 100%.",whyItMatters:"When something is partially transparent, the colors blend with whatever's behind it, which automated tools see as poor contrast. Most often this happens because the element is part of an entry animation (fade-in on scroll) that hasn't completed when the audit ran. The element will likely look correct once the animation finishes.",howToFix:"Check whether this element is inside a scroll-reveal animation (common patterns: classes like .fade-in, .reveal, .animate-on-scroll; or JavaScript using IntersectionObserver to add a .visible class). If so, this is NOT a real contrast problem — the element will become opaque once the user scrolls past it. If you want the audit to skip these false positives, ensure your site honors prefers-reduced-motion (which serves the final state immediately) or remove the opacity:0 default."};function fn(e,t){return e==="color-contrast"&&t&&(t.self<.99||t.ancestor<.99)?Ct:Tt(e)}const O="inflight:audit";async function De(){return(await chrome.storage.local.get(O))[O]??null}async function Te(e){await chrome.storage.local.set({[O]:e})}async function vn(){await chrome.storage.local.remove(O)}async function yn(){const e=await De();e&&e.state==="running"&&await Te({...e,state:"interrupted",lastProgressAt:new Date().toISOString()})}async function bn(e){const t=await De();t&&await Te({...t,...e,lastProgressAt:new Date().toISOString()})}function wn(){return crypto.randomUUID()}const J=(e,t)=>t.some(n=>e instanceof n);let ce,de;function Et(){return ce||(ce=[IDBDatabase,IDBObjectStore,IDBIndex,IDBCursor,IDBTransaction])}function Rt(){return de||(de=[IDBCursor.prototype.advance,IDBCursor.prototype.continue,IDBCursor.prototype.continuePrimaryKey])}const Q=new WeakMap,G=new WeakMap,F=new WeakMap;function qt(e){const t=new Promise((n,o)=>{const r=()=>{e.removeEventListener("success",s),e.removeEventListener("error",i)},s=()=>{n(I(e.result)),r()},i=()=>{o(e.error),r()};e.addEventListener("success",s),e.addEventListener("error",i)});return F.set(t,e),t}function Ot(e){if(Q.has(e))return;const t=new Promise((n,o)=>{const r=()=>{e.removeEventListener("complete",s),e.removeEventListener("error",i),e.removeEventListener("abort",i)},s=()=>{n(),r()},i=()=>{o(e.error||new DOMException("AbortError","AbortError")),r()};e.addEventListener("complete",s),e.addEventListener("error",i),e.addEventListener("abort",i)});Q.set(e,t)}let X={get(e,t,n){if(e instanceof IDBTransaction){if(t==="done")return Q.get(e);if(t==="store")return n.objectStoreNames[1]?void 0:n.objectStore(n.objectStoreNames[0])}return I(e[t])},set(e,t,n){return e[t]=n,!0},has(e,t){return e instanceof IDBTransaction&&(t==="done"||t==="store")?!0:t in e}};function Ce(e){X=e(X)}function Mt(e){return Rt().includes(e)?function(...t){return e.apply(Z(this),t),I(this.request)}:function(...t){return I(e.apply(Z(this),t))}}function Wt(e){return typeof e=="function"?Mt(e):(e instanceof IDBTransaction&&Ot(e),J(e,Et())?new Proxy(e,X):e)}function I(e){if(e instanceof IDBRequest)return qt(e);if(G.has(e))return G.get(e);const t=Wt(e);return t!==e&&(G.set(e,t),F.set(t,e)),t}const Z=e=>F.get(e);function Lt(e,t,{blocked:n,upgrade:o,blocking:r,terminated:s}={}){const i=indexedDB.open(e,t),l=I(i);return o&&i.addEventListener("upgradeneeded",a=>{o(I(i.result),a.oldVersion,a.newVersion,I(i.transaction),a)}),n&&i.addEventListener("blocked",a=>n(a.oldVersion,a.newVersion,a)),l.then(a=>{s&&a.addEventListener("close",()=>s()),r&&a.addEventListener("versionchange",c=>r(c.oldVersion,c.newVersion,c))}).catch(()=>{}),l}const Ft=["get","getKey","getAll","getAllKeys","count"],Pt=["put","add","delete","clear"],V=new Map;function ue(e,t){if(!(e instanceof IDBDatabase&&!(t in e)&&typeof t=="string"))return;if(V.get(t))return V.get(t);const n=t.replace(/FromIndex$/,""),o=t!==n,r=Pt.includes(n);if(!(n in(o?IDBIndex:IDBObjectStore).prototype)||!(r||Ft.includes(n)))return;const s=async function(i,...l){const a=this.transaction(i,r?"readwrite":"readonly");let c=a.store;return o&&(c=c.index(l.shift())),(await Promise.all([c[n](...l),r&&a.done]))[0]};return V.set(t,s),s}Ce(e=>({...e,get:(t,n,o)=>ue(t,n)||e.get(t,n,o),has:(t,n)=>!!ue(t,n)||e.has(t,n)}));const _t=["continue","continuePrimaryKey","advance"],he={},ee=new WeakMap,Ee=new WeakMap,Nt={get(e,t){if(!_t.includes(t))return e[t];let n=he[t];return n||(n=he[t]=function(...o){ee.set(this,Ee.get(this)[t](...o))}),n}};async function*Ut(...e){let t=this;if(t instanceof IDBCursor||(t=await t.openCursor(...e)),!t)return;t=t;const n=new Proxy(t,Nt);for(Ee.set(n,t),F.set(n,Z(t));t;)yield n,t=await(ee.get(n)||t.continue()),ee.delete(n)}function pe(e,t){return t===Symbol.asyncIterator&&J(e,[IDBIndex,IDBObjectStore,IDBCursor])||t==="iterate"&&J(e,[IDBIndex,IDBObjectStore])}Ce(e=>({...e,get(t,n,o){return pe(t,n)?Ut:e.get(t,n,o)},has(t,n){return pe(t,n)||e.has(t,n)}}));const Gt="wcag-forensic-log",Vt=1,x="audits";let z=null;function oe(){return z||(z=Lt(Gt,Vt,{upgrade(e){if(!e.objectStoreNames.contains(x)){const t=e.createObjectStore(x,{keyPath:["componentId","capturedAt"]});t.createIndex("byCapturedAt","capturedAt"),t.createIndex("byComponentId","componentId")}}})),z}function te(e){return e===null||typeof e!="object"?JSON.stringify(e):Array.isArray(e)?"["+e.map(o=>te(o)).join(",")+"]":"{"+Object.keys(e).sort().map(o=>{const r=e[o];return JSON.stringify(o)+":"+te(r)}).join(",")+"}"}async function zt(e){const t=te(e),n=new TextEncoder().encode(t),o=await crypto.subtle.digest("SHA-256",n);return Array.from(new Uint8Array(o)).map(r=>r.toString(16).padStart(2,"0")).join("")}async function An(e,t,n){if(e.length===0)return null;const o=e[0],r=o.componentId,s=o.pageUrl??o.scope,i=o.startedAt,l={componentId:r,pageUrl:s,scope:o.scope,grade:t.letter,totals:{critical:t.totals.critical,serious:t.totals.serious,moderate:t.totals.moderate,minor:t.totals.minor,unique:t.totals.unique},axeVersion:o.axeVersion,capturedAt:i,statesAudited:e.length},c={hash:await zt(l),componentId:r,pageUrl:s,scope:o.scope,grade:t.letter,totals:l.totals,axeVersion:o.axeVersion,capturedAt:i,statesAudited:e.length,durationMs:n};try{await(await oe()).put(x,c)}catch(d){return console.warn("[forensic-log] failed to record audit",d),c}return c}async function In(e,t,n){try{const o=await oe(),r=await o.get(x,[e,t]);if(!r){console.warn("[forensic-log] no entry to attach receipt to",{componentId:e,capturedAt:t});return}r.receipt=n,await o.put(x,r)}catch(o){console.warn("[forensic-log] failed to attach receipt",o)}}async function kn(){try{return(await(await oe()).getAll(x)).sort((n,o)=>n.capturedAt<o.capturedAt?1:-1)}catch(e){return console.warn("[forensic-log] listAudits failed",e),[]}}export{we as A,an as B,rn as C,Ae as D,ne as E,Lt as F,jt as G,tn as H,Be as I,pn as J,D as K,nn as L,kt as M,Te as N,hn as O,wn as P,bn as Q,un as R,An as S,Ie as T,In as U,Jt as V,yn as W,mn as X,gn as Y,Bt as a,en as b,$t as c,Qt as d,Ze as e,on as f,B as g,Zt as h,St as i,At as j,cn as k,Xe as l,ft as m,xt as n,sn as o,Tt as p,De as q,vn as r,Xt as s,dn as t,Yt as u,kn as v,fn as w,gt as x,ln as y,te as z};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function s(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerPolicy&&(r.referrerPolicy=e.referrerPolicy),e.crossOrigin==="use-credentials"?r.credentials="include":e.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=s(e);fetch(e.href,r)}})();
|