fix: Improve CSS color contrast, consistency, and focus handling
Secondary text (#888/#999/#aaa on #fcfcfc) failed WCAG AA 4.5:1.
Centralizing colors as custom properties prevents palette drift.
outline: none on focused inputs hid focus rings in Windows High
Contrast Mode — replaced with outline: 2px solid transparent.
Scoped broad form { display: grid } to a .form class.

Assisted-by: Claude Opus 4.6 via Claude Code
change umyxlwqryxqwwurqurlpllwwonnsmmlz
commit 94a3c176af2022675597b816f88c2b8faccc39f3
author Alpha Chen <alpha@kejadlen.dev>
date
parent xtlpyvtq
diff --git a/lib/ketchup/views/series/new.rb b/lib/ketchup/views/series/new.rb
index a5611dc..76d2fcc 100644
--- a/lib/ketchup/views/series/new.rb
+++ b/lib/ketchup/views/series/new.rb
@@ -28,7 +28,7 @@ module Views
                   ) { "Create" }
                 end
 
-                form(method: "post", action: "/series", id: "new-series-form", class: "new-series-form", novalidate: true) do
+                form(method: "post", action: "/series", id: "new-series-form", class: "form new-series-form", novalidate: true) do
                   input(type: "hidden", name: "_csrf", value: @csrf.call("/series"))
                   div(class: "field") do
                     label(for: "series-note-editor") { "Note" }
diff --git a/public/css/app.css b/public/css/app.css
index 166ed55..db8a181 100644
--- a/public/css/app.css
+++ b/public/css/app.css
@@ -19,9 +19,8 @@
 body {
   font-family: "IBM Plex Mono", "SF Mono", ui-monospace, monospace;
   font-size: var(--step--1);
-  line-height: 1.5;
-  color: #222;
-  background: #fcfcfc;
+  color: var(--color-text);
+  background: var(--color-surface);
   min-height: 100dvh;
   display: flex;
   flex-direction: column;
@@ -46,12 +45,11 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   gap: var(--space-s);
   padding: var(--space-xs) var(--space-l);
   font-size: var(--step--1);
-  border-block-end: 1px solid #ddd;
+  border-block-end: 1px solid var(--color-border);
 }
 
 .site-name {
   font-weight: 600;
-  font-size: var(--step--1);
   letter-spacing: 0.03em;
   text-decoration: none;
 }
@@ -65,29 +63,29 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .header-action {
-  color: #888;
+  color: var(--color-text-secondary);
   text-decoration: none;
   letter-spacing: 0.03em;
 }
 
 .header-action:hover {
-  color: #222;
+  color: var(--color-text);
   font-weight: 600;
 }
 
 .header-action--active {
-  color: #c00;
+  color: var(--color-accent);
   font-weight: 600;
 }
 
 .header-user {
-  color: #888;
+  color: var(--color-text-secondary);
   text-decoration: none;
   letter-spacing: 0.03em;
 }
 
 .header-user:hover {
-  color: #222;
+  color: var(--color-text);
   font-weight: 600;
 }
 
@@ -98,8 +96,8 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 .site-footer {
   margin-block-start: auto;
   padding: var(--space-xs) var(--space-l);
-  font-size: 10px;
-  color: #aaa;
+  font-size: var(--step--2);
+  color: var(--color-text-tertiary);
   text-align: center;
 }
 
@@ -119,7 +117,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   color: #333;
   font-size: var(--step--2);
   background: #fff;
-  border: 1px solid #ddd;
+  border: 1px solid var(--color-border);
   border-radius: 6px;
   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
   min-width: 20rem;
@@ -132,7 +130,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 
 .flash-undo-btn {
   all: unset;
-  color: #c00;
+  color: var(--color-accent);
   cursor: pointer;
 }
 
@@ -140,9 +138,15 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   text-decoration: underline;
 }
 
+.flash-undo-btn:focus-visible {
+  outline: 2px solid var(--color-accent);
+  outline-offset: 2px;
+  border-radius: 2px;
+}
+
 .flash-close-btn {
   all: unset;
-  color: #aaa;
+  color: var(--color-text-tertiary);
   cursor: pointer;
   font-size: var(--step-0);
   vertical-align: middle;
@@ -153,6 +157,12 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   color: #666;
 }
 
+.flash-close-btn:focus-visible {
+  outline: 2px solid var(--color-text-secondary);
+  outline-offset: 2px;
+  border-radius: 2px;
+}
+
 /* -------------------- */
 /* Dashboard layout */
 /* -------------------- */
@@ -197,7 +207,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 .agenda-week {
   display: flex;
   margin-block-end: var(--space-m);
-  border: 1px solid #eee;
+  border: 1px solid var(--color-border-light);
   border-radius: 6px;
   overflow: hidden;
 }
