diff --git a/DECISIONS.md b/DECISIONS.md
index ffd11fd..c7ac03d 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -1045,3 +1045,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md`
- **Clock default is `locale`** (the device's own 12/24-hour habit) rather than a fixed format.
- **Saved data is validated field by field** on load; anything unknown or broken falls back to that field's default instead of discarding everything.
- **The store is built by a factory** (`createPreferencesStore(storage)`) so tests can simulate a reload or a throwing storage; the app uses one instance on `localStorage`.
+
+## Step 10K.2
+
+- **Settings is a route (`/settings`) inside the normal layout**, reached only from the header gear, so the seven-tab navigation is unchanged and the back button works.
+- **Reduced motion is a three-way choice (Device / On / Off)**: "Device" stores no override, so the default keeps following the OS (decision 0.5.76).
+- **Items appear with the step that makes them work** (rename and new company, backup, version, developer tools), so no placeholder controls ship.
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index dc788e4..639de91 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -14,6 +14,7 @@ import { HQ } from "./screens/HQ";
import { Locations } from "./screens/Locations";
import { Market } from "./screens/Market";
import { Pilots } from "./screens/Pilots";
+import { Settings } from "./screens/Settings";
import { StartScreen } from "./screens/StartScreen";
import { usePlayerState, useRefetchOnArrival } from "./state/player";
import { Button } from "./ui";
@@ -106,6 +107,8 @@ export function App() {
element={SCREEN_COMPONENTS[screen.path]}
/>
))}
+ {/* Settings opens from the header gear, not a tab (CR-011). */}
+ } />
diff --git a/apps/web/src/layout/Layout.tsx b/apps/web/src/layout/Layout.tsx
index 3519c70..2a33897 100644
--- a/apps/web/src/layout/Layout.tsx
+++ b/apps/web/src/layout/Layout.tsx
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
-import { NavLink, Outlet } from "react-router";
+import { Link, NavLink, Outlet } from "react-router";
import { GameClock } from "../clock/GameClock";
import { SCREENS, type ScreenPath } from "../routes";
import { Icon } from "../ui";
@@ -50,6 +50,13 @@ export function Layout({
{clock}
+
+
+
{banner}
diff --git a/apps/web/src/screens/Settings.tsx b/apps/web/src/screens/Settings.tsx
new file mode 100644
index 0000000..894a7ae
--- /dev/null
+++ b/apps/web/src/screens/Settings.tsx
@@ -0,0 +1,140 @@
+import type { ReactNode } from "react";
+import { DEFAULT_CAMERA, useCamera } from "../map/camera";
+import { useLoadedPlayer } from "../state/actions";
+import { usePreferences, type ClockFormat, type CountdownFormat } from "../state/preferences";
+import { Button, Panel, Screen, ScreenHeader } from "../ui";
+
+/** Developer tools only exist in non-production builds (CR-011). */
+export function isDevBuild(): boolean {
+ return !import.meta.env.PROD;
+}
+
+/** Settings (CR-011): company, display, help, about and, in development, developer tools. */
+export function Settings() {
+ const state = useLoadedPlayer();
+ const prefs = usePreferences();
+
+ return (
+
+
+
+
+
+
+
+ label="Clock"
+ value={prefs.clock}
+ options={[
+ ["locale", "Device"],
+ ["12h", "12-hour"],
+ ["24h", "24-hour"],
+ ]}
+ onChange={(clock) => prefs.set({ clock })}
+ />
+
+ label="Countdowns"
+ value={prefs.countdown}
+ options={[
+ ["real", "Real time"],
+ ["game", "Game time"],
+ ["both", "Both"],
+ ]}
+ onChange={(countdown) => prefs.set({ countdown })}
+ />
+
+ label="Reduced motion"
+ value={prefs.reducedMotion === null ? "device" : prefs.reducedMotion ? "on" : "off"}
+ options={[
+ ["device", "Device"],
+ ["on", "On"],
+ ["off", "Off"],
+ ]}
+ onChange={(v) => prefs.set({ reducedMotion: v === "device" ? null : v === "on" })}
+ />
+
+
+
+
+
+
+
+
+
+
+ Deadhead: Space Freight Tycoon
+
+
+ {isDevBuild() && (
+
+ Development build tools.
+
+ )}
+
+ );
+}
+
+function Section({ title, children }: { title: string; children: ReactNode }) {
+ const id = `settings-${title.toLowerCase()}`;
+ return (
+
+
+ {title}
+
+ {children}
+
+ );
+}
+
+function Row({ label, children }: { label: string; children: ReactNode }) {
+ return (
+
+ {label}
+ {children}
+
+ );
+}
+
+/** A labelled radio group of a few options, styled as a segmented control. */
+function Choice(props: {
+ label: string;
+ value: T;
+ options: [T, string][];
+ onChange: (value: T) => void;
+}) {
+ return (
+
+ );
+}
diff --git a/apps/web/src/ui/icons.tsx b/apps/web/src/ui/icons.tsx
index a6c6fad..8c4dc54 100644
--- a/apps/web/src/ui/icons.tsx
+++ b/apps/web/src/ui/icons.tsx
@@ -82,6 +82,12 @@ const PATHS = {
>
),
close: ,
+ gear: (
+ <>
+
+
+ >
+ ),
plus: ,
target: (
<>
diff --git a/apps/web/test/settings.test.tsx b/apps/web/test/settings.test.tsx
new file mode 100644
index 0000000..6f22241
--- /dev/null
+++ b/apps/web/test/settings.test.tsx
@@ -0,0 +1,39 @@
+import { screen, within } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { DEFAULT_PREFERENCES, usePreferences } from "../src/state/preferences";
+import { fakeApi, renderApp, samplePlayerState } from "./render";
+
+const owner = () => samplePlayerState({ tutorial: null });
+
+describe("settings screen (Step 10K.2)", () => {
+ afterEach(() => {
+ vi.unstubAllEnvs();
+ usePreferences.getState().set(DEFAULT_PREFERENCES);
+ });
+
+ it("opens from the header gear with every section", async () => {
+ renderApp(fakeApi({ getPlayerState: owner }), "/");
+ await userEvent.click(await screen.findByRole("link", { name: "Settings" }));
+ expect(await screen.findByRole("heading", { level: 1, name: "Settings" })).toBeInTheDocument();
+ for (const section of ["Company", "Display", "Help", "About", "Developer"]) {
+ expect(screen.getByRole("region", { name: section }), section).toBeInTheDocument();
+ }
+ });
+
+ it("leaves out the Developer section in a production build", async () => {
+ vi.stubEnv("PROD", true);
+ renderApp(fakeApi({ getPlayerState: owner }), "/settings");
+ await screen.findByRole("region", { name: "Display" });
+ expect(screen.queryByRole("region", { name: "Developer" })).not.toBeInTheDocument();
+ });
+
+ it("saves display choices to the device preferences", async () => {
+ renderApp(fakeApi({ getPlayerState: owner }), "/settings");
+ const display = await screen.findByRole("region", { name: "Display" });
+ await userEvent.click(within(display).getByRole("radio", { name: "24-hour" }));
+ await userEvent.click(within(display).getByRole("radio", { name: "Both" }));
+ expect(usePreferences.getState()).toMatchObject({ clock: "24h", countdown: "both" });
+ expect(within(display).getByRole("radio", { name: "24-hour" })).toBeChecked();
+ });
+});
diff --git a/memory-bank/progress.md b/memory-bank/progress.md
index 66419ab..b1490de 100644
--- a/memory-bank/progress.md
+++ b/memory-bank/progress.md
@@ -253,7 +253,7 @@ Spec: `memory-bank/change-requests/CR-010-veteran-pilot-profit-share.md` (receiv
Spec: `memory-bank/change-requests/CR-011-settings-backup.md` (received as "CR-008 … Phase 10H"). New decisions recorded as plan §0.5.71–0.5.76.
- [x] **Step 10K.1 — Client: device preferences store.** `state/preferences.ts`: `usePreferences` (Zustand) with clock (`locale`/`12h`/`24h`), countdown (`real`/`game`/`both`), reduced-motion override (null = follow the device), dismissed tips and saved map camera; `set`, `dismissTip`, `showTipsAgain`. Saved under `deadhead:preferences:v1`; loading validates each field and falls back to defaults; every read and write is wrapped. `reducedMotionOn(prefs)`. Test: `preferences.test.ts` (defaults incl. mocked OS reduced motion, survives a reload, broken data ignored, throwing storage works in memory).
-- [ ] Step 10K.2 — Client: settings screen and entry point
+- [x] **Step 10K.2 — Client: settings screen and entry point.** A gear link (`aria-label="Settings"`, new `gear` icon) at the right of the header opens `/settings` (a route inside the layout, not a nav tab). `screens/Settings.tsx`: sections Company (name), Display (Clock: Device/12-hour/24-hour; Countdowns: Real time/Game time/Both; Reduced motion: Device/On/Off, saved to device preferences), Help (Reset map view, Show tips again), About (game name) and Developer, which renders only when `import.meta.env.PROD` is false. Remaining items arrive with their steps. Test: `settings.test.tsx`.
- [ ] Step 10K.3 — Client: display options applied everywhere
- [ ] Step 10K.4 — Client: reset map view and show tips again
- [ ] Step 10K.5 — Server: rename company