3.3.2 Labels or instructions
- Replace bare <label>Lecteur·ices :</label> (no 'for', no associated control)
with <fieldset class="admin-jury-lecteurs"><legend>Lecteur·ices</legend>
giving AT a proper programmatic label for the entire lecteur group
4.1.2 Name, role, value — Externe checkboxes lacked group context
- Add aria-label="Promoteur·ice — externe" on the promoteur Externe checkbox
- Add aria-label="Lecteur·ice N — nom" on every lecteur name input
- Add aria-label="Lecteur·ice N — externe" on every lecteur Externe checkbox
- All three attributes added to both PHP-rendered rows and the addJuryRow() JS
that builds new rows dynamically
2.1.1 Keyboard — remove buttons already had aria-label; verified and updated
label text to "Supprimer le lecteur·ice N" (consistent with new numbering)
CSS (admin.css)
- Add .admin-body fieldset fieldset.admin-jury-lecteurs rule: removes
border/padding/background from the nested fieldset so it reads as a
sub-group inside the outer jury fieldset, not a double-bordered card
Audit (no code change)
- WCAG 1.4.4: all font-size values use rem — no px text sizing anywhere
- WCAG 1.4.12: only overflow:hidden on media containers and .sr-only utility;
no essential text content is clipped by text-spacing overrides
- WCAG 4.1.2 bulk JS: result is a redirect to flash-messages.php which already
emits role="alert"/role="status" — no additional JS announcement needed
Add <span class="sr-only">, YEAR</span> to each thesis card <p> in
public/index.php. Screen readers now read "Author – Title, 2024" instead
of bare "Author – Title", so two theses sharing the same title produce
distinct accessible names (WCAG 2.4.4 Link Purpose — In Context).
Also audit and close WCAG 2.4.3: the tfe.php back link (<a class="tfe-back-link">
← Retour</a>) is already the first child of <header class="tfe-left">
in DOM order, preceding <h1 class="tfe-title">. No code change needed;
TODO item marked done.
- Updated 6 admin templates: add.php, edit.php, login.php, account.php,
import.php, pages-edit.php — replaced <div class="admin-submit-wrap">
with <div class="admin-form-footer">
- Updated 8 CSS selectors in admin.css:
- .admin-form-footer { margin-top/padding-top } (was .admin-submit-wrap)
- .admin-form > div:not(.admin-form-footer) grid exclusion guard (×3)
- .admin-login-box .admin-form > div:not(.admin-form-footer) overrides (×2)
- .admin-login-box .admin-form-footer compact spacing override
- No visual change; purely a semantic rename to a descriptive class name
- Also marked status-badge.php partial and WCAG 1.3.1 badge tasks as
already-done in todo/02-php-components.md and todo/04-accessibility.md
(partial + CSS were fully implemented but todo had not been updated)
WCAG 1.4.1 — Active nav link had no non-colour indicator in the admin panel.
Public nav already had border-bottom via common.css; admin nav had nothing.
admin.css:
- Add `[aria-current="page"]` rule on admin nav links:
border-bottom: 2px solid currentColor; padding-bottom: 1px
This gives a visible underline as a non-colour signal for the active page.
- Fix `--admin-purple` undefined CSS variable in pagination button hover.
The variable was referenced but never defined in variables.css (which was
refactored to use --accent-primary / --accent-secondary). Replaced both
border-color and color usages with var(--accent-primary) (#9557b5 — same
value), restoring the intended purple hover tint on pagination buttons.
todo/01-css-semantic-refactor.md:
- Audited ~15 pending CSS/HTML tasks; all were already implemented.
Marked as done: .admin-main, .admin-page-title, .admin-form-row,
.admin-label, .admin-input/select/textarea, .admin-table, .admin-fieldset,
tfe.css class replacements, search.css h2 selector, admin-alert replacement,
login.php/edit.php inline style removal, form partial hints (<small>).
todo/04-accessibility.md:
- Marked WCAG 1.4.1 admin nav and --admin-purple audit items as completed.
- Add Database::getThesesListCount(array $filters) — runs the same WHERE
clauses as getThesesList() but with COUNT(DISTINCT t.id); used to compute
total pages without loading all rows.
- Extend Database::getThesesList() with $limit/$offset parameters; when
$limit > 0 appends LIMIT/OFFSET and re-binds positional params individually
to avoid the PDO mixed-style restriction.
- Fix getThesesList() SELECT: add LEFT JOIN access_types + at.name as
access_type — the column was referenced in the template but never fetched.
- Wire admin/index.php: read ?page=, compute $totalPages/$offset, pass
$perPage=25 + $offset to getThesesList(); include pagination.php partial
below the table with filter-preserving $baseParams.
- Add result-count line (<p class="admin-list-meta">) showing "X–Y sur Z TFE"
when multiple pages exist.
- Add .admin-body .pagination-wrap / .pagination-btn / .pagination-info styles
to admin.css (scoped to .admin-body to avoid colliding with public pages).
Add a TTL-based cache for the expensive checks on the admin system page,
eliminating repeated systemctl subprocess calls (~4×~100ms), curl self-pings
(~200-500ms), disk_*_space() and PHP ini reads on every page load.
Changes:
- storage/migrations/007_system_cache.sql: new migration creating the
system_cache table (key TEXT PK, value TEXT, updated_at INTEGER)
- storage/schema.sql: system_cache table added before pages table
- Applied migration to live storage/posterg.db
- src/SystemCache.php: new class with get/set/isStale/ageSeconds/invalidate;
uses SQLite INSERT … ON CONFLICT upsert; no external dependencies
- src/Database.php: added getDatabasePath(): string accessor
- public/admin/system.php:
- Bootstrap SystemCache at request start using the existing DB PDO handle
- system_status: cached with 2-min TTL (systemctl + curl checks)
- php_info: cached with 1-hour TTL (PHP ini values are runtime-constant)
- disk_info: cached with 5-min TTL (total/free/used/pct tuple)
- Logs section: unchanged — always reads live log tail per active tab
- ?refresh=1 GET param invalidates all three cache keys before rendering
- Status panel heading shows cache badge: '⚡ Cache — il y a Xs' (hit)
or '⟳ Actualisé' (miss/fresh), styled via new .sys-cache-badge rules
- public/assets/css/system.css: .sys-cache-badge / --hit / --miss styles
All admin action files (account, tag, page, edit, visibility, maintenance,
publish, formulaire) now call App::flash('error'|'success', ...) instead of
writing to raw per-page session keys ($_SESSION['error'], 'admin_error',
'edit_error', 'admin_success', 'edit_success', 'form_error').
All admin display pages (add, edit, account, tags, pages, index) now include
templates/partials/flash-messages.php instead of manually reading and
unsetting the legacy session keys and inlining their own alert HTML.
App::consumeFlash() already drained all legacy key variants as a safety net,
so the partial works correctly whether called from pages that were already
migrated or any remaining stragglers. No behaviour change for end users.
Add templates/partials/status-badge.php — a single reusable partial that
renders the <span class="status-badge …"> element for three badge types:
'publish' — Publié / En attente derived from a boolean is_published value
'access' — access-type label (Libre / Interne / Interdit) with slug-based
CSS modifier class and appropriate symbol (○ ◑ ●)
'ok' — generic green/yellow boolean badge with caller-supplied labels
(used for 'Active'/'Non configurée' and 'Présent'/'Absent' in
account.php)
All three variants emit aria-label with a context prefix and wrap the
decorative symbol in aria-hidden="true" — behaviour identical to the
inline code they replace.
Callers set $badgeType + $badgeValue (+ optional $badgeOkLabel /
$badgeWarnLabel / $badgeContext) before the include; the partial unsets
all working variables after rendering so they do not bleed into the
including scope.
Files changed:
templates/partials/status-badge.php — new partial
public/admin/index.php — table status column now uses partial
(removes 15 lines of inline if/else/php)
public/admin/account.php — two credential status rows now use partial
(removes 8 lines of inline if/else)
Four reusable PHP partials extracted to templates/partials/form/:
- text-field.php — single-line input (text/number/url); wraps input+hint in div,
skips the inner wrapper when no hint is present. Supports $type,
$placeholder, $required, $attrs, $hint, $id overrides.
- select-field.php — <select> with leading empty option; matches $selected against
option id OR option name string (handles view-sourced data where
orientation/ap/finality come back as name strings, not FK ids).
- checkbox-list.php — checkbox group (languages, formats); renders .admin-checkbox-list
with typed-string comparison so int ids from DB match string values.
- file-field.php — file input with accept/multiple/hint; appends [] to name when
$multiple is true.
Both add.php and edit.php rewritten to use the partials:
- ~15 repeated text-field divs collapsed to single-line include calls
- ~6 repeated select divs collapsed to single-line include calls
- 4 checkbox-list blocks collapsed to 2 calls each
- 3 file input blocks collapsed to single-line include calls
- Textarea fields (synopsis, context_note) kept inline — no partial for <textarea>
- Banner preview block in edit.php kept inline — conditional UI not generalised
Line count: add.php 251→93 (-158), edit.php 289→171 (-118)
Remove five presentational classes from admin forms and replace with
structural CSS selectors scoped to .admin-form:
- .admin-form-row → .admin-form > div:not(.admin-submit-wrap)
Grid layout (260px label col + 1fr input col) applied directly to div
children of the form; submit-wrap div excluded via :not().
- .admin-label → .admin-form > div:not(.admin-submit-wrap) > label
Scoped to the direct label child of each form row div; does not bleed
into nested checkbox labels inside .admin-checkbox-list.
- .admin-input / .admin-select / .admin-textarea
→ .admin-form input:not([type=checkbox|radio|file|hidden|submit])
→ .admin-form select
→ .admin-form textarea
Also extended to .admin-inline-form input/select (tags page) so the
tags table inputs retain identical base styling and focus colour.
Templates updated: add.php, edit.php, login.php, account.php,
pages-edit.php, import.php, tags.php,
templates/partials/form/jury-fieldset.php — all class= attributes for
the five removed classes stripped.
import.php: added 'admin-form' class alongside 'admin-import-area' so
its single file-input row gets the grid row treatment; submit div was
already using admin-submit-wrap so it is correctly excluded.
No visual change — selectors target the same elements as before.
- admin.css: replace .admin-alert / .admin-alert--error / .admin-alert--success
selectors with [role="alert"][data-type="error"] and [role="status"][data-type="success"]
- All 10 admin templates updated: <div class="admin-alert admin-alert--{type}">
becomes <p role="alert|status" data-type="error|success"> (or <div> for the
import.php multi-item list that contains a <ul>)
- flash-messages.php partial updated to match
- WCAG benefit: role="alert" is an ARIA live region — errors are announced
immediately by screen readers without focus movement (fixes WCAG 3.3.1, 4.1.2)
- role="status" (polite live region) used for success messages — announced
without interrupting the user
- Removes two BEM modifier classes; CSS now targets element semantics directly
The jury composition fieldset (président·e, promoteur·ice + external checkbox, dynamic
lecteur·ices list with JS add/remove) was copy-pasted verbatim between the two longest
admin forms.
- Created templates/partials/form/jury-fieldset.php
- Consumes $juryPresident, $juryPromoteur, $juryPromoteurExt, $juryLecteurs[]
- Handles both add-mode (falls back to old()/wasSelected() flash helpers) and
edit-mode (pre-populates from DB-loaded variables)
- $juryIdx initialised from max(count($juryLecteurs), 1) — correct for both modes
- add.php: 311 → 251 lines (-60); entire fieldset + <script> replaced with one require
- edit.php: 359 → 289 lines (-70); PHP variable extraction kept inline before require
The pagination nav was duplicated between public/index.php and public/search.php
with structural differences: index.php used string concatenation for query params
and had first/last-page buttons (« »); search.php used http_build_query but had
only prev/next (‹ ›) and a flat <span> rather than a <ul>/<li> structure.
- Add templates/partials/pagination.php: accepts $page, $totalPages, $baseParams[]
(any array of query params to preserve); builds URLs with http_build_query;
renders a semantic <nav>/<ul>/<li> block with first/prev/info/next/last buttons,
correct aria-disabled + tabindex on disabled links, and aria-label on each button.
Returns immediately (no output) when $totalPages <= 1.
- Replace inline pagination block in index.php with:
$baseParams = array_filter(['year' => $year]);
include pagination.php
- Replace inline pagination block in search.php with:
$baseParams = array_diff_key($_GET, ['page' => '']);
include pagination.php
This also upgrades search.php to the full first/last button set it was missing.
Both callers verified with php -l. No functional change to existing behaviour.
Replace four presentational class names in admin.css with structural selectors
that target native HTML elements already present in every admin template:
.admin-main → .admin-body main
.admin-page-title → .admin-body main > h1
.admin-table → .admin-body table
.admin-fieldset → .admin-body fieldset
.admin-fieldset-legend → .admin-body legend
Also migrate the .admin-main > section / h2 / dl / dt / dd block to
.admin-body main > section so the thanks-page section styles survive.
Add .admin-body main > table { margin-top: 1.5rem } to absorb the inline
style="margin-top:1.5rem" that was on tags.php's <table class="admin-table">.
All 10 affected admin templates updated (add, edit, account, index, import,
pages, pages-edit, tags, system, thanks) — class attributes removed where
the element alone is now the selector. Zero visual changes.
- admin.css: remove .admin-hint and .admin-field-hint class rules; add
.admin-body form small with the same font-size/color/margin properties
plus display:block so it stacks below sibling inputs; stub comment left
where .admin-field-hint was to document the change
- add.php: 5× <p class="admin-hint"> → <small>
- edit.php: 3× <p class="admin-hint"> → <small>
- import.php: <div class="admin-hint"> → <small> (block hint below CSV input)
- pages-edit.php: class="admin-hint" removed from already-correct <small>
- account.php: <p class="admin-field-hint"> → <small>
Hint text is now styled purely via the semantic element selector; no class
required on any hint element in admin templates.
- Create public/assets/css/system.css with all 280 lines of CSS that were
inline in system.php: tab bar, status cards, PHP info grid, disk bar,
log viewer, nginx config viewer, and syntax-highlight classes.
- Disk bar dynamic values (width %, colour) moved from PHP-interpolated CSS
rules to CSS custom properties (--disk-pct, --disk-color) set on the
element via an inline style attribute; static .disk-bar rule in system.css
consumes them via var().
- system.php JS block (tab-select auto-nav + copy-to-clipboard) moved to
$extraJsInline heredoc; footer.php emits it before </body> — keeps it
out of the document <head> and removes the bare <script> after </main>.
- system.php now sets $extraCss = ['/assets/css/system.css'] so head.php
emits a proper <link> in <head>, consistent with all other admin pages.
- No behaviour change; system.php is now zero inline CSS/JS.
Replace 6 CSS class selectors across tfe.css, main.css, and search.css with
semantic element-based selectors, removing the corresponding classes from the
HTML templates entirely.
tfe.css:
- .tfe-meta-list → article dl / article dl > div / article dl dt / article dl dd
- .tfe-media-block → aside figure (+ img, video, embed children)
- .tfe-file-caption → aside figcaption
main.css:
- .card__media → .home-body figure (+ img/video children and hover/motion rules)
- .card__caption → .home-body li > a > p
search.css:
- .repertoire-col > h2 → .repertoire-index section > h2
Template changes:
- tfe.php: removed class= from <dl>, <figure>, and <figcaption>
- index.php: removed class= from <figure> and <p class=card__caption>;
stripped orphaned card__media from the gradient <div> (only --gradient needed)
No visual change — selectors match the same elements as before since the
semantic HTML was already in place from prior refactoring work.
- login.php: removed style= on .admin-form-row and .admin-label (already covered
by .admin-login-box scoped rules); extracted submit-wrap spacing and full-width
button to .admin-login-box .admin-submit-wrap and .admin-login-box .admin-btn
- account.php: style="margin-top:3rem" on danger-zone heading moved to
.admin-section-title--danger modifier; <span style="color:..."> replaced with
<small> element styled via .admin-danger-zone__description small
- add.php / edit.php / pages-edit.php: all style="align-items:start" removed from
.admin-form-row (redundant — already the CSS default at line 116 of admin.css);
banner preview inline styles extracted to .admin-banner-preview / .admin-banner-preview img;
add-jury button margin extracted to .admin-add-jury-btn; cancel links use .admin-cancel-link
Zero inline style= attributes remain in login, account, add, edit, pages-edit.
- admin/index.php: replace <div class="admin-stats"> with <dl>; inner
<div class="admin-stat__number"> → <dd>, <div class="admin-stat__label"> → <dt>;
use CSS order to keep number visually first; add scope="col" to all 9 <th> cells
- admin/thanks.php: replace all four <div class="admin-thesis-info"> wrappers
with <section> elements; remove the class entirely; add scope="col" to
the files table <th> cells
- admin/tags.php: add scope="col" to all 3 <th> cells
- admin/pages.php: add scope="col" to all 4 <th> cells
- admin.css: rename .admin-thesis-info selectors to .admin-main > section
(element + context selector — no class needed); add display:flex +
flex-direction:column to .admin-stat so CSS order property works correctly
Addresses TODO items: section X (admin-stats dl, th scope), XI (tags th scope),
XII (admin-thesis-info → section), XIII (pages.php th scope)
The default cache directory for the file-based rate limiter was
src/cache/rate_limit/, placing transient JSON files inside the source tree.
This meant:
- The directory was deployed via rsync on every deploy (wasted I/O)
- .gitignore had to track a src/-internal path
- Developers running tests could leave stale cache state in the source tree
Changes:
- src/RateLimit.php: default $cacheDir changed from __DIR__.'/cache/rate_limit'
to dirname(__DIR__).'/storage/cache/rate_limit'; dirname(__DIR__) resolves to
the project root regardless of how the file is loaded (with or without bootstrap)
- .gitignore: replaced 'src/cache/rate_limit/' with 'storage/cache/' (broader,
covers any future cache subdirs under storage/)
- storage/cache/.gitkeep: added so the directory is tracked in VCS and created
on fresh clones/deploys, but its contents are ignored
- justfile: added '--exclude storage/cache/*' to the deploy rsync recipe so
rate-limit state is never transferred to the server
- src/cache/: removed (no longer needed)
All RateLimit unit tests pass.
CSS: .site-search → header form[role="search"],
.site-search__icon → header form[role="search"] svg,
.site-search__input → header form[role="search"] input,
.site-search__input::placeholder → header form[role="search"] input::placeholder
HTML: Removed class="site-search", class="site-search__icon", and
class="site-search__input" from header.php and search-bar.php.
The form already uses role="search" and contains a single svg + input,
so the semantic selectors are unambiguous.
Create the central App helper that eliminates ~170 lines of duplicated
bootstrap/auth/CSRF preamble across 24 page and action handler files.
src/App.php provides:
- boot(): loads Database + ensures CSRF token (public pages)
- adminGuard(): requires AdminAuth login + boot (admin pages)
- verifyCsrf() / rotateCsrf(): centralised CSRF lifecycle
- flash() / consumeFlash(): unified flash messages with legacy key drain
(error, success, admin_error, admin_success, edit_error, edit_success,
form_error all consumed transparently for incremental migration)
- redirect(): flash + Location header + exit in one call
- render(): head → header → content → footer pipeline with auto admin
footer selection
App.php is auto-loaded from config/bootstrap.php so all existing pages
get the class for free without any changes.
templates/partials/flash-messages.php uses App::consumeFlash() to replace
the 5+ copy-pasted flash blocks across admin templates.
All existing tests pass. No existing page files modified — this is a
non-breaking addition that enables incremental controller extraction.
- config.php: getDatabasePath() detects php built-in CLI server
(php_sapi_name() === 'cli-server') and routes to test.db; all
other SAPIs (nginx/fpm) get posterg.db. DB_ENV env-var still
overrides either way.
- migrate.sh: auto-initialise the target DB from storage/schema.sql
when the file is absent or has no tables yet. Existing DBs with
data are left completely untouched (table_count check, no re-run
of schema on populated DB). Idempotent: safe to run repeatedly.
- justfile: serve still calls migrate (which now handles init too),
no DB_ENV prefix needed since sapi detection handles routing.
Imported theses were invisible on the public site because:
1. is_published defaulted to 0 (schema default) — the INSERT never
set it, so all imported rows stayed unpublished and were filtered
out by v_theses_public (WHERE is_published = 1) and every public
DB method.
2. The access column (CSV col 16 'Autorisation') was read into $access
but never written to access_type_id — silently dropped.
Fix: INSERT now includes is_published = 1 and access_type_id (resolved
from access_types.name via ucfirst/strtolower normalisation, defaulting
to 1/Libre when the CSV cell is empty or unrecognised).
admin/index.php — status badges (WCAG 1.4.1 Use of Colour):
- Published badge: prefix ● symbol (aria-hidden) + aria-label="Statut : Publié"
- Pending badge: prefix ◌ symbol (aria-hidden) + aria-label="Statut : En attente"
- Access badges (Libre/Interne/Interdit): prefix ○/◑/● symbol per type (aria-hidden)
+ aria-label="Accès : [type]"; symbol chosen from a PHP map keyed on the slug
Each badge now communicates its state through shape AND colour, not colour alone.
admin/index.php — ✕ Réinitialiser link (WCAG 2.5.3 / 1.1.1):
- ✕ wrapped in <span aria-hidden="true"> so the decorative symbol is skipped by
screen readers; accessible name remains "Réinitialiser"
admin/add.php + admin/edit.php — jury remove buttons (WCAG 2.5.3):
- All four ✕ remove buttons (2 static template rows + 2 JS-generated innerHTML strings)
given aria-label="Supprimer ce lecteur"; the bare ✕ Unicode character has no
speech equivalent so the aria-label replaces rather than supplements the label
- pages-edit.php: EasyMDE CDN JS URL moved to $extraJs (rendered by footer.php before </body>);
inline EasyMDE init block moved to $extraJsInline, emitted by footer.php via new
`<?php if (!empty($extraJsInline))` guard - fixes invalid <script> floating in <body> (WCAG 4.1.1)
- pages-edit.php: add <small> keyboard-trap hint below the editor textarea:
'Appuyez sur Échap pour quitter l'éditeur au clavier.' (WCAG 2.1.2)
- templates/admin/footer.php: extend to support $extraJsInline (raw inline script string)
- index.php: add <h1 class="sr-only">Mémoires de l'ERG</h1> inside <main> so the page has
a document heading (WCAG 2.4.6; h2 columns in search.php already had a sr-only h1)
- TODO.md: mark completed items as [x]: skip links (2.4.1), focus-visible / outline:none
removal (2.4.7), search.php h1 + index.php h1 (2.4.6), pages-edit.php invalid HTML (4.1.1),
EasyMDE keyboard trap hint (2.1.2)
- templates/admin/head.php: all 7 nav links (+ conditional Modifier + Déconnexion)
wrapped in <ul class="admin-nav__list">/<li>; .active class removed, replaced
with aria-current="page" on each <a> based on $currentPage match
- Déconnexion link: removed inline style="margin-left:auto;opacity:.6;"; moved to
new .admin-nav__logout <li> class in admin.css
- public/assets/admin.css: replaced .admin-nav__link rules with .admin-nav__list a
selectors; added .admin-nav__list (flex list, gap 2.5rem, flex:1); added
.admin-nav__list a[aria-current="page"] rule (border-bottom underline indicator);
added .admin-nav__logout / .admin-nav__logout a for the push-right logout item
- Removes .admin-nav__link class entirely from the codebase (was only used in
templates/admin/head.php and admin.css)
Fixes WCAG 2.4.6 (nav landmark content model), 1.4.1 (colour-only active indicator),
and section VIII of the semantic HTML admin audit.
Replace presentational divs in index.php and main.css with elements that
carry correct semantic meaning, fixing multiple WCAG 2.1 AA issues:
index.php:
- <div class="cards-container"> → <ul class="cards-container"> (list of navigable items)
- <a class="card-link"><div class="card">…</div></a> → <li class="card"><a> (block link
is the <a>, <li> is the container; removes the redundant .card div wrapper)
- <div class="card__media"> → <figure class="card__media"> when wrapping an <img>;
gradient placeholder stays as <div> (presentational, aria-hidden)
- Improved alt text: "Couverture — [title] par [authors]" instead of bare title
- Removed <div class="card__info"> wrapper; caption is now a bare <p class="card__caption">
directly inside the <a>
- <div class="filter-info"> → <p class="filter-info" role="status"> (live-region
semantics; announces filter state to screen readers)
- ✕ symbol in clear-filter link wrapped in <span aria-hidden="true">
- Gradient placeholder div gets aria-hidden="true" (decorative; caption below carries text)
- Empty-state <p style="…"> → <li class="cards-empty"> (removes inline style)
- <div class="pagination-wrap"> → <nav class="pagination-wrap" aria-label="Pagination">
with <ul>/<li> children; page-info <span> → <li aria-current="page">
main.css:
- .cards-container: add list-style:none; margin:0; padding:0 (reset <ul> defaults)
- Remove .card-link rule; replace with .card > a (block flex link, no separate class)
- .card__media: add margin:0 to reset <figure> default margin
- Remove .card__info rules; rename .authors to .card__caption with same styles
- Add .cards-empty rule (removes last inline style from index.php)
- .pagination-wrap: restructured for <nav>/<ul>; inner <ul> carries the flex layout
- prefers-reduced-motion: add .card__media--gradient guard
WCAG criteria addressed: 1.1.1 (alt text), 1.3.1 (info & relationships via semantic
list/figure), 2.4.1 (filter-info now live region), role="status" on filter banner.
- Replace 4x <div class="repertoire-col"> with <section>; remove
.repertoire-col__header class, CSS now targets section > h2
- Wrap all index link groups in <ul>/<li>; delete the four per-column
link classes (year-index-item, cat-index-item, student-index-item,
keyword-index-item); active state switches from .active to
aria-current="page" on the <a>
- Add <h1 class="sr-only">Répertoire</h1> so the index view has a
page-level heading (WCAG 2.4.6)
- Remove redundant <div class="search-results-view"> wrapper; padding
moved to .results-grid and .search-results-header directly
- Replace <div class="results-grid"> with <ul class="results-grid">;
each result card becomes <li><a class="result-card">
- Replace <span class="result-card__meta"> with <small> (ancillary
metadata per HTML spec)
- Replace result-count <p> with <output role="status"> (computed value)
- Replace 3x <div class="search-filter-group"><label>…</label><select>
with <label> directly wrapping <select> (implicit association,
removes .search-filter-group divs); CSS updated to display:flex on
the label itself
- Pagination wrapper changed to <nav aria-label="Pagination">;
page-info span gets aria-current="page"
- search.css: delete .search-results-view, four index-item classes,
.cat-index-group, .search-filter-group; consolidate years/other
column link styles under .repertoire-col:first-child ul a and
.repertoire-col:not(:first-child) ul a selectors; add ul reset rule
templates/nav.php:
- Replace <div class="site-nav__links"> with <ul role="list"> + <li> children
- Move À Propos link inside the list (was a loose sibling <a>)
- Remove .site-nav__link and .site-nav__link--active classes from all <a> elements
- Active state now driven solely by aria-current="page" (already present)
public/assets/common.css:
- Remove .site-nav__links, .site-nav__link, .site-nav__link:hover, .site-nav__link--active rules
- Add .site-nav ul (flex, gap, list-style reset), .site-nav ul a, .site-nav ul a:hover
- Active indicator: .site-nav ul a[aria-current="page"] — self-documenting, screen-reader-announced
Fixes TODO section I (nav semantic HTML audit). All three BEM nav-link classes deleted;
zero references remain in the codebase.
- Replace three <span class='search-filter-label'> with proper <label for='...'> elements in
search.php filter bar; add id attributes to the corresponding <select> elements so the
label/control association is programmatic (WCAG 1.3.1, 3.3.2).
- Rewrite the rate-limit 429 early-exit in search.php from a bare one-liner echo to a full
HTML document with lang='fr', viewport meta, and inline dark styles matching maintenance.php;
inject the retry countdown into the user-facing message (Template audit F).
- Fix PHP 8.x __wakeup() deprecation in Database.php singleton guard: replace the throw
statement with trigger_error(..., E_USER_ERROR) and add an explicit void return type
(Refactor audit C).