@@ -209,7 +219,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   align-items: center;
   gap: 1px;
   padding: var(--space-3xs) 0 var(--space-2xs);
-  border-inline-end: 1px solid #eee;
+  border-inline-end: 1px solid var(--color-border-light);
 }
 
 .agenda-week-day:last-child {
@@ -218,13 +228,13 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 
 .agenda-week-label {
   font-size: 9px;
-  color: #aaa;
+  color: var(--color-text-tertiary);
   font-weight: 500;
   text-transform: uppercase;
   letter-spacing: 0.03em;
   padding-block-end: var(--space-3xs);
   margin-block-end: var(--space-3xs);
-  border-block-end: 1px solid #eee;
+  border-block-end: 1px solid var(--color-border-light);
   width: 100%;
   text-align: center;
 }
@@ -236,15 +246,15 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .agenda-week-day--today .agenda-week-count {
-  color: #222;
+  color: var(--color-text);
 }
 
 .agenda-week-day--overdue .agenda-week-label {
-  color: #c00;
+  color: var(--color-accent);
 }
 
 .agenda-week-day--overdue .agenda-week-count {
-  color: #c00;
+  color: var(--color-accent);
 }
 
 .agenda-day {
@@ -254,19 +264,19 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 .agenda-day-header {
   font-size: var(--step--2);
   font-weight: 600;
-  color: #888;
+  color: var(--color-text-secondary);
   margin-block-end: var(--space-3xs);
 }
 
 .agenda-day--today .agenda-day-header {
-  color: #222;
+  color: var(--color-text);
 }
 
 .agenda-task {
   display: flex;
   align-items: center;
   gap: var(--space-2xs);
-  margin-block-end: 3px;
+  margin-block-end: var(--space-3xs);
 }
 
 .agenda-task .complete-btn {
@@ -281,7 +291,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   display: block;
   font-size: var(--step--2);
   padding: 3px 0;
-  color: #222;
+  color: var(--color-text);
   text-decoration: none;
   white-space: nowrap;
   overflow: hidden;
@@ -290,7 +300,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .agenda-day-pill:hover {
-  color: #555;
+  color: var(--color-text-secondary);
 }
 
 /* -------------------- */
@@ -319,27 +329,33 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   font-weight: 600;
   text-transform: uppercase;
   letter-spacing: 0.08em;
-  color: #666;
+  color: var(--color-text-secondary);
 }
 
 .section-edit-btn {
   all: unset;
   order: 1;
   font-size: var(--step--1);
-  color: #666;
+  color: var(--color-text-secondary);
   cursor: pointer;
 }
 
 .section-edit-btn:hover {
-  color: #222;
+  color: var(--color-text);
+}
+
+.section-edit-btn:focus-visible {
+  outline: 2px solid var(--color-text-secondary);
+  outline-offset: 2px;
+  border-radius: 2px;
 }
 
 .section-edit-btn--cancel {
-  color: #aaa;
+  color: var(--color-text-tertiary);
 }
 
 .section-edit-btn--cancel:hover {
-  color: #666;
+  color: var(--color-text-secondary);
 }
 
 .section-edit-btn:disabled {
@@ -355,7 +371,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 
 .main-column .section-header::after {
   height: 1px;
-  background: #ddd;
+  background: var(--color-border);
 }
 
 /* -------------------- */
@@ -368,7 +384,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .empty {
-  color: #999;
+  color: var(--color-text-secondary);
   font-size: var(--step--1);
   padding-block: var(--space-2xs);
 }
@@ -391,15 +407,11 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .task-card:hover {
-  background: #f0f0f0;
-}
-
-.task-card--overdue {
-  /* No decorative borders — spacing-driven */
+  background: var(--color-surface-hover);
 }
 
 .task-card--overdue:hover {
-  background: #ffe5e5;
+  background: var(--color-accent-bg);
 }
 
 .task-body {
@@ -419,21 +431,21 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .task-card--overdue .task-name {
-  color: #c00;
+  color: var(--color-accent);
   font-weight: 500;
 }
 
 .task-meta {
   display: block;
   font-size: var(--step--2);
-  color: #888;
+  color: var(--color-text-secondary);
   margin-block-start: 1px;
 }
 
 .task-urgency {
   font-size: var(--step--2);
   font-weight: 500;
-  color: #c00;
+  color: var(--color-accent);
   white-space: nowrap;
   font-variant-numeric: tabular-nums;
 }
@@ -450,7 +462,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   width: 18px;
   height: 18px;
   padding: 0;
-  border: 1.5px solid #999;
+  border: 1.5px solid var(--color-text-secondary);
   border-radius: 3px;
   background: transparent;
   color: transparent;
@@ -468,21 +480,21 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .complete-btn:hover {
-  border-color: #090;
-  color: #090;
-  background: #f0fff0;
+  border-color: var(--color-success);
+  color: var(--color-success);
+  background: var(--color-success-bg);
   font-weight: 900;
   -webkit-text-stroke: 1px;
 }
 
 .task-card--overdue .complete-btn {
-  border-color: #c00;
+  border-color: var(--color-accent);
 }
 
 .task-card--overdue .complete-btn:hover {
-  border-color: #090;
-  color: #090;
-  background: #f0fff0;
+  border-color: var(--color-success);
+  color: var(--color-success);
+  background: var(--color-success-bg);
 }
 
 /* -------------------- */
@@ -498,17 +510,11 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .series-note--editable {
-  border-color: #ddd;
+  border-color: var(--color-border);
 }
 
 .series-note--editable:focus-within {
-  border-color: #999;
-}
-
-
-
-.series-note:focus-within {
-  border-color: #999;
+  border-color: var(--color-text-secondary);
 }
 
 /* -------------------- */
@@ -523,7 +529,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .detail-fields dt {
-  color: #999;
+  color: var(--color-text-secondary);
 }
 
 .detail-fields dd {
@@ -532,14 +538,14 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .detail-fields .detail-overdue {
-  color: #c00;
+  color: var(--color-accent);
 }
 
 .detail-input {
   font-size: inherit;
   padding: 2px 6px;
   margin: -3px -7px;
-  border: 1px solid #ddd;
+  border: 1px solid var(--color-border);
   border-radius: 4px;
   background: transparent;
   color: #444;
@@ -551,13 +557,13 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .detail-input:focus {
-  border-color: #999;
-  outline: none;
+  border-color: var(--color-text-secondary);
+  outline: 2px solid transparent;
   background: #fff;
 }
 
 .detail-placeholder {
-  color: #999;
+  color: var(--color-text-secondary);
 }
 
 .detail-edit-interval {
@@ -569,13 +575,6 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 .detail-input-count {
   width: 100%;
   min-width: 0;
-  -moz-appearance: textfield;
-}
-
-.detail-input-count::-webkit-inner-spin-button,
-.detail-input-count::-webkit-outer-spin-button {
-  -webkit-appearance: none;
-  margin: 0;
 }
 
 .detail-input-unit {
@@ -598,7 +597,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 .task-history h3 {
   font-size: var(--step--1);
   font-weight: 600;
-  color: #999;
+  color: var(--color-text-secondary);
   margin-block-end: var(--space-3xs);
 }
 
@@ -612,7 +611,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .task-history-item + .task-history-item {
-  border-top: 1px solid #f0f0f0;
+  border-top: 1px solid var(--color-surface-hover);
 }
 
 .task-history-row {
@@ -624,7 +623,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .task-history-check {
-  color: #27ae60;
+  color: var(--color-success);
   font-size: var(--step--2);
 }
 
@@ -634,7 +633,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .task-history-date:hover {
-  border-bottom-color: #999;
+  border-bottom-color: var(--color-text-secondary);
 }
 
 .task-history-date-input {
@@ -642,7 +641,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
   font-size: inherit;
   color: inherit;
   background: transparent;
-  border: 1px solid #999;
+  border: 1px solid var(--color-text-secondary);
   border-radius: 3px;
   padding: 0 var(--space-3xs);
 }
@@ -650,7 +649,7 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 .task-history-add-note {
   font-size: var(--step--2);
   color: #ccc;
-  margin-left: auto;
+  margin-inline-start: auto;
   cursor: pointer;
   display: none;
 }
@@ -660,26 +659,26 @@ h2 { font-size: var(--step-0); font-weight: 600; }
 }
 
 .task-history-note-editor {
-  margin-top: var(--space-3xs);
-  margin-left: 16px;
+  margin-block-start: var(--space-3xs);
+  margin-inline-start: 16px;
   border: 1px solid transparent;
   border-radius: 4px;
   line-height: 1.4;
 }
 
 .task-history-note-editor:hover {
-  border-color: #eee;
+  border-color: var(--color-border-light);
 }
 
 .task-history-note-editor:focus-within {
-  border-color: #999;
+  border-color: var(--color-text-secondary);
 }
 
 /* -------------------- */
 /* Forms */
 /* -------------------- */
 
-form {
+.form {
   display: grid;
   gap: var(--space-s);
 }
@@ -718,9 +717,9 @@ input[type="number"]::-webkit-outer-spin-button {
 input:focus,
 textarea:focus,
 select:focus {
-  outline: none;
-  border-color: #999;
-  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
+  outline: 2px solid transparent;
+  border-color: var(--color-text-secondary);
+  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.15);
 }
 
 textarea {
@@ -821,5 +820,3 @@ button[type="submit"]:not(.complete-btn):hover {
     text-align: center;
   }
 }
-
-
diff --git a/public/css/utopia.css b/public/css/utopia.css
index fbc9ad1..f5d9f67 100644
--- a/public/css/utopia.css
+++ b/public/css/utopia.css
@@ -63,3 +63,21 @@
 :root {
   --fluid-16-48: clamp(1rem, 0.1818rem + 3.6364vw, 3rem);
 }
+
+/* -------------------- */
+/* Semantic color tokens */
+/* -------------------- */
+
+:root {
+  --color-text: #222;
+  --color-text-secondary: #555;
+  --color-text-tertiary: #767676;
+  --color-border: #ddd;
+  --color-border-light: #eee;
+  --color-surface: #fcfcfc;
+  --color-surface-hover: #f0f0f0;
+  --color-accent: #c00;
+  --color-success: #27ae60;
+  --color-success-bg: #f0fff0;
+  --color-accent-bg: #ffe5e5;
+}
diff --git a/templates/snapshots.css b/templates/snapshots.css
index 7537d7c..8a12304 100644
--- a/templates/snapshots.css
+++ b/templates/snapshots.css
@@ -22,7 +22,7 @@ body:has(.sidebar) {
   bottom: 0;
   width: 14rem;
   background: #151525;
-  border-right: 1px solid #2a2a44;
+  border-inline-end: 1px solid #2a2a44;
   overflow-y: auto;
   z-index: 100;
   padding: var(--space-s) 0;
@@ -53,7 +53,7 @@ body:has(.sidebar) {
   color: #888;
   text-decoration: none;
   font-size: var(--step--2);
-  border-left: 2px solid transparent;
+  border-inline-start: 2px solid transparent;
   transition: color 0.15s, border-color 0.15s, background 0.15s;
   text-transform: capitalize;
 }
@@ -65,7 +65,7 @@ body:has(.sidebar) {
 
 .sidebar-link.active {
   color: #fff;
-  border-left-color: #5e81ac;
+  border-inline-start-color: #5e81ac;
   background: #1e1e36;
 }
 
@@ -75,7 +75,7 @@ body:has(.sidebar) {
   display: inline-grid;
   place-content: center;
   border-radius: 50%;
-  margin-left: auto;
+  margin-inline-start: auto;
   flex-shrink: 0;
 }
 
@@ -85,7 +85,7 @@ body:has(.sidebar) {
 /* ── Content ── */
 
 .content {
-  margin-left: 14rem;
+  margin-inline-start: 14rem;
   padding: var(--space-l);
 }
 
@@ -96,14 +96,14 @@ body:has(.sidebar) {
   top: 0;
   z-index: 50;
   background: #1a1a2e;
-  padding-bottom: var(--space-s);
-  margin-bottom: var(--space-l);
+  padding-block-end: var(--space-s);
+  margin-block-end: var(--space-l);
   border-bottom: 1px solid #2a2a44;
 }
 
 .toolbar h1 {
   color: #fff;
-  margin-bottom: var(--space-s);
+  margin-block-end: var(--space-s);
   font-size: var(--step-2);
 }
 
@@ -165,16 +165,16 @@ body:has(.sidebar) {
 /* ── Snapshot cards ── */
 
 .snapshot {
-  margin-bottom: var(--space-xl);
+  margin-block-end: var(--space-xl);
   scroll-margin-top: 7rem;
 }
 
 .snapshot-header {
-  margin-bottom: var(--space-xs);
+  margin-block-end: var(--space-xs);
 }
 
 .snapshot h2 {
-  margin-bottom: 0;
+  margin-block-end: 0;
   color: #ccc;
   font-size: var(--step-1);
   text-transform: capitalize;
@@ -182,8 +182,8 @@ body:has(.sidebar) {
 
 .snapshot .path {
   display: block;
-  margin-top: var(--space-3xs);
-  margin-bottom: var(--space-s);
+  margin-block-start: var(--space-3xs);
+  margin-block-end: var(--space-s);
   font-size: var(--step--2);
   color: #666;
 }
@@ -223,7 +223,7 @@ body:has(.sidebar) {
 }
 
 .side h3 {
-  margin-bottom: var(--space-xs);
+  margin-block-end: var(--space-xs);
   color: #999;
   font-size: var(--step--1);
   text-transform: uppercase;
@@ -526,14 +526,14 @@ body:has(.sidebar) {
 
 body > h1 {
   color: #fff;
-  margin-bottom: var(--space-l);
+  margin-block-end: var(--space-l);
 }
 
 /* Gallery tab-bar sits at top level */
 body > .tab-bar {
-  margin-bottom: var(--space-l);
+  margin-block-end: var(--space-l);
   border-bottom: 1px solid #333;
-  padding-bottom: var(--space-xs);
+  padding-block-end: var(--space-xs);
 }
 
 /* Gallery images at intrinsic size instead of stretching to fill the