Fix the broken input focus outline, unify input styling, tokenize scales

The root cause of 'some inputs show a focus outline, most don't': the
global input:focus/textarea:focus/select:focus rule (outline:none,
border-color: accent) has specificity (0,0,1,1). Every per-component
rule shaped '.wrapper input { border: 1px solid var(--border) }' (.form
input, .field-row input, .kanban__new input, .sidebar__select select,
.modal__field select) ties it exactly, and -- being unconditional --
silently won that tie by simply appearing later in the file, so the
input's border stayed var(--border) forever regardless of focus. Only
two controls escaped: .project-card__name-input (a class applied
directly to the element, (0,0,1,0), too low to ever win the tie) and
.card-row__text (has its own .card-row__text:focus, (0,0,2,0),
genuinely higher). Confirmed with a live computed-style test against
the real stylesheet before and after.

Fix: one canonical .field class (plus .field--compact for inline 'add'
rows and the sidebar, .field--autosize for the dashboard's JS-grown
textarea), applied directly to the <input>/<textarea>/<select> itself
in every view -- structurally immune to the same tie, since a bare
class can never out-specificity input:focus. This also collapses five
near-duplicate rules (each with its own padding/radius/font-size) into
one definition, and fixes .kanban__new input's stray
background: var(--surface) (every other field uses var(--bg); this one
nearly matched its own var(--surface) sidebar panel).

Also: .btn-danger hardcoded #b3261e/#fff instead of the already-
existing var(--error)/var(--accent-text) tokens, so danger buttons
didn't adapt in dark mode like every other error-coloured element;
now they do. Added .field:disabled styling (opacity/cursor), matching
buttons -- latent until now since no input bound :disabled yet.

Second pass, promoting repeated-but-consistent raw values to tokens:
- border-radius: 6/7/8/10/12/999px -- the stray 7px (.sidebar__link)
  folded into the 6px tier -- become --radius-sm/md/lg/xl/pill.
- disabled/ghost opacity: 0.6 was used for every disabled button
  except .status-row__delete's 0.4 (now unified) and drag-ghost states'
  0.5 (semantically different, kept separate) -- --opacity-disabled/
  --opacity-ghost.
