80 ? "bg-destructive" : utilPct > 50 ? "bg-yellow-500" : "bg-success")}
+ className={cn("h-full rounded-full transition-all", utilPct > 80 ? "bg-destructive" : utilPct > 50 ? "bg-warning" : "bg-success")}
style={{ width: `${utilPct}%` }}
/>
@@ -298,7 +298,7 @@ function ImportModal({
{/* Detected format badge */}
{preview.detected_format ? (
<> Detected: {preview.detected_format}>
diff --git a/frontend/src/pages/accounts/AccountList.tsx b/frontend/src/pages/accounts/AccountList.tsx
index b5a18e9..45e0a00 100644
--- a/frontend/src/pages/accounts/AccountList.tsx
+++ b/frontend/src/pages/accounts/AccountList.tsx
@@ -315,7 +315,7 @@ function AccountGroup({
80 ? "bg-destructive" : utilPct > 50 ? "bg-yellow-500" : "bg-success")}
+ className={cn("h-full rounded-full transition-all", utilPct > 80 ? "bg-destructive" : utilPct > 50 ? "bg-warning" : "bg-success")}
style={{ width: `${utilPct}%` }}
/>
diff --git a/frontend/src/pages/budgets/BudgetPage.tsx b/frontend/src/pages/budgets/BudgetPage.tsx
index 3bf6d33..170cbca 100644
--- a/frontend/src/pages/budgets/BudgetPage.tsx
+++ b/frontend/src/pages/budgets/BudgetPage.tsx
@@ -12,7 +12,11 @@ function RadialGauge({ percent, size = 80 }: { percent: number; size?: number })
const circumference = 2 * Math.PI * r;
const clamped = Math.min(percent, 100);
const offset = circumference - (clamped / 100) * circumference;
- const color = percent >= 100 ? "#ef4444" : percent >= 80 ? "#f97316" : "#22c55e";
+ const color = percent >= 100
+ ? "hsl(var(--destructive))"
+ : percent >= 80
+ ? "hsl(var(--warning))"
+ : "hsl(var(--success))";
return (
@@ -139,7 +143,7 @@ export default function BudgetPage() {
item.is_over_budget
? "border-destructive/50"
: item.alert_triggered
- ? "border-orange-500/50"
+ ? "border-warning/50"
: "border-border"
)}
>
@@ -170,7 +174,7 @@ export default function BudgetPage() {
{item.is_over_budget ? (
) : item.alert_triggered ? (
-
+
) : (
)}
diff --git a/frontend/src/pages/dashboard/Dashboard.tsx b/frontend/src/pages/dashboard/Dashboard.tsx
index a530470..ddaba24 100644
--- a/frontend/src/pages/dashboard/Dashboard.tsx
+++ b/frontend/src/pages/dashboard/Dashboard.tsx
@@ -53,13 +53,13 @@ export default function Dashboard() {
{/* 2FA nudge */}
{!totpEnabled && (
-
-
+
+
- Enable two-factor authentication
+ Enable two-factor authentication
to secure your account.
-
+
Set up 2FA
@@ -111,14 +111,14 @@ export default function Dashboard() {
({ date: p.date, value: Number(p.net_worth) }))}>
-
-
+
+
-
- `£${(v/1000).toFixed(0)}k`} width={45} />
+
+ `£${(v/1000).toFixed(0)}k`} width={45} />
formatCurrency(v, nwReport.base_currency)} />
-
+
) : (
@@ -136,8 +136,8 @@ export default function Dashboard() {
{ieReport && ieReport.points.length > 0 ? (
({ month: p.month, income: Number(p.income), expenses: Number(p.expenses) }))}>
-
- `£${(v/1000).toFixed(0)}k`} width={45} />
+
+ `£${(v/1000).toFixed(0)}k`} width={45} />
formatCurrency(v, "GBP")} />
diff --git a/frontend/src/pages/investments/AssetDetail.tsx b/frontend/src/pages/investments/AssetDetail.tsx
index 9898267..d1827b5 100644
--- a/frontend/src/pages/investments/AssetDetail.tsx
+++ b/frontend/src/pages/investments/AssetDetail.tsx
@@ -6,6 +6,7 @@ import { useUiStore } from "@/store/uiStore";
import { cn } from "@/utils/cn";
import { ArrowLeft, TrendingUp, TrendingDown } from "lucide-react";
import Plot from "react-plotly.js";
+import { cssVar } from "@/utils/cssVar";
export default function AssetDetail() {
const { assetId } = useParams<{ assetId: string }>();
@@ -152,8 +153,8 @@ export default function AssetDetail() {
high: highs as number[],
low: lows as number[],
close: closes as number[],
- increasing: { line: { color: "#22c55e" } },
- decreasing: { line: { color: "#ef4444" } },
+ increasing: { line: { color: cssVar("--success") } },
+ decreasing: { line: { color: cssVar("--destructive") } },
name: holding?.symbol ?? "Price",
},
{
@@ -161,16 +162,16 @@ export default function AssetDetail() {
x: dates,
y: volumes as number[],
yaxis: "y2",
- marker: { color: "rgba(99,102,241,0.3)" },
+ marker: { color: cssVar("--primary", 0.3) },
name: "Volume",
},
]}
layout={{
paper_bgcolor: "transparent",
plot_bgcolor: "transparent",
- font: { color: "var(--muted-foreground)", size: 11 },
- xaxis: { rangeslider: { visible: false }, gridcolor: "var(--border)", showgrid: true },
- yaxis: { gridcolor: "var(--border)", showgrid: true, domain: [0.25, 1] },
+ font: { color: cssVar("--muted-foreground"), size: 11 },
+ xaxis: { rangeslider: { visible: false }, gridcolor: cssVar("--border"), showgrid: true },
+ yaxis: { gridcolor: cssVar("--border"), showgrid: true, domain: [0.25, 1] },
yaxis2: { domain: [0, 0.2], showgrid: false },
margin: { t: 10, r: 10, b: 40, l: 60 },
showlegend: false,
diff --git a/frontend/src/pages/predictions/PredictionsPage.tsx b/frontend/src/pages/predictions/PredictionsPage.tsx
index 42cbad3..9317ffa 100644
--- a/frontend/src/pages/predictions/PredictionsPage.tsx
+++ b/frontend/src/pages/predictions/PredictionsPage.tsx
@@ -12,6 +12,7 @@ import {
XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, ReferenceLine,
} from "recharts";
import Plot from "react-plotly.js";
+import { cssVar } from "@/utils/cssVar";
const TABS = [
{ id: "spending", label: "Spending", icon: BarChart3 },
@@ -105,11 +106,11 @@ function SpendingTab() {
-
- `£${v}`} width={55} />
+
+ `£${v}`} width={55} />
formatCurrency(v, "GBP")} />
-
-
+
+
@@ -146,13 +147,13 @@ function BudgetAlerts() {
{f.category_name}
- 0.75 ? "text-destructive" : "text-yellow-500")}>
+ 0.75 ? "text-destructive" : "text-warning")}>
{(f.probability_overspend * 100).toFixed(0)}% overspend risk
0.75 ? "bg-destructive" : "bg-yellow-500")}
+ className={cn("h-full rounded-full", f.probability_overspend > 0.75 ? "bg-destructive" : "bg-warning")}
style={{ width: `${Math.min(100, forecastPct)}%` }}
/>
@@ -238,12 +239,12 @@ function NetWorthTab() {
Net Worth Projection
-
- `£${(v / 1000).toFixed(0)}k`} width={55} />
+
+ `£${(v / 1000).toFixed(0)}k`} width={55} />
formatCurrency(v, "GBP")} />
- {lastHistory && }
-
+ {lastHistory && }
+
@@ -342,8 +343,8 @@ function MonteCarloTab() {
x: data.percentiles.p90.map(p => p.date),
y: data.percentiles.p90.map(p => p.value),
fill: "tonexty",
- fillcolor: "rgba(99,102,241,0.15)",
- line: { color: "#6366f1", width: 1 },
+ fillcolor: cssVar("--primary", 0.15),
+ line: { color: cssVar("--primary"), width: 1 },
name: "P90",
mode: "lines",
},
@@ -352,8 +353,8 @@ function MonteCarloTab() {
x: data.percentiles.p75.map(p => p.date),
y: data.percentiles.p75.map(p => p.value),
fill: "tonexty",
- fillcolor: "rgba(99,102,241,0.2)",
- line: { color: "#6366f1", width: 1 },
+ fillcolor: cssVar("--primary", 0.2),
+ line: { color: cssVar("--primary"), width: 1 },
name: "P75",
mode: "lines",
},
@@ -361,7 +362,7 @@ function MonteCarloTab() {
type: "scatter" as const,
x: data.percentiles.p50.map(p => p.date),
y: data.percentiles.p50.map(p => p.value),
- line: { color: "#22c55e", width: 2.5 },
+ line: { color: cssVar("--success"), width: 2.5 },
name: "P50 (Median)",
mode: "lines",
},
@@ -370,8 +371,8 @@ function MonteCarloTab() {
x: data.percentiles.p25.map(p => p.date),
y: data.percentiles.p25.map(p => p.value),
fill: "tonexty",
- fillcolor: "rgba(239,68,68,0.1)",
- line: { color: "#ef4444", width: 1 },
+ fillcolor: cssVar("--destructive", 0.1),
+ line: { color: cssVar("--destructive"), width: 1 },
name: "P25",
mode: "lines",
},
@@ -380,8 +381,8 @@ function MonteCarloTab() {
x: data.percentiles.p10.map(p => p.date),
y: data.percentiles.p10.map(p => p.value),
fill: "tonexty",
- fillcolor: "rgba(239,68,68,0.15)",
- line: { color: "#ef4444", width: 1 },
+ fillcolor: cssVar("--destructive", 0.15),
+ line: { color: cssVar("--destructive"), width: 1 },
name: "P10",
mode: "lines",
},
@@ -389,10 +390,10 @@ function MonteCarloTab() {
layout={{
paper_bgcolor: "transparent",
plot_bgcolor: "transparent",
- font: { color: "var(--muted-foreground)", size: 11 },
- xaxis: { gridcolor: "var(--border)", showgrid: true },
+ font: { color: cssVar("--muted-foreground"), size: 11 },
+ xaxis: { gridcolor: cssVar("--border"), showgrid: true },
yaxis: {
- gridcolor: "var(--border)",
+ gridcolor: cssVar("--border"),
showgrid: true,
tickformat: "£,.0f",
},
@@ -461,15 +462,15 @@ function CashFlowTab() {
-
-
+
+
- v.slice(5)} />
- `£${(v / 1000).toFixed(1)}k`} width={55} />
+ v.slice(5)} />
+ `£${(v / 1000).toFixed(1)}k`} width={55} />
formatCurrency(v, "GBP")} />
-
-
+
+
diff --git a/frontend/src/pages/reports/ReportsPage.tsx b/frontend/src/pages/reports/ReportsPage.tsx
index fa2abf3..6524bdc 100644
--- a/frontend/src/pages/reports/ReportsPage.tsx
+++ b/frontend/src/pages/reports/ReportsPage.tsx
@@ -249,15 +249,15 @@ function NetWorthTab() {
({ ...p, net_worth: Number(p.net_worth), total_assets: Number(p.total_assets), total_liabilities: Number(p.total_liabilities) }))}>
-
-
+
+
-
-
- `£${(v/1000).toFixed(0)}k`} />
+
+
+ `£${(v/1000).toFixed(0)}k`} />
formatCurrency(v, data.base_currency)} />
-
+
@@ -287,9 +287,9 @@ function IncomeExpenseTab() {
Monthly Income vs Expenses
-
-
- `£${(v/1000).toFixed(0)}k`} />
+
+
+ `£${(v/1000).toFixed(0)}k`} />
formatCurrency(v, data.currency)} />
@@ -340,15 +340,15 @@ function CashFlowTab() {
Daily Cash Flow — Last 30 Days
-
-
- `£${(v/1000).toFixed(1)}k`} />
- `£${(v/1000).toFixed(1)}k`} />
+
+
+ `£${(v/1000).toFixed(1)}k`} />
+ `£${(v/1000).toFixed(1)}k`} />
formatCurrency(v, data.currency)} />
-
+
@@ -404,15 +404,15 @@ function SavingsRateTab() {
Savings Rate by Month
-
-
- `£${(v/1000).toFixed(0)}k`} />
- `${v}%`} />
+
+
+ `£${(v/1000).toFixed(0)}k`} />
+ `${v}%`} />
name === "Savings Rate %" ? `${v.toFixed(1)}%` : formatCurrency(v, data.currency)} />
-
+
@@ -580,12 +580,12 @@ function BudgetVsActualTab() {
Budget vs Actual Spending
-
- `£${v}`} />
-
+
+ `£${v}`} />
+
formatCurrency(v, data.currency)} />
-
+
@@ -614,9 +614,9 @@ function SpendingTrendsTab() {
Spending by Category (6 months)
-
-
- `£${v}`} />
+
+
+ `£${v}`} />
formatCurrency(v, data.currency)} />
{data.categories.slice(0, 8).map((cat, i) => (
@@ -674,9 +674,9 @@ function InvestmentsTab() {
Holdings Value
-
- `£${(v/1000).toFixed(0)}k`} />
-
+
+ `£${(v/1000).toFixed(0)}k`} />
+
formatCurrency(v, perf.currency)} />
{holdingsData.map((entry, i) => (
diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx
index f8941c7..9e4dd9d 100644
--- a/frontend/src/pages/settings/SettingsPage.tsx
+++ b/frontend/src/pages/settings/SettingsPage.tsx
@@ -234,7 +234,7 @@ function PasswordCard() {
{[1,2,3,4].map(i => {
const score = Math.min(4, Math.floor(next.length / 3));
- return
;
+ return
;
})}
)}
diff --git a/frontend/src/utils/cssVar.ts b/frontend/src/utils/cssVar.ts
new file mode 100644
index 0000000..f3bcf8b
--- /dev/null
+++ b/frontend/src/utils/cssVar.ts
@@ -0,0 +1,21 @@
+/**
+ * Reads a CSS custom property from the root element and returns a valid colour
+ * string suitable for Plotly (which does not resolve CSS var() references).
+ *
+ * The project stores HSL components without hsl() — e.g. "--primary: 252 87% 67%".
+ * This converts them to "hsl(252, 87%, 67%)" or "hsla(..., alpha)" for Plotly.
+ */
+export function cssVar(name: string, alpha?: number): string {
+ const raw = getComputedStyle(document.documentElement)
+ .getPropertyValue(name)
+ .trim();
+
+ const parts = raw.split(/\s+/);
+ if (parts.length === 3) {
+ return alpha !== undefined
+ ? `hsla(${parts[0]}, ${parts[1]}, ${parts[2]}, ${alpha})`
+ : `hsl(${parts[0]}, ${parts[1]}, ${parts[2]})`;
+ }
+
+ return raw;
+}