Index: frontend/src/pages/Dashboard/DashboardLayout.jsx
===================================================================
--- frontend/src/pages/Dashboard/DashboardLayout.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/DashboardLayout.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,44 @@
+import React, { useMemo } from "react";
+import { Outlet, useNavigate } from "react-router-dom";
+
+import { SidebarInset, SidebarProvider } from "@relume_io/relume-ui";
+
+import DashboardSidebar from "./components/DashboardSidebar.jsx";
+import DashboardTopbar from "./components/DashboardTopbar.jsx";
+
+const DashboardLayout = () => {
+  const navigate = useNavigate();
+
+  const user = useMemo(() => {
+    try {
+      const raw = localStorage.getItem("authUser");
+      return raw ? JSON.parse(raw) : null;
+    } catch {
+      return null;
+    }
+  }, []);
+
+  const username = user?.username ?? "";
+
+  const onLogout = () => {
+    localStorage.removeItem("authToken");
+    localStorage.removeItem("authUser");
+    navigate("/", { replace: true });
+  };
+
+  return (
+    <SidebarProvider>
+      <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>
+        </div>
+      </SidebarInset>
+    </SidebarProvider>
+  );
+};
+
+export default DashboardLayout;
Index: frontend/src/pages/Dashboard/components/DashboardSidebar.jsx
===================================================================
--- frontend/src/pages/Dashboard/components/DashboardSidebar.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/components/DashboardSidebar.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,106 @@
+import React from "react";
+import { NavLink, useLocation } from "react-router-dom";
+
+import {
+  Button,
+  Sidebar,
+  SidebarContent,
+  SidebarHeader,
+  SidebarMenu,
+  SidebarMenuButton,
+  SidebarMenuItem,
+} from "@relume_io/relume-ui";
+
+import {
+  MdTune,
+  MdFitnessCenter,
+  MdMonitorWeight,
+  MdAccountBalanceWallet,
+  MdTrendingUp,
+  MdChecklist,
+  MdAdd,
+} from "react-icons/md";
+
+import logo from "../../../assets/logo.png";
+
+const navItems = [
+  { title: "Control Center", to: "/dashboard/control-center", icon: MdTune },
+  { title: "Training", to: "/dashboard/training", icon: MdFitnessCenter },
+  { title: "Weight", to: "/dashboard/weight", icon: MdMonitorWeight },
+  { title: "Finance", to: "/dashboard/finance", icon: MdAccountBalanceWallet },
+  { title: "Investing", to: "/dashboard/investing", icon: MdTrendingUp },
+  { title: "Discipline", to: "/dashboard/discipline", icon: MdChecklist },
+];
+
+const DashboardSidebar = () => {
+  const location = useLocation();
+
+  return (
+    <Sidebar
+      className="py-6"
+      closeButtonClassName="fixed top-4 right-4"
+      collapsible="none"
+    >
+      <SidebarHeader className="hidden lg:block">
+        <NavLink to="/" className="flex items-center gap-3 px-2">
+          <img src={logo} alt="Trekr" className="h-10 w-auto rounded-2xl" />
+        </NavLink>
+      </SidebarHeader>
+
+      <SidebarContent className="mt-6">
+        <SidebarMenu>
+          {navItems.map((item) => (
+            <SidebarMenuItem key={item.title}>
+              {(() => {
+                const isActive =
+                  location.pathname === item.to ||
+                  location.pathname.startsWith(`${item.to}/`);
+
+                return (
+                  <SidebarMenuButton
+                    asChild
+                    isActive={isActive}
+                    className={
+                      isActive
+                        ? "!bg-green-400 !text-black hover:!bg-green-400 active:!bg-green-400"
+                        : ""
+                    }
+                  >
+                    <NavLink
+                      to={item.to}
+                      className="flex w-full items-center gap-3"
+                    >
+                      <item.icon
+                        className={
+                          isActive
+                            ? "size-6 shrink-0 text-black"
+                            : "size-6 shrink-0"
+                        }
+                      />
+                      <span className={isActive ? "text-black" : ""}>
+                        {item.title}
+                      </span>
+                    </NavLink>
+                  </SidebarMenuButton>
+                );
+              })()}
+            </SidebarMenuItem>
+          ))}
+        </SidebarMenu>
+
+        <div className="mt-6 px-2">
+          <Button
+            variant="secondary"
+            className="w-full justify-start gap-2 !bg-green-400 !text-black hover:!bg-green-500"
+            type="button"
+          >
+            <MdAdd className="size-5" />
+            <span>Add Custom</span>
+          </Button>
+        </div>
+      </SidebarContent>
+    </Sidebar>
+  );
+};
+
+export default DashboardSidebar;
Index: frontend/src/pages/Dashboard/components/DashboardTopbar.jsx
===================================================================
--- frontend/src/pages/Dashboard/components/DashboardTopbar.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/components/DashboardTopbar.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,59 @@
+import React from "react";
+import { Link } from "react-router-dom";
+
+import { MdLogout } from "react-icons/md";
+
+import logo from "../../../assets/logo.png";
+
+const DashboardTopbar = ({ username, onLogout }) => {
+  return (
+    <>
+      {/* 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="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>
+
+          <div className="flex items-center gap-3">
+            <span className="text-sm opacity-80">{username || "—"}</span>
+            <button
+              type="button"
+              className="btn btn-ghost btn-sm"
+              onClick={onLogout}
+              aria-label="Log out"
+              title="Log out"
+            >
+              <MdLogout className="size-5" />
+            </button>
+          </div>
+        </div>
+      </div>
+
+      {/* 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="flex items-center gap-4">
+          <Link
+            to="/dashboard"
+            className="flex items-center gap-3"
+            aria-label="Trekr dashboard"
+          >
+            <img src={logo} alt="Trekr" className="h-10 w-auto rounded-2xl" />
+          </Link>
+        </div>
+        <div className="flex items-center gap-3">
+          <span className="text-sm opacity-80">{username || "—"}</span>
+          <button
+            type="button"
+            className="btn btn-ghost btn-sm"
+            onClick={onLogout}
+            aria-label="Log out"
+            title="Log out"
+          >
+            <MdLogout className="size-5" />
+          </button>
+        </div>
+      </div>
+    </>
+  );
+};
+
+export default DashboardTopbar;
Index: frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/ControlCenter/ControlCenter.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+const ControlCenter = () => {
+  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>
+    </div>
+  );
+};
+
+export default ControlCenter;
Index: frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Discipline/Discipline.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+const Discipline = () => {
+  return (
+    <div className="space-y-4">
+      <h1 className="text-2xl font-bold">Discipline</h1>
+      <div className="card bg-base-200 border border-base-300">
+        <div className="card-body">
+          <p className="opacity-80">Discipline dashboard content goes here.</p>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Discipline;
Index: frontend/src/pages/Dashboard/pages/Finance/Finance.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Finance/Finance.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Finance/Finance.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+const Finance = () => {
+  return (
+    <div className="space-y-4">
+      <h1 className="text-2xl font-bold">Finance</h1>
+      <div className="card bg-base-200 border border-base-300">
+        <div className="card-body">
+          <p className="opacity-80">Finance dashboard content goes here.</p>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Finance;
Index: frontend/src/pages/Dashboard/pages/Investing/Investing.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/Investing.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/Investing.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,85 @@
+import React, { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+
+import InvestingCenteredCard from "./components/InvestingCenteredCard.jsx";
+import InvestingStartCtaCard from "./components/InvestingStartCtaCard.jsx";
+
+const Investing = () => {
+  const navigate = useNavigate();
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [isTracking, setIsTracking] = useState(false);
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  useEffect(() => {
+    let isMounted = true;
+    const run = async () => {
+      setIsLoading(true);
+      setError("");
+      try {
+        const res = await api.get("/investing/status");
+        const tracking = Boolean(res?.data?.tracking);
+        if (!isMounted) return;
+        setIsTracking(tracking);
+        if (tracking) {
+          navigate("/dashboard/investing/tracking", { replace: true });
+        }
+      } catch (err) {
+        if (!isMounted) return;
+        const message =
+          err?.response?.data?.message || "Failed to load investing status";
+        setError(message);
+      } finally {
+        if (isMounted) setIsLoading(false);
+      }
+    };
+
+    run();
+    return () => {
+      isMounted = false;
+    };
+  }, [navigate]);
+
+  const onStart = async () => {
+    setError("");
+    setIsSubmitting(true);
+    try {
+      await api.post("/investing/start", {});
+      setIsTracking(true);
+      navigate("/dashboard/investing/tracking", { replace: true });
+    } catch (err) {
+      const message =
+        err?.response?.data?.message ||
+        Object.values(err?.response?.data?.errors ?? {})[0] ||
+        "Failed to start tracking";
+      setError(message);
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  if (isLoading) {
+    return <InvestingCenteredCard title="Investing" message="Loading…" />;
+  }
+
+  if (isTracking) {
+    return <InvestingCenteredCard title="Investing" message="Redirecting…" />;
+  }
+
+  return (
+    <div className="min-h-[70vh] w-full flex items-center justify-center">
+      <div className="w-full max-w-3xl">
+        <InvestingStartCtaCard
+          error={error}
+          onStart={onStart}
+          isSubmitting={isSubmitting}
+        />
+      </div>
+    </div>
+  );
+};
+
+export default Investing;
Index: frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/InvestingTracking.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,264 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
+import { fetchCurrentPricesBySymbol } from "../../../../api/twelveData";
+
+import InvestingTrackingHeader from "./components/InvestingTrackingHeader.jsx";
+import InvestingProgressCard from "./components/InvestingProgressCard.jsx";
+import InvestingAssetsTable from "./components/InvestingAssetsTable.jsx";
+
+function formatDate(value) {
+  if (!value) return "—";
+  const d = new Date(value);
+  if (Number.isNaN(d.getTime())) return String(value);
+  return d.toLocaleDateString(undefined, {
+    year: "numeric",
+    month: "short",
+    day: "2-digit",
+  });
+}
+
+function formatMoney(value) {
+  if (value === null || value === undefined || value === "") return "—";
+  const num = Number(value);
+  if (Number.isNaN(num)) return String(value);
+  return num.toLocaleString(undefined, {
+    style: "currency",
+    currency: "USD",
+    maximumFractionDigits: 2,
+  });
+}
+
+export default function InvestingTracking() {
+  const navigate = useNavigate();
+  const pageSize = 5;
+
+  const [assets, setAssets] = useState([]);
+  const [page, setPage] = useState(0);
+  const [hasMore, setHasMore] = useState(false);
+  const [isLoading, setIsLoading] = useState(true);
+  const [isLoadingMore, setIsLoadingMore] = useState(false);
+  const [isDeletingId, setIsDeletingId] = useState(null);
+  const [pendingDelete, setPendingDelete] = useState(null);
+  const [error, setError] = useState("");
+
+  const [quotesBySymbol, setQuotesBySymbol] = useState({});
+  const [isQuotesLoading, setIsQuotesLoading] = useState(false);
+  const [quotesError, setQuotesError] = useState("");
+
+  const columns = useMemo(
+    () => [
+      { key: "tickerSymbol", label: "Ticker" },
+      { key: "quantity", label: "Quantity" },
+      { key: "buyPrice", label: "Buy Price" },
+      { key: "buyDate", label: "Buy Date" },
+      { key: "currentPrice", label: "Current Price" },
+      { key: "roi", label: "ROI" },
+      { key: "actions", label: "" },
+    ],
+    [],
+  );
+
+  const fetchPage = useCallback(async (nextPage) => {
+    const resp = await api.get("/investing/assets", {
+      params: { page: nextPage, size: pageSize },
+    });
+    const data = resp?.data ?? {};
+    const nextAssets = Array.isArray(data.assets) ? data.assets : [];
+    const nextHasMore = Boolean(data.hasMore);
+    return { nextAssets, nextHasMore };
+  }, []);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoading(true);
+        setError("");
+        const { nextAssets, nextHasMore } = await fetchPage(0);
+        if (cancelled) return;
+        setAssets(nextAssets);
+        setHasMore(nextHasMore);
+        setPage(0);
+      } catch (e) {
+        if (cancelled) return;
+        setError(e?.response?.data?.message || "Failed to load assets.");
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    })();
+    return () => {
+      cancelled = true;
+    };
+  }, [fetchPage]);
+
+  const tickerSymbols = useMemo(() => {
+    const set = new Set();
+    for (const a of assets) {
+      if (a?.tickerSymbol) set.add(String(a.tickerSymbol).toUpperCase());
+    }
+    return Array.from(set).sort();
+  }, [assets]);
+
+  useEffect(() => {
+    if (tickerSymbols.length === 0) {
+      setQuotesBySymbol({});
+      setIsQuotesLoading(false);
+      setQuotesError("");
+      return;
+    }
+
+    let cancelled = false;
+    let intervalId;
+
+    const fetchQuotes = async () => {
+      try {
+        setIsQuotesLoading(true);
+        setQuotesError("");
+        if (cancelled) return;
+        const map = await fetchCurrentPricesBySymbol(tickerSymbols);
+        if (cancelled) return;
+        setQuotesBySymbol(map);
+      } catch (err) {
+        if (cancelled) return;
+        const msg =
+          err?.message ||
+          "Could not fetch current prices (check API key / provider availability).";
+        setQuotesError(msg);
+        // Keep any previously loaded quotes instead of wiping the table.
+        // (If this is the first load, quotesBySymbol will already be empty.)
+      } finally {
+        if (!cancelled) setIsQuotesLoading(false);
+      }
+    };
+
+    fetchQuotes();
+    // Keep refresh infrequent to avoid provider rate limits.
+    intervalId = setInterval(fetchQuotes, 5 * 60_000);
+
+    return () => {
+      cancelled = true;
+      if (intervalId) clearInterval(intervalId);
+    };
+  }, [tickerSymbols.join(",")]);
+
+  const onLoadMore = async () => {
+    if (isLoadingMore || !hasMore) return;
+    const nextPage = page + 1;
+    try {
+      setIsLoadingMore(true);
+      setError("");
+      const { nextAssets, nextHasMore } = await fetchPage(nextPage);
+      setAssets((prev) => [...prev, ...nextAssets]);
+      setHasMore(nextHasMore);
+      setPage(nextPage);
+    } catch (e) {
+      setError(e?.response?.data?.message || "Failed to load more assets.");
+    } finally {
+      setIsLoadingMore(false);
+    }
+  };
+
+  const doDelete = async (assetId) => {
+    if (!assetId || isDeletingId) return;
+    try {
+      setIsDeletingId(assetId);
+      setError("");
+      await api.delete(`/investing/assets/${assetId}`);
+      setAssets((prev) => prev.filter((a) => a.assetId !== assetId));
+      setPendingDelete(null);
+    } catch (e) {
+      setError(e?.response?.data?.message || "Failed to delete asset.");
+    } finally {
+      setIsDeletingId(null);
+    }
+  };
+
+  const onRequestDelete = (asset) => {
+    if (!asset?.assetId) return;
+    setPendingDelete({
+      assetId: asset.assetId,
+      tickerSymbol: asset.tickerSymbol,
+    });
+  };
+
+  return (
+    <div className="space-y-6">
+      <InvestingTrackingHeader
+        onAddAsset={() => navigate("/dashboard/investing/assets/new")}
+      />
+      <InvestingProgressCard graphSrc={graphPlaceholder} />
+
+      {quotesError ? (
+        <div className="text-sm opacity-70">
+          Prices unavailable: {quotesError}
+        </div>
+      ) : null}
+
+      {pendingDelete ? (
+        <div className="modal modal-open" role="dialog" aria-modal="true">
+          <div className="modal-box">
+            <h3 className="text-lg font-semibold">Delete investment?</h3>
+            <p className="opacity-80 mt-2">
+              This will permanently delete{" "}
+              <span className="font-semibold">
+                {pendingDelete.tickerSymbol || "this asset"}
+              </span>
+              .
+            </p>
+
+            <div className="modal-action">
+              <button
+                type="button"
+                className="btn btn-ghost"
+                onClick={() => setPendingDelete(null)}
+                disabled={isDeletingId === pendingDelete.assetId}
+              >
+                Cancel
+              </button>
+              <button
+                type="button"
+                className="btn bg-red-500! text-white! hover:bg-red-600! border-0"
+                onClick={() => doDelete(pendingDelete.assetId)}
+                disabled={isDeletingId === pendingDelete.assetId}
+              >
+                {isDeletingId === pendingDelete.assetId
+                  ? "Deleting…"
+                  : "Delete"}
+              </button>
+            </div>
+          </div>
+          <button
+            type="button"
+            className="modal-backdrop"
+            aria-label="Close"
+            onClick={() => {
+              if (isDeletingId === pendingDelete.assetId) return;
+              setPendingDelete(null);
+            }}
+          />
+        </div>
+      ) : null}
+
+      <InvestingAssetsTable
+        columns={columns}
+        assets={assets}
+        isLoading={isLoading}
+        error={error}
+        quotesBySymbol={quotesBySymbol}
+        isQuotesLoading={isQuotesLoading}
+        isLoadingMore={isLoadingMore}
+        hasMore={hasMore}
+        onLoadMore={onLoadMore}
+        pageSize={pageSize}
+        formatDate={formatDate}
+        formatMoney={formatMoney}
+        onAddAsset={() => navigate("/dashboard/investing/assets/new")}
+        onRequestDelete={onRequestDelete}
+        isDeletingId={isDeletingId}
+      />
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/NewInvestment.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/NewInvestment.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/NewInvestment.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,250 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+import { searchTickers } from "../../../../api/yahooFinance";
+
+export default function NewInvestment() {
+  const navigate = useNavigate();
+
+  const todayIso = useMemo(() => {
+    const now = new Date();
+    const yyyy = now.getFullYear();
+    const mm = String(now.getMonth() + 1).padStart(2, "0");
+    const dd = String(now.getDate()).padStart(2, "0");
+    return `${yyyy}-${mm}-${dd}`;
+  }, []);
+
+  const initialForm = useMemo(
+    () => ({ tickerSymbol: "", quantity: "", buyPrice: "", buyDate: "" }),
+    [],
+  );
+
+  const [form, setForm] = useState(initialForm);
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  const [tickerQuery, setTickerQuery] = useState("");
+  const [tickerOptions, setTickerOptions] = useState([]);
+  const [isLoadingTickers, setIsLoadingTickers] = useState(false);
+
+  useEffect(() => {
+    const q = tickerQuery.trim();
+    if (q.length < 2) {
+      setTickerOptions([]);
+      setIsLoadingTickers(false);
+      return;
+    }
+
+    let cancelled = false;
+    const handle = setTimeout(async () => {
+      try {
+        setIsLoadingTickers(true);
+        if (cancelled) return;
+        const data = await searchTickers(q, 20);
+        if (cancelled) return;
+        setTickerOptions(Array.isArray(data) ? data : []);
+      } catch {
+        if (!cancelled) setTickerOptions([]);
+      } finally {
+        if (!cancelled) setIsLoadingTickers(false);
+      }
+    }, 250);
+
+    return () => {
+      cancelled = true;
+      clearTimeout(handle);
+    };
+  }, [tickerQuery]);
+
+  const onSubmit = async (e) => {
+    e.preventDefault();
+    setError("");
+
+    if (!form.tickerSymbol) {
+      setError("Please select a ticker symbol");
+      return;
+    }
+
+    if (form.buyDate && form.buyDate > todayIso) {
+      setError("Buy date cannot be in the future");
+      return;
+    }
+
+    const quantityNum = form.quantity === "" ? null : Number(form.quantity);
+    if (!quantityNum || Number.isNaN(quantityNum) || quantityNum <= 0) {
+      setError("Quantity must be greater than 0");
+      return;
+    }
+
+    const buyPriceNum = form.buyPrice === "" ? null : Number(form.buyPrice);
+    if (
+      buyPriceNum !== null &&
+      (Number.isNaN(buyPriceNum) || buyPriceNum < 0)
+    ) {
+      setError("Buy price must be 0 or greater");
+      return;
+    }
+
+    try {
+      setIsSubmitting(true);
+      await api.post("/investing/assets", {
+        tickerSymbol: form.tickerSymbol,
+        quantity: quantityNum,
+        buyPrice: buyPriceNum,
+        buyDate: form.buyDate === "" ? null : form.buyDate,
+      });
+
+      navigate("/dashboard/investing/tracking", { replace: true });
+    } catch (err) {
+      const message =
+        err?.response?.data?.message ||
+        Object.values(err?.response?.data?.errors ?? {})[0] ||
+        "Failed to add investment";
+      setError(message);
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  return (
+    <div className="max-w-2xl">
+      <div className="card bg-base-200 border border-base-300">
+        <div className="card-body">
+          <h1 className="text-2xl font-bold">Add new investment</h1>
+          <p className="opacity-80">
+            Add an asset you bought. (We’ll build performance tracking next.)
+          </p>
+
+          {error ? (
+            <div className="alert alert-error mt-4">
+              <span>{error}</span>
+            </div>
+          ) : null}
+
+          <form onSubmit={onSubmit} className="mt-6 space-y-4">
+            <div>
+              <label className="label">
+                <span className="label-text">Ticker symbol</span>
+              </label>
+              <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
+                <input
+                  className="input input-bordered w-full"
+                  placeholder="Search (e.g. AAPL, TSLA, NVDA)"
+                  value={tickerQuery}
+                  onChange={(e) => setTickerQuery(e.target.value)}
+                />
+                <select
+                  className="select select-bordered w-full"
+                  value={form.tickerSymbol}
+                  onChange={(e) =>
+                    setForm((p) => ({ ...p, tickerSymbol: e.target.value }))
+                  }
+                  required
+                >
+                  <option value="" disabled>
+                    {isLoadingTickers
+                      ? "Loading tickers…"
+                      : tickerOptions.length
+                        ? "Select a ticker"
+                        : "Search to load tickers"}
+                  </option>
+                  {tickerOptions.map((o) => {
+                    const symbol = o?.symbol ?? "";
+                    const name = o?.name ?? "";
+                    const exchange = o?.exchange ?? "";
+                    const label = [
+                      symbol,
+                      name ? `— ${name}` : "",
+                      exchange ? `(${exchange})` : "",
+                    ]
+                      .filter(Boolean)
+                      .join(" ");
+                    return (
+                      <option key={symbol} value={symbol}>
+                        {label}
+                      </option>
+                    );
+                  })}
+                </select>
+              </div>
+              <p className="text-xs opacity-70 mt-2">
+                Only tickers returned by Yahoo Finance can be selected.
+              </p>
+            </div>
+
+            <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
+              <div>
+                <label className="label">
+                  <span className="label-text">Quantity</span>
+                </label>
+                <input
+                  type="number"
+                  step="any"
+                  min="0"
+                  className="input input-bordered w-full"
+                  placeholder="10"
+                  value={form.quantity}
+                  onChange={(e) =>
+                    setForm((p) => ({ ...p, quantity: e.target.value }))
+                  }
+                  required
+                />
+              </div>
+
+              <div>
+                <label className="label">
+                  <span className="label-text">Buy price (optional)</span>
+                </label>
+                <input
+                  type="number"
+                  step="any"
+                  min="0"
+                  className="input input-bordered w-full"
+                  placeholder="187.20"
+                  value={form.buyPrice}
+                  onChange={(e) =>
+                    setForm((p) => ({ ...p, buyPrice: e.target.value }))
+                  }
+                />
+              </div>
+            </div>
+
+            <div>
+              <label className="label">
+                <span className="label-text">Buy date (optional)</span>
+              </label>
+              <input
+                type="date"
+                className="input input-bordered w-full"
+                value={form.buyDate}
+                onChange={(e) =>
+                  setForm((p) => ({ ...p, buyDate: e.target.value }))
+                }
+                max={todayIso}
+              />
+            </div>
+
+            <div className="flex flex-col gap-3 sm:flex-row sm:justify-end">
+              <button
+                type="button"
+                className="btn btn-ghost"
+                onClick={() => navigate("/dashboard/investing/tracking")}
+                disabled={isSubmitting}
+              >
+                Cancel
+              </button>
+              <button
+                type="submit"
+                className="btn bg-green-400! text-black! hover:bg-green-500! border-0"
+                disabled={isSubmitting}
+              >
+                {isSubmitting ? "Saving…" : "Save investment"}
+              </button>
+            </div>
+          </form>
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingAssetsTable.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingAssetsTable.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingAssetsTable.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,151 @@
+import React from "react";
+import { MdDelete } from "react-icons/md";
+
+export default function InvestingAssetsTable({
+  columns,
+  assets,
+  isLoading,
+  error,
+  quotesBySymbol,
+  isQuotesLoading,
+  isLoadingMore,
+  hasMore,
+  onLoadMore,
+  pageSize,
+  formatDate,
+  formatMoney,
+  onAddAsset,
+  onRequestDelete,
+  onDelete,
+  isDeletingId,
+}) {
+  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="text-lg font-semibold">Assets</h2>
+          <span className="text-sm opacity-70">
+            Showing {pageSize} per page
+          </span>
+        </div>
+
+        {error ? (
+          <div className="alert alert-error mt-4">
+            <span>{error}</span>
+          </div>
+        ) : null}
+
+        <div className="mt-4 overflow-x-auto">
+          <table className="table">
+            <thead>
+              <tr>
+                {columns.map((c) => (
+                  <th key={c.key}>{c.label}</th>
+                ))}
+              </tr>
+            </thead>
+            <tbody>
+              {isLoading ? (
+                <tr>
+                  <td colSpan={columns.length} className="opacity-70">
+                    Loading…
+                  </td>
+                </tr>
+              ) : assets.length === 0 ? (
+                <tr>
+                  <td colSpan={columns.length} className="py-10">
+                    <div className="flex flex-col items-center text-center gap-3">
+                      <p className="opacity-80">No investments yet.</p>
+                      <button
+                        type="button"
+                        className="btn bg-green-400! text-black! hover:bg-green-500! border-0"
+                        onClick={onAddAsset}
+                      >
+                        Add your first investment
+                      </button>
+                    </div>
+                  </td>
+                </tr>
+              ) : (
+                assets.map((a) => (
+                  <tr key={a.assetId}>
+                    <td className="font-semibold">{a.tickerSymbol ?? "—"}</td>
+                    <td>{a.quantity ?? "—"}</td>
+                    <td>{formatMoney(a.buyPrice)}</td>
+                    <td>{formatDate(a.buyDate)}</td>
+                    <td>
+                      {isQuotesLoading
+                        ? "Loading…"
+                        : formatMoney(
+                            quotesBySymbol?.[
+                              String(a.tickerSymbol ?? "").toUpperCase()
+                            ],
+                          )}
+                    </td>
+                    <td>
+                      {(() => {
+                        const symbol = String(
+                          a.tickerSymbol ?? "",
+                        ).toUpperCase();
+                        const current = Number(quotesBySymbol?.[symbol]);
+                        const buy = Number(a.buyPrice);
+                        if (!symbol || Number.isNaN(current)) return "—";
+                        if (Number.isNaN(buy) || buy <= 0) return "—";
+                        const roiPct = ((current - buy) / buy) * 100;
+                        const cls =
+                          roiPct > 0
+                            ? "text-green-400 font-semibold"
+                            : roiPct < 0
+                              ? "text-red-400 font-semibold"
+                              : "opacity-80";
+                        const sign = roiPct > 0 ? "+" : "";
+                        return (
+                          <span className={cls}>
+                            {sign}
+                            {roiPct.toFixed(2)}%
+                          </span>
+                        );
+                      })()}
+                    </td>
+                    <td className="text-right">
+                      <button
+                        type="button"
+                        className="btn btn-ghost btn-sm text-red-400 hover:text-red-300"
+                        title="Delete"
+                        onClick={() => {
+                          if (onRequestDelete) {
+                            onRequestDelete(a);
+                            return;
+                          }
+                          onDelete?.(a.assetId);
+                        }}
+                        disabled={isDeletingId === a.assetId}
+                      >
+                        <MdDelete className="size-5" />
+                      </button>
+                    </td>
+                  </tr>
+                ))
+              )}
+            </tbody>
+          </table>
+        </div>
+
+        <div className="mt-4 flex justify-center">
+          {!isLoading && assets.length === 0 ? null : hasMore ? (
+            <button
+              type="button"
+              className="btn"
+              onClick={onLoadMore}
+              disabled={isLoadingMore}
+            >
+              {isLoadingMore ? "Loading…" : "Load more"}
+            </button>
+          ) : (
+            <span className="text-sm opacity-70">No more assets.</span>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingCenteredCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingCenteredCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingCenteredCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+export default function InvestingCenteredCard({ title, message }) {
+  return (
+    <div className="min-h-[60vh] w-full flex items-center justify-center">
+      <div className="w-full max-w-2xl">
+        <div className="card bg-base-200 border border-base-300">
+          <div className="card-body">
+            <h1 className="text-2xl font-bold">{title}</h1>
+            <p className="opacity-80">{message}</p>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingProgressCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,27 @@
+import React from "react";
+
+export default function InvestingProgressCard({ graphSrc }) {
+  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>
+          </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>
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingStartCtaCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingStartCtaCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingStartCtaCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,36 @@
+import React from "react";
+
+export default function InvestingStartCtaCard({
+  error,
+  onStart,
+  isSubmitting,
+}) {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body">
+        <h1 className="text-2xl font-bold">Investing</h1>
+        <p className="opacity-80">
+          You’re not tracking investments yet. Start tracking to log your assets
+          and visualize your progress.
+        </p>
+
+        {error ? (
+          <div className="alert alert-error mt-4">
+            <span>{error}</span>
+          </div>
+        ) : null}
+
+        <div className="mt-6">
+          <button
+            type="button"
+            className="btn btn-lg w-full bg-green-400! text-black! hover:bg-green-500! border-0"
+            onClick={onStart}
+            disabled={isSubmitting}
+          >
+            {isSubmitting ? "Starting…" : "Start Tracking Investments"}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Investing/components/InvestingTrackingHeader.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Investing/components/InvestingTrackingHeader.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Investing/components/InvestingTrackingHeader.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,19 @@
+import React from "react";
+
+export default function InvestingTrackingHeader({ onAddAsset }) {
+  return (
+    <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
+      <div>
+        <h1 className="text-2xl font-bold">Investing</h1>
+        <p className="opacity-80">Track your assets over time.</p>
+      </div>
+      <button
+        type="button"
+        className="btn bg-green-400! text-black! hover:bg-green-500! border-0"
+        onClick={onAddAsset}
+      >
+        + Add new investment
+      </button>
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Training/NewTrainingSession.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/NewTrainingSession.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/NewTrainingSession.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,241 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+
+import WorkoutTypeSelect from "./components/WorkoutTypeSelect.jsx";
+import CaloriesEstimate from "./components/CaloriesEstimate.jsx";
+
+const NewTrainingSession = () => {
+  const navigate = useNavigate();
+
+  const [workoutTypes, setWorkoutTypes] = useState([]);
+  const [profileWeightKg, setProfileWeightKg] = useState(null);
+  const [isLoading, setIsLoading] = useState(true);
+
+  const [type, setType] = useState("");
+  const [durationMinutes, setDurationMinutes] = useState("");
+  const [autoCalculateCalories, setAutoCalculateCalories] = useState(true);
+  const [calories, setCalories] = useState("");
+
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoading(true);
+        setError("");
+
+        const [typesRes, profileRes] = await Promise.all([
+          api.get("/training/workout-types"),
+          api.get("/training/profile"),
+        ]);
+
+        if (cancelled) return;
+
+        const types = Array.isArray(typesRes?.data) ? typesRes.data : [];
+        setWorkoutTypes(types);
+
+        const weight = profileRes?.data?.weight;
+        setProfileWeightKg(
+          weight === null || weight === undefined || weight === ""
+            ? null
+            : Number(weight),
+        );
+      } catch (err) {
+        if (cancelled) return;
+        setError(
+          err?.response?.data?.message ||
+            "Failed to load workout types/training profile.",
+        );
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    })();
+    return () => {
+      cancelled = true;
+    };
+  }, []);
+
+  const selectedWorkout = useMemo(
+    () => workoutTypes.find((t) => t.type === type) ?? null,
+    [workoutTypes, type],
+  );
+
+  const estimatedCalories = useMemo(() => {
+    const met = selectedWorkout?.met;
+    const durationNum = durationMinutes === "" ? NaN : Number(durationMinutes);
+    const weightNum = profileWeightKg;
+    if (!Number.isFinite(Number(met))) return null;
+    if (!Number.isFinite(durationNum) || durationNum <= 0) return null;
+    if (!Number.isFinite(weightNum) || weightNum <= 0) return null;
+
+    const caloriesValue = Number(met) * weightNum * (durationNum / 60);
+    if (!Number.isFinite(caloriesValue)) return null;
+    return Math.round(caloriesValue * 100) / 100;
+  }, [durationMinutes, profileWeightKg, selectedWorkout?.met]);
+
+  const onSubmit = async (e) => {
+    e.preventDefault();
+    setError("");
+
+    if (isLoading) return;
+
+    if (!type) {
+      setError("Please select a workout type.");
+      return;
+    }
+
+    const durationNum = durationMinutes === "" ? NaN : Number(durationMinutes);
+    if (!Number.isFinite(durationNum) || durationNum < 1) {
+      setError("Duration must be at least 1 minute.");
+      return;
+    }
+
+    const caloriesNum = calories === "" ? NaN : Number(calories);
+    if (!autoCalculateCalories) {
+      if (!Number.isFinite(caloriesNum) || caloriesNum < 0) {
+        setError("Calories must be 0 or greater.");
+        return;
+      }
+    }
+
+    try {
+      setIsSubmitting(true);
+      await api.post("/training/sessions", {
+        type,
+        durationMinutes: durationNum,
+        autoCalculateCalories,
+        calories: autoCalculateCalories ? null : caloriesNum,
+      });
+
+      navigate("/dashboard/training/tracking", { replace: true });
+    } catch (err) {
+      setError(
+        err?.response?.data?.message || "Failed to add training session.",
+      );
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  return (
+    <div className="min-h-[70vh] w-full flex items-center justify-center">
+      <form
+        onSubmit={onSubmit}
+        className="card bg-base-200 border border-base-300 w-full max-w-2xl"
+      >
+        <div className="card-body">
+          <div className="flex items-start justify-between gap-4">
+            <div>
+              <h1 className="text-2xl font-bold">Add training session</h1>
+              <p className="opacity-80 mt-1">
+                Choose a workout type, enter the duration, and we’ll save your
+                session.
+              </p>
+            </div>
+          </div>
+
+          {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+
+          {isLoading ? (
+            <p className="opacity-80 mt-2">Loading workout types…</p>
+          ) : null}
+
+          <div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
+            <WorkoutTypeSelect
+              workoutTypes={workoutTypes}
+              value={type}
+              onChange={(e) => setType(e.target.value)}
+              disabled={isLoading}
+            />
+
+            <div>
+              <label className="label">Duration (minutes)</label>
+              <input
+                type="number"
+                className="input input-bordered w-full"
+                value={durationMinutes}
+                onChange={(e) => setDurationMinutes(e.target.value)}
+                min={1}
+                step={1}
+                required
+                disabled={isLoading}
+              />
+            </div>
+          </div>
+
+          <div className="mt-4">
+            <label className="label cursor-pointer justify-start gap-3">
+              <input
+                type="checkbox"
+                className="checkbox"
+                checked={autoCalculateCalories}
+                onChange={(e) => {
+                  const next = e.target.checked;
+                  setAutoCalculateCalories(next);
+                  if (
+                    !next &&
+                    (calories === "" || calories === null) &&
+                    estimatedCalories !== null
+                  ) {
+                    setCalories(String(estimatedCalories));
+                  }
+                }}
+              />
+              <span className="label-text">Auto-calculate calories burned</span>
+            </label>
+            <p className="text-xs opacity-70 mt-1">
+              Uses your training profile weight and the workout type.
+            </p>
+            <CaloriesEstimate
+              estimatedCalories={estimatedCalories}
+              profileWeightKg={profileWeightKg}
+            />
+          </div>
+
+          <div className="mt-2">
+            <label className="label">Calories (kcal)</label>
+            <input
+              type="number"
+              className="input input-bordered w-full"
+              value={calories}
+              onChange={(e) => setCalories(e.target.value)}
+              min={0}
+              step={1}
+              disabled={autoCalculateCalories}
+              placeholder={
+                autoCalculateCalories
+                  ? "Calculated automatically"
+                  : "Enter calories"
+              }
+              required={!autoCalculateCalories}
+            />
+          </div>
+
+          <div className="mt-6 flex flex-col gap-3 sm:flex-row sm:justify-end">
+            <button
+              type="button"
+              className="btn btn-ghost"
+              onClick={() => navigate("/dashboard/training/tracking")}
+              disabled={isSubmitting}
+            >
+              Cancel
+            </button>
+            <button
+              type="submit"
+              className="btn bg-green-400! text-black! hover:bg-green-500!"
+              disabled={isSubmitting || isLoading}
+            >
+              {isSubmitting ? "Saving…" : "Save session"}
+            </button>
+          </div>
+        </div>
+      </form>
+    </div>
+  );
+};
+
+export default NewTrainingSession;
Index: frontend/src/pages/Dashboard/pages/Training/Training.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/Training.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/Training.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,114 @@
+import React, { useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import api from "../../../../api/axios";
+
+import TrainingCenteredCard from "./components/TrainingCenteredCard.jsx";
+import TrainingStartCtaCard from "./components/TrainingStartCtaCard.jsx";
+import TrainingStartForm from "./components/TrainingStartForm.jsx";
+
+const Training = () => {
+  const navigate = useNavigate();
+
+  const [isLoading, setIsLoading] = useState(true);
+  const [isTracking, setIsTracking] = useState(false);
+  const [step, setStep] = useState("cta"); // cta | form
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  const initialForm = useMemo(() => ({ gender: "", age: "", weight: "" }), []);
+  const [form, setForm] = useState(initialForm);
+
+  useEffect(() => {
+    let isMounted = true;
+    const run = async () => {
+      setIsLoading(true);
+      setError("");
+      try {
+        const res = await api.get("/training/status");
+        const tracking = Boolean(res?.data?.tracking);
+        if (!isMounted) return;
+        setIsTracking(tracking);
+        if (tracking) {
+          navigate("/dashboard/training/tracking", { replace: true });
+        }
+      } catch (err) {
+        if (!isMounted) return;
+        const message =
+          err?.response?.data?.message || "Failed to load training status";
+        setError(message);
+      } finally {
+        if (isMounted) setIsLoading(false);
+      }
+    };
+
+    run();
+    return () => {
+      isMounted = false;
+    };
+  }, [navigate]);
+
+  const onStart = () => {
+    setError("");
+    setStep("form");
+  };
+
+  const onCancel = () => {
+    setStep("cta");
+    setForm(initialForm);
+    setError("");
+  };
+
+  const onSubmit = async (e) => {
+    e.preventDefault();
+    setError("");
+    setIsSubmitting(true);
+    try {
+      await api.post("/training/start", {
+        gender: form.gender,
+        age: form.age === "" ? null : Number(form.age),
+        weight: form.weight === "" ? null : Number(form.weight),
+      });
+
+      setIsTracking(true);
+      navigate("/dashboard/training/tracking", { replace: true });
+    } catch (err) {
+      const message =
+        err?.response?.data?.message ||
+        Object.values(err?.response?.data?.errors ?? {})[0] ||
+        "Failed to start tracking";
+      setError(message);
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  if (isLoading) {
+    return <TrainingCenteredCard title="Training" message="Loading…" />;
+  }
+
+  if (isTracking) {
+    return <TrainingCenteredCard title="Training" message="Redirecting…" />;
+  }
+
+  return (
+    <div className="min-h-[70vh] w-full flex items-center justify-center">
+      <div className="w-full max-w-3xl">
+        {step === "cta" ? (
+          <TrainingStartCtaCard error={error} onStart={onStart} />
+        ) : (
+          <TrainingStartForm
+            form={form}
+            setForm={setForm}
+            onSubmit={onSubmit}
+            onCancel={onCancel}
+            error={error}
+            isSubmitting={isSubmitting}
+          />
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default Training;
Index: frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/TrainingTracking.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,120 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import api from "../../../../api/axios";
+import graphPlaceholder from "../../../../assets/graph-placeholder.svg";
+
+import TrainingTrackingHeader from "./components/TrainingTrackingHeader.jsx";
+import TrainingProgressCard from "./components/TrainingProgressCard.jsx";
+import TrainingSessionsTable from "./components/TrainingSessionsTable.jsx";
+
+function formatDate(value) {
+  if (!value) return "—";
+  const d = new Date(value);
+  if (Number.isNaN(d.getTime())) return String(value);
+  return d.toLocaleDateString(undefined, {
+    year: "numeric",
+    month: "short",
+    day: "2-digit",
+  });
+}
+
+function titleCase(value) {
+  if (!value) return "—";
+  const s = String(value);
+  return s.charAt(0).toUpperCase() + s.slice(1);
+}
+
+export default function TrainingTracking() {
+  const navigate = useNavigate();
+  const pageSize = 5;
+
+  const [sessions, setSessions] = useState([]);
+  const [page, setPage] = useState(0);
+  const [hasMore, setHasMore] = useState(false);
+  const [isLoading, setIsLoading] = useState(true);
+  const [isLoadingMore, setIsLoadingMore] = useState(false);
+  const [error, setError] = useState("");
+
+  const columns = useMemo(
+    () => [
+      { key: "date", label: "Date" },
+      { key: "type", label: "Type" },
+      { key: "duration", label: "Duration (min)" },
+      { key: "calories", label: "Calories" },
+    ],
+    [],
+  );
+
+  const fetchPage = useCallback(async (nextPage) => {
+    const resp = await api.get("/training/sessions", {
+      params: { page: nextPage, size: pageSize },
+    });
+    const data = resp?.data ?? {};
+    const nextSessions = Array.isArray(data.sessions) ? data.sessions : [];
+    const nextHasMore = Boolean(data.hasMore);
+    return { nextSessions, nextHasMore };
+  }, []);
+
+  useEffect(() => {
+    let cancelled = false;
+    (async () => {
+      try {
+        setIsLoading(true);
+        setError("");
+        const { nextSessions, nextHasMore } = await fetchPage(0);
+        if (cancelled) return;
+        setSessions(nextSessions);
+        setHasMore(nextHasMore);
+        setPage(0);
+      } catch (e) {
+        if (cancelled) return;
+        setError(
+          e?.response?.data?.message || "Failed to load training sessions.",
+        );
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    })();
+    return () => {
+      cancelled = true;
+    };
+  }, [fetchPage]);
+
+  const onLoadMore = async () => {
+    if (isLoadingMore || !hasMore) return;
+    const nextPage = page + 1;
+    try {
+      setIsLoadingMore(true);
+      setError("");
+      const { nextSessions, nextHasMore } = await fetchPage(nextPage);
+      setSessions((prev) => [...prev, ...nextSessions]);
+      setHasMore(nextHasMore);
+      setPage(nextPage);
+    } catch (e) {
+      setError(e?.response?.data?.message || "Failed to load more sessions.");
+    } finally {
+      setIsLoadingMore(false);
+    }
+  };
+
+  return (
+    <div className="space-y-6">
+      <TrainingTrackingHeader
+        onAddSession={() => navigate("/dashboard/training/sessions/new")}
+      />
+      <TrainingProgressCard graphSrc={graphPlaceholder} />
+      <TrainingSessionsTable
+        columns={columns}
+        sessions={sessions}
+        isLoading={isLoading}
+        error={error}
+        isLoadingMore={isLoadingMore}
+        hasMore={hasMore}
+        onLoadMore={onLoadMore}
+        pageSize={pageSize}
+        formatDate={formatDate}
+        titleCase={titleCase}
+      />
+    </div>
+  );
+}
Index: frontend/src/pages/Dashboard/pages/Training/components/CaloriesEstimate.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/CaloriesEstimate.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/CaloriesEstimate.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,21 @@
+import React from "react";
+
+const CaloriesEstimate = ({ estimatedCalories, profileWeightKg }) => {
+  return (
+    <div className="mt-2">
+      {estimatedCalories === null ? (
+        <p className="text-xs opacity-70">
+          Estimated calories: —
+          {profileWeightKg === null ? " (missing profile weight)" : ""}
+        </p>
+      ) : (
+        <p className="text-xs opacity-80">
+          Estimated calories:{" "}
+          <span className="font-semibold">{estimatedCalories}</span> kcal
+        </p>
+      )}
+    </div>
+  );
+};
+
+export default CaloriesEstimate;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingCenteredCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingCenteredCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingCenteredCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,17 @@
+import React from "react";
+
+const TrainingCenteredCard = ({ title, message, children }) => {
+  return (
+    <div className="min-h-[60vh] w-full flex items-center justify-center">
+      <div className="card bg-base-200 border border-base-300 w-full max-w-xl">
+        <div className="card-body items-center text-center">
+          {title ? <h1 className="text-3xl font-bold">{title}</h1> : null}
+          {message ? <p className="opacity-80">{message}</p> : null}
+          {children}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default TrainingCenteredCard;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingProgressCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,23 @@
+import React from "react";
+
+const TrainingProgressCard = ({ graphSrc }) => {
+  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>
+          <span className="badge badge-ghost">Graph placeholder</span>
+        </div>
+        <div className="mt-4 w-full overflow-hidden rounded-xl border border-base-300 bg-base-100">
+          <img
+            src={graphSrc}
+            alt="Training progress graph placeholder"
+            className="block h-65 w-full object-cover"
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default TrainingProgressCard;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingSessionsTable.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingSessionsTable.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingSessionsTable.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,81 @@
+import React from "react";
+
+const TrainingSessionsTable = ({
+  columns,
+  sessions,
+  isLoading,
+  error,
+  isLoadingMore,
+  hasMore,
+  onLoadMore,
+  pageSize,
+  formatDate,
+  titleCase,
+}) => {
+  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">Recent sessions</h2>
+          <span className="badge badge-ghost">Showing up to {pageSize}</span>
+        </div>
+
+        {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+
+        <div className="mt-4 overflow-x-auto">
+          <table className="table table-zebra w-full">
+            <thead>
+              <tr>
+                {columns.map((c) => (
+                  <th key={c.key}>{c.label}</th>
+                ))}
+              </tr>
+            </thead>
+            <tbody>
+              {isLoading ? (
+                <tr>
+                  <td colSpan={columns.length}>
+                    <span className="opacity-80">Loading…</span>
+                  </td>
+                </tr>
+              ) : sessions.length === 0 ? (
+                <tr>
+                  <td colSpan={columns.length}>
+                    <span className="opacity-80">No sessions yet.</span>
+                  </td>
+                </tr>
+              ) : (
+                sessions.map((s) => (
+                  <tr
+                    key={
+                      s.trainingId ??
+                      `${s.date}-${s.type}-${s.duration}-${s.calories}`
+                    }
+                  >
+                    <td>{formatDate(s.date)}</td>
+                    <td>{titleCase(s.type)}</td>
+                    <td>{s.duration ?? "—"}</td>
+                    <td>{s.calories ?? "—"}</td>
+                  </tr>
+                ))
+              )}
+            </tbody>
+          </table>
+        </div>
+
+        <div className="mt-4 flex items-center justify-center">
+          <button
+            type="button"
+            className="btn btn-outline"
+            onClick={onLoadMore}
+            disabled={isLoading || isLoadingMore || !hasMore}
+          >
+            {isLoadingMore ? "Loading…" : hasMore ? "Load more" : "No more"}
+          </button>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default TrainingSessionsTable;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingStartCtaCard.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingStartCtaCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingStartCtaCard.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,26 @@
+import React from "react";
+
+const TrainingStartCtaCard = ({ error, onStart }) => {
+  return (
+    <div className="card bg-base-200 border border-base-300">
+      <div className="card-body items-center text-center">
+        <h1 className="text-4xl font-bold">You are not tracking training</h1>
+        <p className="opacity-80 max-w-xl">
+          Start tracking to log sessions, see trends, and build consistency.
+        </p>
+
+        {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+
+        <button
+          type="button"
+          className="btn btn-lg mt-6 w-full max-w-md bg-green-400! text-black! hover:bg-green-500!"
+          onClick={onStart}
+        >
+          Start Tracking
+        </button>
+      </div>
+    </div>
+  );
+};
+
+export default TrainingStartCtaCard;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingStartForm.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingStartForm.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingStartForm.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,94 @@
+import React from "react";
+
+const TrainingStartForm = ({
+  form,
+  setForm,
+  onSubmit,
+  onCancel,
+  error,
+  isSubmitting,
+}) => {
+  return (
+    <form
+      onSubmit={onSubmit}
+      className="card bg-base-200 border border-base-300"
+    >
+      <div className="card-body items-center text-center">
+        <h1 className="text-3xl font-bold">Start tracking training</h1>
+        <p className="opacity-80 max-w-xl">
+          Fill in a few details to set up your training profile.
+        </p>
+
+        {error ? <p className="text-error text-sm mt-2">{error}</p> : null}
+
+        <div className="mt-4 grid w-full grid-cols-1 gap-4 md:grid-cols-3">
+          <div>
+            <label className="label">Gender</label>
+            <select
+              className="select select-bordered w-full"
+              value={form.gender}
+              onChange={(e) =>
+                setForm((p) => ({ ...p, gender: e.target.value }))
+              }
+              required
+            >
+              <option value="" disabled>
+                Select…
+              </option>
+              <option value="male">Male</option>
+              <option value="female">Female</option>
+              <option value="other">Other</option>
+            </select>
+          </div>
+
+          <div>
+            <label className="label">Age</label>
+            <input
+              type="number"
+              className="input input-bordered w-full"
+              value={form.age}
+              onChange={(e) => setForm((p) => ({ ...p, age: e.target.value }))}
+              min={1}
+              max={120}
+              required
+            />
+          </div>
+
+          <div>
+            <label className="label">Weight (kg)</label>
+            <input
+              type="number"
+              className="input input-bordered w-full"
+              value={form.weight}
+              onChange={(e) =>
+                setForm((p) => ({ ...p, weight: e.target.value }))
+              }
+              min={1}
+              step="0.1"
+              required
+            />
+          </div>
+        </div>
+
+        <div className="mt-6 flex w-full flex-col items-center gap-3 sm:flex-row sm:justify-center">
+          <button
+            className="btn btn-lg w-full sm:w-auto bg-green-400! text-black! hover:bg-green-500!"
+            type="submit"
+            disabled={isSubmitting}
+          >
+            {isSubmitting ? "Starting…" : "Start Tracking"}
+          </button>
+          <button
+            type="button"
+            className="btn btn-ghost btn-lg w-full sm:w-auto"
+            onClick={onCancel}
+          >
+            Cancel
+          </button>
+        </div>
+      </div>
+    </form>
+  );
+};
+
+export default TrainingStartForm;
Index: frontend/src/pages/Dashboard/pages/Training/components/TrainingTrackingHeader.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/TrainingTrackingHeader.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/TrainingTrackingHeader.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,22 @@
+import React from "react";
+
+const TrainingTrackingHeader = ({ onAddSession }) => {
+  return (
+    <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
+      <div>
+        <h1 className="text-2xl font-bold">Training</h1>
+        <p className="opacity-80 mt-1">Your recent sessions and progress.</p>
+      </div>
+
+      <button
+        type="button"
+        className="btn bg-green-400! text-black! hover:bg-green-500!"
+        onClick={onAddSession}
+      >
+        + Add new training session
+      </button>
+    </div>
+  );
+};
+
+export default TrainingTrackingHeader;
Index: frontend/src/pages/Dashboard/pages/Training/components/WorkoutTypeSelect.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Training/components/WorkoutTypeSelect.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Training/components/WorkoutTypeSelect.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,27 @@
+import React from "react";
+
+const WorkoutTypeSelect = ({ workoutTypes, value, onChange, disabled }) => {
+  return (
+    <div>
+      <label className="label">Workout type</label>
+      <select
+        className="select select-bordered w-full"
+        value={value}
+        onChange={onChange}
+        required
+        disabled={disabled}
+      >
+        <option value="" disabled>
+          Select…
+        </option>
+        {workoutTypes.map((t) => (
+          <option key={t.type} value={t.type}>
+            {t.label ?? t.type}
+          </option>
+        ))}
+      </select>
+    </div>
+  );
+};
+
+export default WorkoutTypeSelect;
Index: frontend/src/pages/Dashboard/pages/Weight/Weight.jsx
===================================================================
--- frontend/src/pages/Dashboard/pages/Weight/Weight.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
+++ frontend/src/pages/Dashboard/pages/Weight/Weight.jsx	(revision 8c01a1f14ebd23dc0aeffe2d391546a92ddde9f9)
@@ -0,0 +1,16 @@
+import React from "react";
+
+const Weight = () => {
+  return (
+    <div className="space-y-4">
+      <h1 className="text-2xl font-bold">Weight</h1>
+      <div className="card bg-base-200 border border-base-300">
+        <div className="card-body">
+          <p className="opacity-80">Weight dashboard content goes here.</p>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default Weight;