Font-size and spacing values also repeat but don't reduce to a clean
scale without arbitrary judgement calls either way -- left alone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 22:07:35 +01:00
co-authored by Claude Sonnet 5
parent a85bb182c7
commit a3462af005
8 changed files with 145 additions and 113 deletions
+97 -106
View File
@@ -14,6 +14,19 @@
/* Kanban inbox column: a touch lighter than a status column (var(--bg)). */
--inbox-bg: #fcfcfd;
color-scheme: light dark;
/* Same radius used consistently by everything of that kind -- not a full
scale, just names for the handful of values already in use. */
--radius-sm: 6px; /* compact controls: chips, small icon buttons, .field--compact */
--radius-md: 8px; /* buttons, .field, dropdown menus */
--radius-lg: 10px; /* tiles: project cards, kanban columns */
--radius-xl: 12px; /* panels: .card, .sidebar, .modal__dialog */
--radius-pill: 999px; /* status chip */
/* Both used consistently for their one meaning each -- not "disabled at
0.6, ghost at 0.5" by convention, just what those states are. */
--opacity-disabled: 0.6;
--opacity-ghost: 0.5;
}
@media (prefers-color-scheme: dark) {
@@ -36,7 +49,14 @@
/* No browser focus ring on form controls -- the border colour change is
highlight enough (components with more elaborate focus styles, e.g.
.card-row__text, override this with their own higher-specificity rule). */
.card-row__text, override this with their own higher-specificity rule).
Specificity is a single pseudo-class + element, (0,0,1,1) -- .field below
is deliberately a single class with no element in the selector, (0,0,1,0),
so it can never win a specificity tie against this regardless of source
order (a wrapper-class + descendant-element rule, e.g. the old ".form
input", ties with this rule instead and -- being unconditional -- can
silently beat it by simply appearing later in the file, permanently
hiding the focus state; that was a real, previously-shipped bug). */
input:focus,
textarea:focus,
select:focus {
@@ -44,6 +64,47 @@ select:focus {
border-color: var(--accent);
}
/* The one themed text input/select/textarea, applied directly to the
control itself everywhere -- login, dashboard, project, configuration,
profile, sidebar. See the note above for why "directly to the control"
(a single class, no wrapping selector) matters here, not just for
consistency. */
.field {
display: block;
width: 100%;
box-sizing: border-box;
padding: 0.55rem 0.7rem;
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 1rem;
}
.field:disabled {
opacity: var(--opacity-disabled);
cursor: not-allowed;
}
/* Same control, smaller -- inline "add" rows and the sidebar, where it's a
flex child beside a button or icon rather than filling a form row. */
.field--compact {
flex: 1;
width: auto;
min-width: 0;
padding: 0.4rem 0.5rem;
border-radius: var(--radius-sm);
font-size: 0.9rem;
}
/* The dashboard's project-name field: grown by JS to fit its content, so no
manual resize handle or scrollbar. */
.field--autosize {
resize: none;
overflow: hidden;
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
@@ -81,7 +142,7 @@ body {
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 0.35rem 0.55rem;
font-size: 1rem;
line-height: 1;
@@ -140,7 +201,7 @@ body {
overflow-y: auto;
padding: 1rem 0.75rem 1.5rem;
border: 1px solid var(--border);
border-radius: 12px;
border-radius: var(--radius-xl);
background: var(--surface);
}
@@ -157,7 +218,7 @@ body {
cursor: pointer;
font-size: 1rem;
padding: 0.3rem 0.5rem;
border-radius: 6px;
border-radius: var(--radius-sm);
}
.sidebar__close:hover {
@@ -219,7 +280,7 @@ body {
align-items: center;
gap: 0.6rem;
padding: 0.45rem 0.6rem;
border-radius: 7px;
border-radius: var(--radius-sm);
color: var(--text);
text-decoration: none;
font-size: 0.9rem;
@@ -272,18 +333,6 @@ body {
padding: 0.2rem 0.6rem;
}
.sidebar__select select {
flex: 1;
min-width: 0;
font: inherit;
font-size: 0.9rem;
padding: 0.35rem 0.4rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text);
}
.sidebar__inbox-cards {
max-height: 40vh;
overflow-y: auto;
@@ -298,7 +347,7 @@ body {
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
border-radius: var(--radius-xl);
padding: 1.75rem;
}
@@ -325,7 +374,7 @@ h1 {
padding: 0.75rem 1rem;
background: var(--warn-bg);
border: 1px solid var(--warn-border);
border-radius: 8px;
border-radius: var(--radius-md);
font-size: 0.9rem;
}
@@ -372,7 +421,7 @@ h1 {
cursor: pointer;
font-size: 0.9rem;
padding: 0.2rem 0.4rem;
border-radius: 6px;
border-radius: var(--radius-sm);
}
.passkey-notice__dismiss:hover {
@@ -397,7 +446,7 @@ h1 {
gap: 0.75rem;
padding: 0.6rem 0.75rem;
border: 1px solid var(--border);
border-radius: 8px;
border-radius: var(--radius-md);
background: var(--bg);
}
@@ -424,7 +473,7 @@ h1 {
cursor: pointer;
font-size: 0.85rem;
padding: 0.3rem 0.5rem;
border-radius: 6px;
border-radius: var(--radius-sm);
}
.passkeys__remove:hover {
@@ -450,7 +499,7 @@ h1 {
padding: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
border-radius: var(--radius-lg);
color: inherit;
text-decoration: none;
}
@@ -481,22 +530,6 @@ h1 {
font-size: 0.9rem;
}
.project-card__name-input {
display: block;
width: 100%;
box-sizing: border-box;
padding: 0.55rem 0.7rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font: inherit;
font-size: 1rem;
/* Grown in JS to fit its content -- no manual resize handle, no scrollbar. */
resize: none;
overflow: hidden;
}
/* --- project detail: cards ------------------------------------------ */
.cards {
@@ -512,7 +545,7 @@ h1 {
align-items: center;
gap: 0.5rem;
border: 1px solid var(--border);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 0.4rem 0.55rem;
background: var(--surface);
}
@@ -520,7 +553,7 @@ h1 {
.card-row__status {
flex: none;
padding: 0.15rem 0.55rem;
border-radius: 999px;
border-radius: var(--radius-pill);
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
@@ -538,7 +571,7 @@ h1 {
flex: 1;
min-width: 0;
border: 1px solid transparent;
border-radius: 6px;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
font-size: 1rem;
@@ -563,7 +596,7 @@ h1 {
cursor: pointer;
font-size: 1rem;
padding: 0.2rem 0.4rem;
border-radius: 6px;
border-radius: var(--radius-sm);
}
.card-row__delete:hover {
@@ -625,7 +658,7 @@ h1 {
border: 1px solid var(--border);
background: var(--bg);
color: var(--text);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 0.35rem 0.7rem;
font: inherit;
font-size: 0.9rem;
@@ -653,7 +686,7 @@ h1 {
list-style: none;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
border-radius: var(--radius-md);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}
@@ -667,7 +700,7 @@ h1 {
color: var(--text);
font: inherit;
padding: 0.45rem 0.6rem;
border-radius: 6px;
border-radius: var(--radius-sm);
cursor: pointer;
}
@@ -733,14 +766,14 @@ h1 {
align-items: center;
gap: 0.5rem;
border: 1px solid var(--border);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 0.5rem 0.6rem;
background: var(--surface);
cursor: grab;
}
.status-row--ghost {
opacity: 0.5;
opacity: var(--opacity-ghost);
}
.status-row__name {
@@ -757,7 +790,7 @@ h1 {
cursor: pointer;
font-size: 1rem;
padding: 0.2rem 0.4rem;
border-radius: 6px;
border-radius: var(--radius-sm);
}
.status-row__delete:hover:not(:disabled) {
@@ -765,7 +798,7 @@ h1 {
}
.status-row__delete:disabled {
opacity: 0.4;
opacity: var(--opacity-disabled);
cursor: not-allowed;
}
@@ -778,23 +811,11 @@ h1 {
margin-top: 0.75rem;
}
.field-row input {
flex: 1;
min-width: 0;
font: inherit;
font-size: 0.9rem;
padding: 0.4rem 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--bg);
color: var(--text);
}
.field-row button[type='submit'] {
flex: none;
padding: 0.4rem 0.7rem;
font-size: 0.9rem;
border-radius: 6px;
border-radius: var(--radius-sm);
}
.modal__field {
@@ -804,15 +825,6 @@ h1 {
margin-top: 0.75rem;
}
.modal__field select {
padding: 0.5rem 0.6rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font: inherit;
}
/* --- kanban board ---------------------------------------------------- */
.kanban {
@@ -827,7 +839,7 @@ h1 {
flex: 0 0 16rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
border-radius: var(--radius-lg);
padding: 0.75rem;
}
@@ -862,7 +874,7 @@ h1 {
ghost placeholder while something is dragged over it. */
.kanban__cards:empty {
border: 1px dashed var(--border);
border-radius: 8px;
border-radius: var(--radius-md);
}
.kanban__cards:empty::before {
@@ -879,14 +891,14 @@ h1 {
.kanban-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 0.55rem 0.65rem;
font-size: 0.9rem;
cursor: grab;
}
.kanban-card--ghost {
opacity: 0.5;
opacity: var(--opacity-ghost);
}
.kanban__new {
@@ -895,23 +907,11 @@ h1 {
margin-top: 0.6rem;
}
.kanban__new input {
flex: 1;
min-width: 0;
font: inherit;
font-size: 0.9rem;
padding: 0.4rem 0.5rem;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface);
color: var(--text);
}
.kanban__new button[type="submit"] {
flex: none;
padding: 0.4rem 0.7rem;
font-size: 0.9rem;
border-radius: 6px;
border-radius: var(--radius-sm);
}
/* --- confirmation modal --------------------------------------------------- */
@@ -939,7 +939,7 @@ h1 {
max-width: 24rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
border-radius: var(--radius-xl);
padding: 1.5rem;
}
@@ -958,7 +958,7 @@ h1 {
.btn-secondary,
.btn-danger {
border: 1px solid var(--border);
border-radius: 8px;
border-radius: var(--radius-md);
padding: 0.5rem 0.9rem;
font: inherit;
font-size: 0.95rem;
@@ -971,14 +971,14 @@ h1 {
}
.btn-danger {
background: #b3261e;
border-color: #b3261e;
color: #fff;
background: var(--error);
border-color: var(--error);
color: var(--accent-text);
}
.btn-secondary:disabled,
.btn-danger:disabled {
opacity: 0.6;
opacity: var(--opacity-disabled);
cursor: default;
}
@@ -1000,20 +1000,11 @@ h1 {
font-size: 0.9rem;
}
.form input {
padding: 0.55rem 0.7rem;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--bg);
color: var(--text);
font-size: 1rem;
}
button[type="submit"],
.btn-primary {
padding: 0.6rem 1rem;
border: none;
border-radius: 8px;
border-radius: var(--radius-md);
background: var(--accent);
color: var(--accent-text);
font-size: 1rem;
@@ -1028,7 +1019,7 @@ button[type="submit"],
button[type="submit"]:disabled,
.btn-primary:disabled {
opacity: 0.6;
opacity: var(--opacity-disabled);
cursor: progress;
}