Index: frontend/src/components/graphs/MultiPercentChangeAreaChart.jsx
===================================================================
--- frontend/src/components/graphs/MultiPercentChangeAreaChart.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
+++ frontend/src/components/graphs/MultiPercentChangeAreaChart.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -0,0 +1,104 @@
+import React, { Suspense, lazy, useMemo } from "react";
+
+import { formatBucketLabel } from "../../utils/timeSeries.js";
+
+// Lazy-load ApexCharts (same as PercentChangeAreaChart)
+const ApexChart = lazy(() => import("react-apexcharts"));
+
+/**
+ * Multi-series % change area chart.
+ *
+ * seriesList: [{ name: string, points: [{ts, value}], color: string }]
+ * granularity: daily | weekly | monthly | yearly
+ */
+export default function MultiPercentChangeAreaChart({
+  seriesList,
+  granularity,
+  height = 320,
+}) {
+  const series = useMemo(() => {
+    return (seriesList ?? []).map((s) => ({
+      name: s.name,
+      data: (s.points ?? []).map((p) => ({ x: p.ts, y: Number(p.value ?? 0) })),
+    }));
+  }, [seriesList]);
+
+  const colors = useMemo(() => (seriesList ?? []).map((s) => s.color), [seriesList]);
+
+  const options = useMemo(() => {
+    return {
+      chart: {
+        type: "area",
+        stacked: false,
+        toolbar: { show: false },
+        zoom: { enabled: false },
+        animations: { enabled: true, easing: "easeinout", speed: 450 },
+        fontFamily: "inherit",
+        foreColor: "rgba(255,255,255,0.72)",
+      },
+      stroke: { curve: "smooth", width: 2 },
+      fill: {
+        type: "gradient",
+        gradient: {
+          shadeIntensity: 0.18,
+          opacityFrom: 0.22,
+          opacityTo: 0.03,
+          stops: [0, 90, 100],
+        },
+      },
+      dataLabels: { enabled: false },
+      grid: {
+        borderColor: "rgba(255,255,255,0.08)",
+        strokeDashArray: 4,
+      },
+      legend: {
+        show: false, // we render our own legend outside for better layout
+      },
+      xaxis: {
+        type: "datetime",
+        labels: {
+          datetimeUTC: false,
+          formatter: (value, timestamp) => formatBucketLabel(timestamp, granularity),
+        },
+        axisBorder: { color: "rgba(255,255,255,0.10)" },
+        axisTicks: { color: "rgba(255,255,255,0.10)" },
+      },
+      yaxis: {
+        labels: {
+          formatter: (v) => `${Math.round(v)}%`,
+        },
+      },
+      tooltip: {
+        theme: "dark",
+        y: {
+          formatter: (v) => `${Number(v ?? 0).toFixed(1)}%`,
+        },
+      },
+      colors,
+      annotations: {
+        yaxis: [
+          {
+            y: 0,
+            borderColor: "rgba(255,255,255,0.25)",
+            strokeDashArray: 4,
+          },
+        ],
+      },
+    };
+  }, [colors, granularity]);
+
+  return (
+    <div className="w-full">
+      <Suspense
+        fallback={
+          <div className="flex items-center justify-center" style={{ height }}>
+            <span className="loading loading-spinner loading-md" />
+          </div>
+        }
+      >
+        <ApexChart options={options} series={series} type="area" height={height} />
+      </Suspense>
+    </div>
+  );
+}
+
Index: frontend/src/pages/Dashboard/DashboardLayout.jsx
===================================================================
--- frontend/src/pages/Dashboard/DashboardLayout.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/DashboardLayout.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -29,10 +29,24 @@
   return (
     <SidebarProvider>
+      {/* App shell background */}
+      <div className="pointer-events-none fixed inset-0 -z-10">
+        <div className="absolute inset-0 bg-base-300/10" />
+        <div className="absolute inset-0 bg-radial-[circle_at_20%_20%] from-green-400/10 via-transparent to-transparent" />
+        <div className="absolute inset-0 bg-radial-[circle_at_80%_30%] from-blue-400/10 via-transparent to-transparent" />
+        <div className="absolute inset-0 bg-linear-to-b from-black/10 via-transparent to-black/25" />
+      </div>
+
       <DashboardSidebar />
+
       <SidebarInset className="pt-16 lg:pt-0">
         <DashboardTopbar username={username} onLogout={onLogout} />
+
         <div className="h-[calc(100vh-4rem)] overflow-auto">
-          <div className="container mx-auto px-6 py-8 md:px-8 md:py-10 lg:py-12">
-            <Outlet />
+          <div className="mx-auto w-full max-w-7xl px-4 py-6 sm:px-6 sm:py-8 lg:px-8 lg:py-10">
+            <div className="rounded-3xl border border-white/10 bg-base-100/60 shadow-[0_0_0_1px_rgba(255,255,255,0.04),0_24px_90px_rgba(0,0,0,0.35)] backdrop-blur-xl">
+              <div className="p-4 sm:p-6 lg:p-8">
+                <Outlet />
+              </div>
+            </div>
           </div>
         </div>
Index: frontend/src/pages/Dashboard/components/DashboardSidebar.jsx
===================================================================
--- frontend/src/pages/Dashboard/components/DashboardSidebar.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/components/DashboardSidebar.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -89,5 +89,5 @@
   return (
     <Sidebar
-      className="py-6"
+      className="py-6 border-r border-white/10 bg-base-100/60 backdrop-blur-xl"
       closeButtonClassName="fixed top-4 right-4"
       collapsible="none"
@@ -114,6 +114,6 @@
                     className={
                       isActive
-                        ? "!bg-green-400 !text-black hover:!bg-green-400 active:!bg-green-400"
-                        : ""
+                        ? "bg-green-400! text-black! hover:bg-green-400! active:bg-green-400!"
+                        : "hover:bg-white/10!"
                     }
                   >
@@ -156,6 +156,6 @@
                     className={
                       isActive
-                        ? "!bg-green-400 !text-black hover:!bg-green-400 active:!bg-green-400"
-                        : ""
+                        ? "bg-green-400! text-black! hover:bg-green-400! active:bg-green-400!"
+                        : "hover:bg-white/10!"
                     }
                   >
@@ -185,5 +185,5 @@
           <Button
             variant="secondary"
-            className="w-full justify-start gap-2 !bg-green-400 !text-black hover:!bg-green-500"
+            className="w-full justify-start gap-2 bg-green-400! text-black! hover:bg-green-500!"
             type="button"
             onClick={() => setCustomOpen(true)}
Index: frontend/src/pages/Dashboard/components/DashboardTopbar.jsx
===================================================================
--- frontend/src/pages/Dashboard/components/DashboardTopbar.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/components/DashboardTopbar.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -10,5 +10,5 @@
     <>
       {/* Desktop */}
-      <div className="sticky top-0 z-30 hidden min-h-16 w-full items-center border-b border-base-300 bg-base-100 px-6 lg:flex lg:px-8">
+      <div className="sticky top-0 z-30 hidden min-h-16 w-full items-center border-b border-white/10 bg-base-100/70 px-6 backdrop-blur-xl lg:flex lg:px-8">
         <div className="mx-auto grid size-full grid-cols-1 items-center justify-end justify-items-end gap-4 lg:grid-cols-[1fr_max-content] lg:justify-between lg:justify-items-stretch">
           <div className="flex items-center gap-3"></div>
@@ -18,5 +18,5 @@
             <button
               type="button"
-              className="btn btn-ghost btn-sm"
+              className="btn btn-ghost btn-sm hover:bg-white/10 transition"
               onClick={onLogout}
               aria-label="Log out"
@@ -30,5 +30,5 @@
 
       {/* Mobile */}
-      <div className="fixed left-0 right-0 top-0 z-30 flex min-h-16 w-full items-center justify-between border-b border-base-300 bg-base-100 px-6 lg:hidden">
+      <div className="fixed left-0 right-0 top-0 z-30 flex min-h-16 w-full items-center justify-between border-b border-white/10 bg-base-100/70 px-4 backdrop-blur-xl sm:px-6 lg:hidden">
         <div className="flex items-center gap-4">
           <Link
@@ -44,5 +44,5 @@
           <button
             type="button"
-            className="btn btn-ghost btn-sm"
+            className="btn btn-ghost btn-sm hover:bg-white/10 transition"
             onClick={onLogout}
             aria-label="Log out"
Index: frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -1,16 +1,581 @@
-import React from "react";
-
-const ControlCenter = () => {
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+
+import api from "../../../../api/axios";
+import { getFinanceStatus, getIncomes } from "../../../../api/finance";
+import { getDisciplineStatus } from "../../../../api/discipline";
+import { getDailyCompletions } from "../../../../api/dailyCompletion";
+
+import MultiPercentChangeAreaChart from "../../../../components/graphs/MultiPercentChangeAreaChart.jsx";
+import TimeRangeToggle from "../../../../components/graphs/TimeRangeToggle.jsx";
+import { percentChangeSeries, sumByTimeBucket } from "../../../../utils/timeSeries.js";
+
+import TrainingProgressCard from "../Training/components/TrainingProgressCard.jsx";
+import WeightProgressCard from "../Weight/components/WeightProgressCard.jsx";
+import FinanceProgressCard from "../Finance/components/FinanceProgressCard.jsx";
+import DisciplineProgressCard from "../Discipline/components/DisciplineProgressCard.jsx";
+import InvestingProgressCard from "../Investing/components/InvestingProgressCard.jsx";
+
+function Legend({ items, enabledKeys, onToggle, onShowAll, onHideAll }) {
+  if (!items?.length) return null;
+
   return (
-    <div className="space-y-4">
-      <h1 className="text-2xl font-bold">Control Center</h1>
-      <div className="card bg-base-200 border border-base-300">
-        <div className="card-body">
-          <p className="opacity-80">Control Center content goes here.</p>
-        </div>
+    <div className="flex flex-col gap-2">
+      <div className="flex flex-wrap justify-end gap-2">
+        <button
+          type="button"
+          className="btn btn-ghost btn-xs hover:bg-white/10"
+          onClick={onShowAll}
+        >
+          Show all
+        </button>
+        <button
+          type="button"
+          className="btn btn-ghost btn-xs hover:bg-white/10"
+          onClick={onHideAll}
+        >
+          Hide all
+        </button>
+      </div>
+
+      <div className="flex flex-wrap gap-x-4 gap-y-2 text-xs">
+        {items.map((it) => {
+          const enabled = enabledKeys?.has(it.key);
+          return (
+            <button
+              key={it.key}
+              type="button"
+              onClick={() => onToggle?.(it.key)}
+              className={
+                "flex items-center gap-2 rounded-full border px-2.5 py-1 transition " +
+                (enabled
+                  ? "border-base-300 bg-base-100 hover:bg-base-100/70"
+                  : "border-transparent bg-base-300/30 opacity-60 hover:opacity-80")
+              }
+              title={enabled ? "Click to hide" : "Click to show"}
+            >
+              <span
+                className="inline-block h-2.5 w-2.5 rounded-full"
+                style={{ backgroundColor: it.color }}
+                aria-hidden
+              />
+              <span className="opacity-90">{it.label}</span>
+            </button>
+          );
+        })}
       </div>
     </div>
   );
-};
-
-export default ControlCenter;
+}
+
+export default function ControlCenter() {
+  const [range, setRange] = useState("weekly");
+
+  const [visibleSeries, setVisibleSeries] = useState(() => new Set());
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [error, setError] = useState("");
+
+  const [tracking, setTracking] = useState({
+    weight: false,
+    training: false,
+    finance: false,
+    discipline: false,
+    investing: false,
+  });
+
+  const [trainingSessions, setTrainingSessions] = useState([]);
+  const [weightProfile, setWeightProfile] = useState(null);
+  const [weightIntakes, setWeightIntakes] = useState([]);
+  const [financeIncomes, setFinanceIncomes] = useState([]);
+  const [disciplineCompletions, setDisciplineCompletions] = useState([]);
+  const [investingAssets, setInvestingAssets] = useState([]);
+
+  const fetchAllPaged = useCallback(async (fnPage, pageSize, maxPages = 400) => {
+    let p = 0;
+    let hasMorePages = true;
+    const all = [];
+    while (hasMorePages) {
+      const res = await fnPage(p, pageSize);
+      const items = res.items;
+      all.push(...items);
+      hasMorePages = Boolean(res.hasMore) && items.length > 0;
+      p += 1;
+      if (p > maxPages) break;
+    }
+    return all;
+  }, []);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoading(true);
+        setError("");
+
+        // Determine tracking flags (cheap endpoints)
+        const [weightRes, trainingRes, investingRes, financeTracking, disciplineTracking] =
+          await Promise.all([
+            api.get("/weight/status"),
+            api.get("/training/status"),
+            api.get("/investing/status"),
+            getFinanceStatus(),
+            getDisciplineStatus(),
+          ]);
+
+        const nextTracking = {
+          weight: Boolean(weightRes?.data?.tracking),
+          training: Boolean(trainingRes?.data?.tracking),
+          investing: Boolean(investingRes?.data?.tracking),
+          finance: Boolean(financeTracking),
+          discipline: Boolean(disciplineTracking),
+        };
+
+        if (cancelled) return;
+        setTracking(nextTracking);
+
+        // Data fetches (only for tracked features)
+        const tasks = [];
+
+        if (nextTracking.training) {
+          tasks.push(
+            fetchAllPaged(
+              async (page, size) => {
+                const resp = await api.get("/training/sessions", { params: { page, size } });
+                const data = resp?.data ?? {};
+                return {
+                  items: Array.isArray(data.sessions) ? data.sessions : [],
+                  hasMore: Boolean(data.hasMore),
+                };
+              },
+              500,
+              200,
+            ).then((all) => setTrainingSessions(all)),
+          );
+        }
+
+        if (nextTracking.weight) {
+          tasks.push(
+            api.get("/weight/profile").then((res) => setWeightProfile(res?.data ?? null)),
+          );
+          tasks.push(
+            fetchAllPaged(
+              async (page, size) => {
+                const resp = await api.get("/weight/intakes", { params: { page, size } });
+                const data = resp?.data ?? {};
+                return {
+                  items: Array.isArray(data.intakes) ? data.intakes : [],
+                  hasMore: Boolean(data.hasMore),
+                };
+              },
+              500,
+              200,
+            ).then((all) => setWeightIntakes(all)),
+          );
+        }
+
+        if (nextTracking.finance) {
+          tasks.push(
+            fetchAllPaged(
+              async (page, size) => {
+                const data = await getIncomes({ page, size });
+                return {
+                  items: Array.isArray(data?.incomes) ? data.incomes : [],
+                  hasMore: Boolean(data?.hasMore),
+                };
+              },
+              500,
+              200,
+            ).then((all) => setFinanceIncomes(all)),
+          );
+        }
+
+        if (nextTracking.discipline) {
+          tasks.push(
+            fetchAllPaged(
+              async (page, size) => {
+                const data = await getDailyCompletions({ page, size });
+                return {
+                  items: Array.isArray(data?.completions) ? data.completions : [],
+                  hasMore: Boolean(data?.hasMore),
+                };
+              },
+              500,
+              400,
+            ).then((all) => setDisciplineCompletions(all)),
+          );
+        }
+
+        if (nextTracking.investing) {
+          tasks.push(
+            fetchAllPaged(
+              async (page, size) => {
+                const resp = await api.get("/investing/assets", { params: { page, size } });
+                const data = resp?.data ?? {};
+                return {
+                  items: Array.isArray(data.assets) ? data.assets : [],
+                  hasMore: Boolean(data.hasMore),
+                };
+              },
+              200,
+              200,
+            ).then((all) => setInvestingAssets(all)),
+          );
+        }
+
+        await Promise.all(tasks);
+      } catch (e) {
+        if (cancelled) return;
+        setError(e?.response?.data?.message || "Failed to load Control Center.");
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    })();
+
+    return () => {
+      cancelled = true;
+    };
+  }, [fetchAllPaged]);
+
+  const weightGoalCalories = weightProfile?.goalCalories ?? null;
+  const isBulking = useMemo(() => {
+    const current = Number(weightProfile?.weight);
+    const goal = Number(weightProfile?.goalWeight);
+    if (!Number.isFinite(current) || !Number.isFinite(goal)) return null;
+    return goal > current;
+  }, [weightProfile]);
+
+  const combinedLegend = useMemo(
+    () =>
+      [
+        tracking.training ? { key: "training", label: "Training", color: "#22c55e" } : null,
+        tracking.weight ? { key: "weight", label: "Weight", color: "#60a5fa" } : null,
+        tracking.finance ? { key: "finance", label: "Finance", color: "#a855f7" } : null,
+        tracking.discipline ? { key: "discipline", label: "Discipline", color: "#fbbf24" } : null,
+        tracking.investing ? { key: "investing", label: "Investing", color: "#14b8a6" } : null,
+      ].filter(Boolean),
+    [tracking],
+  );
+
+  // Initialize visible series to "all tracked" whenever tracking changes.
+  useEffect(() => {
+    setVisibleSeries(new Set((combinedLegend ?? []).map((x) => x.key)));
+  }, [combinedLegend]);
+
+  const onToggleSeries = useCallback((key) => {
+    setVisibleSeries((prev) => {
+      const next = new Set(prev);
+      if (next.has(key)) next.delete(key);
+      else next.add(key);
+      return next;
+    });
+  }, []);
+
+  const onShowAllSeries = useCallback(() => {
+    setVisibleSeries(new Set((combinedLegend ?? []).map((x) => x.key)));
+  }, [combinedLegend]);
+
+  const onHideAllSeries = useCallback(() => {
+    setVisibleSeries(new Set());
+  }, []);
+
+  const combinedSeriesList = useMemo(() => {
+    const out = [];
+
+    if (tracking.training) {
+      const buckets = sumByTimeBucket(
+        trainingSessions,
+        range,
+        (s) => s.date,
+        (s) => s.calories,
+      );
+      out.push({
+        name: "Training",
+        key: "training",
+        color: "#22c55e",
+        points: percentChangeSeries(buckets),
+      });
+    }
+
+    if (tracking.finance) {
+      const buckets = sumByTimeBucket(
+        financeIncomes,
+        range,
+        (i) => i.date,
+        (i) => i.amount,
+      );
+      out.push({
+        name: "Finance",
+        key: "finance",
+        color: "#a855f7",
+        points: percentChangeSeries(buckets),
+      });
+    }
+
+    if (tracking.discipline) {
+      // average completion per bucket
+      const sums = sumByTimeBucket(
+        disciplineCompletions,
+        range,
+        (c) => c.date,
+        (c) => Number(c?.procent) || 0,
+      );
+      const counts = sumByTimeBucket(
+        disciplineCompletions,
+        range,
+        (c) => c.date,
+        () => 1,
+      );
+      const countByTs = new Map(counts.map((p) => [p.ts, p.value]));
+      const avgPoints = sums.map((p) => {
+        const count = Number(countByTs.get(p.ts) ?? 0);
+        const avg = count > 0 ? Number(p.value ?? 0) / count : 0;
+        return { ts: p.ts, value: avg };
+      });
+
+      out.push({
+        name: "Discipline",
+        key: "discipline",
+        color: "#fbbf24",
+        points: percentChangeSeries(avgPoints),
+      });
+    }
+
+    if (tracking.investing) {
+      const buckets = sumByTimeBucket(
+        investingAssets,
+        range,
+        (a) => a.buyDate,
+        (a) => (Number(a?.quantity) || 0) * (Number(a?.buyPrice) || 0),
+      );
+      out.push({
+        name: "Investing",
+        key: "investing",
+        color: "#14b8a6",
+        points: percentChangeSeries(buckets),
+      });
+    }
+
+    if (tracking.weight) {
+      // replicate WeightProgressCard base series (closeness %) then % change.
+      const totals = sumByTimeBucket(
+        weightIntakes,
+        range,
+        (i) => i.date,
+        (i) => Number(i?.calories) || 0,
+      );
+      const goalTotals = sumByTimeBucket(
+        weightIntakes,
+        range,
+        (i) => i.date,
+        (i) => {
+          const goal = Number(weightGoalCalories);
+          if (!Number.isFinite(goal) || goal <= 0) return 0;
+          const burned = Number(i?.burnedCalories) || 0;
+          const trained = Boolean(i?.trainedThatDay);
+          return trained ? goal + burned : goal;
+        },
+      );
+      const goalByTs = new Map(goalTotals.map((p) => [p.ts, p.value]));
+      const closeness = totals.map((p) => {
+        const adjustedGoal = Number(goalByTs.get(p.ts) ?? 0);
+        const calories = Number(p.value ?? 0);
+        if (!Number.isFinite(adjustedGoal) || adjustedGoal <= 0) return { ts: p.ts, value: 0 };
+        const pct = 100 - (Math.abs(calories - adjustedGoal) / adjustedGoal) * 100;
+        const clamped = Math.min(200, Math.max(0, pct));
+        return { ts: p.ts, value: clamped };
+      });
+      let weightSeries = percentChangeSeries(closeness);
+      if (isBulking === false) {
+        weightSeries = weightSeries.map((p) => ({ ...p, value: -Number(p.value ?? 0) }));
+      }
+      // Weight graph is reversed (matches WeightProgressCard)
+      weightSeries = weightSeries.map((p) => ({ ...p, value: -Number(p.value ?? 0) }));
+
+      out.push({
+        name: "Weight",
+        key: "weight",
+        color: "#60a5fa",
+        points: weightSeries,
+      });
+    }
+
+    return out;
+  }, [
+    disciplineCompletions,
+    financeIncomes,
+    investingAssets,
+    isBulking,
+    range,
+    tracking,
+    trainingSessions,
+    weightGoalCalories,
+    weightIntakes,
+  ]);
+
+  const filteredCombinedSeriesList = useMemo(() => {
+    return (combinedSeriesList ?? []).filter((s) => visibleSeries.has(s.key));
+  }, [combinedSeriesList, visibleSeries]);
+
+  const trackedCount = Object.values(tracking).filter(Boolean).length;
+
+  if (isLoading) {
+    return (
+      <div className="space-y-6">
+        <h1 className="text-2xl font-bold">Control Center</h1>
+        <div className="card bg-base-200 border border-base-300">
+          <div className="card-body">
+            <p className="opacity-80">Loading…</p>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  if (error) {
+    return (
+      <div className="space-y-6">
+        <h1 className="text-2xl font-bold">Control Center</h1>
+        <div className="alert alert-error">
+          <span>{error}</span>
+        </div>
+      </div>
+    );
+  }
+
+  if (trackedCount === 0) {
+    return (
+      <div className="space-y-6">
+        <h1 className="text-2xl font-bold">Control Center</h1>
+        <div className="card bg-base-200 border border-base-300">
+          <div className="card-body">
+            <p className="opacity-80">
+              Start tracking at least one area (Weight / Training / Finance / Discipline / Investing) to see your dashboard.
+            </p>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="space-y-8">
+      <div className="flex items-start justify-between gap-4 flex-wrap">
+        <div>
+          <h1 className="text-2xl font-bold tracking-tight">Control Center</h1>
+          <p className="mt-1 text-sm opacity-70">
+            Correlate your progress across everything you track.
+          </p>
+        </div>
+        <TimeRangeToggle value={range} onChange={setRange} />
+      </div>
+
+      <div className="card bg-base-200/60 border border-white/10 shadow-[0_0_0_1px_rgba(255,255,255,0.04)] backdrop-blur">
+        <div className="card-body">
+          <div className="flex items-start justify-between gap-4 flex-wrap">
+            <div>
+              <h2 className="card-title">Correlation</h2>
+              <p className="mt-1 text-xs opacity-70">
+                Toggle lines to isolate signals. All metrics are % change per {range} bucket.
+              </p>
+            </div>
+            <Legend
+              items={combinedLegend}
+              enabledKeys={visibleSeries}
+              onToggle={onToggleSeries}
+              onShowAll={onShowAllSeries}
+              onHideAll={onHideAllSeries}
+            />
+          </div>
+
+          <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100 p-2">
+            {filteredCombinedSeriesList.length === 0 ? (
+              <div className="flex h-80 items-center justify-center text-sm opacity-70">
+                Toggle at least one line in the legend to display the chart.
+              </div>
+            ) : filteredCombinedSeriesList.every((s) => (s.points?.length ?? 0) < 2) ? (
+              <div className="flex h-80 items-center justify-center text-sm opacity-70">
+                Add more history to see correlations.
+              </div>
+            ) : (
+              <MultiPercentChangeAreaChart
+                seriesList={filteredCombinedSeriesList}
+                granularity={range}
+                height={340}
+              />
+            )}
+          </div>
+
+          <div className="mt-3 text-xs opacity-70">
+            Each line uses the same time bucket toggle, but has its own base metric (calories, income totals, completion average, etc.).
+          </div>
+        </div>
+      </div>
+
+      <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 gap-6">
+        {tracking.training ? (
+          <div>
+            <div className="mb-2 flex items-center justify-between">
+              <h3 className="font-semibold">Training</h3>
+              <span className="badge badge-ghost" style={{ borderColor: "#22c55e" }}>
+                % change
+              </span>
+            </div>
+            <TrainingProgressCard sessions={trainingSessions} />
+          </div>
+        ) : null}
+
+        {tracking.weight ? (
+          <div>
+            <div className="mb-2 flex items-center justify-between">
+              <h3 className="font-semibold">Weight</h3>
+              <span className="badge badge-ghost" style={{ borderColor: "#60a5fa" }}>
+                % change
+              </span>
+            </div>
+            <WeightProgressCard
+              intakes={weightIntakes}
+              goalCalories={weightGoalCalories}
+              isBulking={isBulking}
+            />
+          </div>
+        ) : null}
+
+        {tracking.finance ? (
+          <div>
+            <div className="mb-2 flex items-center justify-between">
+              <h3 className="font-semibold">Finance</h3>
+              <span className="badge badge-ghost" style={{ borderColor: "#a855f7" }}>
+                % change
+              </span>
+            </div>
+            <FinanceProgressCard incomes={financeIncomes} />
+          </div>
+        ) : null}
+
+        {tracking.discipline ? (
+          <div>
+            <div className="mb-2 flex items-center justify-between">
+              <h3 className="font-semibold">Discipline</h3>
+              <span className="badge badge-ghost" style={{ borderColor: "#fbbf24" }}>
+                % change
+              </span>
+            </div>
+            <DisciplineProgressCard completions={disciplineCompletions} />
+          </div>
+        ) : null}
+
+        {tracking.investing ? (
+          <div>
+            <div className="mb-2 flex items-center justify-between">
+              <h3 className="font-semibold">Investing</h3>
+              <span className="badge badge-ghost" style={{ borderColor: "#14b8a6" }}>
+                % change
+              </span>
+            </div>
+            <InvestingProgressCard assets={investingAssets} />
+          </div>
+        ) : null}
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Discipline/DisciplineTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/DisciplineTracking.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/Discipline/DisciplineTracking.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -1,4 +1,3 @@
 import React, { useCallback, useEffect, useMemo, useState } from "react";
-import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
 
 import {
@@ -16,8 +15,10 @@
 
 import TaskList from "./components/TaskList.jsx";
+import DisciplineProgressCard from "./components/DisciplineProgressCard.jsx";
 
 export default function DisciplineTracking() {
   const pageSize = 50;
   const completionPageSize = 14;
+  const graphCompletionPageSize = 500;
 
   const todayIso = useMemo(() => {
@@ -65,4 +66,23 @@
       nextHasMore: Boolean(data?.hasMore),
     };
+  }, []);
+
+  const [graphCompletions, setGraphCompletions] = useState([]);
+
+  const fetchAllCompletionsForGraph = useCallback(async () => {
+    let p = 0;
+    let hasMorePages = true;
+    const all = [];
+
+    while (hasMorePages) {
+      const data = await getDailyCompletions({ page: p, size: graphCompletionPageSize });
+      const chunk = Array.isArray(data?.completions) ? data.completions : [];
+      all.push(...chunk);
+      hasMorePages = Boolean(data?.hasMore) && chunk.length > 0;
+      p += 1;
+      if (p > 400) break;
+    }
+
+    return all;
   }, []);
 
@@ -96,7 +116,11 @@
         setIsLoadingCompletions(true);
         setCompletionError("");
-        const { nextCompletions, nextHasMore } = await fetchCompletionsPage(0);
+        const [{ nextCompletions, nextHasMore }, allForGraph] = await Promise.all([
+          fetchCompletionsPage(0),
+          fetchAllCompletionsForGraph(),
+        ]);
         if (cancelled) return;
         setDailyCompletions(nextCompletions);
+        setGraphCompletions(allForGraph);
         setCompletionHasMore(nextHasMore);
         setCompletionPage(0);
@@ -115,5 +139,5 @@
       cancelled = true;
     };
-  }, [fetchCompletionsPage]);
+  }, [fetchAllCompletionsForGraph, fetchCompletionsPage]);
 
   const onLoadMoreCompletions = async () => {
@@ -240,4 +264,14 @@
         setCompletionHasMore(nextHasMore);
         setCompletionPage(0);
+
+        // also refresh graph dataset
+        try {
+          const allForGraph = await getDailyCompletions({ page: 0, size: 1000 });
+          setGraphCompletions(
+            Array.isArray(allForGraph?.completions) ? allForGraph.completions : [],
+          );
+        } catch {
+          // ignore
+        }
       } catch {
         // ignore
@@ -316,17 +350,5 @@
           ) : null}
 
