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
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