diff --git a/css/styles.css b/css/styles.css
index 4b4a73b..f23d23b 100644
--- a/css/styles.css
+++ b/css/styles.css
@@ -753,6 +753,19 @@ h1::before {
font-weight: 500;
}
+.wcag-link {
+ color: var(--info);
+ text-decoration: none;
+ font-weight: 600;
+ border-bottom: 1px dotted var(--info);
+ transition: color 0.15s, border-color 0.15s;
+}
+
+.wcag-link:hover {
+ color: var(--accent);
+ border-bottom-color: var(--accent);
+}
+
.issue-recommendation {
background: var(--success-bg);
padding: 10px 12px;
diff --git a/js/results.js b/js/results.js
index e92627e..a018585 100644
--- a/js/results.js
+++ b/js/results.js
@@ -143,7 +143,7 @@ function createIssueCard(issue, issueNumber, globalIndex) {
${issue.description}
- ${issue.wcag_criterion ? `WCAG ${issue.wcag_criterion}
` : ''}
+ ${issue.wcag_criterion ? `${wcagCriterionLinks(issue.wcag_criterion)}
` : ''}
${issue.recommendation ? `Tip: ${issue.recommendation}
` : ''}
`;
}
diff --git a/js/utils.js b/js/utils.js
index 2319279..2f5fd41 100644
--- a/js/utils.js
+++ b/js/utils.js
@@ -59,6 +59,80 @@ function getSeverityIcon(severity) {
return map[severity] || '\u2022';
}
+/* WCAG 2.1 criterion → Understanding page slug */
+const WCAG_SLUGS = {
+ '1.1.1': 'non-text-content',
+ '1.2.1': 'audio-only-and-video-only-prerecorded',
+ '1.2.2': 'captions-prerecorded',
+ '1.2.3': 'audio-description-or-media-alternative-prerecorded',
+ '1.2.4': 'captions-live',
+ '1.2.5': 'audio-description-prerecorded',
+ '1.3.1': 'info-and-relationships',
+ '1.3.2': 'meaningful-sequence',
+ '1.3.3': 'sensory-characteristics',
+ '1.3.4': 'orientation',
+ '1.3.5': 'identify-input-purpose',
+ '1.4.1': 'use-of-color',
+ '1.4.2': 'audio-control',
+ '1.4.3': 'contrast-minimum',
+ '1.4.4': 'resize-text',
+ '1.4.5': 'images-of-text',
+ '1.4.6': 'contrast-enhanced',
+ '1.4.10': 'reflow',
+ '1.4.11': 'non-text-contrast',
+ '1.4.12': 'text-spacing',
+ '1.4.13': 'content-on-hover-or-focus',
+ '2.1.1': 'keyboard',
+ '2.1.2': 'no-keyboard-trap',
+ '2.2.1': 'timing-adjustable',
+ '2.2.2': 'pause-stop-hide',
+ '2.3.1': 'three-flashes-or-below-threshold',
+ '2.4.1': 'bypass-blocks',
+ '2.4.2': 'page-titled',
+ '2.4.3': 'focus-order',
+ '2.4.4': 'link-purpose-in-context',
+ '2.4.5': 'multiple-ways',
+ '2.4.6': 'headings-and-labels',
+ '2.4.7': 'focus-visible',
+ '2.5.3': 'label-in-name',
+ '3.1.1': 'language-of-page',
+ '3.1.2': 'language-of-parts',
+ '3.1.5': 'reading-level',
+ '3.2.1': 'on-focus',
+ '3.2.2': 'on-input',
+ '3.2.3': 'consistent-navigation',
+ '3.2.4': 'consistent-identification',
+ '3.3.1': 'error-identification',
+ '3.3.2': 'labels-or-instructions',
+ '3.3.3': 'error-suggestion',
+ '3.3.4': 'error-prevention-legal-financial-data',
+ '4.1.1': 'parsing',
+ '4.1.2': 'name-role-value',
+ '4.1.3': 'status-messages',
+};
+
+/**
+ * Returns an HTML string of clickable WCAG criterion links.
+ * Handles comma-separated criteria (e.g. "1.3.1, 4.1.2") and "PDF/UA".
+ */
+function wcagCriterionLinks(criterion) {
+ if (!criterion) return '';
+
+ if (criterion.trim().toUpperCase() === 'PDF/UA') {
+ return 'PDF/UA';
+ }
+
+ return criterion.split(',').map(part => {
+ const num = part.trim();
+ const slug = WCAG_SLUGS[num];
+ if (slug) {
+ const url = `https://www.w3.org/WAI/WCAG21/Understanding/${slug}`;
+ return `WCAG ${num}`;
+ }
+ return `WCAG ${num}`;
+ }).join(', ');
+}
+
function escapeAttr(str) {
return String(str).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '"');
}