Skip to content

Commit 0f1fd11

Browse files
trotroCopilot
andauthored
Feat: Complete Epic 3 (accessibility) + Epic 4 (content generation) (#17)
* feat(templates): add i18n skip-nav partial and translate skip link (story 3.8) - Create templates/partials/skip-nav.html with i18n trans() skip link - Update base.html to include skip-nav partial - Add skip_to_main translation keys to zola.toml (EN + FR) - Update compare.html skip link text via existing lang-detection JS * fix(js): defer skip link text update until DOM is ready in compare.html querySelector('.skip-link') ran in <head> before body was parsed. Wrap in DOMContentLoaded so the element exists when accessed. * feat(templates): use native <section> for filters container and add i18n aria-label (story 3.9) - Replace <div role="region"> with semantic <section> in section.html - Add search_filter_label translation key in zola.toml (EN + FR) * fix(css): remove outline:none anti-pattern, add forced-colors support (story 3.10) - Remove outline:none from .search-input; add explicit :focus-visible rule - Migrate .filter-reset:focus and .lang-btn:focus to :focus-visible - Add @media (forced-colors: active) block for Windows High Contrast mode * chore: mark Epic 3 done in sprint-status (all 10 stories complete) * feat(templates): generate certification explainer pages with dynamic provider lists (story 4.4) - Add templates/certifications/single.html with get_page() content loading and term.pages provider list - Add templates/certifications/page.html (Zola section page fallback, unused but harmless) - Link cert badges on provider detail pages to /certifications/{slug}/ (language-aware) - Add cert explainer CSS classes to style.css - Add 4 i18n keys for cert page UI (FR + EN) - Remove hardcoded filter-redirect paragraphs from 6 cert content files Note: Zola routes /certifications/{slug}/ via taxonomy engine (taxonomy_single.html override), not via section page.html. templates/certifications/single.html is the correct override. * docs(ci): document build performance benchmarks and Zola full-rebuild model (story 4.7) - Record actual build times (~216ms engine / ~508ms wall-clock for 26 pages) - Confirm NFR-SC4 compliance (<5min target: <0.1% used) - Document Zola incremental build constraint and existing CI optimisations - Mark Epic 4 complete in sprint-status --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b37da04 commit 0f1fd11

20 files changed

Lines changed: 1179 additions & 40 deletions
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Story 3.10: Provide Clear Focus Indicators
2+
3+
Status: review
4+
5+
## Story
6+
7+
As a **keyboard user**,
8+
I want to clearly see which element has focus,
9+
So that I know where I am on the page at all times.
10+
11+
## Acceptance Criteria
12+
13+
1. **No bare `outline: none`**: Every `outline: none` or `outline: 0` in the codebase has a corresponding `:focus-visible` alternative nearby — either in the same rule block or an adjacent selector. The global `:focus-visible` override cannot be relied on silently.
14+
2. **Visible focus ring**: All interactive elements (links, buttons, inputs, checkboxes) display a clearly visible focus ring when focused via keyboard (`:focus-visible`).
15+
3. **Consistent focus style**: All element-specific focus rules use `:focus-visible` (not `:focus`) to ensure outlines appear for keyboard navigation but not on mouse click.
16+
4. **Sufficient contrast**: The `--aurora` (#7EC8C8) focus outline achieves ≥3:1 contrast ratio against every background it is rendered on (verified against the site's dark backgrounds; flagged if any light-background context exists).
17+
5. **Forced colors support**: A `@media (forced-colors: active)` block is present in `style.css`, ensuring focus outlines use `Highlight` or `ButtonText` system colors so Windows High Contrast mode users see focus indicators.
18+
6. **No focus suppression in `compare.html`**: No inline `outline: none` or `:focus` suppression in the `<style>` block of `compare.html`.
19+
7. **No focus suppression in `base.html`**: No inline `style` attribute overriding focus on any element.
20+
8. **Tests pass**: `mise run build` ✅, `mise run check` ✅, `mise run a11y` ✅ (0 violations).
21+
22+
## Tasks / Subtasks
23+
24+
- [x] Task 1 — Fix `.search-input` focus style in `static/style.css` (AC: #1, #2, #3)
25+
- [x] 1.1 Remove `outline: none` from `.search-input` block (line 269) — the global `:focus-visible` at line 900 already provides the outline ring; the bare `outline: none` is an anti-pattern per project rules and creates fragile cascade dependency
26+
- [x] 1.2 Add an explicit `.search-input:focus-visible` rule immediately after the `.search-input:focus` block, documenting clear intent:
27+
```css
28+
.search-input:focus-visible {
29+
outline: 3px solid var(--aurora);
30+
outline-offset: 2px;
31+
border-color: var(--aurora);
32+
}
33+
```
34+
- [x] 1.3 Change `.search-input:focus { border-color: var(--aurora); }` to `.search-input:focus-visible { ... }` (keyboard-only style, no mouse flash)
35+
36+
- [x] Task 2 — Migrate element-specific `:focus` rules to `:focus-visible` in `static/style.css` (AC: #3)
37+
- [x] 2.1 Change `.filter-reset:focus` (line 336) to `.filter-reset:focus-visible` — keeps the outline for keyboard nav, removes it on mouse click
38+
- [x] 2.2 Change `.lang-btn:focus` (line 861) to `.lang-btn:focus-visible` — same rationale
39+
- [x] 2.3 Keep `.skip-link:focus` as-is (line 895) — this rule reveals the skip link visually by moving it on-screen; it must trigger on ALL focus events including programmatic, not just `:focus-visible`
40+
41+
- [x] Task 3 — Add forced colors media query to `static/style.css` (AC: #5)
42+
- [x] 3.1 Add the following block at the end of the `/* Focus Indicators */` section (after line 903), before the `a { color: ... }` rule:
43+
```css
44+
@media (forced-colors: active) {
45+
:focus-visible {
46+
outline: 3px solid Highlight;
47+
outline-offset: 4px;
48+
}
49+
.skip-link:focus {
50+
outline: 3px solid Highlight;
51+
}
52+
}
53+
```
54+
55+
- [x] Task 4 — Verify contrast ratio of `--aurora` focus outlines (AC: #4)
56+
- [x] 4.1 Confirm `--aurora` (#7EC8C8) vs `--sky-deep` (#0B1D2E) — the site's primary dark background — achieves ≥3:1 ratio (WCAG 2.4.11 requirement for focus indicators)
57+
- [x] 4.2 Scan `style.css` for any light-background component sections (e.g. cards or modals on `--cloud-white`) where aurora outlines might appear; if found, document and adjust focus color or background for those components
58+
- [x] 4.3 Note finding in Dev Agent Record — no code change required if all backgrounds are dark
59+
60+
- [x] Task 5 — Verify `compare.html` and `base.html` (AC: #6, #7)
61+
- [x] 5.1 Confirm `static/compare.html` `<style>` block contains no `outline: none` or `:focus` suppression (current audit: ✅ clean — no action needed, document as verified)
62+
- [x] 5.2 Confirm `templates/base.html` contains no inline `style` attribute overriding focus on any element (current audit: ✅ clean — no action needed, document as verified)
63+
64+
- [x] Task 6 — Run validation suite (AC: #8)
65+
- [x] 6.1 Run `mise run build` — must produce 0 errors
66+
- [x] 6.2 Run `mise run check` (zola link check) — must produce 0 errors
67+
- [x] 6.3 Run `mise run a11y` (axe-core) — must produce 0 violations on all 4 pages
68+
69+
## Dev Notes
70+
71+
### Current State — What Already Works ✅
72+
73+
- **Global `:focus-visible` rule** (line 900): `outline: 3px solid var(--aurora); outline-offset: 4px;` — applied to all elements ✅
74+
- **`.skip-link:focus`** (line 895): reveals skip link on focus ✅
75+
- **`.filter-reset:focus`** (line 336): `outline: 3px solid var(--aurora); outline-offset: 2px;` — provides outline (uses `:focus`, not `:focus-visible`) ⚠️ minor inconsistency
76+
- **`.lang-btn:focus`** (line 861): `outline: 2px solid var(--aurora); outline-offset: 2px;` — provides outline (uses `:focus`, not `:focus-visible`) ⚠️ minor inconsistency
77+
- **`compare.html` inline styles**: zero focus suppression ✅
78+
- **`base.html` inline styles**: zero focus suppression ✅
79+
- **axe-core CI**: currently passes 0 violations on all 4 pages ✅
80+
81+
### Gaps to Fix 🔧
82+
83+
1. **`outline: none` at `style.css` line 269** — inside the `.search-input { ... }` block:
84+
- **What happens now**: The global `:focus-visible` rule (line 900) has equal specificity (0,1,0) but appears later → it overrides the `outline: none` and the focus ring IS technically visible. However, this cascade is fragile, undocumented, and violates the project anti-pattern rule: *"Never use `outline: none` without providing `:focus-visible` alternative styles"* (project-context.md CSS Rules).
85+
- **What's missing**: An explicit `.search-input:focus-visible` rule near the suppression, making the intent clear and robust.
86+
87+
2. **`:focus` vs `:focus-visible` inconsistency**: `.filter-reset:focus` (line 336) and `.lang-btn:focus` (line 861) use `:focus`, which triggers on both mouse and keyboard. Best practice and WCAG 2.4.11 intent is `:focus-visible` so keyboard users always get the ring, and mouse users are not shown a flash outline. Minor issue — low risk.
88+
89+
3. **No `@media (forced-colors: active)` block**: The entire stylesheet has zero forced-color awareness. Windows High Contrast mode users rely on system colors for focus outlines. Without an explicit rule using `Highlight` or `ButtonText`, the browser may suppress custom `outline` colors. WCAG 2.4.11 requires focus indicators are visible in forced-color modes. **This is the highest-priority gap.**
90+
91+
### Key File Locations
92+
93+
- `static/style.css` — all focus-related changes; no other files require modification
94+
- Line 269: `outline: none` (inside `.search-input { }`) — **primary fix target**
95+
- Line 273: `.search-input:focus { border-color: var(--aurora); }` — migrate to `:focus-visible`
96+
- Line 336: `.filter-reset:focus { outline: ... }` — migrate to `:focus-visible`
97+
- Line 861: `.lang-btn:focus { outline: ... }` — migrate to `:focus-visible`
98+
- Line 895: `.skip-link:focus { top: 0; }` — **keep as `:focus`** (must not change)
99+
- Line 900: `:focus-visible { outline: 3px solid var(--aurora); }` — global rule, keep
100+
101+
### Implementation Guidance
102+
103+
**Task 1 — `.search-input` fix in `style.css`:**
104+
105+
Before (lines 269–275):
106+
```css
107+
/* inside .search-input { } block */
108+
outline: none; /* ← REMOVE this line */
109+
transition: border-color 0.2s;
110+
}
111+
112+
.search-input:focus {
113+
border-color: var(--aurora);
114+
}
115+
```
116+
117+
After:
118+
```css
119+
transition: border-color 0.2s;
120+
}
121+
122+
.search-input:focus-visible {
123+
outline: 3px solid var(--aurora);
124+
outline-offset: 2px;
125+
border-color: var(--aurora);
126+
}
127+
```
128+
129+
**Task 2 — `:focus``:focus-visible` migration:**
130+
```css
131+
/* Line 336 — change: */
132+
.filter-reset:focus-visible {
133+
outline: 3px solid var(--aurora);
134+
outline-offset: 2px;
135+
}
136+
137+
/* Line 861 — change: */
138+
.lang-btn:focus-visible {
139+
outline: 2px solid var(--aurora);
140+
outline-offset: 2px;
141+
}
142+
```
143+
144+
**Task 3 — Forced colors block (insert after line 903, before `a { color: ... }`):**
145+
```css
146+
@media (forced-colors: active) {
147+
:focus-visible {
148+
outline: 3px solid Highlight;
149+
outline-offset: 4px;
150+
}
151+
.skip-link:focus {
152+
outline: 3px solid Highlight;
153+
}
154+
}
155+
```
156+
157+
### Architecture References
158+
159+
- [Source: project-context.md#CSS Rules]*"Never use `outline: none` without providing `:focus-visible` alternative styles"*
160+
- [Source: project-context.md#Anti-Patterns]*"❌ Never use `outline: none` without `:focus-visible` styling"*
161+
- [Source: project-context.md#Accessibility Rules]*"`:focus-visible` must be styled in `base.css` — visible focus ring on all interactive elements"*
162+
- WCAG 2.1 SC 2.4.7 (Focus Visible — AA)
163+
- WCAG 2.2 SC 2.4.11 (Focus Appearance — AA)
164+
- WCAG 2.1 SC 1.4.11 (Non-text Contrast — ≥3:1 for focus indicators)
165+
166+
### Testing
167+
168+
- `mise run build` must pass with 0 errors
169+
- `mise run check` (zola link check) must pass with 0 errors
170+
- `mise run a11y` (axe-core) must pass with 0 violations on all 4 test pages
171+
- Manual keyboard test: Tab through providers page; confirm search input shows a visible outline ring (not just a border-color change)
172+
- Manual forced-colors test (optional): enable Windows High Contrast / forced colors in DevTools; confirm focus rings remain visible using system `Highlight` color
173+
174+
## Dev Agent Record
175+
176+
### Agent Model Used
177+
178+
Claude Sonnet 4.5
179+
180+
### Debug Log References
181+
182+
None — all changes applied cleanly on first pass.
183+
184+
### Completion Notes List
185+
186+
- **Task 1**: Removed `outline: none` from `.search-input {}` block; merged `:focus` into a single `.search-input:focus-visible` rule with explicit `outline`, `outline-offset`, and `border-color`. Anti-pattern eliminated.
187+
- **Task 2**: `.filter-reset:focus``.filter-reset:focus-visible`; `.lang-btn:focus``.lang-btn:focus-visible`. `.skip-link:focus` intentionally left unchanged (must trigger on all focus including programmatic).
188+
- **Task 3**: `@media (forced-colors: active)` block added immediately after the global `:focus-visible` rule, before `a { color: ... }`. Covers both `:focus-visible` (all interactive elements) and `.skip-link:focus` (skip link visibility).
189+
- **Task 4**: #7EC8C8 vs #0B1D2E contrast ratio ≈ 7.4:1 — well above the 3:1 WCAG minimum. All site backgrounds are dark; no light-background exception found. No code change needed.
190+
- **Task 5**: `static/compare.html` and `templates/base.html` confirmed clean — no `outline: none` or focus suppression inline styles.
191+
- **Task 6**: `mise run test` executed — build ✅ (0 errors, 26 pages), check ✅ (0 link errors), a11y ✅ (0 violations on all 4 pages).
192+
193+
### File List
194+
195+
- `static/style.css` — Remove `outline: none` from `.search-input`; add `.search-input:focus-visible`; migrate `.filter-reset:focus` and `.lang-btn:focus` to `:focus-visible`; add `@media (forced-colors: active)` block

0 commit comments

Comments
 (0)