From 0604293529f07a168ec017ea1f3c5a8ddfb2b6d5 Mon Sep 17 00:00:00 2001 From: whoknowsla <168711133+whoknowsla@users.noreply.github.com> Date: Sun, 24 May 2026 05:24:47 +0300 Subject: [PATCH 1/3] fix(site): WCAG accessibility pass across all pages Adds keyboard support, dialog semantics, form labels, contrast bumps, focus indicators, and live regions so the site meets WCAG 2.1 AA on the static pages. Highlights: - Phase cards on index now keyboard-activatable (role/tabindex + Enter/Space) - Phase modal gets role="dialog", focus trap, focus restore, initial focus - Quiz options in the stage-based renderer switched from
to
@@ -257,26 +265,29 @@

Lesson Catalog

Every lesson across all 20 phases. Search, filter, sort.

- - + + -
-
+
- - - - - + + + + + @@ -448,7 +459,8 @@

Lesson Catalog

statusSelect.addEventListener('change', render); document.querySelectorAll('.catalog-table th[data-sort]').forEach(function (th) { - th.addEventListener('click', function () { + var btn = th.querySelector('.sort-btn'); + var handler = function () { var col = th.getAttribute('data-sort'); if (sortCol === col) { sortDir = sortDir * -1; @@ -456,12 +468,18 @@

Lesson Catalog

sortCol = col; sortDir = 1; } - document.querySelectorAll('.catalog-table th .sort-arrow').forEach(function (a) { - a.textContent = ''; + document.querySelectorAll('.catalog-table th[data-sort]').forEach(function (otherTh) { + otherTh.setAttribute('aria-sort', 'none'); + var arrow = otherTh.querySelector('.sort-arrow'); + if (arrow) arrow.textContent = ''; }); - th.querySelector('.sort-arrow').textContent = sortDir === 1 ? ' \u25B2' : ' \u25BC'; + th.setAttribute('aria-sort', sortDir === 1 ? 'ascending' : 'descending'); + var arrow = th.querySelector('.sort-arrow'); + if (arrow) arrow.textContent = sortDir === 1 ? ' \u25B2' : ' \u25BC'; render(); - }); + }; + if (btn) btn.addEventListener('click', handler); + else th.addEventListener('click', handler); }); render(); diff --git a/site/glossary.html b/site/glossary.html index ddd949f95..ad5bca607 100644 --- a/site/glossary.html +++ b/site/glossary.html @@ -180,10 +180,10 @@ CatalogRoadmapGlossary - + - + @@ -208,9 +208,10 @@

AI Glossary

What people say vs what things actually mean

- + +
-
+
diff --git a/site/index.html b/site/index.html index 142afcf72..f21c7cced 100644 --- a/site/index.html +++ b/site/index.html @@ -580,9 +580,9 @@ CatalogRoadmapGlossary - + - + @@ -630,29 +630,29 @@

AI Engineering
from Scratch

Finished Lessons - + 0 / 0
Phases - + 0 / 0
Languages - + 4
Glossary Terms - + ···
-
-
Curriculum · 20 phases · 435 lessons
+
+

Curriculum · 20 phases · 435 lessons

Tap a phase to expand its lessons. Each one ships when its math, code, and test are all written.
@@ -662,9 +662,9 @@

AI Engineering
from Scratch

-
diff --git a/site/lesson.html b/site/lesson.html index 661a63604..910791e64 100644 --- a/site/lesson.html +++ b/site/lesson.html @@ -2710,7 +2710,9 @@ } html += ''; if (q.explanation) { - html += '
' + escapeHtml(q.explanation) + '
'; + // Render empty; text is injected on reveal so the live region + // fires a content-change announcement (not just a visibility flip). + html += '
'; } html += ''; } @@ -2750,7 +2752,13 @@ } var exp = questionDiv.querySelector('.quiz-explanation'); - if (exp) exp.style.display = 'block'; + if (exp) { + var explanationText = exp.getAttribute('data-explanation'); + if (explanationText && !exp.textContent) { + exp.textContent = explanationText; + } + exp.style.display = 'block'; + } if (persist && window.AIFSProgress && lessonPath) { window.AIFSProgress.recordAnswer(lessonPath, questionDiv.id, chosen, chosen === correct); diff --git a/site/style.css b/site/style.css index c26acd5fd..6828b4029 100644 --- a/site/style.css +++ b/site/style.css @@ -129,7 +129,8 @@ body { outline-offset: 2px; } -/* Visually-hidden but accessible to assistive tech. */ +/* Visually-hidden but accessible to assistive tech. clip is kept as a + fallback for older WebKit; modern browsers honor clip-path. */ .sr-only { position: absolute; width: 1px; @@ -138,6 +139,7 @@ body { margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); + clip-path: inset(50%); white-space: nowrap; border: 0; } From 73a27e671c782cf9458ce2e9d9ecb5aca3f9e3f9 Mon Sep 17 00:00:00 2001 From: whoknowsla <168711133+whoknowsla@users.noreply.github.com> Date: Sun, 24 May 2026 05:53:57 +0300 Subject: [PATCH 3/3] fix(site): retrigger copy-button live region on repeated clicks Clear the status region before setting the message on the next animation frame so screen readers announce each successful copy, not just the first. Identical textContent updates are de-duped by AT otherwise. --- site/app.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/site/app.js b/site/app.js index 6c79eadd1..d2f1a8e14 100644 --- a/site/app.js +++ b/site/app.js @@ -380,7 +380,14 @@ btn.textContent = '✓'; // Announce via a dedicated live region — the button's aria-label // overrides its textContent, so AT won't hear "✓" otherwise. - if (status) status.textContent = 'Command copied to clipboard'; + // Clear first, then set on the next frame so repeated clicks + // produce a fresh AT announcement (live regions debounce identical text). + if (status) { + status.textContent = ''; + window.requestAnimationFrame(function () { + status.textContent = 'Command copied to clipboard'; + }); + } if (revertTimer) clearTimeout(revertTimer); revertTimer = setTimeout(function () { btn.textContent = originalLabel;
Phase Lesson Type Language Status