diff --git a/src/components/__tests__/motion-reduce-spin-coverage.test.ts b/src/components/__tests__/motion-reduce-spin-coverage.test.ts
new file mode 100644
index 000000000..c9a730571
--- /dev/null
+++ b/src/components/__tests__/motion-reduce-spin-coverage.test.ts
@@ -0,0 +1,67 @@
+import { describe, it, expect } from "vitest";
+import { readdirSync, readFileSync, statSync } from "node:fs";
+import { join } from "node:path";
+
+/**
+ * v1.4.43 W11 — motion-reduce sweep guard.
+ *
+ * Every `animate-spin` site in component / app code must also carry
+ * `motion-reduce:animate-none` so that motion-sensitive users see a
+ * static icon instead of a continuous rotation. The audit found 21
+ * sites missing the modifier; this test prevents regressions.
+ *
+ * Scope: `src/**` excluding test files. The check is textual to avoid
+ * rendering every loading state — a CI-cheap guard that catches drift
+ * any time a new spinner is added without the modifier.
+ */
+const ROOT = join(process.cwd(), "src");
+const EXCLUDE_DIRS = new Set(["__tests__", "node_modules"]);
+
+function collectTsxFiles(dir: string, acc: string[] = []): string[] {
+ for (const entry of readdirSync(dir)) {
+ if (EXCLUDE_DIRS.has(entry)) continue;
+ const full = join(dir, entry);
+ const stat = statSync(full);
+ if (stat.isDirectory()) {
+ collectTsxFiles(full, acc);
+ } else if (
+ entry.endsWith(".tsx") &&
+ !entry.endsWith(".test.tsx") &&
+ !entry.endsWith(".test.ts")
+ ) {
+ acc.push(full);
+ }
+ }
+ return acc;
+}
+
+describe("motion-reduce coverage — every animate-spin pairs with motion-reduce:animate-none", () => {
+ const files = collectTsxFiles(ROOT);
+
+ it("collects component files to scan", () => {
+ expect(files.length).toBeGreaterThan(50);
+ });
+
+ it("every animate-spin site declares motion-reduce:animate-none", () => {
+ const violations: string[] = [];
+ for (const file of files) {
+ const src = readFileSync(file, "utf8");
+ const lines = src.split("\n");
+ lines.forEach((line, idx) => {
+ if (!line.includes("animate-spin")) return;
+ // Skip comments and string literals that aren't classNames.
+ if (line.trim().startsWith("//") || line.trim().startsWith("*"))
+ return;
+ if (!line.includes("motion-reduce:animate-none")) {
+ violations.push(`${file}:${idx + 1}: ${line.trim()}`);
+ }
+ });
+ }
+ expect(
+ violations,
+ `The following animate-spin sites are missing motion-reduce:animate-none. ` +
+ `Append the modifier so motion-sensitive users see a static icon:\n` +
+ violations.join("\n"),
+ ).toEqual([]);
+ });
+});
diff --git a/src/components/admin/api-token-overview-section.tsx b/src/components/admin/api-token-overview-section.tsx
index fc5faf58f..f42166114 100644
--- a/src/components/admin/api-token-overview-section.tsx
+++ b/src/components/admin/api-token-overview-section.tsx
@@ -166,7 +166,7 @@ export function ApiTokenOverviewSection() {
{isLoading ? (
-
+
) : !tokens?.length ? (
// v1.4.15 phase-C5: empty state explains where tokens come
diff --git a/src/components/admin/backups-section.tsx b/src/components/admin/backups-section.tsx
index 13a4c913e..b9a7d9b6d 100644
--- a/src/components/admin/backups-section.tsx
+++ b/src/components/admin/backups-section.tsx
@@ -362,7 +362,7 @@ export function BackupsSection() {
className="min-h-11"
>
{runBackup.isPending ? (
-
+
) : (
)}
@@ -418,7 +418,7 @@ export function BackupsSection() {
className="min-h-11"
>
{upload.isPending ? (
-
+
) : (
)}
@@ -429,7 +429,7 @@ export function BackupsSection() {
{isLoading ? (
-
+
{t("admin.section.backups.loading")}
@@ -459,7 +459,7 @@ export function BackupsSection() {
className="min-h-11"
>
{runBackup.isPending ? (
-
+
) : (
)}
@@ -518,7 +518,7 @@ export function BackupsSection() {
className="min-h-11"
>
{downloadingId === row.id ? (
-
+
) : (
)}
diff --git a/src/components/admin/system-status-section.tsx b/src/components/admin/system-status-section.tsx
index 506ee5305..549ae3afa 100644
--- a/src/components/admin/system-status-section.tsx
+++ b/src/components/admin/system-status-section.tsx
@@ -228,7 +228,7 @@ export function SystemStatusSection() {
data-testid="system-status-retry"
>
{isFetching ? (
-
+
) : (
)}
@@ -237,7 +237,7 @@ export function SystemStatusSection() {
) : (
-
+
{t("admin.loadingStatus")}
diff --git a/src/components/charts/health-chart.tsx b/src/components/charts/health-chart.tsx
index 514b20911..059351ebc 100644
--- a/src/components/charts/health-chart.tsx
+++ b/src/components/charts/health-chart.tsx
@@ -1286,7 +1286,7 @@ export function HealthChart({
{isLoading ? (
-
+
) : !chartData?.length ? null : (chartData?.length ?? 0) < 3 ? (
// v1.4.16 B1a — sparse-data placeholder. <3 daily points is too
diff --git a/src/components/doctor-report/doctor-report-dialog.tsx b/src/components/doctor-report/doctor-report-dialog.tsx
index 75416295d..4a3bc1566 100644
--- a/src/components/doctor-report/doctor-report-dialog.tsx
+++ b/src/components/doctor-report/doctor-report-dialog.tsx
@@ -483,7 +483,7 @@ export function DoctorReportDialog({
@@ -572,7 +572,7 @@ function SectionToggles({
{t("doctorReport.sections.title")}
{availabilityLoading && (
-
+
)}
diff --git a/src/components/medications/DrugLevelChart.tsx b/src/components/medications/DrugLevelChart.tsx
index f5435c7dc..5b1e44bbe 100644
--- a/src/components/medications/DrugLevelChart.tsx
+++ b/src/components/medications/DrugLevelChart.tsx
@@ -232,7 +232,7 @@ export function DrugLevelChart({ medication, asOf }: DrugLevelChartProps) {
className="flex h-[220px] items-center justify-center"
data-slot="drug-level-chart-loading"
>
-
+
) : !hasDoses ? (
diff --git a/src/components/mood/mood-list.tsx b/src/components/mood/mood-list.tsx
index 42bae8831..5bbbafdf6 100644
--- a/src/components/mood/mood-list.tsx
+++ b/src/components/mood/mood-list.tsx
@@ -301,7 +301,7 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
{isLoading ? (
-
+
) : !data?.entries?.length ? (
// v1.4.15 phase-C5: replace bare-text empty rectangle with
diff --git a/src/components/settings/about-section.tsx b/src/components/settings/about-section.tsx
index 5f4860240..16d1a1810 100644
--- a/src/components/settings/about-section.tsx
+++ b/src/components/settings/about-section.tsx
@@ -211,7 +211,7 @@ export function AboutSection() {
{isLoading || !version ? (
-
+
{t("common.loading")}
diff --git a/src/components/settings/account-section.tsx b/src/components/settings/account-section.tsx
index 9f8712f78..ba846da25 100644
--- a/src/components/settings/account-section.tsx
+++ b/src/components/settings/account-section.tsx
@@ -359,7 +359,7 @@ export function AccountSection() {
if (isLoading) {
return (
-
+
);
}
@@ -617,7 +617,7 @@ export function AccountSection() {
className="w-full shrink-0 sm:w-auto"
>
{tourRestarting ? (
-
+
) : (
)}
@@ -708,7 +708,7 @@ export function AccountSection() {