From 9cb9b51b41284ee9d29cf4156104ae98ddc57d2b Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Thu, 30 Jul 2026 14:43:34 +0700 Subject: [PATCH] Fix the accessibility findings from the audit (#12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contrast: --mute-2 and --trash both failed AA in both themes — every 10px mono label (recent chapter, section headings, chapter hint) sat between 2.5:1 and 3.7:1, and the trash icon, the one irreversible control, was the least visible thing in the strip. Retuned per theme: labels now 4.8-5.2:1, trash 4.3-4.5:1 against a 3:1 requirement. Focus: the search input clears its own outline and the underline lives on the parent, so :focus-within on .searchbar carries the ring. The inline error slot gets role=status, so a failed write is announced rather than looking like an ignored tap. The tab strip drops role=tablist/tab: these are real links that change the URL, and the roles promised a tabpanel contract that was never implemented, leaving aria-selected unset on every one. aria-current carries the state instead. prefers-reduced-motion now names pseudo-elements — `*` does not match them, so the busy bar and the error dot, both ::before, kept animating for the users who asked them not to. Log out was a 54x15 target; an ::after pad makes it 78x45 without moving the label. Also: the two dark-tuned hex borders in the desktop block become theme-aware tokens, and the preload swaps mono for DM Sans, the larger face and the one the body text needs. Co-Authored-By: Claude Opus 5 --- backend/static/filter.js | 7 +++++-- backend/static/style.css | 40 +++++++++++++++++++++++++++---------- backend/templates/app.html | 24 +++++++++++++++------- backend/templates/card.html | 4 +++- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/backend/static/filter.js b/backend/static/filter.js index 2902d02..0bf9d8a 100644 --- a/backend/static/filter.js +++ b/backend/static/filter.js @@ -20,8 +20,11 @@ })(); function setActiveTab(el) { - el.parentElement.querySelectorAll("[role=tab]").forEach(function (t) { - t.classList.toggle("active", t === el); + el.parentElement.querySelectorAll("a").forEach(function (t) { + var on = t === el; + t.classList.toggle("active", on); + if (on) t.setAttribute("aria-current", "page"); + else t.removeAttribute("aria-current"); }); } diff --git a/backend/static/style.css b/backend/static/style.css index d5477fa..9d2ebaa 100644 --- a/backend/static/style.css +++ b/backend/static/style.css @@ -64,7 +64,9 @@ --paper-hot: #f0d3cb; /* title of a series with a new chapter */ --paper-dim: #ddd5cb; /* resting title */ --mute: #8d857c; /* secondary text, idle icons */ - --mute-2: #5a5450; /* labels, hints */ + /* Every mute-2 use is 10px mono, so it has to clear 4.5:1 on --ink even + though it reads as the quietest step. */ + --mute-2: #877f76; /* labels, hints */ --faint: #3a3733; /* meta separators */ --faint-2: #57504b; /* cover monogram */ @@ -73,7 +75,11 @@ --ember-ink: #150907; /* text on solid ember */ --ember-soft: #eda798; /* text on ember wash */ --brass: #b8912f; /* favourite — a second, cooler metal */ - --trash: #6b5450; /* remove, resting */ + --trash: #977671; /* remove, resting — icons need 3:1, not 4.5:1 */ + + /* Desktop cell borders for the two coloured action states. */ + --play-hot-line: #3a1d18; + --fav-line: #332b14; --asura: #7d93a5; --demonic: #a98a78; @@ -106,7 +112,7 @@ --paper-hot: #a33018; --paper-dim: #191715; --mute: #6b645d; - --mute-2: #857d75; + --mute-2: #6c655e; /* clears 4.5:1 on --ash too, not just --ink */ --faint: #c9c2ba; --faint-2: #a8a098; @@ -115,7 +121,10 @@ --ember-ink: #fff; --ember-soft: #8d2c17; --brass: #8a681c; - --trash: #a98276; + --trash: #8c6558; + + --play-hot-line: #f0cfc6; + --fav-line: #e3d3a4; --asura: #4f6b80; --demonic: #8a6a55; @@ -180,6 +189,7 @@ button { cursor: pointer; } .topbar form { margin: 0; } .ghost { + position: relative; padding: 0; border: none; border-bottom: 1px solid var(--field-line); @@ -190,6 +200,8 @@ button { cursor: pointer; } text-transform: uppercase; } .ghost:hover { color: var(--paper); border-bottom-color: var(--paper); } +/* The label is 15px tall by design; the thumb gets 44 without moving it. */ +.ghost::after { content: ""; position: absolute; inset: -15px -12px; } .chrome { display: flex; flex-direction: column; } @@ -202,6 +214,9 @@ button { cursor: pointer; } border-bottom: 1px solid var(--rule); color: var(--mute-2); } +/* The input drops its own outline, so the bar it sits in carries the focus + ring — same move the two other inputs make with their border. */ +.searchbar:focus-within { border-bottom-color: var(--paper); color: var(--paper); } .searchbar svg { width: 15px; height: 15px; flex: none; } .search { flex: 1; @@ -226,7 +241,7 @@ button { cursor: pointer; } border-bottom: 1px solid var(--rule); } .tabs::-webkit-scrollbar { display: none; } -.tabs [role="tab"] { +.tabs a { flex: none; display: flex; align-items: center; @@ -236,8 +251,8 @@ button { cursor: pointer; } font: 400 17px var(--font-display); white-space: nowrap; } -.tabs [role="tab"]:hover { color: var(--paper-dim); } -.tabs [role="tab"].active { +.tabs a:hover { color: var(--paper-dim); } +.tabs a.active { color: var(--paper); border-bottom: 2px solid var(--paper); margin-bottom: -1px; @@ -661,7 +676,7 @@ button { cursor: pointer; } border-bottom: 1px solid var(--rule); } .tabs { order: 1; flex: none; gap: 20px; padding: 0; border-bottom: none; } - .tabs [role="tab"] { padding: 10px 0 14px; font-size: 18px; } + .tabs a { padding: 10px 0 14px; font-size: 18px; } .searchbar { order: 2; flex: 1; margin: 0; border-bottom: none; } .recent h2, .recent-strip { padding-left: 32px; padding-right: 32px; } .recent-card, .recent-cover { width: 100px; } @@ -683,8 +698,8 @@ button { cursor: pointer; } than by the hairline the phone layout uses. */ .actions > .play + *, .actions > *:not(.lifecycle) + .lifecycle { margin-left: 10px; box-shadow: none; } - .is-new .actions .play { border-color: #3a1d18; } - .actions .on { border-color: #332b14; } + .is-new .actions .play { border-color: var(--play-hot-line); } + .actions .on { border-color: var(--fav-line); } /* Panels line up with the body text, i.e. past the cover and its gap. */ .chapter-form, .confirm-row, .error-inline { @@ -693,5 +708,8 @@ button { cursor: pointer; } } @media (prefers-reduced-motion: reduce) { - * { animation: none !important; transition: none !important; } + /* Pseudo-elements need naming explicitly — `*` does not match them, and the + busy bar and error dot are both ::before. Their static form still reads: + the bar stays drawn and .actions stays dimmed. */ + *, *::before, *::after { animation: none !important; transition: none !important; } } diff --git a/backend/templates/app.html b/backend/templates/app.html index a5df111..e4e9b80 100644 --- a/backend/templates/app.html +++ b/backend/templates/app.html @@ -8,7 +8,9 @@ mangaBookmark - + {{/* Body text before meta lines: DM Sans is the biggest face and the one + most of the page is set in; the mono is small and arrives from CSS. */}} + @@ -31,21 +33,29 @@ autocomplete="off" aria-label="Search titles"> - diff --git a/backend/templates/card.html b/backend/templates/card.html index 6f02b20..5ab8dee 100644 --- a/backend/templates/card.html +++ b/backend/templates/card.html @@ -131,6 +131,8 @@ - + {{/* role=status announces a failed write; without it the tap just looks + ignored to a screen reader. */}} + {{end}}