-          <div className="card bg-base-200 border border-base-300">
-            <div className="card-body">
-              <h2 className="card-title">Progress</h2>
-              <p className="text-sm opacity-80">(Placeholder graph for now)</p>
-              <div className="mt-4">
-                <img
-                  src={graphPlaceholder}
-                  alt="Discipline graph placeholder"
-                  className="w-full"
-                />
-              </div>
-            </div>
-          </div>
+          <DisciplineProgressCard completions={graphCompletions} />
 
           <div className="card bg-base-200 border border-base-300">
Index: frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineProgressCard.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
+++ frontend/src/pages/Dashboard/pages/Discipline/components/DisciplineProgressCard.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -0,0 +1,78 @@
+import React, { useMemo, useState } from "react";
+
+import TimeRangeToggle from "../../../../../components/graphs/TimeRangeToggle.jsx";
+import PercentChangeAreaChart from "../../../../../components/graphs/PercentChangeAreaChart.jsx";
+import { percentChangeSeries, sumByTimeBucket } from "../../../../../utils/timeSeries.js";
+
+export default function DisciplineProgressCard({ completions }) {
+  const [range, setRange] = useState("weekly");
+
+  const points = useMemo(() => {
+    // Bucket daily completion percentages by time range.
+    // For weekly/monthly/yearly we compute the *average* completion % in that period.
+    const sums = sumByTimeBucket(
+      completions,
+      range,
+      (c) => c.date,
+      (c) => Number(c?.procent) || 0,
+    );
+
+    const counts = sumByTimeBucket(
+      completions,
+      range,
+      (c) => c.date,
+      () => 1,
+    );
+
+    const countByTs = new Map(counts.map((p) => [p.ts, p.value]));
+
+    const avgPoints = sums.map((p) => {
+      const count = Number(countByTs.get(p.ts) ?? 0);
+      const avg = count > 0 ? Number(p.value ?? 0) / count : 0;
+      return { ts: p.ts, value: avg };
+    });
+
+    return percentChangeSeries(avgPoints);
+  }, [completions, range]);
+
+  const latest = points?.length ? points[points.length - 1] : null;
+  const latestPct = latest ? Number(latest.value ?? 0) : 0;
+  const latestBase = latest ? Number(latest.base ?? 0) : 0;
+
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <div className="flex items-center justify-between gap-4">
+          <h2 className="card-title">Progress</h2>
+          <div className="flex items-center gap-3">
+            <span className="badge badge-ghost">
+              {latestPct >= 0 ? "+" : ""}
+              {latestPct.toFixed(1)}% (last)
+            </span>
+            <TimeRangeToggle value={range} onChange={setRange} />
+          </div>
+        </div>
+
+        <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100 p-2">
+          {points.length < 2 ? (
+            <div className="flex h-65 items-center justify-center text-sm opacity-70">
+              Add at least 2 daily completion records to see % change.
+            </div>
+          ) : (
+            <PercentChangeAreaChart
+              points={points}
+              granularity={range}
+              height={260}
+              positiveColor="#fbbf24"
+            />
+          )}
+        </div>
+
+        <div className="mt-2 text-xs opacity-70">
+          Showing percentage change in average completion % per {range} bucket. Latest bucket avg: {Math.round(latestBase)}%.
+        </div>
+      </div>
+    </div>
+  );
+}
+
Index: frontend/src/pages/Dashboard/pages/Finance/FinanceTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Finance/FinanceTracking.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/Finance/FinanceTracking.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -2,5 +2,4 @@
 import { useNavigate } from "react-router-dom";
 
