diff --git a/DECISIONS.md b/DECISIONS.md
index 2e3e731..c0a8fc1 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -576,3 +576,8 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **The loan card shows while a balance remains** and disappears at zero; the "Pay off your loan" goal (Step 10A.15) marks the moment.
- **Paid-off progress is measured against `dealLoanAmount`.**
- **The UI blocks bad pay-down amounts before sending** (not a whole positive number, above the balance, above credits) with a short reason; the server still enforces the same rules.
+
+## Step 10A.15
+
+- **HQ shows the next three unclaimed goals in content order,** whether ready or not, so the player always sees what comes next; claimed goals drop out and the following goal takes their place.
+- **Goal titles and rewards come from content;** the API only sends each goal's id and status.
diff --git a/apps/web/src/components/GoalsCard.tsx b/apps/web/src/components/GoalsCard.tsx
new file mode 100644
index 0000000..7e07e6f
--- /dev/null
+++ b/apps/web/src/components/GoalsCard.tsx
@@ -0,0 +1,58 @@
+import { content } from "@deadhead/content";
+import { formatCredits } from "../format";
+import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions";
+
+/** How many unclaimed goals HQ shows at once (Step 10A.15). */
+export const GOALS_SHOWN = 3;
+
+/** The next few onboarding goals, with rewards; ready ones can be claimed. */
+export function GoalsCard() {
+ const state = useLoadedPlayer();
+ const claim = useGameAction("claimGoal");
+ const next = state.goals.filter((g) => g.status !== "claimed").slice(0, GOALS_SHOWN);
+ if (next.length === 0) return null;
+
+ return (
+
+