Files
budget2.0/src/index.css
T
Lucas WintherandClaude Opus 5 824c2f5f59 docs: make AGENTS.md the only agent guide, restructured into five sections
AGENTS.md now holds the instructions and CLAUDE.md and GEMINI.md are
pointers to it. Three files of instructions become three files that
disagree; one file every agent reads cannot.

The content is reorganised rather than rewritten, into: the invariants,
the guards that fail the build, the validation this codebase deliberately
does not perform, the environment, and where to read next. Every guard
now names the test that catches it, and the two rules with no test behind
them are called out as such, because "only review catches this" is
information the reader needs.

Restructuring surfaced one rule that was missing here entirely — sw.js
must stay unhashed and be served no-cache — which had only ever been
written down in ARCHITECTURE.md.

Dropped: the `bun init` boilerplate code samples. Every rule they carried
is still stated; the hello-world test and the three frontend snippets are
not.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-02 21:57:38 +02:00

228 lines
8.3 KiB
CSS
Executable File

@import "../styles/globals.css";
/* The @font-face rules are deliberately NOT here. Bun's CSS bundler resolves
every url() from disk and inlines woff2 as base64 — relative OR absolute,
and `loader: { ".woff2": "file" }` does not change it. That made this file
303 KB of render-blocking bytes and defeated the unicode-range subsetting,
since a subset embedded in the stylesheet downloads whether or not one
character needs it. They live in src/fontCss.ts and are injected at boot by
src/frontend.tsx, which is the only arrangement that is identical in dev and
in a subpath production build without a placeholder file. The cost is that
the faces are requested after the JS parses rather than from the HTML;
`font-display: swap` keeps text readable throughout. */
/*
* ============================================================================
* Budget 2.0 design foundation
* ============================================================================
*
* Two layers, deliberately separate:
*
* 1. `--budget-*` semantic tokens, defined once per theme below. These are
* the app's own vocabulary — paper, ink, rule, surplus, overspend — and
* are the ONLY colours the app's own components should name.
* 2. shadcn's variable set, pointed at layer 1. Re-theming shadcn rather
* than fighting it means every primitive (dialog, select, badge, button)
* inherits both themes for free.
*
* WHY THE PAIRS MATTER. AGENTS.md's rule is "never define a bare `--accent`",
* and it was paid for with an app-wide contrast bug: shadcn uses `--accent` as
* a hover BACKGROUND and `--accent-foreground` as the text on it, so setting
* one without the other repaints the background and leaves the text behind.
* Every shadcn pair below is therefore redefined TOGETHER, in both themes.
*
* The brand teal is `--primary`, NOT `--accent`. That is the mapping the
* earlier bug got wrong: shadcn's `--accent` is a subtle hover tint, and a
* saturated teal there turns every hover into a coloured slab with dark text
* on it. `--accent` stays a tint of the surface.
*
* Every text pair here was measured, not eyeballed: the weakest is 5.50:1
* (muted on paper, light theme) against a 4.5:1 requirement, and field
* borders clear the 3:1 non-text requirement on both of their backgrounds.
*/
:root {
/* --- Layer 1: the app's own vocabulary (light) --- */
--budget-paper: #f3f5f7; /* page ground: cool statement paper, NOT cream */
--budget-raised: #ffffff; /* cards and rows sitting above the paper */
--budget-ink: #16212b; /* primary text: ink navy, not black */
--budget-ink-muted: #546575; /* labels, captions, secondary figures */
--budget-rule: #dce3e9; /* hairlines between rows */
--budget-rule-strong: #768899; /* field borders; needs 3:1, hairlines do not */
--budget-accent: #0d6b60; /* muted teal: links, primary actions, spend fill */
--budget-accent-ink: #ffffff; /* text ON the teal */
--budget-tint: #e8edf1; /* hover wash, table header fill */
--surplus: #1f6640; /* carried-forward surplus */
--overspend: #a5382a; /* negative balances only */
/* Elevation is light and cool, never a grey drop shadow. */
--budget-shadow-card: 0 1px 2px rgb(22 33 43 / 0.04), 0 8px 24px -12px rgb(22 33 43 / 0.12);
--budget-shadow-raised: 0 1px 1px rgb(22 33 43 / 0.05), 0 2px 6px -2px rgb(22 33 43 / 0.08);
--font-display: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-body: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-money: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
/* --- Layer 2: shadcn, pointed at layer 1. Pairs, always. --- */
--radius: 0.5rem; /* crisper than shadcn's 0.625rem default */
--background: var(--budget-paper);
--foreground: var(--budget-ink);
--card: var(--budget-raised);
--card-foreground: var(--budget-ink);
--popover: var(--budget-raised);
--popover-foreground: var(--budget-ink);
--primary: var(--budget-accent);
--primary-foreground: var(--budget-accent-ink);
--secondary: var(--budget-tint);
--secondary-foreground: var(--budget-ink);
--muted: var(--budget-tint);
--muted-foreground: var(--budget-ink-muted);
--accent: var(--budget-tint); /* a TINT. see the note above. */
--accent-foreground: var(--budget-ink); /* its pair, never omitted. */
--destructive: var(--overspend);
--border: var(--budget-rule);
--input: var(--budget-rule-strong);
--ring: var(--budget-accent);
}
.dark {
/* --- Layer 1 (dark). Deep slate, never pure black: a pure-black ground
makes the hairlines and the carry meter float. --- */
--budget-paper: #0c1319;
--budget-raised: #141e28;
--budget-ink: #e8eef4;
--budget-ink-muted: #94a7b8;
--budget-rule: #22303e;
--budget-rule-strong: #647c92;
--budget-accent: #3ab5a4; /* lifted: the light teal is unreadable on slate */
--budget-accent-ink: #08110f; /* dark text on the lifted teal, not white */
--budget-tint: #1c2836;
--surplus: #57bb83;
--overspend: #f0866a;
/* Dark elevation reads as a lighter surface plus a hairline, not a shadow:
shadows are invisible on a dark ground. */
--budget-shadow-card: 0 1px 0 rgb(255 255 255 / 0.04) inset, 0 12px 32px -16px rgb(0 0 0 / 0.7);
--budget-shadow-raised: 0 1px 0 rgb(255 255 255 / 0.03) inset;
/* --- Layer 2 (dark). Same pairs, same rule. --- */
--background: var(--budget-paper);
--foreground: var(--budget-ink);
--card: var(--budget-raised);
--card-foreground: var(--budget-ink);
--popover: var(--budget-raised);
--popover-foreground: var(--budget-ink);
--primary: var(--budget-accent);
--primary-foreground: var(--budget-accent-ink);
--secondary: var(--budget-tint);
--secondary-foreground: var(--budget-ink);
--muted: var(--budget-tint);
--muted-foreground: var(--budget-ink-muted);
--accent: var(--budget-tint);
--accent-foreground: var(--budget-ink);
--destructive: var(--overspend);
--border: var(--budget-rule);
--input: var(--budget-rule-strong);
--ring: var(--budget-accent);
}
/* Makes `font-sans` / `font-mono` utilities and shadcn primitives inherit the
shipped faces instead of the OS stack. */
@theme inline {
--font-sans: var(--font-body);
--font-mono: var(--font-money);
--color-budget-accent: var(--budget-accent);
--color-budget-paper: var(--budget-paper);
--color-budget-raised: var(--budget-raised);
--color-budget-rule: var(--budget-rule);
--color-budget-ink: var(--budget-ink);
--color-budget-ink-muted: var(--budget-ink-muted);
--color-surplus: var(--surplus);
--color-overspend: var(--overspend);
}
@layer base {
:root {
color-scheme: light;
}
.dark {
color-scheme: dark;
}
body {
/* Was `grid place-items-center`, which centred short pages VERTICALLY —
the month view floated in the middle of the viewport on an empty month.
The app shell owns horizontal centring now, with a max width. */
@apply m-0 min-w-[320px] min-h-screen bg-background text-foreground antialiased;
font-family: var(--font-body);
font-feature-settings: "cv11"; /* Inter's single-storey a-alternates off */
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: var(--font-display);
font-weight: 640;
letter-spacing: -0.019em;
text-wrap: balance;
}
/* One visible, consistent focus treatment. Keyboard users get a ring on the
accent colour, which clears 3:1 on every surface in both themes. */
:focus-visible {
outline: 2px solid var(--budget-accent);
outline-offset: 2px;
border-radius: 2px;
}
}
@layer utilities {
/* Applied to every money figure: mono face, tabular digits so columns of
numbers align on the decimal without a monospaced layout everywhere. */
.font-money {
font-family: var(--font-money);
font-variant-numeric: tabular-nums;
font-feature-settings: "zero"; /* slashed zero: 0 vs O at a glance */
letter-spacing: -0.01em;
}
.text-overspend {
color: var(--overspend);
}
.text-surplus {
color: var(--surplus);
}
/* The one card treatment, so elevation cannot drift screen to screen. */
.budget-card {
background: var(--budget-raised);
border: 1px solid var(--budget-rule);
border-radius: var(--radius);
box-shadow: var(--budget-shadow-card);
}
}
@keyframes spin {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion) {
*,
::before,
::after {
animation: none !important;
transition: none !important;
}
}