diff --git a/canvas/src/components/mobile/__tests__/palette-context.test.tsx b/canvas/src/components/mobile/__tests__/palette-context.test.tsx
new file mode 100644
index 00000000..4dd5c09e
--- /dev/null
+++ b/canvas/src/components/mobile/__tests__/palette-context.test.tsx
@@ -0,0 +1,131 @@
+// @vitest-environment jsdom
+/**
+ * palette-context: MobileAccentProvider + usePalette hook coverage.
+ *
+ * Covers:
+ * - usePalette(dark=false) without provider → MOL_LIGHT
+ * - usePalette(dark=true) without provider → MOL_DARK
+ * - usePalette with provider accent=null → base palette unchanged
+ * - usePalette with provider accent=base.accent → base palette unchanged (identity guard)
+ * - usePalette with provider accent="#ff0000" → accent + online overridden
+ * - MobileAccentProvider renders children
+ * - Never mutates the static MOL_LIGHT/MOL_DARK singletons
+ *
+ * The pure functions (getPalette, normalizeStatus, tierCode) are covered
+ * in palette.test.ts — only the React context/hook is tested here.
+ */
+import { afterEach, describe, expect, it, vi } from "vitest";
+import { cleanup, render } from "@testing-library/react";
+import React from "react";
+
+import { MobileAccentProvider, usePalette } from "../palette-context";
+import { MOL_DARK, MOL_LIGHT } from "../palette";
+
+afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+});
+
+// ─── Test helpers ──────────────────────────────────────────────────────────────
+// Each helper renders exactly one usePalette value as a testid element.
+// Using unique testids per scenario avoids "multiple elements" DOM pollution
+// when tests run in the same jsdom worker without strict cleanup timing.
+
+function AccentDump({ dark }: { dark: boolean }) {
+ const palette = usePalette(dark);
+ return {palette.accent};
+}
+
+function OnlineDump({ dark }: { dark: boolean }) {
+ const palette = usePalette(dark);
+ return {palette.online};
+}
+
+// ─── MobileAccentProvider ──────────────────────────────────────────────────────
+describe("MobileAccentProvider", () => {
+ it("renders children", () => {
+ const { getByText } = render(
+
+ child content
+ ,
+ );
+ expect(getByText("child content").textContent).toBe("child content");
+ });
+});
+
+// ─── usePalette — no provider ─────────────────────────────────────────────────
+describe("usePalette without MobileAccentProvider", () => {
+ it("returns MOL_LIGHT when dark=false", () => {
+ const { getByTestId } = render();
+ expect(getByTestId("accent-val").textContent).toBe(MOL_LIGHT.accent);
+ });
+
+ it("returns MOL_DARK when dark=true", () => {
+ const { getByTestId } = render();
+ expect(getByTestId("accent-val").textContent).toBe(MOL_DARK.accent);
+ });
+});
+
+// ─── usePalette — with MobileAccentProvider ────────────────────────────────────
+describe("usePalette with MobileAccentProvider", () => {
+ it("returns base palette unchanged when accent=null", () => {
+ const { getByTestId } = render(
+
+
+ ,
+ );
+ expect(getByTestId("accent-val").textContent).toBe(MOL_LIGHT.accent);
+ });
+
+ it("returns base palette unchanged when accent matches base.accent (identity guard)", () => {
+ const { getByTestId } = render(
+
+
+ ,
+ );
+ expect(getByTestId("accent-val").textContent).toBe(MOL_LIGHT.accent);
+ });
+
+ it("overrides accent when provider supplies a different colour", () => {
+ const CUSTOM = "#ff0000";
+ const { getByTestId } = render(
+
+
+ ,
+ );
+ expect(getByTestId("accent-val").textContent).toBe(CUSTOM);
+ });
+
+ it("also overrides online when accent is overridden", () => {
+ const CUSTOM = "#ff8800";
+ const { getByTestId } = render(
+
+
+ ,
+ );
+ expect(getByTestId("online-val").textContent).toBe(CUSTOM);
+ });
+});
+
+// ─── Immutability ─────────────────────────────────────────────────────────────
+describe("MOL_LIGHT and MOL_DARK singletons are never mutated", () => {
+ it("MOL_LIGHT.accent unchanged after custom-accent render", () => {
+ const before = MOL_LIGHT.accent;
+ render(
+
+
+ ,
+ );
+ expect(MOL_LIGHT.accent).toBe(before);
+ });
+
+ it("MOL_DARK.accent unchanged after custom-accent render", () => {
+ const before = MOL_DARK.accent;
+ render(
+
+
+ ,
+ );
+ expect(MOL_DARK.accent).toBe(before);
+ });
+});