-import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
 import {
   getFinanceProfile,
@@ -12,4 +11,5 @@
 import FinanceIncomesTable from "./components/FinanceIncomesTable.jsx";
 import FinanceStartForm from "./components/FinanceStartForm.jsx";
+import FinanceProgressCard from "./components/FinanceProgressCard.jsx";
 
 function toNumber(value) {
@@ -21,4 +21,5 @@
   const navigate = useNavigate();
   const pageSize = 5;
+  const graphPageSize = 500;
 
   const [profile, setProfile] = useState(null);
@@ -76,4 +77,23 @@
   }, []);
 
+  const [graphIncomes, setGraphIncomes] = useState([]);
+
+  const fetchAllIncomesForGraph = useCallback(async () => {
+    let p = 0;
+    let hasMorePages = true;
+    const all = [];
+
+    while (hasMorePages) {
+      const data = await getIncomes({ page: p, size: graphPageSize });
+      const chunk = Array.isArray(data?.incomes) ? data.incomes : [];
+      all.push(...chunk);
+      hasMorePages = Boolean(data?.hasMore) && chunk.length > 0;
+      p += 1;
+      if (p > 200) break;
+    }
+
+    return all;
+  }, []);
+
   useEffect(() => {
     let cancelled = false;
@@ -83,5 +103,9 @@
         setError("");
 
-        const [p, inc] = await Promise.all([getFinanceProfile(), fetchPage(0)]);
+        const [p, inc, allInc] = await Promise.all([
+          getFinanceProfile(),
+          fetchPage(0),
+          fetchAllIncomesForGraph(),
+        ]);
         if (cancelled) return;
         setProfile(p);
@@ -94,4 +118,5 @@
         });
         setIncomes(inc.nextIncomes);
