@@ -1810,17 +1810,25 @@
var sidebar = document.getElementById('lessonSidebar');
if (!toggle || !sidebar) return;
+ function syncExpanded() {
+ toggle.setAttribute('aria-expanded', sidebar.classList.contains('open') ? 'true' : 'false');
+ }
+
toggle.addEventListener('click', function () {
sidebar.classList.toggle('open');
+ syncExpanded();
});
document.addEventListener('click', function (e) {
if (window.innerWidth <= 900 && sidebar.classList.contains('open')) {
if (!sidebar.contains(e.target) && e.target !== toggle) {
sidebar.classList.remove('open');
+ syncExpanded();
}
}
});
+
+ syncExpanded();
}
function initScrollProgress() {
@@ -2690,22 +2698,23 @@
for (var qi = 0; qi < questions.length; qi++) {
var q = questions[qi];
var qid = id + '-q' + qi;
- html += '';
- html += '
' + (qi + 1) + '. ' + escapeHtml(q.question) + '
';
- html += '
';
+ var optsId = qid + '-opts';
+ html += '
';
+ html += '
' + (qi + 1) + '. ' + escapeHtml(q.question) + '
';
+ html += '
';
for (var oi = 0; oi < q.options.length; oi++) {
- html += '
';
- html += ' ';
+ html += '';
+ html += ' ';
html += '' + escapeHtml(q.options[oi]) + ' ';
- html += '
';
+ html += '';
}
html += '
';
if (q.explanation) {
- html += '
' + escapeHtml(q.explanation) + '
';
+ html += '
' + escapeHtml(q.explanation) + '
';
}
html += '
';
}
- html += '
';
+ html += '
';
html += '
';
return html;
}
diff --git a/site/prereqs.html b/site/prereqs.html
index b34f3c7cd..87324cba4 100644
--- a/site/prereqs.html
+++ b/site/prereqs.html
@@ -406,10 +406,10 @@
Catalog
Roadmap
Glossary
-
-
- N
+
+ N
diff --git a/site/style.css b/site/style.css
index 1f8268224..c26acd5fd 100644
--- a/site/style.css
+++ b/site/style.css
@@ -9,9 +9,13 @@
--bg-surface-hover: #ece9dc;
--ink: #1a1a1a;
--ink-soft: #4a4a4a;
- --ink-mute: #7a7a78;
+ /* Bumped from #7a7a78 → #686867 to meet WCAG AA 4.5:1 on --bg. */
+ --ink-mute: #686867;
--rule: #1a1a1a;
+ /* Decorative paper rules stay subtle; component borders use --rule-strong
+ for WCAG 1.4.11 (3:1) contrast against the background. */
--rule-soft: rgba(26, 26, 26, 0.16);
+ --rule-strong: rgba(26, 26, 26, 0.45);
--paper-rule: rgba(26, 26, 26, 0.08);
--blueprint: #3553ff;
@@ -50,9 +54,11 @@
--bg-surface-hover: #1b2244;
--ink: #e8e6dc;
--ink-soft: #a8a6a0;
- --ink-mute: #7a7878;
+ /* Bumped from #7a7878 → #9a9a98 to meet WCAG AA 4.5:1 on --bg. */
+ --ink-mute: #9a9a98;
--rule: #e8e6dc;
--rule-soft: rgba(232, 230, 220, 0.18);
+ --rule-strong: rgba(232, 230, 220, 0.5);
--paper-rule: rgba(232, 230, 220, 0.08);
--blueprint: #6b8eff;
@@ -123,6 +129,30 @@ body {
outline-offset: 2px;
}
+/* Visually-hidden but accessible to assistive tech. */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* Global keyboard-focus indicator. Individual components may still override
+ for tighter styling, but every focusable element gets a visible ring. */
+a:focus-visible,
+button:focus-visible,
+[role="button"]:focus-visible,
+[tabindex]:focus-visible,
+summary:focus-visible {
+ outline: 2px solid var(--blueprint);
+ outline-offset: 2px;
+}
+
.container {
max-width: 1200px;
margin: 0 auto;
@@ -296,7 +326,7 @@ p.dropcap::first-letter {
align-items: center;
gap: 8px;
padding: 6px 12px;
- border: 1px solid var(--rule-soft);
+ border: 1px solid var(--rule-strong);
background: var(--bg-surface);
font-family: var(--font-mono);
font-size: 0.78rem;
@@ -334,7 +364,7 @@ p.dropcap::first-letter {
.theme-toggle {
background: transparent;
- border: 1px solid var(--rule-soft);
+ border: 1px solid var(--rule-strong);
width: 36px;
height: 36px;
cursor: pointer;
@@ -628,6 +658,17 @@ p.dropcap::first-letter {
border-bottom-color: var(--blueprint);
}
+.modal-lesson > .modal-lesson-pending {
+ color: var(--ink-mute);
+ font-family: var(--font-body);
+ font-size: 0.96rem;
+ font-weight: 500;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
.modal-lesson-status {
width: 12px;
height: 12px;
@@ -1031,7 +1072,7 @@ body.js-anim .toc-row.in-view {
/* ── Search trigger button in the header ─────────────────────────────── */
.search-toggle {
background: transparent;
- border: 1px solid var(--rule-soft);
+ border: 1px solid var(--rule-strong);
width: 36px;
height: 36px;
cursor: pointer;
From 44d165ed8ec65cc98d3bc362f58fb61b0c9e3e52 Mon Sep 17 00:00:00 2001
From: whoknowsla <168711133+whoknowsla@users.noreply.github.com>
Date: Sun, 24 May 2026 05:43:38 +0300
Subject: [PATCH 2/3] fix(site): address PR review notes on focus trap and live
regions
- app.js: switch focus-trap visibility check from offsetParent to
getClientRects(); offsetParent is null for position:fixed elements
even when they are visible and focusable
- index.html / app.js: drop ineffective aria-live on the copy button
(its aria-label overrides textContent so AT never heard "Copied!");
announce via a dedicated sr-only role=status region instead
- style.css: add modern clip-path: inset(50%) to .sr-only, keep clip
as a legacy WebKit fallback
- lesson.html: render quiz-explanation empty and inject text on reveal
so the live region fires a content-change announcement, not just a
visibility flip
---
site/app.js | 13 +++++++++++--
site/index.html | 3 ++-
site/lesson.html | 12 ++++++++++--
site/style.css | 4 +++-
4 files changed, 26 insertions(+), 6 deletions(-)
diff --git a/site/app.js b/site/app.js
index 534798f92..6c79eadd1 100644
--- a/site/app.js
+++ b/site/app.js
@@ -222,9 +222,11 @@
}
function getFocusables(root) {
+ // getClientRects().length is more reliable than offsetParent for visibility:
+ // offsetParent returns null for position: fixed elements even when visible.
return Array.prototype.slice.call(root.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
- )).filter(function (el) { return el.offsetParent !== null; });
+ )).filter(function (el) { return el.getClientRects().length > 0; });
}
function trapTab(e, container) {
@@ -369,14 +371,21 @@
function initCopyButton() {
var btn = document.getElementById('copyBtn');
var code = document.getElementById('cloneCmd');
+ var status = document.getElementById('copyStatus');
if (!btn || !code) return;
var originalLabel = btn.textContent;
var revertTimer = null;
btn.addEventListener('click', function () {
navigator.clipboard.writeText(code.textContent).then(function () {
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';
if (revertTimer) clearTimeout(revertTimer);
- revertTimer = setTimeout(function () { btn.textContent = originalLabel; }, 1500);
+ revertTimer = setTimeout(function () {
+ btn.textContent = originalLabel;
+ if (status) status.textContent = '';
+ }, 1500);
});
});
}
diff --git a/site/index.html b/site/index.html
index f21c7cced..f944a47f7 100644
--- a/site/index.html
+++ b/site/index.html
@@ -688,8 +688,9 @@
The entire curriculum is on GitHub. Clone it, fork it, learn at your own pace. No paywall, no signup. Every lesson has runnable code in Python, TypeScript, Rust, or Julia, depending on what fits the concept best.
git clone https://github.com/rohitg00/ai-engineering-from-scratch.git
- cp
+ cp
+