@accesslint/core 0.8.1 → 0.8.3

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.
Files changed (37) hide show
  1. package/dist/index.cjs +5 -5
  2. package/dist/index.iife.js +5 -5
  3. package/dist/index.js +345 -293
  4. package/dist/rules/adaptable/empty-table-header.d.ts.map +1 -1
  5. package/dist/rules/adaptable/td-has-header.d.ts.map +1 -1
  6. package/dist/rules/aria/aria-allowed-attr.d.ts.map +1 -1
  7. package/dist/rules/distinguishable/color-contrast-enhanced.d.ts.map +1 -1
  8. package/dist/rules/distinguishable/color-contrast-helpers.d.ts +4 -0
  9. package/dist/rules/distinguishable/color-contrast-helpers.d.ts.map +1 -1
  10. package/dist/rules/distinguishable/color-contrast.d.ts.map +1 -1
  11. package/dist/rules/distinguishable/link-in-text-block.d.ts.map +1 -1
  12. package/dist/rules/distinguishable/meta-viewport.d.ts.map +1 -1
  13. package/dist/rules/engine.d.ts.map +1 -1
  14. package/dist/rules/index.d.ts +5 -0
  15. package/dist/rules/index.d.ts.map +1 -1
  16. package/dist/rules/keyboard-accessible/focus-visible.d.ts.map +1 -1
  17. package/dist/rules/keyboard-accessible/scrollable-region.d.ts.map +1 -1
  18. package/dist/rules/labels-and-names/button-name.d.ts.map +1 -1
  19. package/dist/rules/labels-and-names/form-label.d.ts.map +1 -1
  20. package/dist/rules/labels-and-names/frame-title.d.ts.map +1 -1
  21. package/dist/rules/labels-and-names/input-button-name.d.ts.map +1 -1
  22. package/dist/rules/labels-and-names/label-content-mismatch.d.ts.map +1 -1
  23. package/dist/rules/navigable/empty-heading.d.ts.map +1 -1
  24. package/dist/rules/navigable/heading-order.d.ts.map +1 -1
  25. package/dist/rules/navigable/link-name.d.ts.map +1 -1
  26. package/dist/rules/navigable/p-as-heading.d.ts.map +1 -1
  27. package/dist/rules/text-alternatives/image-alt-words.d.ts.map +1 -1
  28. package/dist/rules/text-alternatives/img-alt.d.ts.map +1 -1
  29. package/dist/rules/text-alternatives/input-image-alt.d.ts.map +1 -1
  30. package/dist/rules/text-alternatives/object-alt.d.ts.map +1 -1
  31. package/dist/rules/text-alternatives/role-img-alt.d.ts.map +1 -1
  32. package/dist/rules/text-alternatives/svg-img-alt.d.ts.map +1 -1
  33. package/dist/rules/time-based-media/audio-transcript.d.ts.map +1 -1
  34. package/dist/rules/time-based-media/video-captions.d.ts.map +1 -1
  35. package/dist/rules/types.d.ts +8 -1
  36. package/dist/rules/types.d.ts.map +1 -1
  37. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- const z = [
1
+ const j = [
2
2
  "a[href]",
3
3
  "button:not([disabled])",
4
4
  'input:not([disabled]):not([type="hidden"])',
@@ -133,22 +133,22 @@ function $e(e) {
133
133
  return null;
134
134
  }
135
135
  }
136
- function j(e) {
136
+ function W(e) {
137
137
  var n;
138
138
  const a = K.get(e);
139
139
  if (a !== void 0) return a;
140
140
  const i = ((n = e.getAttribute("role")) == null ? void 0 : n.trim().toLowerCase()) || null || $e(e);
141
141
  return K.set(e, i), i;
142
142
  }
143
- let J = /* @__PURE__ */ new WeakMap();
143
+ let Q = /* @__PURE__ */ new WeakMap();
144
144
  function st() {
145
- J = /* @__PURE__ */ new WeakMap();
145
+ Q = /* @__PURE__ */ new WeakMap();
146
146
  }
147
- function x(e) {
148
- const a = J.get(e);
147
+ function w(e) {
148
+ const a = Q.get(e);
149
149
  if (a !== void 0) return a;
150
150
  const t = lt(e);
151
- return J.set(e, t), t;
151
+ return Q.set(e, t), t;
152
152
  }
153
153
  function lt(e) {
154
154
  var r, o, s, l, d;
@@ -310,25 +310,25 @@ const ut = /* @__PURE__ */ new Set([
310
310
  "aria-relevant",
311
311
  "aria-roledescription"
312
312
  ]);
313
- function q(e) {
313
+ function E(e) {
314
314
  let a = e;
315
315
  for (; a; ) {
316
- if (De(a)) return !0;
316
+ if (He(a)) return !0;
317
317
  a = a.parentElement;
318
318
  }
319
319
  return !1;
320
320
  }
321
- let Q = /* @__PURE__ */ new WeakMap();
321
+ let Z = /* @__PURE__ */ new WeakMap();
322
322
  function mt() {
323
- Q = /* @__PURE__ */ new WeakMap();
323
+ Z = /* @__PURE__ */ new WeakMap();
324
324
  }
325
325
  function h(e) {
326
- const a = Q.get(e);
326
+ const a = Z.get(e);
327
327
  if (a !== void 0) return a;
328
328
  let t;
329
- return e.getAttribute("aria-hidden") === "true" || e instanceof HTMLElement && (e.hidden || e.style.display === "none") ? t = !0 : e.parentElement ? t = h(e.parentElement) : t = !1, Q.set(e, t), t;
329
+ return e.getAttribute("aria-hidden") === "true" || e instanceof HTMLElement && (e.hidden || e.style.display === "none") ? t = !0 : e.parentElement ? t = h(e.parentElement) : t = !1, Z.set(e, t), t;
330
330
  }
331
- function De(e) {
331
+ function He(e) {
332
332
  if (e.getAttribute("aria-hidden") === "true" || e instanceof HTMLElement && e.hidden) return !0;
333
333
  if (typeof getComputedStyle == "function") {
334
334
  const a = getComputedStyle(e);
@@ -345,7 +345,7 @@ function k(e) {
345
345
  a += s.textContent ?? "";
346
346
  else if (s.nodeType === 1) {
347
347
  const l = s;
348
- if (!De(l)) {
348
+ if (!He(l)) {
349
349
  const d = (t = l.tagName) == null ? void 0 : t.toLowerCase();
350
350
  if (d === "img" || d === "area") {
351
351
  const c = l.getAttribute("aria-labelledby");
@@ -373,7 +373,7 @@ function k(e) {
373
373
  }
374
374
  return a;
375
375
  }
376
- function Z(e) {
376
+ function ee(e) {
377
377
  let a = "";
378
378
  for (const t of e.childNodes)
379
379
  if (t.nodeType === 3)
@@ -383,11 +383,11 @@ function Z(e) {
383
383
  if (n === "style" || n === "script" || n === "svg" || i.getAttribute("aria-hidden") === "true" || i instanceof HTMLElement && i.style.display === "none") continue;
384
384
  const r = i.getAttribute("role");
385
385
  if (r === "img" || r === "presentation" || r === "none") continue;
386
- a += Z(i);
386
+ a += ee(i);
387
387
  }
388
388
  return a;
389
389
  }
390
- function Pe(e) {
390
+ function De(e) {
391
391
  let a = e;
392
392
  for (; a; ) {
393
393
  if (a instanceof HTMLElement && a.style.visibility === "hidden") return !0;
@@ -395,7 +395,7 @@ function Pe(e) {
395
395
  }
396
396
  return !1;
397
397
  }
398
- function Fe(e) {
398
+ function Pe(e) {
399
399
  var n, r;
400
400
  const a = e.getAttribute("aria-labelledby");
401
401
  if (a) {
@@ -413,9 +413,9 @@ function Fe(e) {
413
413
  function se(e) {
414
414
  return e.getRootNode() instanceof ShadowRoot;
415
415
  }
416
- let ee = /* @__PURE__ */ new WeakMap();
416
+ let te = /* @__PURE__ */ new WeakMap();
417
417
  function pt() {
418
- ee = /* @__PURE__ */ new WeakMap();
418
+ te = /* @__PURE__ */ new WeakMap();
419
419
  }
420
420
  function bt(e) {
421
421
  return e.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
@@ -447,7 +447,7 @@ function gt(e) {
447
447
  }
448
448
  return a;
449
449
  }
450
- function G(e) {
450
+ function Y(e) {
451
451
  if (e.id) return `#${CSS.escape(e.id)}`;
452
452
  const a = e.getRootNode(), t = a instanceof ShadowRoot ? null : a.documentElement;
453
453
  if (e === t) return e.tagName.toLowerCase();
@@ -472,28 +472,28 @@ function G(e) {
472
472
  }
473
473
  function p(e) {
474
474
  var r;
475
- const a = ee.get(e);
475
+ const a = te.get(e);
476
476
  if (a !== void 0) return a;
477
477
  const t = [];
478
478
  let i = e;
479
479
  for (; i; ) {
480
480
  const o = i.getRootNode();
481
481
  if (o instanceof ShadowRoot)
482
- t.unshift({ selector: G(i), delimiter: " >>> " }), i = o.host;
482
+ t.unshift({ selector: Y(i), delimiter: " >>> " }), i = o.host;
483
483
  else {
484
484
  const s = (r = o.defaultView) == null ? void 0 : r.frameElement;
485
485
  if (s)
486
- t.unshift({ selector: G(i), delimiter: " >>>iframe> " }), i = s;
486
+ t.unshift({ selector: Y(i), delimiter: " >>>iframe> " }), i = s;
487
487
  else {
488
- t.unshift({ selector: G(i), delimiter: "" });
488
+ t.unshift({ selector: Y(i), delimiter: "" });
489
489
  break;
490
490
  }
491
491
  }
492
492
  }
493
493
  const n = t.map((o, s) => (s === 0 ? "" : o.delimiter) + o.selector).join("");
494
- return ee.set(e, n), n;
494
+ return te.set(e, n), n;
495
495
  }
496
- function Un(e) {
496
+ function Wn(e) {
497
497
  const a = [], t = [];
498
498
  let i = e;
499
499
  for (; i; ) {
@@ -609,7 +609,7 @@ const ft = /* @__PURE__ */ new Set([
609
609
  "aria-valuemax",
610
610
  "aria-valuemin",
611
611
  "aria-valuenow"
612
- ]), Y = {
612
+ ]), X = {
613
613
  "aria-autocomplete": /* @__PURE__ */ new Set(["inline", "list", "both", "none"]),
614
614
  "aria-expanded": /* @__PURE__ */ new Set(["true", "false", "undefined"]),
615
615
  "aria-current": /* @__PURE__ */ new Set(["page", "step", "location", "date", "time", "true", "false"]),
@@ -781,10 +781,10 @@ function le(e) {
781
781
  fix: { type: "suggest", suggestion: `Set ${c.name} to a valid number value` }
782
782
  });
783
783
  }
784
- } else if (Y[c.name]) {
784
+ } else if (X[c.name]) {
785
785
  const b = u.split(/\s+/);
786
786
  for (const g of b)
787
- if (!Y[c.name].has(g)) {
787
+ if (!X[c.name].has(g)) {
788
788
  const f = d();
789
789
  t.push({
790
790
  ruleId: "aria/aria-valid-attr-value",
@@ -792,7 +792,7 @@ function le(e) {
792
792
  html: f.html,
793
793
  impact: "critical",
794
794
  message: `Invalid value "${u}" for ${c.name}.`,
795
- fix: { type: "suggest", suggestion: `Set ${c.name} to one of: ${[...Y[c.name]].join(", ")}` }
795
+ fix: { type: "suggest", suggestion: `Set ${c.name} to one of: ${[...X[c.name]].join(", ")}` }
796
796
  });
797
797
  break;
798
798
  }
@@ -818,14 +818,14 @@ function le(e) {
818
818
  if (fe.has(c)) {
819
819
  const g = r.hasAttribute("aria-label"), f = r.hasAttribute("aria-labelledby");
820
820
  if (g || f) {
821
- const v = d(), w = g ? "aria-label" : "aria-labelledby";
821
+ const v = d(), y = g ? "aria-label" : "aria-labelledby";
822
822
  i.push({
823
823
  ruleId: "aria/aria-prohibited-attr",
824
824
  selector: v.selector,
825
825
  html: v.html,
826
826
  impact: "serious",
827
827
  message: `aria-label and aria-labelledby are prohibited on role "${c}".`,
828
- fix: { type: "remove-attribute", attribute: w }
828
+ fix: { type: "remove-attribute", attribute: y }
829
829
  });
830
830
  }
831
831
  }
@@ -851,13 +851,13 @@ function le(e) {
851
851
  }
852
852
  return P = new WeakRef(e), F = { validAttr: a, validAttrValue: t, prohibitedAttr: i }, F;
853
853
  }
854
- let te = /* @__PURE__ */ new WeakMap(), ae = /* @__PURE__ */ new WeakMap(), ie = /* @__PURE__ */ new WeakMap();
854
+ let ae = /* @__PURE__ */ new WeakMap(), ie = /* @__PURE__ */ new WeakMap(), ne = /* @__PURE__ */ new WeakMap();
855
855
  function kt() {
856
- te = /* @__PURE__ */ new WeakMap(), ae = /* @__PURE__ */ new WeakMap(), ie = /* @__PURE__ */ new WeakMap();
856
+ ae = /* @__PURE__ */ new WeakMap(), ie = /* @__PURE__ */ new WeakMap(), ne = /* @__PURE__ */ new WeakMap();
857
857
  }
858
- function y(e) {
859
- let a = te.get(e);
860
- return a || (a = getComputedStyle(e), te.set(e, a), a);
858
+ function x(e) {
859
+ let a = ae.get(e);
860
+ return a || (a = getComputedStyle(e), ae.set(e, a), a);
861
861
  }
862
862
  function I(e, a, t) {
863
863
  const [i, n, r] = [e, a, t].map((o) => {
@@ -927,12 +927,12 @@ function R(e, a, t) {
927
927
  ];
928
928
  }
929
929
  function ye(e) {
930
- const a = ae.get(e);
930
+ const a = ie.get(e);
931
931
  if (a !== void 0) return a;
932
932
  const t = St(e);
933
- return ae.set(e, t), t;
933
+ return ie.set(e, t), t;
934
934
  }
935
- function X(e, a) {
935
+ function J(e, a) {
936
936
  let t = a;
937
937
  for (let i = e.length - 1; i >= 0; i--)
938
938
  t = R(e[i].color, t, e[i].alpha);
@@ -942,13 +942,13 @@ function St(e) {
942
942
  const a = [];
943
943
  let t = e;
944
944
  for (; t; ) {
945
- const n = y(t), r = n.backgroundImage;
945
+ const n = x(t), r = n.backgroundImage;
946
946
  if (r && r !== "none" && r !== "initial") {
947
947
  const d = n.backgroundColor;
948
948
  if (d && d !== "transparent" && d !== "rgba(0, 0, 0, 0)" && d !== "rgba(0 0 0 / 0)") {
949
949
  const c = C(d);
950
950
  if (c)
951
- return a.length > 0 ? X(a, c) : c;
951
+ return a.length > 0 ? J(a, c) : c;
952
952
  }
953
953
  return null;
954
954
  }
@@ -968,13 +968,13 @@ function St(e) {
968
968
  continue;
969
969
  }
970
970
  if (s >= 1)
971
- return a.length > 0 ? X(a, l) : l;
971
+ return a.length > 0 ? J(a, l) : l;
972
972
  a.push({ color: l, alpha: s }), t = t.parentElement;
973
973
  }
974
974
  const i = [255, 255, 255];
975
- return a.length > 0 ? X(a, i) : i;
975
+ return a.length > 0 ? J(a, i) : i;
976
976
  }
977
- function He(e) {
977
+ function Fe(e) {
978
978
  const a = [];
979
979
  let t = 0, i = 0;
980
980
  for (let n = 0; n < e.length; n++)
@@ -989,7 +989,7 @@ function It(e, a = [255, 255, 255]) {
989
989
  let r = 1, o = n + 1;
990
990
  for (; o < e.length && r > 0; o++)
991
991
  e[o] === "(" ? r++ : e[o] === ")" && r--;
992
- const s = e.slice(n + 1, o - 1), l = He(s);
992
+ const s = e.slice(n + 1, o - 1), l = Fe(s);
993
993
  for (const d of l) {
994
994
  const c = d.trim();
995
995
  if (/^(to\s|[\d.]+deg|[\d.]+turn|[\d.]+rad)/i.test(c)) continue;
@@ -1002,29 +1002,29 @@ function It(e, a = [255, 255, 255]) {
1002
1002
  }
1003
1003
  return t;
1004
1004
  }
1005
- const qt = /* @__PURE__ */ new Set(["IMG", "PICTURE", "VIDEO", "SVG"]);
1006
- function Lt(e) {
1007
- const a = ie.get(e);
1005
+ const Et = /* @__PURE__ */ new Set(["IMG", "PICTURE", "VIDEO", "SVG"]);
1006
+ function qt(e) {
1007
+ const a = ne.get(e);
1008
1008
  if (a !== void 0) return a;
1009
1009
  const t = Rt(e);
1010
- return ie.set(e, t), t;
1010
+ return ne.set(e, t), t;
1011
1011
  }
1012
- function Et(e) {
1013
- return qt.has(e.tagName) ? !0 : !!e.querySelector("img, picture, video, svg");
1012
+ function Lt(e) {
1013
+ return Et.has(e.tagName) ? !0 : !!e.querySelector("img, picture, video, svg");
1014
1014
  }
1015
1015
  function Rt(e) {
1016
1016
  let a = e, t = !1;
1017
1017
  for (; a; ) {
1018
- const i = y(a).position;
1018
+ const i = x(a).position;
1019
1019
  if ((i === "absolute" || i === "fixed") && (t = !0), a !== e && i !== "static") {
1020
1020
  for (const n of a.children) {
1021
1021
  if (n === e || n.contains(e)) continue;
1022
- if (Et(n)) {
1022
+ if (Lt(n)) {
1023
1023
  if (t) return !0;
1024
- const o = y(n).position;
1024
+ const o = x(n).position;
1025
1025
  if (o === "absolute" || o === "fixed") return !0;
1026
1026
  }
1027
- const r = y(n);
1027
+ const r = x(n);
1028
1028
  if (r.position === "absolute" || r.position === "fixed") {
1029
1029
  const o = r.backgroundImage;
1030
1030
  if (o && o !== "none" && o !== "initial") return !0;
@@ -1041,11 +1041,11 @@ function Ct(e) {
1041
1041
  return e.endsWith("pt") ? a * (4 / 3) : a;
1042
1042
  }
1043
1043
  function xe(e) {
1044
- const a = y(e), t = Ct(a.fontSize), i = parseInt(a.fontWeight) || (a.fontWeight === "bold" ? 700 : 400);
1044
+ const a = x(e), t = Ct(a.fontSize), i = parseInt(a.fontWeight) || (a.fontWeight === "bold" ? 700 : 400);
1045
1045
  return t >= 23.5 || t >= 18.5 && i >= 700;
1046
1046
  }
1047
1047
  function Tt(e) {
1048
- const a = He(e), t = [];
1048
+ const a = Fe(e), t = [];
1049
1049
  for (const i of a) {
1050
1050
  const n = i.trim();
1051
1051
  if (!n) continue;
@@ -1059,7 +1059,7 @@ function Tt(e) {
1059
1059
  function we(e) {
1060
1060
  return e === "transparent" || e === "rgba(0, 0, 0, 0)" || e === "rgba(0 0 0 / 0)";
1061
1061
  }
1062
- function ne([e, a, t]) {
1062
+ function _([e, a, t]) {
1063
1063
  return "#" + [e, a, t].map((i) => i.toString(16).padStart(2, "0")).join("");
1064
1064
  }
1065
1065
  function Nt(e, a, t) {
@@ -1074,7 +1074,7 @@ function Nt(e, a, t) {
1074
1074
  function Mt(e) {
1075
1075
  let a = 1, t = e;
1076
1076
  for (; t; ) {
1077
- const i = y(t), n = parseFloat(i.opacity);
1077
+ const i = x(t), n = parseFloat(i.opacity);
1078
1078
  isNaN(n) || (a *= n), t = t.parentElement;
1079
1079
  }
1080
1080
  return a;
@@ -1097,18 +1097,18 @@ function $t(e) {
1097
1097
  }
1098
1098
  } catch {
1099
1099
  }
1100
- const i = y(a).backgroundColor;
1100
+ const i = x(a).backgroundColor;
1101
1101
  if (i && !we(i) && U(i) >= 1) break;
1102
1102
  a = a.parentElement;
1103
1103
  }
1104
1104
  return !1;
1105
1105
  }
1106
- const B = /* @__PURE__ */ new Map();
1107
- function Wn(e, a) {
1108
- B.set(e, a), re.delete(e);
1106
+ const G = /* @__PURE__ */ new Map();
1107
+ function Un(e, a) {
1108
+ G.set(e, a), re.delete(e);
1109
1109
  }
1110
- function Dt(e, a) {
1111
- const t = B.get(a);
1110
+ function Ht(e, a) {
1111
+ const t = G.get(a);
1112
1112
  return t ? e.map((i) => {
1113
1113
  const n = t[i.id];
1114
1114
  return n ? {
@@ -1119,20 +1119,20 @@ function Dt(e, a) {
1119
1119
  }) : e;
1120
1120
  }
1121
1121
  const re = /* @__PURE__ */ new Map();
1122
- function Pt(e) {
1122
+ function Dt(e) {
1123
1123
  return e.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1124
1124
  }
1125
- function Ft(e) {
1125
+ function Pt(e) {
1126
1126
  const a = e.split(/\{(\d+)\}/);
1127
1127
  let t = "^";
1128
1128
  for (let i = 0; i < a.length; i++)
1129
- i % 2 === 0 ? t += Pt(a[i]) : t += "(.+?)";
1129
+ i % 2 === 0 ? t += Dt(a[i]) : t += "(.+?)";
1130
1130
  return t += "$", new RegExp(t);
1131
1131
  }
1132
- function Ht(e, a) {
1132
+ function Ft(e, a) {
1133
1133
  let t = re.get(e);
1134
1134
  if (t || (t = /* @__PURE__ */ new Map(), re.set(e, t)), t.has(a)) return t.get(a);
1135
- const i = B.get(e);
1135
+ const i = G.get(e);
1136
1136
  if (!i) return;
1137
1137
  const n = i[a];
1138
1138
  if (!(n != null && n.messages))
@@ -1140,13 +1140,13 @@ function Ht(e, a) {
1140
1140
  const r = [];
1141
1141
  for (const [o, s] of Object.entries(n.messages))
1142
1142
  r.push({
1143
- regex: Ft(o),
1143
+ regex: Pt(o),
1144
1144
  translated: s
1145
1145
  });
1146
1146
  return t.set(a, r), r;
1147
1147
  }
1148
1148
  function zt(e, a, t) {
1149
- const i = Ht(t, e);
1149
+ const i = Ft(t, e);
1150
1150
  if (!i) return a;
1151
1151
  for (const { regex: n, translated: r } of i) {
1152
1152
  const o = a.match(n);
@@ -1159,7 +1159,7 @@ function zt(e, a, t) {
1159
1159
  return a;
1160
1160
  }
1161
1161
  function ze(e, a) {
1162
- return B.has(a) ? e.map((t) => {
1162
+ return G.has(a) ? e.map((t) => {
1163
1163
  const i = zt(t.ruleId, t.message, a);
1164
1164
  return i === t.message ? t : { ...t, message: i };
1165
1165
  }) : e;
@@ -1191,12 +1191,13 @@ const jt = {
1191
1191
  wcag: ["1.1.1"],
1192
1192
  level: "A",
1193
1193
  fixability: "contextual",
1194
+ browserHint: "Screenshot the image to describe its visual content for alt text.",
1194
1195
  description: `Images must have alternate text. Add an alt attribute to <img> elements. Decorative images may use an empty alt attribute (alt=""), role='none', or role='presentation'.`,
1195
- guidance: "Every image needs an alt attribute. For informative images, describe the content or function concisely. For decorative images (backgrounds, spacers, purely visual flourishes), use alt='' to hide them from screen readers. Never omit alt entirely—screen readers may read the filename instead.",
1196
+ guidance: "Every image needs an alt attribute. For informative images, describe the content or function concisely. For decorative images (backgrounds, spacers, purely visual flourishes), use alt='' to hide them from screen readers. Never omit alt entirely—screen readers may read the filename instead. When an image is inside a link or button that already has text, use alt='' if the image is decorative in that context, or write alt text that complements (not duplicates) the existing text.",
1196
1197
  run(e) {
1197
1198
  const a = [];
1198
1199
  for (const t of e.querySelectorAll("img")) {
1199
- if (h(t) || Pe(t)) continue;
1200
+ if (h(t) || De(t)) continue;
1200
1201
  const i = t.getAttribute("role");
1201
1202
  if (i === "presentation" || i === "none") {
1202
1203
  const r = t.getAttribute("tabindex");
@@ -1215,7 +1216,7 @@ const jt = {
1215
1216
  });
1216
1217
  continue;
1217
1218
  }
1218
- !t.hasAttribute("alt") && !x(t) && a.push({
1219
+ !t.hasAttribute("alt") && !w(t) && a.push({
1219
1220
  ruleId: "text-alternatives/img-alt",
1220
1221
  selector: p(t),
1221
1222
  html: m(t),
@@ -1228,27 +1229,28 @@ const jt = {
1228
1229
  return a;
1229
1230
  }
1230
1231
  };
1231
- function Ut(e) {
1232
+ function Wt(e) {
1232
1233
  var i;
1233
- const a = Fe(e);
1234
+ const a = Pe(e);
1234
1235
  if (a) return a;
1235
1236
  const t = e.querySelector("title");
1236
1237
  return (i = t == null ? void 0 : t.textContent) != null && i.trim() ? t.textContent.trim() : "";
1237
1238
  }
1238
- const Wt = {
1239
+ const Ut = {
1239
1240
  id: "text-alternatives/svg-img-alt",
1240
1241
  category: "text-alternatives",
1241
1242
  actRuleIds: ["7d6734"],
1242
1243
  wcag: ["1.1.1"],
1243
1244
  level: "A",
1244
1245
  fixability: "contextual",
1246
+ browserHint: "Screenshot the SVG to understand its content, then add a title element or aria-label.",
1245
1247
  description: "SVG elements with an img, graphics-document, or graphics-symbol role must have an accessible name via a <title> element, aria-label, or aria-labelledby.",
1246
1248
  guidance: "Inline SVGs with role='img' need accessible names. Add a <title> element as the first child of the SVG (screen readers will announce it), or use aria-label on the SVG element. For complex SVGs, use aria-labelledby referencing both a <title> and <desc> element. Decorative SVGs should use aria-hidden='true' instead.",
1247
1249
  run(e) {
1248
1250
  const a = [], t = 'svg[role="img"], [role="graphics-document"], [role="graphics-symbol"]';
1249
1251
  for (const i of e.querySelectorAll(t)) {
1250
1252
  if (h(i)) continue;
1251
- if (!Ut(i)) {
1253
+ if (!Wt(i)) {
1252
1254
  const r = i.getAttribute("role");
1253
1255
  a.push({
1254
1256
  ruleId: "text-alternatives/svg-img-alt",
@@ -1269,12 +1271,13 @@ const Wt = {
1269
1271
  wcag: ["1.1.1", "4.1.2"],
1270
1272
  level: "A",
1271
1273
  fixability: "contextual",
1274
+ browserHint: "Screenshot the image button to see its icon, then set alt to describe the action (e.g., 'Search', 'Submit').",
1272
1275
  description: 'Image inputs (<input type="image">) must have alternate text describing the button action.',
1273
1276
  guidance: "Image buttons (<input type='image'>) act as submit buttons with a custom image. Add alt text via alt, aria-label, or aria-labelledby that describes the action (e.g. alt='Search' or alt='Submit order'), not the image itself. Without it, screen readers announce only 'image' or the filename, giving no clue what the button does.",
1274
1277
  run(e) {
1275
1278
  const a = [];
1276
1279
  for (const t of e.querySelectorAll('input[type="image"]'))
1277
- h(t) || x(t) || a.push({
1280
+ h(t) || w(t) || a.push({
1278
1281
  ruleId: "text-alternatives/input-image-alt",
1279
1282
  selector: p(t),
1280
1283
  html: m(t),
@@ -1325,6 +1328,7 @@ const Wt = {
1325
1328
  level: "A",
1326
1329
  tags: ["best-practice"],
1327
1330
  fixability: "contextual",
1331
+ browserHint: "Screenshot the image to verify the alt text accurately describes it without filler words like 'image of'.",
1328
1332
  description: "Image alt text should not contain words like 'image', 'photo', or 'picture' — screen readers already announce the element type.",
1329
1333
  guidance: "Screen readers already announce 'image' or 'graphic' before reading alt text, so phrases like 'image of', 'photo of', or 'picture of' are redundant. Remove these words and describe what the image shows. For example, change 'image of a dog' to 'golden retriever playing fetch'.",
1330
1334
  run(e) {
@@ -1357,7 +1361,7 @@ const Wt = {
1357
1361
  const a = [];
1358
1362
  for (const t of e.querySelectorAll("area[href]")) {
1359
1363
  if (h(t)) continue;
1360
- x(t) || a.push({
1364
+ w(t) || a.push({
1361
1365
  ruleId: "text-alternatives/area-alt",
1362
1366
  selector: p(t),
1363
1367
  html: m(t),
@@ -1375,13 +1379,14 @@ const Wt = {
1375
1379
  wcag: ["1.1.1"],
1376
1380
  level: "A",
1377
1381
  fixability: "contextual",
1382
+ browserHint: "Screenshot the embedded object to see its content, then add aria-label or title describing it.",
1378
1383
  description: "<object> elements must have alternative text.",
1379
1384
  guidance: "Object elements embed external content that may not be accessible to all users. Provide alternative text via aria-label, aria-labelledby, or a title attribute. The fallback content inside <object> is only shown when the object fails to load and does not serve as an accessible name.",
1380
1385
  run(e) {
1381
1386
  var t;
1382
1387
  const a = [];
1383
1388
  for (const i of e.querySelectorAll("object")) {
1384
- if (h(i) || Pe(i) || i.getAttribute("role") === "presentation" || i.getAttribute("role") === "none" || Fe(i)) continue;
1389
+ if (h(i) || De(i) || i.getAttribute("role") === "presentation" || i.getAttribute("role") === "none" || Pe(i)) continue;
1385
1390
  const n = i.getAttribute("data") || "";
1386
1391
  if (!((i.getAttribute("type") || "").startsWith("image/") || /\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i.test(n))) {
1387
1392
  const s = i.querySelector("img[alt]");
@@ -1405,13 +1410,14 @@ const Wt = {
1405
1410
  wcag: ["1.1.1"],
1406
1411
  level: "A",
1407
1412
  fixability: "contextual",
1413
+ browserHint: "Screenshot the element to see its visual appearance, then provide an aria-label describing what it represents.",
1408
1414
  description: "Elements with role='img' must have an accessible name.",
1409
1415
  guidance: "When you assign role='img' to an element (like a div containing icon fonts or CSS backgrounds), you must provide an accessible name via aria-label or aria-labelledby. Without this, screen reader users have no way to understand what the image represents. If the image is decorative, use role='presentation' or role='none' instead.",
1410
1416
  run(e) {
1411
1417
  const a = [];
1412
1418
  for (const t of e.querySelectorAll('[role="img"]')) {
1413
1419
  if (h(t) || t.tagName.toLowerCase() === "svg" || t.tagName.toLowerCase() === "img") continue;
1414
- x(t) || a.push({
1420
+ w(t) || a.push({
1415
1421
  ruleId: "text-alternatives/role-img-alt",
1416
1422
  selector: p(t),
1417
1423
  html: m(t),
@@ -1422,19 +1428,20 @@ const Wt = {
1422
1428
  }
1423
1429
  return a;
1424
1430
  }
1425
- }, Kt = {
1431
+ }, Jt = {
1426
1432
  id: "time-based-media/video-captions",
1427
1433
  category: "time-based-media",
1428
1434
  actRuleIds: ["eac66b"],
1429
1435
  wcag: ["1.2.2"],
1430
1436
  level: "A",
1431
1437
  fixability: "contextual",
1438
+ browserHint: "Screenshot the video element to see its poster or content for context when writing captions.",
1432
1439
  description: "Video elements must have captions via <track kind='captions'> or <track kind='subtitles'>.",
1433
1440
  guidance: "Captions provide text alternatives for audio content in videos, benefiting deaf users and those who cannot hear audio. Add a <track> element with kind='captions' pointing to a WebVTT caption file. Captions should include both dialogue and important sound effects.",
1434
1441
  run(e) {
1435
1442
  const a = [];
1436
1443
  for (const t of e.querySelectorAll("video")) {
1437
- if (h(t) || q(t) || t.hasAttribute("muted") || t.hasAttribute("autoplay")) continue;
1444
+ if (h(t) || E(t) || t.hasAttribute("muted") || t.hasAttribute("autoplay")) continue;
1438
1445
  t.querySelector('track[kind="captions"], track[kind="subtitles"]') || a.push({
1439
1446
  ruleId: "time-based-media/video-captions",
1440
1447
  selector: p(t),
@@ -1445,19 +1452,20 @@ const Wt = {
1445
1452
  }
1446
1453
  return a;
1447
1454
  }
1448
- }, Jt = {
1455
+ }, Kt = {
1449
1456
  id: "time-based-media/audio-transcript",
1450
1457
  category: "time-based-media",
1451
1458
  actRuleIds: ["e7aa44"],
1452
1459
  wcag: ["1.2.1"],
1453
1460
  level: "A",
1454
1461
  fixability: "contextual",
1462
+ browserHint: "Inspect the page around the audio element for existing transcript links or associated text content.",
1455
1463
  description: "Audio elements should have a text alternative or transcript.",
1456
1464
  guidance: "Audio-only content like podcasts or recordings needs a text alternative for deaf users. Provide a transcript either on the same page or linked nearby. The transcript should include all spoken content and descriptions of relevant sounds.",
1457
1465
  run(e) {
1458
1466
  const a = [];
1459
1467
  for (const t of e.querySelectorAll("audio")) {
1460
- if (h(t) || q(t) || t.querySelector('track[kind="captions"], track[kind="descriptions"]') || t.hasAttribute("aria-describedby")) continue;
1468
+ if (h(t) || E(t) || t.querySelector('track[kind="captions"], track[kind="descriptions"]') || t.hasAttribute("aria-describedby")) continue;
1461
1469
  const n = t.parentElement;
1462
1470
  n && n.querySelector('a[href*="transcript"], a[href*="text"]') || a.push({
1463
1471
  ruleId: "time-based-media/audio-transcript",
@@ -1556,7 +1564,7 @@ const na = {
1556
1564
  run(e) {
1557
1565
  const a = [];
1558
1566
  for (const t of e.querySelectorAll("[autocomplete]")) {
1559
- if (h(t) || q(t) || t.disabled || t.getAttribute("aria-disabled") === "true") continue;
1567
+ if (h(t) || E(t) || t.disabled || t.getAttribute("aria-disabled") === "true") continue;
1560
1568
  const i = t.getAttribute("autocomplete").trim();
1561
1569
  i && (ia(i) || a.push({
1562
1570
  ruleId: "adaptable/autocomplete-valid",
@@ -1620,7 +1628,7 @@ function ra(e) {
1620
1628
  return `Unknown check type: ${String(e.type)}`;
1621
1629
  }
1622
1630
  }
1623
- function E(e, a, t) {
1631
+ function L(e, a, t) {
1624
1632
  let i = e;
1625
1633
  if (i.includes("{{tag}}") && (i = i.replace(/\{\{tag\}\}/g, a.tagName.toLowerCase())), i.includes("{{value}}")) {
1626
1634
  let n = "";
@@ -1638,6 +1646,7 @@ function N(e) {
1638
1646
  level: e.level,
1639
1647
  tags: e.tags,
1640
1648
  fixability: e.fixability,
1649
+ browserHint: e.browserHint,
1641
1650
  description: e.description,
1642
1651
  guidance: e.guidance,
1643
1652
  run(t) {
@@ -1651,7 +1660,7 @@ function N(e) {
1651
1660
  selector: p(o),
1652
1661
  html: m(o),
1653
1662
  impact: e.impact,
1654
- message: E(e.message, o, e.check),
1663
+ message: L(e.message, o, e.check),
1655
1664
  ...e.fix ? { fix: e.fix } : {},
1656
1665
  element: o
1657
1666
  });
@@ -1667,7 +1676,7 @@ function N(e) {
1667
1676
  selector: p(d),
1668
1677
  html: m(d),
1669
1678
  impact: e.impact,
1670
- message: E(e.message, d, e.check),
1679
+ message: L(e.message, d, e.check),
1671
1680
  ...e.fix ? { fix: e.fix } : {},
1672
1681
  element: d
1673
1682
  });
@@ -1682,7 +1691,7 @@ function N(e) {
1682
1691
  selector: p(s),
1683
1692
  html: m(s),
1684
1693
  impact: e.impact,
1685
- message: E(e.message, s, e.check),
1694
+ message: L(e.message, s, e.check),
1686
1695
  ...e.fix ? { fix: e.fix } : {},
1687
1696
  element: s
1688
1697
  });
@@ -1706,7 +1715,7 @@ function N(e) {
1706
1715
  selector: p(u),
1707
1716
  html: m(u),
1708
1717
  impact: e.impact,
1709
- message: E(e.message, u, e.check),
1718
+ message: L(e.message, u, e.check),
1710
1719
  ...e.fix ? { fix: e.fix } : {},
1711
1720
  element: u
1712
1721
  }) : !d && g && i.push({
@@ -1714,7 +1723,7 @@ function N(e) {
1714
1723
  selector: p(u),
1715
1724
  html: m(u),
1716
1725
  impact: e.impact,
1717
- message: E(e.message, u, e.check),
1726
+ message: L(e.message, u, e.check),
1718
1727
  ...e.fix ? { fix: e.fix } : {},
1719
1728
  element: u
1720
1729
  });
@@ -1729,7 +1738,7 @@ function N(e) {
1729
1738
  selector: p(s),
1730
1739
  html: m(s),
1731
1740
  impact: e.impact,
1732
- message: E(e.message, s, e.check),
1741
+ message: L(e.message, s, e.check),
1733
1742
  ...e.fix ? { fix: e.fix } : {},
1734
1743
  element: s
1735
1744
  });
@@ -1771,7 +1780,7 @@ function N(e) {
1771
1780
  selector: p(b),
1772
1781
  html: m(b),
1773
1782
  impact: e.impact,
1774
- message: E(e.message, b, e.check),
1783
+ message: L(e.message, b, e.check),
1775
1784
  ...e.fix ? { fix: e.fix } : {},
1776
1785
  element: b
1777
1786
  });
@@ -1890,7 +1899,7 @@ function je(e, a) {
1890
1899
  return NaN;
1891
1900
  }
1892
1901
  }
1893
- function V(e) {
1902
+ function B(e) {
1894
1903
  return isNaN(e) ? !1 : (e = (e % 360 + 360) % 360, e >= 85 && e <= 95 || e >= 265 && e <= 275);
1895
1904
  }
1896
1905
  function pa(e) {
@@ -1899,21 +1908,21 @@ function pa(e) {
1899
1908
  );
1900
1909
  if (a) {
1901
1910
  const n = je(parseFloat(a[1]), a[2]);
1902
- if (V(n)) return !0;
1911
+ if (B(n)) return !0;
1903
1912
  }
1904
1913
  const t = e.match(
1905
1914
  /matrix\(\s*(-?[\d.e]+)\s*,\s*(-?[\d.e]+)\s*,\s*(-?[\d.e]+)\s*,\s*(-?[\d.e]+)/i
1906
1915
  );
1907
1916
  if (t) {
1908
1917
  const n = parseFloat(t[1]), r = parseFloat(t[2]), o = Math.atan2(r, n) * (180 / Math.PI);
1909
- if (V(o)) return !0;
1918
+ if (B(o)) return !0;
1910
1919
  }
1911
1920
  const i = e.match(
1912
1921
  /matrix3d\(\s*(-?[\d.e]+)\s*,\s*(-?[\d.e]+)\s*,\s*(-?[\d.e]+)\s*,\s*(-?[\d.e]+)\s*,\s*(-?[\d.e]+)\s*,\s*(-?[\d.e]+)/i
1913
1922
  );
1914
1923
  if (i) {
1915
1924
  const n = parseFloat(i[1]), r = parseFloat(i[2]), o = Math.atan2(r, n) * (180 / Math.PI);
1916
- if (V(o)) return !0;
1925
+ if (B(o)) return !0;
1917
1926
  }
1918
1927
  return !1;
1919
1928
  }
@@ -1921,7 +1930,7 @@ function ba(e) {
1921
1930
  const a = e.match(/(-?[\d.]+)(deg|rad|turn|grad)/i);
1922
1931
  if (!a) return !1;
1923
1932
  const t = je(parseFloat(a[1]), a[2]);
1924
- return V(t);
1933
+ return B(t);
1925
1934
  }
1926
1935
  const ha = {
1927
1936
  id: "adaptable/orientation-lock",
@@ -1998,14 +2007,14 @@ function fa(e, a) {
1998
2007
  for (const s of e.querySelectorAll("*")) {
1999
2008
  if (h(s)) continue;
2000
2009
  r = !0;
2001
- const l = j(s);
2010
+ const l = W(s);
2002
2011
  l && n.add(l);
2003
2012
  }
2004
2013
  for (const s of t) {
2005
2014
  const l = i.getElementById(s);
2006
2015
  if (l && !h(l)) {
2007
2016
  r = !0;
2008
- const d = j(l);
2017
+ const d = W(l);
2009
2018
  d && n.add(d);
2010
2019
  }
2011
2020
  }
@@ -2082,7 +2091,7 @@ const va = {
2082
2091
  const r = Se[n];
2083
2092
  let o = i.parentElement, s = !1;
2084
2093
  for (; o && o !== e.documentElement; ) {
2085
- const l = j(o);
2094
+ const l = W(o);
2086
2095
  if (l && r.includes(l)) {
2087
2096
  s = !0;
2088
2097
  break;
@@ -2188,6 +2197,7 @@ const ka = {
2188
2197
  wcag: ["1.3.1"],
2189
2198
  level: "A",
2190
2199
  fixability: "contextual",
2200
+ browserHint: "Screenshot the table to understand its visual layout, then add scope or headers attributes to associate data cells with headers.",
2191
2201
  description: "Data cells in tables larger than 3x3 should have associated headers.",
2192
2202
  guidance: "In complex tables, screen reader users need header associations to understand data cells. Use th elements with scope attribute, or the headers attribute on td elements. For simple tables (≤3x3), this is less critical as context is usually clear.",
2193
2203
  run(e) {
@@ -2214,9 +2224,9 @@ const ka = {
2214
2224
  let g = !1;
2215
2225
  const f = i.querySelector("thead"), v = (f == null ? void 0 : f.querySelector("tr")) ?? i.querySelector("tbody > tr, tr");
2216
2226
  if (v)
2217
- for (const w of v.querySelectorAll("th, td")) {
2218
- const A = Ie(w), S = parseInt(w.getAttribute("colspan") || "1", 10);
2219
- if (w.tagName.toLowerCase() === "th" && b >= A && b < A + S) {
2227
+ for (const y of v.querySelectorAll("th, td")) {
2228
+ const A = Ie(y), S = parseInt(y.getAttribute("colspan") || "1", 10);
2229
+ if (y.tagName.toLowerCase() === "th" && b >= A && b < A + S) {
2220
2230
  g = !0;
2221
2231
  break;
2222
2232
  }
@@ -2266,6 +2276,7 @@ const ka = {
2266
2276
  level: "A",
2267
2277
  tags: ["best-practice"],
2268
2278
  fixability: "contextual",
2279
+ browserHint: "Screenshot the table to see which header cells are visually empty, then add text content or aria-label.",
2269
2280
  description: "Table header cells should have visible text.",
2270
2281
  guidance: "Empty table headers provide no information to screen reader users. Either add descriptive text to the header, or if the header is intentionally empty (like a corner cell), consider using a td element instead or adding a visually hidden label.",
2271
2282
  run(e) {
@@ -2273,7 +2284,7 @@ const ka = {
2273
2284
  for (const t of e.querySelectorAll("th")) {
2274
2285
  if (h(t)) continue;
2275
2286
  const i = t.closest("table");
2276
- (i == null ? void 0 : i.getAttribute("role")) === "presentation" || (i == null ? void 0 : i.getAttribute("role")) === "none" || x(t) || a.push({
2287
+ (i == null ? void 0 : i.getAttribute("role")) === "presentation" || (i == null ? void 0 : i.getAttribute("role")) === "none" || w(t) || a.push({
2277
2288
  ruleId: "adaptable/empty-table-header",
2278
2289
  selector: p(t),
2279
2290
  html: m(t),
@@ -2283,7 +2294,7 @@ const ka = {
2283
2294
  }
2284
2295
  return a;
2285
2296
  }
2286
- }, qa = {
2297
+ }, Ea = {
2287
2298
  id: "distinguishable/meta-viewport",
2288
2299
  category: "distinguishable",
2289
2300
  actRuleIds: ["b4f0c3"],
@@ -2291,6 +2302,7 @@ const ka = {
2291
2302
  level: "AA",
2292
2303
  tags: ["page-level"],
2293
2304
  fixability: "mechanical",
2305
+ browserHint: "After fixing the viewport meta tag, resize the viewport to 320px wide and screenshot to verify content remains readable and usable.",
2294
2306
  description: "Viewport meta tag must not disable user scaling.",
2295
2307
  guidance: "Users with low vision need to zoom content up to 200% or more. Setting user-scalable=no or maximum-scale=1 prevents zooming and fails WCAG. Remove these restrictions. If your layout breaks at high zoom, fix the responsive design rather than preventing zoom.",
2296
2308
  run(e) {
@@ -2328,7 +2340,7 @@ const ka = {
2328
2340
  function ce(e) {
2329
2341
  return e.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
2330
2342
  }
2331
- function Ue(e, a) {
2343
+ function We(e, a) {
2332
2344
  const t = e.getAttribute("style");
2333
2345
  if (!t) return null;
2334
2346
  const i = new RegExp(
@@ -2375,7 +2387,7 @@ function Ue(e, a) {
2375
2387
  }
2376
2388
  return null;
2377
2389
  }
2378
- function We(e, a, t, i) {
2390
+ function Ue(e, a, t, i) {
2379
2391
  function n(r) {
2380
2392
  var o;
2381
2393
  if (r !== e) {
@@ -2387,7 +2399,7 @@ function We(e, a, t, i) {
2387
2399
  }
2388
2400
  for (const s of r.childNodes)
2389
2401
  if (s.nodeType === 3 && ((o = s.textContent) != null && o.trim())) {
2390
- const l = parseFloat(y(r).fontSize);
2402
+ const l = parseFloat(x(r).fontSize);
2391
2403
  if (l > 0 && t / l < i) return !0;
2392
2404
  break;
2393
2405
  }
@@ -2397,7 +2409,7 @@ function We(e, a, t, i) {
2397
2409
  }
2398
2410
  return n(e);
2399
2411
  }
2400
- function La(e) {
2412
+ function qa(e) {
2401
2413
  var a;
2402
2414
  for (const t of e.childNodes)
2403
2415
  if (t.nodeType === 3 && ((a = t.textContent) != null && a.trim()))
@@ -2419,7 +2431,7 @@ function Ve(e) {
2419
2431
  return !1;
2420
2432
  }
2421
2433
  function de(e, a) {
2422
- if (La(e)) return !0;
2434
+ if (qa(e)) return !0;
2423
2435
  for (const t of e.children) {
2424
2436
  const i = t.getAttribute("style") || "";
2425
2437
  if (!new RegExp(
@@ -2434,10 +2446,10 @@ function Be(e, a, t, i) {
2434
2446
  const n = [];
2435
2447
  for (const r of e.querySelectorAll("[style]")) {
2436
2448
  if (h(r) || !Oe(r) || Ve(r) || !de(r, t)) continue;
2437
- const o = Ue(r, t);
2449
+ const o = We(r, t);
2438
2450
  if (!o) continue;
2439
2451
  let s = !1;
2440
- if (o.em !== null ? s = o.em < i : o.px !== null && (s = We(r, t, o.px, i)), s) {
2452
+ if (o.em !== null ? s = o.em < i : o.px !== null && (s = Ue(r, t, o.px, i)), s) {
2441
2453
  const l = o.em !== null ? `${o.em}${t === "line-height" ? "" : "em"}` : `${o.px}px`;
2442
2454
  n.push({
2443
2455
  ruleId: a,
@@ -2450,10 +2462,10 @@ function Be(e, a, t, i) {
2450
2462
  }
2451
2463
  return n;
2452
2464
  }
2453
- function Ea(e) {
2465
+ function La(e) {
2454
2466
  let a = e, t = !1;
2455
2467
  for (; a; ) {
2456
- const i = y(a);
2468
+ const i = x(a);
2457
2469
  parseFloat(i.width) > 500 && (t = !0), (i.whiteSpace === "nowrap" || i.whiteSpace === "pre") && (t = !0);
2458
2470
  const r = i.overflowX, o = i.overflowY;
2459
2471
  if ((r === "scroll" || r === "auto") && o !== "scroll" && o !== "auto")
@@ -2486,15 +2498,15 @@ const Ra = {
2486
2498
  run(e) {
2487
2499
  const a = [];
2488
2500
  for (const t of e.querySelectorAll("[style]")) {
2489
- if (h(t) || !Oe(t) || Ve(t) || !de(t, "line-height") || Ea(t)) continue;
2501
+ if (h(t) || !Oe(t) || Ve(t) || !de(t, "line-height") || La(t)) continue;
2490
2502
  if (t instanceof HTMLElement && t.scrollHeight > 0) {
2491
- const r = parseFloat(y(t).lineHeight);
2503
+ const r = parseFloat(x(t).lineHeight);
2492
2504
  if (r > 0 && t.scrollHeight <= r * 1.5) continue;
2493
2505
  }
2494
- const i = Ue(t, "line-height");
2506
+ const i = We(t, "line-height");
2495
2507
  if (!i) continue;
2496
2508
  let n = !1;
2497
- if (i.em !== null ? n = i.em < 1.5 : i.px !== null && (n = We(t, "line-height", i.px, 1.5)), n) {
2509
+ if (i.em !== null ? n = i.em < 1.5 : i.px !== null && (n = Ue(t, "line-height", i.px, 1.5)), n) {
2498
2510
  const r = i.em !== null ? `${i.em}` : `${i.px}px`;
2499
2511
  a.push({
2500
2512
  ruleId: "distinguishable/line-height",
@@ -2535,7 +2547,7 @@ const Ra = {
2535
2547
  ]);
2536
2548
  function $a(e) {
2537
2549
  let a = e.parentElement;
2538
- for (; a && !Na.has(y(a).display); )
2550
+ for (; a && !Na.has(x(a).display); )
2539
2551
  a = a.parentElement;
2540
2552
  if (!a) return null;
2541
2553
  const t = a.ownerDocument.createTreeWalker(a, NodeFilter.SHOW_TEXT);
@@ -2550,49 +2562,50 @@ function $a(e) {
2550
2562
  }
2551
2563
  s = s.parentElement;
2552
2564
  }
2553
- l || (i += r.data, !n && r.parentElement && (n = C(y(r.parentElement).color)));
2565
+ l || (i += r.data, !n && r.parentElement && (n = C(x(r.parentElement).color)));
2554
2566
  }
2555
2567
  const o = i.match(new RegExp("\\p{L}{3,}", "gu"));
2556
2568
  return !n || !o || o.length < 2 ? null : { block: a, textColor: n };
2557
2569
  }
2558
- function qe(e, a) {
2570
+ function Ee(e, a) {
2559
2571
  const t = e.textDecorationLine || e.textDecoration || "";
2560
2572
  return (t.includes("underline") || t.includes("line-through")) && t !== a;
2561
2573
  }
2562
- function O(e) {
2574
+ function V(e) {
2563
2575
  return e === "bold" ? 700 : e === "normal" ? 400 : parseInt(e) || 400;
2564
2576
  }
2565
- function Da(e) {
2577
+ function Ha(e) {
2566
2578
  const a = e.ownerDocument.createTreeWalker(e, NodeFilter.SHOW_TEXT);
2567
2579
  let t;
2568
2580
  for (; t = a.nextNode(); )
2569
2581
  if (t.data.trim()) return !1;
2570
2582
  return !0;
2571
2583
  }
2572
- const Pa = {
2584
+ const Da = {
2573
2585
  id: "distinguishable/link-in-text-block",
2574
2586
  category: "distinguishable",
2575
2587
  wcag: ["1.4.1"],
2576
2588
  level: "A",
2577
2589
  fixability: "visual",
2590
+ browserHint: "Screenshot the text block to see how the link blends with surrounding text, then verify your fix (e.g., underline or border) makes the link visually distinct.",
2578
2591
  description: "Links within text blocks must be distinguishable by more than color alone.",
2579
2592
  guidance: "Users who cannot perceive color differences need other visual cues to identify links. Links in text should have underlines or other non-color indicators. If using color alone, ensure 3:1 contrast with surrounding text AND provide additional indication on focus/hover.",
2580
2593
  run(e) {
2581
2594
  const a = [];
2582
2595
  for (const t of e.querySelectorAll("a[href]")) {
2583
- if (h(t) || !k(t).trim() || Da(t) || t.closest('nav, header, footer, aside, [role="navigation"], [role="banner"], [role="contentinfo"], [role="complementary"]')) continue;
2584
- const i = y(t);
2596
+ if (h(t) || !k(t).trim() || Ha(t) || t.closest('nav, header, footer, aside, [role="navigation"], [role="banner"], [role="contentinfo"], [role="complementary"]')) continue;
2597
+ const i = x(t);
2585
2598
  if (!Ma.has(i.display || "inline")) continue;
2586
2599
  const n = $a(t);
2587
2600
  if (!n) continue;
2588
- const r = y(n.block), o = r.textDecorationLine || r.textDecoration || "";
2589
- if (qe(i, o) || (parseFloat(i.borderBottomWidth) || 0) > 0 && i.borderBottomStyle !== "none" && i.borderBottomStyle !== "hidden" || Math.abs(O(i.fontWeight) - O(r.fontWeight)) >= 300 || i.fontStyle !== r.fontStyle) continue;
2601
+ const r = x(n.block), o = r.textDecorationLine || r.textDecoration || "";
2602
+ if (Ee(i, o) || (parseFloat(i.borderBottomWidth) || 0) > 0 && i.borderBottomStyle !== "none" && i.borderBottomStyle !== "hidden" || Math.abs(V(i.fontWeight) - V(r.fontWeight)) >= 300 || i.fontStyle !== r.fontStyle) continue;
2590
2603
  const l = parseFloat(i.fontSize) || 16, d = parseFloat(r.fontSize) || 16;
2591
2604
  if (d > 0 && l / d >= 1.2) continue;
2592
2605
  let c = !1;
2593
- for (const w of t.querySelectorAll("*")) {
2594
- const A = y(w);
2595
- if (qe(A, o) || Math.abs(O(A.fontWeight) - O(r.fontWeight)) >= 300) {
2606
+ for (const y of t.querySelectorAll("*")) {
2607
+ const A = x(y);
2608
+ if (Ee(A, o) || Math.abs(V(A.fontWeight) - V(r.fontWeight)) >= 300) {
2596
2609
  c = !0;
2597
2610
  break;
2598
2611
  }
@@ -2602,19 +2615,20 @@ const Pa = {
2602
2615
  if (!u) continue;
2603
2616
  const b = I(...u), g = I(...n.textColor), f = $(b, g);
2604
2617
  if (f < 1.1 || f >= 3) continue;
2605
- const v = (w) => "#" + w.map((A) => A.toString(16).padStart(2, "0")).join("");
2618
+ const v = (y) => "#" + y.map((A) => A.toString(16).padStart(2, "0")).join("");
2606
2619
  a.push({
2607
2620
  ruleId: "distinguishable/link-in-text-block",
2608
2621
  selector: p(t),
2609
2622
  html: m(t),
2610
2623
  impact: "serious",
2611
2624
  message: "Link in text block is not visually distinguishable from surrounding text. Add a non-color visual indicator such as an underline or border.",
2612
- context: `link color: ${v(u)} rgb(${u.join(", ")}), surrounding text: ${v(n.textColor)} rgb(${n.textColor.join(", ")}), ratio: ${f.toFixed(2)}:1`
2625
+ context: `link color: ${v(u)} rgb(${u.join(", ")}), surrounding text: ${v(n.textColor)} rgb(${n.textColor.join(", ")}), ratio: ${f.toFixed(2)}:1`,
2626
+ fix: { type: "suggest", suggestion: "Add text-decoration: underline to the link, or add a visible border-bottom. If relying on color contrast alone, ensure at least 3:1 ratio between the link color and surrounding text color." }
2613
2627
  });
2614
2628
  }
2615
2629
  return a;
2616
2630
  }
2617
- }, Fa = /* @__PURE__ */ new Set([
2631
+ }, Pa = /* @__PURE__ */ new Set([
2618
2632
  "SCRIPT",
2619
2633
  "STYLE",
2620
2634
  "NOSCRIPT",
@@ -2630,7 +2644,7 @@ const Pa = {
2630
2644
  "BR",
2631
2645
  "HR"
2632
2646
  ]);
2633
- function Ha(e) {
2647
+ function Fa(e) {
2634
2648
  const a = e.clip;
2635
2649
  if (a && a.startsWith("rect(")) {
2636
2650
  const i = a.match(/[\d.]+/g);
@@ -2648,8 +2662,8 @@ function za(e) {
2648
2662
  if (h(e)) return !0;
2649
2663
  let a = e;
2650
2664
  for (; a; ) {
2651
- const t = y(a);
2652
- if (t.display === "none" || t.visibility === "hidden" || Ha(t)) return !0;
2665
+ const t = x(a);
2666
+ if (t.display === "none" || t.visibility === "hidden" || Fa(t)) return !0;
2653
2667
  a = a.parentElement;
2654
2668
  }
2655
2669
  return !1;
@@ -2657,7 +2671,7 @@ function za(e) {
2657
2671
  function ja(e) {
2658
2672
  return e instanceof HTMLInputElement || e instanceof HTMLTextAreaElement || e instanceof HTMLSelectElement || e instanceof HTMLButtonElement ? e.disabled : !!(e.closest("fieldset[disabled]") || e.getAttribute("aria-disabled") === "true");
2659
2673
  }
2660
- function Ua(e, a) {
2674
+ function Wa(e, a) {
2661
2675
  if (e.tagName !== "LABEL") return !1;
2662
2676
  const t = e, i = t.htmlFor;
2663
2677
  if (i) {
@@ -2669,7 +2683,7 @@ function Ua(e, a) {
2669
2683
  const r = t.id;
2670
2684
  return !!(r && a.querySelector(`[aria-labelledby~="${r}"][aria-disabled="true"]`));
2671
2685
  }
2672
- function Wa(e) {
2686
+ function Ua(e) {
2673
2687
  return e.closest("select") !== null;
2674
2688
  }
2675
2689
  function Oa(e) {
@@ -2694,10 +2708,10 @@ function _a(e) {
2694
2708
  const a = parseFloat(e);
2695
2709
  return isNaN(a) ? NaN : e.trim().endsWith("%") ? a / 100 : a;
2696
2710
  }
2697
- const Le = /([a-z-]+)\(([^)]*)\)/g;
2698
- function Ee(e) {
2711
+ const qe = /([a-z-]+)\(([^)]*)\)/g;
2712
+ function Le(e) {
2699
2713
  let a, t = !1;
2700
- for (Le.lastIndex = 0; a = Le.exec(e); ) {
2714
+ for (qe.lastIndex = 0; a = qe.exec(e); ) {
2701
2715
  t = !0;
2702
2716
  const i = Ba[a[1]];
2703
2717
  if (i === void 0 || _a(a[2]) !== i) return !1;
@@ -2707,12 +2721,12 @@ function Ee(e) {
2707
2721
  function Ga(e) {
2708
2722
  let a = e;
2709
2723
  for (; a; ) {
2710
- const t = y(a), i = t.filter;
2711
- if (i && i !== "none" && i !== "initial" && !Ee(i)) return !0;
2724
+ const t = x(a), i = t.filter;
2725
+ if (i && i !== "none" && i !== "initial" && !Le(i)) return !0;
2712
2726
  const n = t.mixBlendMode;
2713
2727
  if (n && n !== "normal" && n !== "initial") return !0;
2714
2728
  const r = t.backdropFilter;
2715
- if (r && r !== "none" && r !== "initial" && !Ee(r)) return !0;
2729
+ if (r && r !== "none" && r !== "initial" && !Le(r)) return !0;
2716
2730
  a = a.parentElement;
2717
2731
  }
2718
2732
  return !1;
@@ -2720,7 +2734,7 @@ function Ga(e) {
2720
2734
  function Ya(e) {
2721
2735
  let a = e;
2722
2736
  for (; a; ) {
2723
- const t = y(a), i = t.backgroundImage;
2737
+ const t = x(a), i = t.backgroundImage;
2724
2738
  if (i && i !== "none" && i !== "initial")
2725
2739
  return i.includes("gradient(") ? { bgImage: i, gradientEl: a } : null;
2726
2740
  const n = t.backgroundColor;
@@ -2743,11 +2757,11 @@ function Xa(e, a, t, i, n, r, o, s, l) {
2743
2757
  for (const f of d) {
2744
2758
  let v = a;
2745
2759
  t < 1 && (v = R(a, f, t)), i < 1 && (v = R(v, f, i));
2746
- const w = $(
2760
+ const y = $(
2747
2761
  I(v[0], v[1], v[2]),
2748
2762
  I(f[0], f[1], f[2])
2749
2763
  );
2750
- w > c && (c = w, u = f);
2764
+ y > c && (c = y, u = f);
2751
2765
  }
2752
2766
  if (c >= n) return null;
2753
2767
  let b = a;
@@ -2759,7 +2773,8 @@ function Xa(e, a, t, i, n, r, o, s, l) {
2759
2773
  html: m(e),
2760
2774
  impact: "serious",
2761
2775
  message: `Insufficient${o === "AAA" ? " enhanced" : ""} color contrast ratio of ${g}:1 (required ${n}:1).`,
2762
- context: `foreground: ${ne(b)} rgb(${b.join(", ")}), background: gradient, ratio: ${g}:1, required: ${n}:1`
2776
+ context: `foreground: ${_(b)} rgb(${b.join(", ")}), background: gradient, ratio: ${g}:1, required: ${n}:1`,
2777
+ fix: { type: "suggest", suggestion: `Change the text color or gradient background so the contrast ratio meets ${n}:1. The current foreground is ${_(b)}.` }
2763
2778
  };
2764
2779
  }
2765
2780
  function _e(e, a, t) {
@@ -2770,10 +2785,10 @@ function _e(e, a, t) {
2770
2785
  for (; s = r.nextNode(); ) {
2771
2786
  if (!s.textContent || !s.textContent.trim() || Oa(s.textContent)) continue;
2772
2787
  const l = s.parentElement;
2773
- if (!l || o.has(l) || (o.add(l), Fa.has(l.tagName))) continue;
2788
+ if (!l || o.has(l) || (o.add(l), Pa.has(l.tagName))) continue;
2774
2789
  const d = l.tagName;
2775
- if (d === "BODY" || d === "HTML" || Wa(l) || ja(l) || Ua(l, e) || Va(l) || za(l)) continue;
2776
- const c = y(l);
2790
+ if (d === "BODY" || d === "HTML" || Ua(l) || ja(l) || Wa(l, e) || Va(l) || za(l)) continue;
2791
+ const c = x(l);
2777
2792
  if (parseFloat(c.opacity) === 0) continue;
2778
2793
  const u = Mt(l);
2779
2794
  if (u < 0.1) continue;
@@ -2783,46 +2798,47 @@ function _e(e, a, t) {
2783
2798
  const f = C(c.color);
2784
2799
  if (!f) continue;
2785
2800
  const v = U(c.color);
2786
- if (v === 0 || Lt(l)) continue;
2787
- const w = t === "AAA" ? xe(l) ? 4.5 : 7 : xe(l) ? 3 : 4.5;
2801
+ if (v === 0 || qt(l)) continue;
2802
+ const y = t === "AAA" ? xe(l) ? 4.5 : 7 : xe(l) ? 3 : 4.5;
2788
2803
  let A = ye(l);
2789
2804
  if (!A) {
2790
2805
  if (g) continue;
2791
- const L = Ya(l);
2792
- if (L) {
2793
- const _ = L.gradientEl.parentElement ? ye(L.gradientEl.parentElement) : null, W = Xa(
2806
+ const q = Ya(l);
2807
+ if (q) {
2808
+ const O = q.gradientEl.parentElement ? ye(q.gradientEl.parentElement) : null, H = Xa(
2794
2809
  l,
2795
2810
  f,
2796
2811
  v,
2797
2812
  u,
2798
- w,
2813
+ y,
2799
2814
  a,
2800
2815
  t,
2801
- L.bgImage,
2802
- _ ?? [255, 255, 255]
2816
+ q.bgImage,
2817
+ O ?? [255, 255, 255]
2803
2818
  );
2804
- W && i.push(W);
2819
+ H && i.push(H);
2805
2820
  }
2806
2821
  continue;
2807
2822
  }
2808
2823
  let S = f;
2809
2824
  v < 1 && (S = R(f, A, v)), u < 1 && (S = R(S, A, u));
2810
2825
  const nt = I(S[0], S[1], S[2]), rt = I(A[0], A[1], A[2]), be = g ? Nt(S, A, g) : $(nt, rt);
2811
- if (be < w) {
2812
- const L = Math.round(be * 100) / 100, _ = ne(S), W = ne(A);
2826
+ if (be < y) {
2827
+ const q = Math.round(be * 100) / 100, O = _(S), H = _(A);
2813
2828
  i.push({
2814
2829
  ruleId: a,
2815
2830
  selector: p(l),
2816
2831
  html: m(l),
2817
2832
  impact: "serious",
2818
- message: `Insufficient${t === "AAA" ? " enhanced" : ""} color contrast ratio of ${L}:1 (required ${w}:1).`,
2819
- context: `foreground: ${_} rgb(${S.join(", ")}), background: ${W} rgb(${A.join(", ")}), ratio: ${L}:1, required: ${w}:1`
2833
+ message: `Insufficient${t === "AAA" ? " enhanced" : ""} color contrast ratio of ${q}:1 (required ${y}:1).`,
2834
+ context: `foreground: ${O} rgb(${S.join(", ")}), background: ${H} rgb(${A.join(", ")}), ratio: ${q}:1, required: ${y}:1`,
2835
+ fix: { type: "suggest", suggestion: `Change the text color or background color so the contrast ratio meets ${y}:1. Current foreground is ${O}, background is ${H}.` }
2820
2836
  });
2821
2837
  }
2822
2838
  }
2823
2839
  return i;
2824
2840
  }
2825
- const Ka = {
2841
+ const Ja = {
2826
2842
  id: "distinguishable/color-contrast",
2827
2843
  category: "distinguishable",
2828
2844
  actRuleIds: ["afw4f7"],
@@ -2830,11 +2846,12 @@ const Ka = {
2830
2846
  level: "AA",
2831
2847
  fixability: "visual",
2832
2848
  description: "Text elements must have sufficient color contrast against the background.",
2849
+ browserHint: "Violation context includes computed colors and ratio. After changing colors, use JavaScript to read getComputedStyle() on the element and recalculate the contrast ratio. Screenshot the element to verify the fix looks correct in context.",
2833
2850
  guidance: "WCAG SC 1.4.3 requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (>=24px or >=18.66px bold). Increase the contrast by darkening the text or lightening the background, or vice versa.",
2834
2851
  run(e) {
2835
2852
  return _e(e, "distinguishable/color-contrast", "AA");
2836
2853
  }
2837
- }, Ja = {
2854
+ }, Ka = {
2838
2855
  id: "distinguishable/color-contrast-enhanced",
2839
2856
  category: "distinguishable",
2840
2857
  actRuleIds: ["09o5cg"],
@@ -2842,6 +2859,7 @@ const Ka = {
2842
2859
  level: "AAA",
2843
2860
  fixability: "visual",
2844
2861
  description: "Text elements must have enhanced color contrast against the background (WCAG AAA).",
2862
+ browserHint: "Violation context includes computed colors and ratio. After changing colors, use JavaScript to read getComputedStyle() on the element and recalculate the contrast ratio. Screenshot the element to verify the fix looks correct in context.",
2845
2863
  guidance: "WCAG SC 1.4.6 (AAA) requires a contrast ratio of at least 7:1 for normal text and 4.5:1 for large text (>=24px or >=18.66px bold). Higher contrast benefits users with low vision, aging eyes, or poor screen conditions. Increase the contrast by darkening the text or lightening the background, or vice versa.",
2846
2864
  run(e) {
2847
2865
  return _e(e, "distinguishable/color-contrast-enhanced", "AAA");
@@ -3025,6 +3043,7 @@ const di = {
3025
3043
  wcag: ["2.1.1"],
3026
3044
  level: "A",
3027
3045
  fixability: "contextual",
3046
+ browserHint: "Tab to the scrollable region and verify keyboard scrolling works with arrow keys.",
3028
3047
  description: "Scrollable regions must be keyboard accessible.",
3029
3048
  guidance: "Content that scrolls must be accessible to keyboard users. If a region has overflow:scroll or overflow:auto and contains scrollable content, it needs either tabindex='0' to be focusable, or it must contain focusable elements. Without this, keyboard users cannot scroll the content.",
3030
3049
  run(e) {
@@ -3036,17 +3055,17 @@ const di = {
3036
3055
  if (n === "body" || n === "html") continue;
3037
3056
  const r = i.getAttribute("role");
3038
3057
  if (r === "presentation" || r === "none" || r === "listbox" || r === "menu" || r === "tree" || r === "tabpanel") continue;
3039
- const o = y(i), s = o.overflowX, l = o.overflowY;
3058
+ const o = x(i), s = o.overflowX, l = o.overflowY;
3040
3059
  if (!(s === "scroll" || s === "auto" || l === "scroll" || l === "auto")) continue;
3041
3060
  if (i.scrollHeight > 0 || i.clientHeight > 0) {
3042
3061
  const g = i.scrollHeight - i.clientHeight, f = i.scrollWidth - i.clientWidth;
3043
3062
  if (g <= 0 && f <= 0 || g < 14 && f < 14 || i.clientWidth < 64 && i.clientHeight < 64) continue;
3044
- const v = ((t = i.textContent) == null ? void 0 : t.trim().length) ?? 0, w = i.querySelector("img, svg, video, canvas, picture") !== null;
3045
- if (v === 0 && !w) continue;
3063
+ const v = ((t = i.textContent) == null ? void 0 : t.trim().length) ?? 0, y = i.querySelector("img, svg, video, canvas, picture") !== null;
3064
+ if (v === 0 && !y) continue;
3046
3065
  } else
3047
3066
  continue;
3048
3067
  const u = i.getAttribute("tabindex");
3049
- u !== null && u !== "-1" || i.querySelector(z) || a.push({
3068
+ u !== null && u !== "-1" || i.querySelector(j) || a.push({
3050
3069
  ruleId: "keyboard-accessible/scrollable-region",
3051
3070
  selector: p(i),
3052
3071
  html: m(i),
@@ -3094,11 +3113,12 @@ const di = {
3094
3113
  wcag: ["2.4.7"],
3095
3114
  level: "AA",
3096
3115
  fixability: "visual",
3116
+ browserHint: "Tab to the element and screenshot to verify a visible focus indicator appears. Check that the indicator has sufficient contrast against the background.",
3097
3117
  description: "Elements in sequential focus order must have a visible focus indicator.",
3098
3118
  guidance: "Keyboard users need to see which element has focus. Do not remove the default focus outline (outline: none) without providing an alternative visible indicator. Use :focus-visible or :focus styles to ensure focus is always perceivable.",
3099
3119
  run(e) {
3100
3120
  const a = [];
3101
- for (const t of e.querySelectorAll(z)) {
3121
+ for (const t of e.querySelectorAll(j)) {
3102
3122
  if (h(t) || !(t instanceof HTMLElement)) continue;
3103
3123
  const i = t.getAttribute("style") || "";
3104
3124
  if (/outline\s*:\s*(none|0)\s*(;|$|!)/i.test(i)) {
@@ -3128,7 +3148,7 @@ function Ye(e) {
3128
3148
  return { seconds: t, hasValidUrl: i };
3129
3149
  }
3130
3150
  const Xe = 'article, aside, main, nav, section, [role="article"], [role="complementary"], [role="main"], [role="navigation"], [role="region"]', Re = 'main, [role="main"], header, [role="banner"], footer, [role="contentinfo"], nav, [role="navigation"], aside, [role="complementary"], section[aria-label], section[aria-labelledby], [role="region"][aria-label], [role="region"][aria-labelledby], form[aria-label], form[aria-labelledby], [role="form"][aria-label], [role="form"][aria-labelledby], [role="search"]';
3131
- function Ke(e) {
3151
+ function Je(e) {
3132
3152
  return {
3133
3153
  id: e.id,
3134
3154
  category: e.id.split("/")[0],
@@ -3351,10 +3371,10 @@ const bi = {
3351
3371
  run(e) {
3352
3372
  var o, s, l;
3353
3373
  const a = e.querySelector("h1");
3354
- if (a && x(a)) return [];
3374
+ if (a && w(a)) return [];
3355
3375
  const t = e.querySelectorAll('[role="heading"][aria-level="1"]');
3356
3376
  for (const d of t)
3357
- if (x(d)) return [];
3377
+ if (w(d)) return [];
3358
3378
  const i = [], n = (s = (o = e.querySelector("title")) == null ? void 0 : o.textContent) == null ? void 0 : s.trim();
3359
3379
  n && i.push(`Page title: "${n}"`);
3360
3380
  const r = e.querySelector("main");
@@ -3378,6 +3398,7 @@ const bi = {
3378
3398
  level: "A",
3379
3399
  tags: ["best-practice"],
3380
3400
  fixability: "contextual",
3401
+ browserHint: "Screenshot the page to see the visual hierarchy, then take an accessibility tree snapshot to map heading levels to visual sections.",
3381
3402
  description: "Heading levels should increase by one; skipping levels (e.g. h2 to h4) makes navigation harder.",
3382
3403
  guidance: "Screen reader users navigate by headings to understand page structure. Skipping levels (h2 to h4) suggests missing content and creates confusion. Start with h1 for the page title, then use h2 for main sections, h3 for subsections, etc. You can go back up (h3 to h2) when starting a new section.",
3383
3404
  run(e) {
@@ -3406,13 +3427,14 @@ const bi = {
3406
3427
  level: "A",
3407
3428
  tags: ["best-practice"],
3408
3429
  fixability: "contextual",
3430
+ browserHint: "Screenshot the heading area to verify it's visually empty, then add meaningful text or remove the heading element.",
3409
3431
  description: "Headings must have discernible text.",
3410
3432
  guidance: "Screen reader users navigate pages by headings, so empty headings create confusing navigation points. Ensure all headings contain visible text or accessible names. If a heading is used purely for visual styling, use CSS instead of heading elements.",
3411
3433
  run(e) {
3412
3434
  var i;
3413
3435
  const a = [], t = e.querySelectorAll('h1, h2, h3, h4, h5, h6, [role="heading"]');
3414
3436
  for (const n of t)
3415
- if (!h(n) && !x(n)) {
3437
+ if (!h(n) && !w(n)) {
3416
3438
  let r;
3417
3439
  const o = n.nextElementSibling;
3418
3440
  if (o) {
@@ -3437,7 +3459,8 @@ const bi = {
3437
3459
  wcag: ["1.3.1"],
3438
3460
  level: "A",
3439
3461
  tags: ["best-practice"],
3440
- fixability: "visual",
3462
+ fixability: "contextual",
3463
+ browserHint: "Screenshot the page to verify the paragraph visually functions as a heading and choose the correct heading level.",
3441
3464
  description: "Paragraphs should not be styled to look like headings.",
3442
3465
  guidance: "When paragraphs are styled with bold, large fonts to look like headings, screen reader users miss the semantic structure. Use proper heading elements (h1-h6) instead of styled paragraphs. If you need specific styling, apply CSS to the heading elements while maintaining proper heading hierarchy.",
3443
3466
  run(e) {
@@ -3453,14 +3476,15 @@ const bi = {
3453
3476
  selector: p(n),
3454
3477
  html: m(n),
3455
3478
  impact: "serious",
3456
- message: "Paragraph appears to be styled as a heading. Use an h1-h6 element instead."
3479
+ message: "Paragraph appears to be styled as a heading. Use an h1-h6 element instead.",
3480
+ fix: { type: "suggest", suggestion: "Replace the <p> element with the appropriate heading level (h1-h6) based on the document outline. Preserve the text content and move any inline styles to a CSS class on the new heading element." }
3457
3481
  });
3458
3482
  }
3459
3483
  }
3460
3484
  return a;
3461
3485
  }
3462
3486
  };
3463
- function qi(e) {
3487
+ function Ei(e) {
3464
3488
  var n, r;
3465
3489
  const a = [], t = e.getAttribute("href");
3466
3490
  t && a.push(`href: ${t}`);
@@ -3477,32 +3501,33 @@ function qi(e) {
3477
3501
  return a.length > 0 ? a.join(`
3478
3502
  `) : void 0;
3479
3503
  }
3480
- const Li = {
3504
+ const qi = {
3481
3505
  id: "navigable/link-name",
3482
3506
  category: "navigable",
3483
3507
  actRuleIds: ["c487ae"],
3484
3508
  wcag: ["2.4.4", "4.1.2"],
3485
3509
  level: "A",
3486
3510
  fixability: "contextual",
3511
+ browserHint: "Screenshot the link in context to understand its destination, then write descriptive link text.",
3487
3512
  description: "Links must have discernible text via content, aria-label, or aria-labelledby.",
3488
3513
  guidance: "Screen reader users need to know where a link goes. Add descriptive text content, aria-label, or use aria-labelledby. For image links, ensure the image has alt text describing the link destination. Avoid generic text like 'click here' or 'read more'—link text should make sense out of context.",
3489
3514
  run(e) {
3490
3515
  const a = [];
3491
3516
  for (const t of e.querySelectorAll('a[href], area[href], [role="link"]')) {
3492
- if (h(t) || q(t) || t.getRootNode() instanceof ShadowRoot) continue;
3493
- x(t) || a.push({
3517
+ if (h(t) || E(t) || t.getRootNode() instanceof ShadowRoot) continue;
3518
+ w(t) || a.push({
3494
3519
  ruleId: "navigable/link-name",
3495
3520
  selector: p(t),
3496
3521
  html: m(t),
3497
3522
  impact: "serious",
3498
3523
  message: "Link has no discernible text.",
3499
- context: qi(t),
3524
+ context: Ei(t),
3500
3525
  fix: { type: "add-text-content" }
3501
3526
  });
3502
3527
  }
3503
3528
  return a;
3504
3529
  }
3505
- }, Ei = {
3530
+ }, Li = {
3506
3531
  id: "navigable/skip-link",
3507
3532
  category: "navigable",
3508
3533
  wcag: ["2.4.1"],
@@ -3575,19 +3600,19 @@ const Li = {
3575
3600
  description: "Page should not have more than one main landmark.",
3576
3601
  guidance: "Only one main landmark should exist per page. The main landmark identifies the primary content area. If you have multiple content sections, use <section> with appropriate headings instead of multiple main elements.",
3577
3602
  filterTopLevel: !1
3578
- }), Mi = Ke({
3603
+ }), Mi = Je({
3579
3604
  id: "landmarks/banner-is-top-level",
3580
3605
  selector: '[role="banner"]',
3581
3606
  landmarkName: "Banner",
3582
3607
  description: "Banner landmark should not be nested within another landmark.",
3583
3608
  guidance: "The banner landmark should be a top-level landmark, not nested inside article, aside, main, nav, or section. If a header is inside these elements, it automatically becomes a generic header rather than a banner. Remove explicit role='banner' from nested headers or restructure the page."
3584
- }), $i = Ke({
3609
+ }), $i = Je({
3585
3610
  id: "landmarks/contentinfo-is-top-level",
3586
3611
  selector: '[role="contentinfo"]',
3587
3612
  landmarkName: "Contentinfo",
3588
3613
  description: "Contentinfo landmark should not be nested within another landmark.",
3589
3614
  guidance: "The contentinfo landmark should be a top-level landmark. A footer inside article, aside, main, nav, or section becomes a scoped footer, not a contentinfo landmark. Remove explicit role='contentinfo' from nested footers or move the footer outside sectioning elements."
3590
- }), Di = {
3615
+ }), Hi = {
3591
3616
  id: "landmarks/main-is-top-level",
3592
3617
  category: "landmarks",
3593
3618
  wcag: [],
@@ -3610,7 +3635,7 @@ const Li = {
3610
3635
  }
3611
3636
  return a;
3612
3637
  }
3613
- }, Pi = {
3638
+ }, Di = {
3614
3639
  id: "landmarks/complementary-is-top-level",
3615
3640
  category: "landmarks",
3616
3641
  wcag: [],
@@ -3633,7 +3658,7 @@ const Li = {
3633
3658
  }
3634
3659
  return a;
3635
3660
  }
3636
- }, Fi = {
3661
+ }, Pi = {
3637
3662
  id: "landmarks/landmark-unique",
3638
3663
  category: "landmarks",
3639
3664
  wcag: [],
@@ -3654,7 +3679,7 @@ const Li = {
3654
3679
  if (r.length <= 1) continue;
3655
3680
  const o = /* @__PURE__ */ new Map();
3656
3681
  for (const s of r) {
3657
- const l = x(s).toLowerCase() || "", d = o.get(l) || [];
3682
+ const l = w(s).toLowerCase() || "", d = o.get(l) || [];
3658
3683
  d.push(s), o.set(l, d);
3659
3684
  }
3660
3685
  for (const [s, l] of o)
@@ -3670,7 +3695,7 @@ const Li = {
3670
3695
  }
3671
3696
  return a;
3672
3697
  }
3673
- }, Hi = {
3698
+ }, Fi = {
3674
3699
  id: "landmarks/region",
3675
3700
  category: "landmarks",
3676
3701
  wcag: [],
@@ -3736,13 +3761,13 @@ const Li = {
3736
3761
  }
3737
3762
  }, ji = new Set(
3738
3763
  "aa ab ae af ak am an ar as av ay az ba be bg bh bi bm bn bo br bs ca ce ch co cr cs cu cv cy da de dv dz ee el en eo es et eu fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu hy hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl km kn ko kr ks ku kv kw ky la lb lg li ln lo lt lu lv mg mh mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc oj om or os pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo xh yi yo za zh zu".split(" ")
3739
- ), Ui = new Set(
3764
+ ), Wi = new Set(
3740
3765
  "aar abk afr aka amh ara arg asm ava ave aym aze bak bam bel ben bih bis bod bos bre bul cat ces cha che chu chv cor cos cre cym dan deu div dzo ell eng epo est eus ewe fao fas fij fin fra fry ful gla gle glg glv grn guj hat hau hbs heb her hin hmo hrv hun hye ibo iii iku ile ina ind ipk isl ita jav jpn kal kan kas kat kau kaz khm kik kin kir kom kon kor kua kur lao lat lav lim lin lit ltz lub lug mah mal mar mkd mlg mlt mon mri msa mya nau nav nbl nde ndo nep nld nno nob nor nya oci oji ori orm oss pan pli pol por pus que roh ron run rus sag san sin slk slv sme smo sna snd som sot spa sqi srd srp ssw sun swa swe tah tam tat tel tgk tgl tha tir ton tsn tso tuk tur twi uig ukr urd uzb ven vie vol wln wol xho yid yor zha zho zul".split(" ")
3741
- ), Wi = /^[a-z]{2,8}(-[a-z0-9]{1,8})*$/i;
3742
- function Je(e) {
3743
- if (!Wi.test(e)) return !1;
3766
+ ), Ui = /^[a-z]{2,8}(-[a-z0-9]{1,8})*$/i;
3767
+ function Ke(e) {
3768
+ if (!Ui.test(e)) return !1;
3744
3769
  const a = e.split("-")[0].toLowerCase();
3745
- return a.length === 2 ? ji.has(a) : a.length === 3 ? !Ui.has(a) : !1;
3770
+ return a.length === 2 ? ji.has(a) : a.length === 3 ? !Wi.has(a) : !1;
3746
3771
  }
3747
3772
  function Ce(e) {
3748
3773
  var i;
@@ -3789,7 +3814,7 @@ const Oi = {
3789
3814
  run(e) {
3790
3815
  var t;
3791
3816
  const a = (t = e.documentElement.getAttribute("lang")) == null ? void 0 : t.trim();
3792
- return a && !Je(a) ? [{
3817
+ return a && !Ke(a) ? [{
3793
3818
  ruleId: "readable/html-lang-valid",
3794
3819
  selector: "html",
3795
3820
  html: m(e.documentElement),
@@ -3822,7 +3847,7 @@ const Oi = {
3822
3847
  });
3823
3848
  continue;
3824
3849
  }
3825
- n && Ce(t) && (Je(n) || a.push({
3850
+ n && Ce(t) && (Ke(n) || a.push({
3826
3851
  ruleId: "readable/valid-lang",
3827
3852
  selector: p(t),
3828
3853
  html: m(t),
@@ -3865,13 +3890,14 @@ const Oi = {
3865
3890
  wcag: ["4.1.2"],
3866
3891
  level: "A",
3867
3892
  fixability: "contextual",
3893
+ browserHint: "Screenshot the iframe to see what content it displays, then add a title describing its purpose.",
3868
3894
  description: "Frames must have an accessible name.",
3869
3895
  guidance: "Screen readers announce frame titles when users navigate frames. Add a title attribute to <iframe> and <frame> elements that describes the frame's purpose (e.g., <iframe title='Video player'>). Avoid generic titles like 'frame' or 'iframe'. If the frame is decorative, use aria-hidden='true'.",
3870
3896
  run(e) {
3871
3897
  const a = [];
3872
3898
  for (const t of e.querySelectorAll("iframe, frame")) {
3873
3899
  if (h(t) || Ge(t)) continue;
3874
- if (!x(t)) {
3900
+ if (!w(t)) {
3875
3901
  const n = t.getAttribute("src");
3876
3902
  a.push({
3877
3903
  ruleId: "labels-and-names/frame-title",
@@ -3941,7 +3967,7 @@ function Xi(e) {
3941
3967
  }
3942
3968
  return "";
3943
3969
  }
3944
- const Ki = [
3970
+ const Ji = [
3945
3971
  '[role="checkbox"]',
3946
3972
  '[role="combobox"]',
3947
3973
  '[role="listbox"]',
@@ -3953,7 +3979,7 @@ const Ki = [
3953
3979
  '[role="spinbutton"]',
3954
3980
  '[role="switch"]',
3955
3981
  '[role="textbox"]'
3956
- ].join(", "), Ji = /* @__PURE__ */ new Set([
3982
+ ].join(", "), Ki = /* @__PURE__ */ new Set([
3957
3983
  "checkbox",
3958
3984
  "menuitemcheckbox",
3959
3985
  "menuitemradio",
@@ -3970,8 +3996,8 @@ const Ki = [
3970
3996
  function Zi(e) {
3971
3997
  var o, s, l, d;
3972
3998
  const a = (o = e.getAttribute("role")) == null ? void 0 : o.trim().toLowerCase();
3973
- if (a && Ji.has(a) || (e instanceof HTMLInputElement || e instanceof HTMLTextAreaElement) && !(a && Qi.has(a)))
3974
- return x(e);
3999
+ if (a && Ki.has(a) || (e instanceof HTMLInputElement || e instanceof HTMLTextAreaElement) && !(a && Qi.has(a)))
4000
+ return w(e);
3975
4001
  const i = e.getAttribute("aria-labelledby");
3976
4002
  if (i) {
3977
4003
  const c = i.split(/\s+/).map((u) => {
@@ -4001,13 +4027,14 @@ const en = {
4001
4027
  wcag: ["4.1.2"],
4002
4028
  level: "A",
4003
4029
  fixability: "contextual",
4030
+ browserHint: "Screenshot the form to see visual label placement relative to the input, then associate them with a label element or aria-labelledby.",
4004
4031
  description: "Form elements must have labels. Use <label>, aria-label, or aria-labelledby.",
4005
4032
  guidance: "Every form input needs an accessible label so users understand what information to enter. Use a <label> element with a for attribute matching the input's id, wrap the input in a <label>, or use aria-label/aria-labelledby for custom components. Placeholders are not sufficient as labels since they disappear when typing. Labels should describe the information requested, not the field type (e.g., 'Email address', 'Search', 'Phone number').",
4006
4033
  run(e) {
4007
4034
  var i;
4008
- const a = [], t = e.querySelectorAll(`${Qe}, ${Ki}`);
4035
+ const a = [], t = e.querySelectorAll(`${Qe}, ${Ji}`);
4009
4036
  for (const n of t) {
4010
- if (h(n) || q(n)) continue;
4037
+ if (h(n) || E(n)) continue;
4011
4038
  const r = (i = n.getAttribute("role")) == null ? void 0 : i.trim().toLowerCase();
4012
4039
  if (r === "presentation" || r === "none") continue;
4013
4040
  if (!Zi(n)) {
@@ -4071,6 +4098,7 @@ const en = {
4071
4098
  wcag: ["4.1.2"],
4072
4099
  level: "A",
4073
4100
  fixability: "contextual",
4101
+ browserHint: "Screenshot the button to see its visual purpose, then set value or aria-label to describe the action.",
4074
4102
  description: "Input buttons must have discernible text via value, aria-label, or aria-labelledby.",
4075
4103
  guidance: "Input buttons (<input type='submit'>, type='button', type='reset'>) need accessible names so users know what action the button performs. Add a value attribute with descriptive text (e.g., value='Submit Form'), or use aria-label if the value must differ from the accessible name.",
4076
4104
  run(e) {
@@ -4079,9 +4107,9 @@ const en = {
4079
4107
  for (const n of e.querySelectorAll(
4080
4108
  'input[type="submit"], input[type="button"], input[type="reset"]'
4081
4109
  )) {
4082
- if (h(n) || q(n)) continue;
4110
+ if (h(n) || E(n)) continue;
4083
4111
  const r = (t = n.getAttribute("value")) == null ? void 0 : t.trim(), o = (i = n.getAttribute("type")) == null ? void 0 : i.toLowerCase(), s = (o === "submit" || o === "reset") && !n.hasAttribute("value");
4084
- !r && !s && !x(n) && a.push({
4112
+ !r && !s && !w(n) && a.push({
4085
4113
  ruleId: "labels-and-names/input-button-name",
4086
4114
  selector: p(n),
4087
4115
  html: m(n),
@@ -4110,16 +4138,17 @@ const nn = {
4110
4138
  level: "A",
4111
4139
  tags: ["best-practice"],
4112
4140
  fixability: "contextual",
4141
+ browserHint: "Screenshot the control to see its visible label, then ensure aria-label starts with that visible text.",
4113
4142
  description: "Interactive elements with visible text must have accessible names that contain that text.",
4114
4143
  guidance: "For voice control users who activate controls by speaking their visible label, the accessible name must include the visible text. If aria-label is 'Submit form' but the button shows 'Send', voice users saying 'click Send' won't activate it. Ensure aria-label/aria-labelledby contains or matches the visible text.",
4115
4144
  run(e) {
4116
4145
  const a = [];
4117
4146
  for (const t of e.querySelectorAll('button, [role="button"], a[href], input[type="submit"], input[type="button"]')) {
4118
4147
  if (h(t)) continue;
4119
- const i = x(t);
4148
+ const i = w(t);
4120
4149
  if (!i) continue;
4121
4150
  let n = "";
4122
- t instanceof HTMLInputElement ? n = t.value || "" : n = Z(t);
4151
+ t instanceof HTMLInputElement ? n = t.value || "" : n = ee(t);
4123
4152
  const r = n.trim();
4124
4153
  if (!r || r.length <= 2) continue;
4125
4154
  const o = t.hasAttribute("aria-label"), s = t.hasAttribute("aria-labelledby");
@@ -4134,13 +4163,13 @@ const nn = {
4134
4163
  }
4135
4164
  for (const t of e.querySelectorAll("input, select, textarea")) {
4136
4165
  if (h(t) || t instanceof HTMLInputElement && ["hidden", "submit", "button", "image"].includes(t.type)) continue;
4137
- const i = x(t);
4166
+ const i = w(t);
4138
4167
  if (!i || !t.hasAttribute("aria-label")) continue;
4139
4168
  const r = t.id;
4140
4169
  let o = "";
4141
4170
  if (r) {
4142
4171
  const s = e.querySelector(`label[for="${CSS.escape(r)}"]`);
4143
- s && (o = Z(s));
4172
+ s && (o = ee(s));
4144
4173
  }
4145
4174
  o.trim() && (Ne(i, o) || a.push({
4146
4175
  ruleId: "labels-and-names/label-content-mismatch",
@@ -4192,10 +4221,10 @@ const nn = {
4192
4221
  var t;
4193
4222
  const a = [];
4194
4223
  for (const i of e.querySelectorAll('[role="button"], [role="link"], [role="menuitem"]')) {
4195
- if (h(i) || q(i) || se(i)) continue;
4224
+ if (h(i) || E(i) || se(i)) continue;
4196
4225
  const n = i.getAttribute("role");
4197
4226
  if ((i.tagName.toLowerCase() === "button" || i.tagName.toLowerCase() === "a") && n !== "menuitem") continue;
4198
- if (!x(i)) {
4227
+ if (!w(i)) {
4199
4228
  const o = i.querySelector("img[alt]");
4200
4229
  if ((t = o == null ? void 0 : o.getAttribute("alt")) != null && t.trim()) continue;
4201
4230
  a.push({
@@ -4225,13 +4254,13 @@ function M(e) {
4225
4254
  var i;
4226
4255
  const t = [];
4227
4256
  for (const n of a.querySelectorAll(e.selector)) {
4228
- if (h(n) || e.checkComputedHidden && q(n) || e.checkShadowDOM && se(n)) continue;
4257
+ if (h(n) || e.checkComputedHidden && E(n) || e.checkShadowDOM && se(n)) continue;
4229
4258
  if (e.roleSet) {
4230
4259
  const o = (i = n.getAttribute("role")) == null ? void 0 : i.trim().toLowerCase();
4231
4260
  if (!o || !e.roleSet.has(o)) continue;
4232
4261
  }
4233
4262
  if (e.skipNative && n.matches(e.skipNative)) continue;
4234
- x(n) || t.push({
4263
+ w(n) || t.push({
4235
4264
  ruleId: e.id,
4236
4265
  selector: p(n),
4237
4266
  html: m(n),
@@ -4331,15 +4360,16 @@ const hn = {
4331
4360
  wcag: ["4.1.2"],
4332
4361
  level: "A",
4333
4362
  fixability: "contextual",
4363
+ browserHint: "Screenshot the button to identify its icon or visual label, then add a matching aria-label.",
4334
4364
  description: "Buttons must have discernible text.",
4335
4365
  guidance: "Screen reader users need to know what a button does. Add visible text content, aria-label, or aria-labelledby. For icon buttons, use aria-label describing the action (e.g., aria-label='Close'). If the button contains an image, ensure the image has alt text describing the button's action.",
4336
4366
  run(e) {
4337
4367
  const a = [];
4338
4368
  for (const t of e.querySelectorAll('button, [role="button"]')) {
4339
- if (h(t) || q(t)) continue;
4369
+ if (h(t) || E(t)) continue;
4340
4370
  const i = t.getAttribute("role");
4341
4371
  if ((i === "none" || i === "presentation") && !(t.matches('button:not([disabled]), [tabindex]:not([tabindex="-1"])') || t.tagName.toLowerCase() === "button" && !t.disabled) || se(t)) continue;
4342
- x(t) || a.push({
4372
+ w(t) || a.push({
4343
4373
  ruleId: "labels-and-names/button-name",
4344
4374
  selector: p(t),
4345
4375
  html: m(t),
@@ -4364,7 +4394,7 @@ const hn = {
4364
4394
  const a = [];
4365
4395
  for (const t of e.querySelectorAll("details > summary:first-of-type")) {
4366
4396
  if (h(t)) continue;
4367
- x(t) || a.push({
4397
+ w(t) || a.push({
4368
4398
  ruleId: "labels-and-names/summary-name",
4369
4399
  selector: p(t),
4370
4400
  html: m(t),
@@ -4612,30 +4642,45 @@ Referenced by: ${d}` : ""}`,
4612
4642
  description: "ARIA attributes must be allowed for the element's role.",
4613
4643
  guidance: "Each ARIA role supports specific attributes. Using unsupported attributes creates confusion for assistive technologies. Check the ARIA specification for which attributes are valid for each role, or remove the attribute if it's not needed.",
4614
4644
  run(e) {
4615
- const a = [];
4616
- for (const t of e.querySelectorAll("[role], [aria-*]")) {
4617
- if (h(t)) continue;
4618
- const i = j(t);
4619
- if (!i) continue;
4620
- const n = Sn[i];
4621
- if (n)
4622
- for (const r of t.attributes) {
4623
- if (!r.name.startsWith("aria-") || ut.has(r.name) || n.has(r.name)) continue;
4624
- const o = n.size > 0 ? [...n].join(", ") : "none (only global ARIA attributes)";
4645
+ const a = [], t = new Set(e.querySelectorAll("[role]")), i = e.createTreeWalker(
4646
+ e.body || e.documentElement,
4647
+ 1
4648
+ /* NodeFilter.SHOW_ELEMENT */
4649
+ );
4650
+ let n = i.currentNode;
4651
+ for (; n; ) {
4652
+ if (n instanceof Element) {
4653
+ for (const r of n.attributes)
4654
+ if (r.name.startsWith("aria-")) {
4655
+ t.add(n);
4656
+ break;
4657
+ }
4658
+ }
4659
+ n = i.nextNode();
4660
+ }
4661
+ for (const r of t) {
4662
+ if (h(r)) continue;
4663
+ const o = W(r);
4664
+ if (!o) continue;
4665
+ const s = Sn[o];
4666
+ if (s)
4667
+ for (const l of r.attributes) {
4668
+ if (!l.name.startsWith("aria-") || ut.has(l.name) || s.has(l.name)) continue;
4669
+ const d = s.size > 0 ? [...s].join(", ") : "none (only global ARIA attributes)";
4625
4670
  a.push({
4626
4671
  ruleId: "aria/aria-allowed-attr",
4627
- selector: p(t),
4628
- html: m(t),
4672
+ selector: p(r),
4673
+ html: m(r),
4629
4674
  impact: "critical",
4630
- message: `ARIA attribute "${r.name}" is not allowed on role "${i}".`,
4631
- context: `Attribute: ${r.name}="${r.value}", role: ${i}, allowed role-specific attributes: ${o}`,
4632
- fix: { type: "remove-attribute", attribute: r.name }
4675
+ message: `ARIA attribute "${l.name}" is not allowed on role "${o}".`,
4676
+ context: `Attribute: ${l.name}="${l.value}", role: ${o}, allowed role-specific attributes: ${d}`,
4677
+ fix: { type: "remove-attribute", attribute: l.name }
4633
4678
  });
4634
4679
  }
4635
4680
  }
4636
4681
  return a;
4637
4682
  }
4638
- }, qn = /* @__PURE__ */ new Set([
4683
+ }, En = /* @__PURE__ */ new Set([
4639
4684
  "base",
4640
4685
  "col",
4641
4686
  "colgroup",
@@ -4757,10 +4802,10 @@ Referenced by: ${d}` : ""}`,
4757
4802
  video: /* @__PURE__ */ new Set(["application"]),
4758
4803
  wbr: /* @__PURE__ */ new Set(["none", "presentation"])
4759
4804
  };
4760
- function Ln(e) {
4805
+ function qn(e) {
4761
4806
  var t;
4762
4807
  const a = e.tagName.toLowerCase();
4763
- if (qn.has(a))
4808
+ if (En.has(a))
4764
4809
  return "none";
4765
4810
  if (a === "a" && e.hasAttribute("href"))
4766
4811
  return D["a[href]"];
@@ -4772,7 +4817,7 @@ function Ln(e) {
4772
4817
  }
4773
4818
  return D[a] || "any";
4774
4819
  }
4775
- const En = {
4820
+ const Ln = {
4776
4821
  id: "aria/aria-allowed-role",
4777
4822
  category: "aria",
4778
4823
  wcag: ["4.1.2"],
@@ -4789,7 +4834,7 @@ const En = {
4789
4834
  if (!n) continue;
4790
4835
  const r = $e(i);
4791
4836
  if (r && n === r) continue;
4792
- const o = Ln(i);
4837
+ const o = qn(i);
4793
4838
  o === "none" ? a.push({
4794
4839
  ruleId: "aria/aria-allowed-role",
4795
4840
  selector: p(i),
@@ -4864,8 +4909,8 @@ const Mn = {
4864
4909
  const a = [];
4865
4910
  for (const t of e.querySelectorAll('[aria-hidden="true"]')) {
4866
4911
  if (t === e.body) continue;
4867
- const i = [...t.querySelectorAll(z)];
4868
- t.matches(z) && i.push(t);
4912
+ const i = [...t.querySelectorAll(j)];
4913
+ t.matches(j) && i.push(t);
4869
4914
  for (const n of i)
4870
4915
  if (n instanceof HTMLElement) {
4871
4916
  const r = n.getAttribute("tabindex");
@@ -4901,14 +4946,14 @@ const Mn = {
4901
4946
  run(e) {
4902
4947
  return le(e).prohibitedAttr;
4903
4948
  }
4904
- }, Dn = [
4949
+ }, Hn = [
4905
4950
  "a[href]",
4906
4951
  "button:not([disabled])",
4907
4952
  'input:not([disabled]):not([type="hidden"])',
4908
4953
  "select:not([disabled])",
4909
4954
  "textarea:not([disabled])",
4910
4955
  '[tabindex]:not([tabindex="-1"])'
4911
- ].join(", "), Pn = [
4956
+ ].join(", "), Dn = [
4912
4957
  "aria-atomic",
4913
4958
  "aria-busy",
4914
4959
  "aria-controls",
@@ -4925,15 +4970,15 @@ const Mn = {
4925
4970
  ];
4926
4971
  function Me(e) {
4927
4972
  const a = [];
4928
- e.matches(Dn) && a.push("element is focusable");
4929
- for (const t of Pn)
4973
+ e.matches(Hn) && a.push("element is focusable");
4974
+ for (const t of Dn)
4930
4975
  if (e.hasAttribute(t)) {
4931
4976
  a.push(`has ${t}`);
4932
4977
  break;
4933
4978
  }
4934
4979
  return (e.hasAttribute("aria-label") || e.hasAttribute("aria-labelledby")) && a.push("has accessible name"), a;
4935
4980
  }
4936
- const Fn = {
4981
+ const Pn = {
4937
4982
  id: "aria/presentation-role-conflict",
4938
4983
  category: "aria",
4939
4984
  actRuleIds: ["46ca7f"],
@@ -4970,7 +5015,7 @@ const Fn = {
4970
5015
  }
4971
5016
  return a;
4972
5017
  }
4973
- }, Hn = /* @__PURE__ */ new Set([
5018
+ }, Fn = /* @__PURE__ */ new Set([
4974
5019
  "button",
4975
5020
  "checkbox",
4976
5021
  "img",
@@ -5001,9 +5046,9 @@ const Fn = {
5001
5046
  const a = [];
5002
5047
  for (const t of e.querySelectorAll("*")) {
5003
5048
  if (h(t)) continue;
5004
- const i = j(t);
5005
- if (!(!i || !Hn.has(i))) {
5006
- for (const n of t.querySelectorAll(z))
5049
+ const i = W(t);
5050
+ if (!(!i || !Fn.has(i))) {
5051
+ for (const n of t.querySelectorAll(j))
5007
5052
  if (n !== t && !n.disabled) {
5008
5053
  a.push({
5009
5054
  ruleId: "aria/presentational-children-focusable",
@@ -5021,7 +5066,7 @@ const Fn = {
5021
5066
  }, Ze = [
5022
5067
  // Text Alternatives
5023
5068
  jt,
5024
- Wt,
5069
+ Ut,
5025
5070
  Ot,
5026
5071
  Vt,
5027
5072
  _t,
@@ -5029,8 +5074,8 @@ const Fn = {
5029
5074
  Yt,
5030
5075
  Xt,
5031
5076
  // Time-based Media
5032
- Kt,
5033
5077
  Jt,
5078
+ Kt,
5034
5079
  // Adaptable
5035
5080
  na,
5036
5081
  la,
@@ -5046,13 +5091,13 @@ const Fn = {
5046
5091
  Sa,
5047
5092
  Ia,
5048
5093
  // Distinguishable
5049
- qa,
5094
+ Ea,
5050
5095
  Ra,
5051
5096
  Ca,
5052
5097
  Ta,
5053
- Pa,
5054
- Ka,
5098
+ Da,
5055
5099
  Ja,
5100
+ Ka,
5056
5101
  // Keyboard Accessible
5057
5102
  Za,
5058
5103
  ti,
@@ -5073,8 +5118,8 @@ const Fn = {
5073
5118
  ki,
5074
5119
  Si,
5075
5120
  Ii,
5121
+ qi,
5076
5122
  Li,
5077
- Ei,
5078
5123
  // Landmarks
5079
5124
  Ri,
5080
5125
  Ci,
@@ -5082,10 +5127,10 @@ const Fn = {
5082
5127
  Ni,
5083
5128
  Mi,
5084
5129
  $i,
5130
+ Hi,
5085
5131
  Di,
5086
5132
  Pi,
5087
5133
  Fi,
5088
- Hi,
5089
5134
  // Readable
5090
5135
  zi,
5091
5136
  Oi,
@@ -5116,43 +5161,48 @@ const Fn = {
5116
5161
  wn,
5117
5162
  kn,
5118
5163
  In,
5119
- En,
5164
+ Ln,
5120
5165
  Cn,
5121
5166
  Mn,
5122
5167
  $n,
5123
- Fn,
5168
+ Pn,
5124
5169
  zn
5125
5170
  ];
5126
- let me = [], et = /* @__PURE__ */ new Set(), tt = !1, at = !1, T, H;
5171
+ let me = [], et = /* @__PURE__ */ new Set(), tt = !1, at = !1, T, z;
5127
5172
  function Vn(e) {
5128
- e.additionalRules && (me = e.additionalRules), e.disabledRules && (et = new Set(e.disabledRules)), "includeAAA" in e && (tt = !!e.includeAAA), "componentMode" in e && (at = !!e.componentMode), "locale" in e && (T = e.locale || void 0), H = void 0;
5173
+ e.additionalRules && (me = e.additionalRules), e.disabledRules && (et = new Set(e.disabledRules)), "includeAAA" in e && (tt = !!e.includeAAA), "componentMode" in e && (at = !!e.componentMode), "locale" in e && (T = e.locale || void 0), z = void 0;
5129
5174
  }
5130
5175
  function pe() {
5131
- if (H) return H;
5176
+ if (z) return z;
5132
5177
  const a = Ze.filter((t) => {
5133
5178
  var i;
5134
5179
  return !(et.has(t.id) || t.level === "AAA" && !tt || at && ((i = t.tags) != null && i.includes("page-level")));
5135
5180
  }).concat(me);
5136
- return T ? (H = Dt(a, T), H) : a;
5181
+ return T ? (z = Ht(a, T), z) : a;
5137
5182
  }
5138
5183
  function Bn(e) {
5139
5184
  it();
5140
- const a = pe(), t = T, i = [];
5141
- let n = 0;
5185
+ const a = pe(), t = T, i = [], n = [];
5186
+ let r = 0;
5142
5187
  return {
5143
- processChunk(r) {
5144
- const o = performance.now();
5145
- for (; n < a.length; ) {
5188
+ processChunk(o) {
5189
+ const s = performance.now();
5190
+ for (; r < a.length; ) {
5191
+ const l = a[r];
5146
5192
  try {
5147
- i.push(...a[n].run(e));
5148
- } catch {
5193
+ i.push(...l.run(e));
5194
+ } catch (d) {
5195
+ n.push({ ruleId: l.id, error: d instanceof Error ? d.message : String(d) });
5149
5196
  }
5150
- if (n++, performance.now() - o >= r) break;
5197
+ if (r++, performance.now() - s >= o) break;
5151
5198
  }
5152
- return n < a.length;
5199
+ return r < a.length;
5153
5200
  },
5154
5201
  getViolations() {
5155
5202
  return t ? ze(i, t) : i;
5203
+ },
5204
+ getSkippedRules() {
5205
+ return n;
5156
5206
  }
5157
5207
  };
5158
5208
  }
@@ -5160,19 +5210,21 @@ function it() {
5160
5210
  mt(), ot(), st(), kt(), At(), pt();
5161
5211
  }
5162
5212
  function _n(e) {
5163
- var i;
5213
+ var n;
5164
5214
  it();
5165
- const a = pe(), t = [];
5166
- for (const n of a)
5215
+ const a = pe(), t = [], i = [];
5216
+ for (const r of a)
5167
5217
  try {
5168
- t.push(...n.run(e));
5169
- } catch {
5218
+ t.push(...r.run(e));
5219
+ } catch (o) {
5220
+ i.push({ ruleId: r.id, error: o instanceof Error ? o.message : String(o) });
5170
5221
  }
5171
5222
  return {
5172
- url: ((i = e.location) == null ? void 0 : i.href) ?? "",
5223
+ url: ((n = e.location) == null ? void 0 : n.href) ?? "",
5173
5224
  timestamp: Date.now(),
5174
5225
  violations: T ? ze(t, T) : t,
5175
- ruleCount: a.length
5226
+ ruleCount: a.length,
5227
+ skippedRules: i
5176
5228
  };
5177
5229
  }
5178
5230
  function Gn(e, a) {
@@ -5207,7 +5259,7 @@ const Xn = {
5207
5259
  "enough-time/meta-refresh": { description: "Meta refresh must not redirect or refresh automatically.", guidance: "Automatic page refreshes or redirects can disorient users, especially those using screen readers or with cognitive disabilities. They may lose their place or not have time to read content. If a redirect is needed, use a server-side redirect (HTTP 301/302) instead. For timed refreshes, provide user controls.", messages: { "Page redirects after {0} seconds without warning. Use server-side redirect.": "Page redirects after {0} seconds without warning. Use server-side redirect.", "Page auto-refreshes after {0} seconds. Provide user control over refresh.": "Page auto-refreshes after {0} seconds. Provide user control over refresh." } },
5208
5260
  "enough-time/blink": { description: "The <blink> element must not be used.", guidance: "Blinking content can cause seizures in users with photosensitive epilepsy and is distracting for users with attention disorders. The <blink> element is deprecated and should never be used. If you need to draw attention to content, use less intrusive methods like color, borders, or icons.", messages: { "The <blink> element causes accessibility issues. Remove it entirely.": "The <blink> element causes accessibility issues. Remove it entirely." } },
5209
5261
  "enough-time/marquee": { description: "The <marquee> element must not be used.", guidance: "Scrolling or moving content is difficult for many users to read, especially those with cognitive or visual disabilities. The <marquee> element is deprecated. Replace scrolling text with static content. If content must scroll, provide pause/stop controls and ensure it stops after 5 seconds.", messages: { "The <marquee> element causes accessibility issues. Replace with static content.": "The <marquee> element causes accessibility issues. Replace with static content." } },
5210
- "text-alternatives/img-alt": { description: `Images must have alternate text. Add an alt attribute to <img> elements. Decorative images may use an empty alt attribute (alt=""), role='none', or role='presentation'.`, guidance: "Every image needs an alt attribute. For informative images, describe the content or function concisely. For decorative images (backgrounds, spacers, purely visual flourishes), use alt='' to hide them from screen readers. Never omit alt entirely—screen readers may read the filename instead.", messages: { 'Image has whitespace-only alt text. Use alt="" for decorative images or provide descriptive text.': 'Image has whitespace-only alt text. Use alt="" for decorative images or provide descriptive text.', "Image element missing alt attribute.": "Image element missing alt attribute.", 'Element with role="img" has no accessible name. Add aria-label or aria-labelledby.': 'Element with role="img" has no accessible name. Add aria-label or aria-labelledby.' } },
5262
+ "text-alternatives/img-alt": { description: `Images must have alternate text. Add an alt attribute to <img> elements. Decorative images may use an empty alt attribute (alt=""), role='none', or role='presentation'.`, guidance: "Every image needs an alt attribute. For informative images, describe the content or function concisely. For decorative images (backgrounds, spacers, purely visual flourishes), use alt='' to hide them from screen readers. Never omit alt entirely—screen readers may read the filename instead. When an image is inside a link or button that already has text, use alt='' if the image is decorative in that context, or write alt text that complements (not duplicates) the existing text.", messages: { 'Image has whitespace-only alt text. Use alt="" for decorative images or provide descriptive text.': 'Image has whitespace-only alt text. Use alt="" for decorative images or provide descriptive text.', "Image element missing alt attribute.": "Image element missing alt attribute.", 'Element with role="img" has no accessible name. Add aria-label or aria-labelledby.': 'Element with role="img" has no accessible name. Add aria-label or aria-labelledby.' } },
5211
5263
  "text-alternatives/svg-img-alt": { description: "SVG elements with an img, graphics-document, or graphics-symbol role must have an accessible name via a <title> element, aria-label, or aria-labelledby.", guidance: "Inline SVGs with role='img' need accessible names. Add a <title> element as the first child of the SVG (screen readers will announce it), or use aria-label on the SVG element. For complex SVGs, use aria-labelledby referencing both a <title> and <desc> element. Decorative SVGs should use aria-hidden='true' instead.", messages: { "{0} with role='{1}' has no accessible name.": "{0} with role='{1}' has no accessible name." } },
5212
5264
  "text-alternatives/input-image-alt": { description: 'Image inputs (<input type="image">) must have alternate text describing the button action.', guidance: "Image buttons (<input type='image'>) act as submit buttons with a custom image. Add alt text via alt, aria-label, or aria-labelledby that describes the action (e.g. alt='Search' or alt='Submit order'), not the image itself. Without it, screen readers announce only 'image' or the filename, giving no clue what the button does.", messages: { "Image input missing alt text.": "Image input missing alt text." } },
5213
5265
  "text-alternatives/image-redundant-alt": { description: "Image alt text should not duplicate adjacent link or button text. When alt text repeats surrounding text, screen reader users hear the same information twice.", guidance: "When an image is inside a link or button that also has text, make the alt text complementary rather than identical. If the image is purely decorative in that context, use alt='' to avoid repetition.", messages: { 'Alt text "{0}" duplicates surrounding {1} text.': 'Alt text "{0}" duplicates surrounding {1} text.' } },
@@ -5290,7 +5342,7 @@ const Xn = {
5290
5342
  "adaptable/orientation-lock": { description: "Page orientation must not be restricted using CSS transforms.", guidance: "Users with motor disabilities may mount their device in a fixed orientation. Using CSS transforms with @media (orientation: portrait/landscape) to rotate content 90° effectively locks the page to one orientation. Remove the orientation-dependent transform and use responsive design instead.", messages: { "CSS locks page orientation via @media (orientation: {0}) with a 90° transform.": "CSS locks page orientation via @media (orientation: {0}) with a 90° transform." } },
5291
5343
  "aria/presentational-children-focusable": { description: "Elements with a role that makes children presentational must not contain focusable content.", guidance: "Roles like button, checkbox, img, tab, and others make their children presentational — hidden from assistive technologies. If those children are focusable, keyboard users can reach elements that screen reader users cannot perceive. Move focusable content outside the parent or remove the focusability.", messages: { 'Focusable element inside a "{0}" role whose children are presentational.': 'Focusable element inside a "{0}" role whose children are presentational.' } },
5292
5344
  "keyboard-accessible/focus-visible": { description: "Elements in sequential focus order must have a visible focus indicator.", guidance: "Keyboard users need to see which element has focus. Do not remove the default focus outline (outline: none) without providing an alternative visible indicator. Use :focus-visible or :focus styles to ensure focus is always perceivable.", messages: { "Focusable element has outline removed without a visible focus alternative.": "Focusable element has outline removed without a visible focus alternative." } }
5293
- }, Kn = {
5345
+ }, Jn = {
5294
5346
  "navigable/document-title": { description: "Los documentos deben tener un elemento <title> para proporcionar a los usuarios una vista general del contenido.", guidance: "Los usuarios de lectores de pantalla dependen de los títulos de página para identificar y navegar entre pestañas/ventanas. Agregue un elemento <title> descriptivo en <head> que resuma el propósito de la página. Mantenga los títulos únicos en todo el sitio, colocando el contenido específico antes del nombre del sitio (por ejemplo, 'Contáctenos - Acme Corp').", messages: { "Document <title> element is empty.": "El elemento <title> del documento está vacío.", "Document is missing a <title> element.": "Al documento le falta un elemento <title>." } },
5295
5347
  "navigable/bypass": { description: "La página debe tener un mecanismo para omitir bloques de contenido repetidos.", guidance: 'Los usuarios de teclado deben poder omitir contenido repetitivo como la navegación. Proporcione un enlace de salto en la parte superior de la página que enlace al contenido principal (por ejemplo, <a href="#main">Saltar al contenido principal</a>), o use un landmark <main>. Los lectores de pantalla pueden saltar directamente a los landmarks, por lo que un elemento <main> correctamente marcado satisface este requisito.', messages: { "Page has no mechanism to bypass repeated content. Add a <main> landmark or skip link.": "La página no tiene un mecanismo para omitir contenido repetido. Agregue un landmark <main> o un enlace de salto." } },
5296
5348
  "navigable/page-has-heading-one": { description: "La página debe contener un encabezado de nivel uno.", guidance: "Un encabezado de nivel uno (<h1> o role='heading' con aria-level='1') ayuda a los usuarios a comprender el tema de la página y proporciona un punto de referencia para la navegación con lector de pantalla. Cada página debe tener exactamente un h1 que describa el contenido principal, típicamente coincidiendo o similar al título de la página.", messages: { "Page does not contain a level-one heading.": "La página no contiene un encabezado de nivel uno." } },
@@ -5394,10 +5446,10 @@ export {
5394
5446
  Vn as configureRules,
5395
5447
  Bn as createChunkedAudit,
5396
5448
  Gn as diffAudit,
5397
- x as getAccessibleName,
5449
+ w as getAccessibleName,
5398
5450
  k as getAccessibleTextContent,
5399
5451
  pe as getActiveRules,
5400
- j as getComputedRole,
5452
+ W as getComputedRole,
5401
5453
  m as getHtmlSnippet,
5402
5454
  $e as getImplicitRole,
5403
5455
  Yn as getRuleById,
@@ -5405,9 +5457,9 @@ export {
5405
5457
  h as isAriaHidden,
5406
5458
  dt as isValidRole,
5407
5459
  Xn as localeEn,
5408
- Kn as localeEs,
5409
- Un as querySelectorShadowAware,
5410
- Wn as registerLocale,
5460
+ Jn as localeEs,
5461
+ Wn as querySelectorShadowAware,
5462
+ Un as registerLocale,
5411
5463
  Ze as rules,
5412
5464
  _n as runAudit,
5413
5465
  ze as translateViolations,