+        setGraphIncomes(allInc);
         setHasMore(inc.nextHasMore);
         setPage(0);
@@ -107,5 +132,5 @@
       cancelled = true;
     };
-  }, [fetchPage]);
+  }, [fetchAllIncomesForGraph, fetchPage]);
 
   const onEdit = () => {
@@ -205,13 +230,5 @@
       )}
 
-      <div className="card bg-base-200 border border-base-300">
-        <div className="card-body">
-          <h2 className="card-title">Progress</h2>
-          <p className="text-sm opacity-80">(Placeholder graph for now)</p>
-          <div className="mt-4">
-            <img src={graphPlaceholder} alt="Finance graph placeholder" className="w-full" />
-          </div>
-        </div>
-      </div>
+      <FinanceProgressCard incomes={graphIncomes} />
 
       <FinanceIncomesTable
Index: frontend/src/pages/Dashboard/pages/Finance/components/FinanceProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Finance/components/FinanceProgressCard.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
+++ frontend/src/pages/Dashboard/pages/Finance/components/FinanceProgressCard.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -0,0 +1,60 @@
+import React, { useMemo, useState } from "react";
+
+import TimeRangeToggle from "../../../../../components/graphs/TimeRangeToggle.jsx";
+import PercentChangeAreaChart from "../../../../../components/graphs/PercentChangeAreaChart.jsx";
+import { percentChangeSeries, sumByTimeBucket } from "../../../../../utils/timeSeries.js";
+
+export default function FinanceProgressCard({ incomes }) {
+  const [range, setRange] = useState("weekly");
+
+  const points = useMemo(() => {
+    const buckets = sumByTimeBucket(
+      incomes,
+      range,
+      (i) => i.date,
+      (i) => i.amount,
+    );
+    return percentChangeSeries(buckets);
+  }, [incomes, range]);
+
+  const latest = points?.length ? points[points.length - 1] : null;
+  const latestPct = latest ? Number(latest.value ?? 0) : 0;
+  const latestBase = latest ? Number(latest.base ?? 0) : 0;
+
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <div className="flex items-center justify-between gap-4">
+          <h2 className="card-title">Progress</h2>
+          <div className="flex items-center gap-3">
+            <span className="badge badge-ghost">
+              {latestPct >= 0 ? "+" : ""}
+              {latestPct.toFixed(1)}% (last)
+            </span>
+            <TimeRangeToggle value={range} onChange={setRange} />
+          </div>
+        </div>
+
+        <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100 p-2">
+          {points.length < 2 ? (
+            <div className="flex h-65 items-center justify-center text-sm opacity-70">
+              Add at least 2 incomes to see % change.
+            </div>
+          ) : (
+            <PercentChangeAreaChart
+              points={points}
+              granularity={range}
+              height={260}
+              positiveColor="#a855f7"
+            />
+          )}
+        </div>
+
+        <div className="mt-2 text-xs opacity-70">
+          Showing percentage change in income totals per {range} bucket. Latest bucket total: {Math.round(latestBase)}.
+        </div>
+      </div>
+    </div>
+  );
+}
+
Index: frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -3,5 +3,4 @@
 
 import api from "../../../../api/axios";
