Redesign the setup flow and consolidate the CSS into one primitives layer

Onboarding asks explicitly for one or two parents and shows one card per
parent for balances; the balance form is per parent with inline
validation; the partner can be filled from the FK deduction or added
from it. Settings and Dashboard reuse the same form.

CSS: bare buttons carry no colour any more; buttons, cards, choice
cards, fields, alerts, badges, eyebrow and modal chrome are defined once
in primitives.css. Media queries live only in responsive.css. A style
guard test blocks hex, !important and stray media queries. Bilingual
fixes: calendar selects size by content, header no longer wraps. Vitest
is capped at four workers.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 12:27:29 +02:00
parent e4719cb9ae
commit ae6db7e16f
105 changed files with 5683 additions and 3690 deletions
+91 -67
View File
@@ -1,6 +1,6 @@
/* Component styles - must come first */
@import './primitives.css';
@import './components.css';
@import './modals.css';
@import './responsive.css';
/* Design Tokens
@@ -52,6 +52,19 @@
--overlay: rgba(16, 24, 40, 0.5);
/* Translucent tints used only for subtle hover/shadow/scrim effects — kept
* as tokens (rather than inlined rgba() elsewhere) because the style
* guard test (styles.test.ts) forbids rgba() outside this file. */
--c-row-hover: rgba(47, 111, 79, 0.02);
--c-accent-tint: rgba(47, 111, 79, 0.04);
--c-accent-shadow: rgba(47, 111, 79, 0.12);
--c-scrim-strong: rgba(0, 0, 0, 0.4);
--c-band-shadow: rgba(0, 0, 0, 0.1);
--c-label-text-shadow: rgba(0, 0, 0, 0.35);
--c-fill-light: rgba(255, 255, 255, 0.35);
--c-badge-fill: rgba(255, 255, 255, 0.3);
--c-scrim-soft: rgba(0, 0, 0, 0.2);
/* Spacing scale (8px base) */
--sp-1: 4px;
--sp-2: 8px;
@@ -62,6 +75,8 @@
--sp-8: 48px;
/* Type scale */
--fs-5xs: 0.5rem;
--fs-4xs: 0.5625rem;
--fs-2xs: 0.6875rem;
--fs-xs: 0.75rem;
--fs-sm: 0.875rem;
@@ -85,6 +100,16 @@
/* Content width */
--maxw: 960px;
/* Interaction chrome shared by every focusable control */
--c-focus-ring: rgba(47, 111, 79, 0.32);
--fs-3xs: 0.625rem;
--control-h: 40px;
/* Icon glyph sizes (spinner, etc.) — a separate scale from --fs-* since
* these size a single glyph, not running text. */
--icon-lg: 48px;
--icon-md: 36px;
color-scheme: light;
}
@@ -250,36 +275,42 @@ a:focus-visible {
border-radius: 2px;
}
/* Buttons - base */
/* Buttons - base
*
* A plain reset only — no color, no padding beyond zero. Every visible
* button opts into a look via a `.btn` variant (see primitives.css) so a
* semantic element that happens to be a `<button>` (a card, a chip, a day
* cell) never inherits a color scheme it didn't ask for. */
button {
font-family: inherit;
font-size: var(--fs-base);
font: inherit;
color: inherit;
background: none;
border: 0;
padding: 0;
cursor: pointer;
border: none;
border-radius: var(--radius);
padding: var(--sp-3) var(--sp-4);
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
}
button:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 2px;
border-radius: 0;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Inputs & Selects */
input,
/* Inputs & Selects
*
* Matches `.field__control` in primitives.css (same border/radius/padding/
* focus formula) so an unclassed input still looks right. Checkboxes and
* radios are excluded — they keep their native intrinsic size and just pick
* up the accent color, not the text-field chrome. */
input:not([type='checkbox']):not([type='radio']),
select,
textarea {
width: 100%;
min-height: var(--control-h);
font-family: inherit;
font-size: var(--fs-base);
border: 1px solid var(--c-border);
border-radius: var(--radius);
padding: var(--sp-3) var(--sp-4);
border-radius: var(--radius-sm);
padding: var(--sp-2) var(--sp-3);
background: var(--c-surface);
color: var(--c-text);
transition: border-color 0.2s, box-shadow 0.2s;
@@ -290,7 +321,17 @@ select:focus,
textarea:focus {
outline: none;
border-color: var(--c-accent);
box-shadow: 0 0 0 3px rgba(47, 111, 79, 0.1);
box-shadow: 0 0 0 3px var(--c-focus-ring);
}
/* Same crisp keyboard-focus outline as `.field__control:focus-visible`
* (primitives.css) for an unclassed input/select/textarea — see that rule's
* comment. */
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 1px;
}
input:disabled,
@@ -300,6 +341,36 @@ textarea:disabled {
cursor: not-allowed;
}
input[type='checkbox'],
input[type='radio'] {
width: auto;
accent-color: var(--c-accent);
cursor: pointer;
}
/* Number inputs — hide the native up/down spinner everywhere. It doubles
* the field's effective click target with no matching `.field__control`
* affordance and its width isn't accounted for by any of this app's `ch`-
* based sizing. Callers still get numeric-only mobile keyboards via
* `inputmode="numeric"` on the element itself. */
input[type='number'] {
appearance: textfield;
}
input[type='number']::-webkit-outer-spin-button,
input[type='number']::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
/* Date inputs — same height/padding/font as `.field__control` by default
* (the shared `input:not([type=checkbox]):not([type=radio])` rule above
* already covers it), just with a visibly clickable calendar affordance. */
input[type='date']::-webkit-calendar-picker-indicator {
opacity: 0.6;
cursor: pointer;
}
/* Label */
label {
display: block;
@@ -329,14 +400,7 @@ td {
}
tr:hover td {
background-color: rgba(47, 111, 79, 0.02);
}
/* Section container */
main {
max-width: var(--maxw);
margin: 0 auto;
padding: var(--sp-5);
background-color: var(--c-row-hover);
}
/* Utility classes for form */
@@ -347,43 +411,3 @@ main {
.form-group label {
margin-bottom: var(--sp-2);
}
/* Message boxes */
.error-message {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-danger-weak);
color: var(--c-danger);
border-left: 4px solid var(--c-danger);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
}
.success-message {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-success-weak);
color: var(--c-success);
border-left: 4px solid var(--c-success);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
}
.warning-message {
padding: var(--sp-3) var(--sp-4);
background-color: var(--c-warning-weak);
color: var(--c-warning);
border-left: 4px solid var(--c-warning);
border-radius: var(--radius);
margin-bottom: var(--sp-4);
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}