diff --git a/TODO.md b/TODO.md
index 9d4435a..09ab492 100644
--- a/TODO.md
+++ b/TODO.md
@@ -919,3 +919,416 @@ Once the above is applied, the following classes become deletable (element name
| `.admin-account-status__row` | `div` inside `
` |
| `.admin-account-status__label` | `dt` |
| `.admin-danger-zone__description` | `p` |
+
+---
+
+## Accessibility audit (2026-03-26)
+
+WCAG 2.1 AA is the baseline. Issues are grouped by criterion number for traceability.
+Current state: **zero ARIA attributes, zero skip links, zero focus-visible styles, zero
+`prefers-reduced-motion` guards** anywhere in the live codebase.
+
+---
+
+### 1 — Perceivable
+
+#### 1.1.1 Non-text content (alt text)
+
+- [ ] **Home card images use the thesis title as `alt`** — `alt="= $item['title'] ?>"` is a
+ reasonable fallback, but the title alone provides no context about what the image depicts.
+ Prefer `"Couverture — [titre] par [auteurs]"` for cover images, or `""` (empty) for purely
+ decorative banners where the caption below already carries all the text information.
+ For gradient placeholder cards there is no ` ` at all — correct, no alt needed on a
+ CSS gradient div.
+
+- [ ] **TFE page file images use the raw filename as `alt`** — `alt="= $file['file_name'] ?>"`.
+ A filename like `a3f8bc12.jpg` is meaningless to a screen reader user. Use the thesis title
+ or a stored description field. If the `description` column in `thesis_files` is populated,
+ that should be the alt text; fall back to the thesis title.
+
+- [ ] **Search bar SVG icon has no `aria-hidden`** — the magnifying glass SVG in
+ `search-bar.php` is purely decorative (the ` ` carries all meaning). Add
+ `aria-hidden="true"` and `focusable="false"` to the SVG.
+
+- [ ] **Admin `` logo is a text link — fine. But "✕ Réinitialiser" and "✕" remove buttons**
+ use a bare Unicode `✕` as their visible label with no accessible name alternative.
+ For the "✕" jury-remove buttons in `add.php`/`edit.php`, add `aria-label="Supprimer ce membre du jury"`.
+ For "✕ Réinitialiser" in `index.php`, the text is adequate; the `✕` symbol is decorative
+ there and should be wrapped in `✕ `.
+
+#### 1.3.1 Info and relationships
+
+- [ ] **The metadata list on `tfe.php` is a `/
` soup** — a screen reader traversing
+ the page hears "Orientation : Arts Numériques" as a flat run of text with no
+ structure. There is no programmatic association between label and value. Replacing with
+ `// ` (already flagged in the semantic audit) directly fixes this criterion.
+
+- [ ] **Search filter `` elements have no associated ``** — each select is
+ preceded by `Année ` but this span is not a
+ `` and has no `for` attribute. Screen readers cannot associate it with the control.
+ Fix: replace `` with `` and add `id="filter-year"` to
+ the select (or use the wrapping-label pattern).
+
+- [ ] **Admin form rows: `` is correct** — the `for` attribute
+ is present on all single-input rows in `add.php` and `edit.php`. Good. However, the
+ multi-input rows (languages, formats) use `` *without* a `for`
+ because they label a group of checkboxes. These should use `/` instead
+ so the group label is programmatically associated with all its checkboxes.
+
+- [ ] **Status badges in `admin/index.php` convey state by colour alone** — "Publié" (green) /
+ "En attente" (yellow) / "Libre" (green) / "Interne" (blue) / "Interdit" (red) all rely
+ entirely on colour to distinguish states. This fails **1.4.1 Use of Colour**. Add a
+ visible non-colour distinction (e.g. a prefix icon character with `aria-hidden="true"`)
+ and `aria-label="Statut : Publié"` on the badge ``.
+
+- [ ] **`` links give no warning** — `tfe.php` and `apropos.php` open external
+ links in a new tab with no indication. Screen reader users and keyboard users are
+ disoriented when their context silently shifts. Add a visually-hidden `(ouvre dans un nouvel onglet) ` after the link text, or append the information to the
+ `aria-label`.
+
+#### 1.3.4 / 1.3.5 Orientation & Input purpose
+
+- [ ] **No `autocomplete` attributes on personal data fields** — `add.php`/`edit.php` fields
+ like `auteurice` (person name), `mail` (contact) lack `autocomplete` hints. Add
+ `autocomplete="name"`, `autocomplete="email"` where applicable so password managers and
+ autofill can assist (WCAG 1.3.5).
+
+#### 1.4.1 Use of colour (see also 1.3.1 above)
+
+- [ ] **Admin status badges distinguish states by colour only** — covered above.
+
+- [ ] **Active nav link has no non-colour indicator** — `.site-nav__link--active` is applied in
+ PHP but has no CSS rule at all (flagged in the semantic/CSS audit). Even if a rule existed,
+ if it only changes colour it would still fail this criterion for users with colour blindness.
+ The active indicator must include a non-colour signal: underline, border, weight change,
+ or `aria-current="page"` (which is announced by screen readers regardless of visual styling).
+
+#### 1.4.3 Contrast (minimum) — confirmed failures from measurement
+
+- [ ] **Nav links at `opacity: 0.92` on purple background: 4.05:1** — fails AA (4.5:1 required
+ for normal text). At full opacity the white-on-purple ratio is 4.87:1 (just passes), but
+ the `opacity: 0.92` applied to `.site-nav__link` drops it to 4.05:1. Fix: remove the
+ opacity reduction, or increase purple darkness slightly.
+
+- [ ] **`filter-info` purple text `#9557b5` on purple-light background `rgba(149,87,181,0.12)`
+ over white: 4.08:1** — fails AA. The filter info banner ("Année : 2024" or "Découvrez les
+ TFE de 2024") uses purple text on a light purple tint. Use `var(--purple-dark)` (#7b3fa0)
+ instead for the text, which would reach ~5.7:1.
+
+- [ ] **Placeholder text `#aaa` on white: 2.32:1** — fails AA (and fails for large text too).
+ Placeholder text is explicitly included in WCAG 1.4.3. Change to `#767676` minimum
+ (~4.54:1) or preferably `#6b6b6b`.
+
+- [ ] **Gradient card placeholder: white text on L=65% HSL backgrounds — most hues fail** —
+ measured across the full hue range at `hsl(H, 60%, 65%)` (the lighter end of the gradient):
+ only hues 240–250° (blue-indigo) pass AA. Every warm hue (0–230°) and most cool hues fail,
+ with ratios as low as 1.46:1 at yellow (hue=60°). Since hue is derived from `$item['id'] % 360`,
+ any thesis ID will produce a random hue. Fix: either darken the gradient to `L=45%` on the
+ lighter end (would raise almost all hues above 3:1 for large text), or drop the text overlay
+ inside the gradient entirely (the card caption below already shows title/author).
+
+- [ ] **`admin-text-muted` `#888` on `admin-bg-alt` `#242424`: 4.38:1** — fails AA for normal
+ text. This combination appears in table cell muted text, form hints, and sub-labels across
+ the admin. Darken to `#909090` (~4.5:1) or use `#959595`.
+
+- [ ] **Admin purple `#9557b5` used as large-heading colour on dark `#1a1a1a`: 3.57:1** — passes
+ AA for large text (≥18pt/24px bold ≥14pt) but fails for normal text. Audit every place
+ `var(--admin-purple)` appears as text colour on the dark background; ensure it is only
+ used at sizes where 3:1 suffices (large text), not on body copy or small labels.
+
+#### 1.4.4 Resize text
+
+- [ ] **`body` has no `font-size` baseline set** — relies on browser default (16px). Most
+ `.card__info`, `.search-filter-label` etc. use `rem` values which scale correctly. However,
+ a few admin elements use absolute `px` sizes (`font-size: 0.78rem` is fine as it's rem-relative,
+ but `width: 14px; height: 14px` on checkboxes does not scale). Minor — ensure no text
+ is set in `px`.
+
+#### 1.4.10 Reflow (320px viewport)
+
+- [ ] **Répertoire index 4-column grid has no mobile breakpoint** — `search.css` defines the
+ 4-column grid at `1fr 2fr 2fr 1.5fr` with no `@media` fallback. At 320px the columns
+ become ~50px wide each — unusable. Add a breakpoint to stack columns vertically below
+ ~600px (or 768px).
+
+- [ ] **TFE page two-column grid collapses at 900px** — responsive breakpoints exist for `tfe.css`
+ at 900px and 600px. Good. Verify the PDF `` at 700px height also reflows — currently
+ `height: 700px` is fixed and causes horizontal overflow on small screens. Change to
+ `height: clamp(300px, 80vh, 700px)`.
+
+#### 1.4.11 Non-text contrast
+
+- [ ] **Search filter `` border is `#ddd` on white — 1.6:1** — the `border: 1px solid var(--border-color)` where `--border-color: #ddd` gives a 1.6:1 contrast ratio for the UI component boundary. WCAG 1.4.11 requires 3:1. Change to `#949494` minimum or use `#767676`.
+
+- [ ] **Admin form inputs: `border-bottom: 1px solid #333` on `#1a1a1a` background — 1.8:1** —
+ the bottom-border-only inputs have `border-bottom: 1px solid var(--admin-border)` (#333) on
+ `#1a1a1a` background: 1.8:1. Fails 1.4.11. Raise to `#555` minimum (~3.1:1).
+
+#### 1.4.12 Text spacing
+
+- [ ] **No text-spacing override test done** — verify that when users apply the WCAG 1.4.12
+ bookmarklet (line-height 1.5×, letter-spacing 0.12em, word-spacing 0.16em, paragraph spacing
+ 2em) the card grid and TFE metadata list do not overflow their containers. The `overflow: hidden`
+ on `.card__media` and the tight `aspect-ratio: 4/3` on card images can cause content clipping.
+
+---
+
+### 2 — Operable
+
+#### 2.1.1 Keyboard
+
+- [ ] **Disabled pagination links are keyboard-reachable** — `