-import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
 import { fetchCurrentPricesBySymbol } from "../../../../api/twelveData";
 
@@ -35,4 +34,5 @@
   const navigate = useNavigate();
   const pageSize = 5;
+  const graphPageSize = 200;
 
   const [assets, setAssets] = useState([]);
@@ -62,7 +62,7 @@
   );
 
-  const fetchPage = useCallback(async (nextPage) => {
+  const fetchPage = useCallback(async (nextPage, size = pageSize) => {
     const resp = await api.get("/investing/assets", {
-      params: { page: nextPage, size: pageSize },
+      params: { page: nextPage, size },
     });
     const data = resp?.data ?? {};
@@ -72,4 +72,22 @@
   }, []);
 
+  const [graphAssets, setGraphAssets] = useState([]);
+
+  const fetchAllAssetsForGraph = useCallback(async () => {
+    let p = 0;
+    let hasMorePages = true;
+    const all = [];
+
+    while (hasMorePages) {
+      const { nextAssets: chunk, nextHasMore } = await fetchPage(p, graphPageSize);
+      all.push(...chunk);
+      hasMorePages = nextHasMore && chunk.length > 0;
+      p += 1;
+      if (p > 200) break;
+    }
+
+    return all;
+  }, [fetchPage]);
+
   useEffect(() => {
     let cancelled = false;
@@ -78,8 +96,12 @@
         setIsLoading(true);
         setError("");
-        const { nextAssets, nextHasMore } = await fetchPage(0);
-        if (cancelled) return;
-        setAssets(nextAssets);
-        setHasMore(nextHasMore);
+        const [tableFirstPage, allForGraph] = await Promise.all([
+          fetchPage(0, pageSize),
+          fetchAllAssetsForGraph(),
+        ]);
+        if (cancelled) return;
+        setAssets(tableFirstPage.nextAssets);
+        setHasMore(tableFirstPage.nextHasMore);
+        setGraphAssets(allForGraph);
         setPage(0);
       } catch (e) {
@@ -93,5 +115,5 @@
       cancelled = true;
     };
-  }, [fetchPage]);
+  }, [fetchAllAssetsForGraph, fetchPage]);
 
   const tickerSymbols = useMemo(() => {
@@ -102,4 +124,6 @@
     return Array.from(set).sort();
   }, [assets]);
