@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; /* The parts the browser draws for itself. `accent-color` is what tints a native radio or checkbox — left unset it is the platform blue, which is the one colour on screen belonging to no theme here. */ accent-color: var(--budget-accent); } .dark { color-scheme: dark; } ::selection { background: var(--budget-accent); color: var(--budget-accent-ink); } 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. Deliberately the ONLY focus indicator: shadcn's primitives ship a second, softer `ring` box-shadow of their own, and the two together read as a halo inside a line. Those are stripped in button.tsx and input.tsx instead of dropping this, because this one rule covers links, table buttons and anything else that never goes through a primitive. It sets no border-radius on purpose — an earlier version pinned 2px here, which squared the corners of every control that had no radius utility of its own. The outline follows each element's own radius. */ :focus-visible { outline: 2px solid var(--budget-accent); outline-offset: 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; } }