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]>
228 lines
8.3 KiB
CSS
Executable File
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;
|
|
}
|
|
}
|