+
+  const tickerSymbolsKey = useMemo(() => tickerSymbols.join(","), [tickerSymbols]);
 
   useEffect(() => {
@@ -143,5 +167,5 @@
       if (intervalId) clearInterval(intervalId);
     };
-  }, [tickerSymbols.join(",")]);
+  }, [tickerSymbolsKey, tickerSymbols]);
 
   const onLoadMore = async () => {
@@ -190,5 +214,5 @@
         onAddAsset={() => navigate("/dashboard/investing/assets/new")}
       />
-      <InvestingProgressCard graphSrc={graphPlaceholder} />
+      <InvestingProgressCard assets={graphAssets} />
 
       {quotesError ? (
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -1,24 +1,59 @@
-import React from "react";
+import React, { useMemo, useState } from "react";
 
-export default function InvestingProgressCard({ graphSrc }) {
+import TimeRangeToggle from "../../../../../components/graphs/TimeRangeToggle.jsx";
+import PercentChangeAreaChart from "../../../../../components/graphs/PercentChangeAreaChart.jsx";
+import { percentChangeSeries, sumByTimeBucket } from "../../../../../utils/timeSeries.js";
+
+export default function InvestingProgressCard({ assets }) {
+  const [range, setRange] = useState("weekly");
+
+  const points = useMemo(() => {
+    // Option B: "net invested momentum".
+    // Since we only have assets (with buy date/price/quantity) and no transaction history,
+    // we treat each asset as a one-time investment at buyDate.
+    const buckets = sumByTimeBucket(
+      assets,
+      range,
+      (a) => a.buyDate,
+      (a) => (Number(a?.quantity) || 0) * (Number(a?.buyPrice) || 0),
+    );
+    return percentChangeSeries(buckets);
+  }, [assets, range]);
+
+  const latest = points?.length ? points[points.length - 1] : null;
+  const latestPct = latest ? Number(latest.value ?? 0) : 0;
+  const latestBase = latest ? Number(latest.base ?? 0) : 0;
+
   return (
     <div className="card bg-base-200 border border-base-300">
       <div className="card-body">
         <div className="flex items-center justify-between gap-4">
-          <div>
-            <h2 className="text-lg font-semibold">Progress</h2>
-            <p className="opacity-80">
-              Graph placeholder — we’ll visualize portfolio value here.
-            </p>
+          <h2 className="card-title">Progress</h2>
+          <div className="flex items-center gap-3">
+            <span className="badge badge-ghost">
+              {latestPct >= 0 ? "+" : ""}
+              {latestPct.toFixed(1)}% (last)
+            </span>
+            <TimeRangeToggle value={range} onChange={setRange} />
           </div>
         </div>
-        <div className="mt-4 overflow-hidden rounded-xl bg-base-300/30 border border-base-300">
-          <div className="p-4">
-            <img
-              src={graphSrc}
-              alt="Graph placeholder"
-              className="w-full max-h-64 object-contain opacity-90"
+
+        <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100 p-2">
+          {points.length < 2 ? (
+            <div className="flex h-65 items-center justify-center text-sm opacity-70">
+              Add at least 2 investments to see % change.
+            </div>
+          ) : (
+            <PercentChangeAreaChart
+              points={points}
+              granularity={range}
+              height={260}
+              positiveColor="#14b8a6"
             />
-          </div>
+          )}
+        </div>
+
+        <div className="mt-2 text-xs opacity-70">
+          Showing percentage change in invested amount per {range} bucket. Latest bucket invested: {Math.round(latestBase)}.
         </div>
       </div>
Index: frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -27,4 +27,5 @@
   const navigate = useNavigate();
   const pageSize = 5;
+  const graphPageSize = 500;
 
   const [sessions, setSessions] = useState([]);
@@ -45,7 +46,7 @@
   );
 
-  const fetchPage = useCallback(async (nextPage) => {
+  const fetchPage = useCallback(async (nextPage, size = pageSize) => {
     const resp = await api.get("/training/sessions", {
-      params: { page: nextPage, size: pageSize },
+      params: { page: nextPage, size },
     });
     const data = resp?.data ?? {};
@@ -55,4 +56,23 @@
   }, []);
 
+  const fetchAllForGraph = useCallback(async () => {
+    let p = 0;
+    let hasMorePages = true;
+    const all = [];
+
+    while (hasMorePages) {
+      const { nextSessions, nextHasMore } = await fetchPage(p, graphPageSize);
+      all.push(...nextSessions);
+      hasMorePages = nextHasMore && nextSessions.length > 0;
+      p += 1;
+      // Basic safety to avoid accidental infinite loops if backend misbehaves.
+      if (p > 200) break;
+    }
+
+    return all;
+  }, [fetchPage]);
+
+  const [graphSessions, setGraphSessions] = useState([]);
+
   useEffect(() => {
     let cancelled = false;
@@ -61,8 +81,12 @@
         setIsLoading(true);
         setError("");
-        const { nextSessions, nextHasMore } = await fetchPage(0);
+        const [tableFirstPage, allForGraph] = await Promise.all([
+          fetchPage(0, pageSize),
+          fetchAllForGraph(),
+        ]);
         if (cancelled) return;
-        setSessions(nextSessions);
-        setHasMore(nextHasMore);
+        setSessions(tableFirstPage.nextSessions);
+        setHasMore(tableFirstPage.nextHasMore);
+        setGraphSessions(allForGraph);
         setPage(0);
       } catch (e) {
@@ -78,5 +102,5 @@
       cancelled = true;
     };
-  }, [fetchPage]);
+  }, [fetchAllForGraph, fetchPage]);
 
   const onLoadMore = async () => {
@@ -102,5 +126,5 @@
         onAddSession={() => navigate("/dashboard/training/sessions/new")}
       />
-      <TrainingProgressCard sessions={sessions} />
+      <TrainingProgressCard sessions={graphSessions} />
       <TrainingSessionsTable
         columns={columns}
Index: frontend/src/pages/Dashboard/pages/Weight/WeightTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Weight/WeightTracking.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/Weight/WeightTracking.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -3,5 +3,4 @@
 
 import api from "../../../../api/axios";
-import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
 
 import WeightProgressCard from "./components/WeightProgressCard.jsx";
@@ -37,4 +36,5 @@
   const navigate = useNavigate();
   const pageSize = 5;
+  const graphPageSize = 500;
 
   const [profile, setProfile] = useState(null);
@@ -53,9 +53,9 @@
   const [isSubmittingEdit, setIsSubmittingEdit] = useState(false);
 
-  const fetchPage = useCallback(async (nextPage) => {
+  const fetchPage = useCallback(async (nextPage, size = pageSize) => {
     const [profileRes, intakesRes, trainingRes] = await Promise.all([
       api.get("/weight/profile"),
       api.get("/weight/intakes", {
-        params: { page: nextPage, size: pageSize },
+        params: { page: nextPage, size },
       }),
       api.get("/weight/today-training"),
@@ -69,4 +69,23 @@
   }, []);
 
+  const [graphIntakes, setGraphIntakes] = useState([]);
+
+  const fetchAllIntakesForGraph = useCallback(async () => {
+    let p = 0;
+    let hasMorePages = true;
+    const all = [];
+
+    while (hasMorePages) {
+      const { intakesData } = await fetchPage(p, graphPageSize);
+      const chunk = Array.isArray(intakesData.intakes) ? intakesData.intakes : [];
+      all.push(...chunk);
+      hasMorePages = Boolean(intakesData.hasMore) && chunk.length > 0;
+      p += 1;
+      if (p > 200) break;
+    }
+
+    return all;
+  }, [fetchPage]);
+
   useEffect(() => {
     let cancelled = false;
@@ -75,8 +94,12 @@
         setIsLoading(true);
         setError("");
-        const { profileData, intakesData, trainingData } = await fetchPage(0);
+        const [{ profileData, intakesData, trainingData }, allIntakes] = await Promise.all([
+          fetchPage(0, pageSize),
+          fetchAllIntakesForGraph(),
+        ]);
         if (cancelled) return;
         setProfile(profileData);
         setIntakes(Array.isArray(intakesData.intakes) ? intakesData.intakes : []);
+        setGraphIntakes(allIntakes);
         setHasMore(Boolean(intakesData.hasMore));
         setHasTodayIntake(Boolean(intakesData.hasTodayIntake));
@@ -100,5 +123,5 @@
       cancelled = true;
     };
-  }, [fetchPage, navigate]);
+  }, [fetchAllIntakesForGraph, fetchPage, navigate]);
 
   const onLoadMore = async () => {
@@ -213,5 +236,13 @@
       />
 
-      <WeightProgressCard graphSrc={graphPlaceholder} />
+      <WeightProgressCard
+        intakes={graphIntakes}
+        goalCalories={goalCalories}
+        isBulking={
+          Number.isFinite(Number(profile?.weight)) && Number.isFinite(Number(profile?.goalWeight))
+            ? Number(profile.goalWeight) > Number(profile.weight)
+            : null
+        }
+      />
 
       <WeightIntakesTable
Index: frontend/src/pages/Dashboard/pages/Weight/components/WeightIntakesTable.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Weight/components/WeightIntakesTable.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/Weight/components/WeightIntakesTable.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -136,12 +136,4 @@
 
         <div className="mt-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
-          <button
-            type="button"
-            className="btn bg-green-400! text-black! hover:bg-green-500! border-0"
-            onClick={onAddIntake}
-            disabled={isLoading}
-          >
-            + Add today's intake
-          </button>
 
           <button
Index: frontend/src/pages/Dashboard/pages/Weight/components/WeightProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Weight/components/WeightProgressCard.jsx	(revision b3d5fed65cdab945e6c84835e2e04bc1814d9699)
+++ frontend/src/pages/Dashboard/pages/Weight/components/WeightProgressCard.jsx	(revision a8381b18babc398c094b70c3ca5dc6cb0483c327)
@@ -1,25 +1,110 @@
-import React from "react";
+import React, { useMemo, useState } from "react";
 
-export default function WeightProgressCard({ graphSrc }) {
+import TimeRangeToggle from "../../../../../components/graphs/TimeRangeToggle.jsx";
+import PercentChangeAreaChart from "../../../../../components/graphs/PercentChangeAreaChart.jsx";
+import { percentChangeSeries, sumByTimeBucket } from "../../../../../utils/timeSeries.js";
+
+function clamp(n, min, max) {
+  const x = Number(n);
+  if (!Number.isFinite(x)) return min;
+  return Math.min(max, Math.max(min, x));
+}
+
+export default function WeightProgressCard({ intakes, goalCalories, isBulking }) {
+  const [range, setRange] = useState("weekly");
+
+
+  const points = useMemo(() => {
+    // For a bucket (week/month/year) we aggregate:
+    // - total calories
+    // - total adjusted goal
+    // then compute bucketCloseness% from those totals.
+    const totals = sumByTimeBucket(
+      intakes,
+      range,
+      (i) => i.date,
+      (i) => Number(i?.calories) || 0,
+    );
+
+    const goalTotals = sumByTimeBucket(
+      intakes,
+      range,
+      (i) => i.date,
+      (i) => {
+        const goal = Number(goalCalories);
+        if (!Number.isFinite(goal) || goal <= 0) return 0;
+        const burned = Number(i?.burnedCalories) || 0;
+        const trained = Boolean(i?.trainedThatDay);
+        return trained ? goal + burned : goal;
+      },
+    );
+
+    const goalByTs = new Map(goalTotals.map((p) => [p.ts, p.value]));
+
+    const closenessPoints = totals.map((p) => {
+      const adjustedGoal = Number(goalByTs.get(p.ts) ?? 0);
+      const calories = Number(p.value ?? 0);
+
+      if (!Number.isFinite(adjustedGoal) || adjustedGoal <= 0) {
+        return { ts: p.ts, value: 0 };
+      }
+
+      const pct = 100 - (Math.abs(calories - adjustedGoal) / adjustedGoal) * 100;
+      return { ts: p.ts, value: clamp(pct, 0, 200) };
+    });
+
+    const pctChange = percentChangeSeries(closenessPoints);
+    const adjusted =
+      isBulking === false
+        ? // Cutting: improvements should read as "positive" when the user stays under goal.
+          // Flip the sign so the line direction matches the user's intent.
+          pctChange.map((p) => ({ ...p, value: -Number(p.value ?? 0) }))
+        : pctChange;
+
+    // Reverse the graph line direction (invert Y) as requested.
+    return adjusted.map((p) => ({ ...p, value: -Number(p.value ?? 0) }));
+  }, [intakes, range, goalCalories, isBulking]);
+
+  const latest = points?.length ? points[points.length - 1] : null;
+  const latestPct = latest ? Number(latest.value ?? 0) : 0;
+  const latestBase = latest ? Number(latest.base ?? 0) : 0;
+
   return (
     <div className="card bg-base-200 border border-base-300">
       <div className="card-body">
         <div className="flex items-center justify-between gap-4">
-          <div>
-            <h2 className="text-lg font-semibold">Progress</h2>
-            <p className="opacity-80">
-              Graph placeholder — calorie intake over time will appear here.
-            </p>
+          <h2 className="card-title">Progress</h2>
+          <div className="flex items-center gap-3">
+            <span className="badge badge-ghost">
+              {latestPct >= 0 ? "+" : ""}
+              {latestPct.toFixed(1)}% (last)
+            </span>
+            <TimeRangeToggle value={range} onChange={setRange} />
           </div>
-          <span className="badge badge-ghost">Graph placeholder</span>
         </div>
-        <div className="mt-4 overflow-hidden rounded-xl bg-base-300/30 border border-base-300">
-          <div className="p-4">
-            <img
-              src={graphSrc}
-              alt="Weight progress graph placeholder"
-              className="w-full max-h-64 object-contain opacity-90"
+
+        <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100 p-2">
+          {points.length < 2 ? (
+            <div className="flex h-65 items-center justify-center text-sm opacity-70">
+              Add at least 2 daily intakes to see % change.
+            </div>
+          ) : (
+            <PercentChangeAreaChart
+              points={points}
+              granularity={range}
+              height={260}
+              positiveColor="#60a5fa"
             />
-          </div>
+          )}
+        </div>
+
+        <div className="mt-2 text-xs opacity-70">
+          Showing percentage change in goal-accuracy (closeness %) per {range} bucket.
+          {isBulking === null ? (
+            <> Trend is not adjusted (missing current/goal weight). </>
+          ) : (
+            <> Trend is adjusted for your goal ({isBulking ? "bulking" : "cutting"}). </>
+          )}
+          Latest bucket closeness: {Math.round(latestBase)}%.
         </div>
       </div>
