Fix the accessibility findings from the audit

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 14:43:34 +07:00
parent 8876e5e364
commit f081780d8d
4 changed files with 54 additions and 21 deletions
+5 -2
View File
@@ -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");
});
}
+29 -11
View File
@@ -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; }
}