diff --git a/.planning/research/v1427-r4-simplifier.md b/.planning/research/v1427-r4-simplifier.md
new file mode 100644
index 000000000..467574502
--- /dev/null
+++ b/.planning/research/v1427-r4-simplifier.md
@@ -0,0 +1,418 @@
+---
+file: .planning/research/v1427-r4-simplifier.md
+purpose: R4 code-simplifier review for v1.4.27 (read-only)
+contributor: R4
+baseline: develop @ 617d4518
+scope: 422 commits / 268 files / +19,103 / -3,142 between main..develop
+---
+
+# v1.4.27 R4 — code-simplifier review
+
+Read-only sweep of the v1.4.27 diff for duplication, dead code, inline
+constants, premature abstraction, convoluted control flow, stale
+comments, component bloat, and test-fixture duplication. Most findings
+are Medium-Low because R3b B7 already retired the v1.4.25 W4 dead-code
+candidates and B7-C8 already centralised the i18n resolver and source-
+priority schema derivation.
+
+Severity legend: **High** (ship-blocking-on-merit), **Medium** (worth a
+v1.4.27.1 or early v1.4.28 follow-up), **Low** (background hygiene the
+next sweep can fold in).
+
+## Summary
+
+| Tier | Count |
+|---|---|
+| High | 1 |
+| Medium | 7 |
+| Low | 9 |
+| Total | 17 |
+
+| Theme | Findings |
+|---|---|
+| Insights sub-page duplication | F-H1, F-M1, F-M2 |
+| Premature abstraction / unused props | F-M3, F-M4, F-L1 |
+| Test-fixture duplication | F-M5 |
+| Component bloat | F-M6, F-L2 |
+| Stale comments / version markers | F-M7, F-L3 |
+| Inline constants / convoluted control flow | F-L4, F-L5, F-L6, F-L7 |
+| Minor cosmetic | F-L8, F-L9 |
+
+---
+
+## High
+
+### F-H1 — Seven insights sub-pages duplicate the same data-fetch + empty-state scaffold
+
+`src/app/insights/{blutdruck,bmi,gewicht,puls,schlaf,stimmung,medikamente}/page.tsx` each carry an identical block:
+
+```ts
+const { data: analytics } = useQuery({
+ queryKey: ["analytics"],
+ queryFn: async () => {
+ const res = await fetch("/api/analytics");
+ if (!res.ok) throw new Error("Failed");
+ const json = await res.json();
+ return json.data as AnalyticsData;
+ },
+ enabled: isAuthenticated,
+ staleTime: 60 * 1000,
+});
+
+if (
+ isAuthenticated &&
+ analytics &&
+ !hasMetricData("WEIGHT", {
+ summaries: analytics.summaries,
+ hasMood: false,
+ hasMedication: false,
+ })
+) {
+ return (
+
+ } title={…} description={…}
+ action={}
+ />
+
+ );
+}
+```
+
+The same six-line `useQuery({ queryKey: ["analytics"] … })` repeats in
+**eight** locations (verified):
+
+- `src/app/insights/page.tsx:122`
+- `src/app/insights/blutdruck/page.tsx:57`
+- `src/app/insights/bmi/page.tsx:57`
+- `src/app/insights/gewicht/page.tsx:51`
+- `src/app/insights/puls/page.tsx:60`
+- `src/app/insights/schlaf/page.tsx:39`
+- `src/app/insights/stimmung/page.tsx:50` (via `["insights","comprehensive"]` variant)
+- `src/components/insights/sleep-overview.tsx:71`
+- `src/components/insights/insights-layout-shell.tsx:47`
+
+`InsightsLayoutShell` already runs the same fetch and threads the
+result into `availability` for the tab strip; the sub-page replicas
+hit the same React-Query cache key so they're "free" beyond
+duplicated typings (`AnalyticsData` redeclared seven times) and the
+identical 14-line `EmptyState` block.
+
+**Fix**: Extract `useInsightsAnalytics()` + a ``
+helper that takes `(metric, icon, addType)` and renders the
+`SubPageShell` + `EmptyState` + CTA in one place. Each sub-page
+shrinks ~30-40 LOC and the next metric (`steps`, `active-energy`,
+`vo2-max`) added in v1.4.28 is a one-file change. Same goes for the
+five-line `` block repeated identically across
+six pages — fold into `` and unwrap inside the card.
+
+Estimated impact: ~250 LOC across seven page files collapses to
+~70-90 LOC plus one shared hook + one shared component.
+
+---
+
+## Medium
+
+### F-M1 — `AnalyticsData` interface declared seven times
+
+Same shape repeats verbatim across every insights sub-page plus
+`InsightsLayoutShell`:
+
+```ts
+interface AnalyticsData {
+ summaries: Record;
+}
+```
+
+(blutdruck:37, bmi:20, gewicht:21, puls:43, schlaf:30, stimmung — uses
+a slimmer `ComprehensiveMoodData`, sleep-overview — uses its own,
+insights-layout-shell uses `AnalyticsPayload`). Move to
+`src/lib/analytics/types.ts` (already houses `DataSummary`) and import.
+
+### F-M2 — `dynamic(() => import("@/components/charts/health-chart"))` repeats six times
+
+The same `next/dynamic` block plus the `{ default: mod.HealthChart }`
+unwrap appears unchanged in:
+
+- `blutdruck/page.tsx:40-46`
+- `bmi/page.tsx:33-39`
+- `gewicht/page.tsx:34-40`
+- `puls/page.tsx:35-41`
+- `dashboard/glp1-tile.tsx:38-44` (plus the same shape for `DrugLevelChart`)
+- `insights/page.tsx` (similar)
+
+Each consumer pays the same boilerplate to keep SSR off the chart's
+Recharts bundle. Promote a single `src/components/charts/health-chart.dynamic.ts`
+re-export (`export const HealthChartDynamic = dynamic(...)`) so the
+six consumer files lose four lines apiece.
+
+### F-M3 — `` prop landed with zero consumers
+
+`src/components/ui/empty-state.tsx:48` introduced a `ctaSize` prop in
+this release for the eight insights sub-page CTAs (CF-36 in the
+mobile-fix-plan). The prop, the `[&>a]:min-h-11 …` selector logic,
+and the doc-comment all landed — but no consumer passes `ctaSize="lg"`:
+
+```
+grep -rn "ctaSize" src/ → 4 hits, all inside empty-state.tsx itself
+```
+
+Either the eight CTAs (puls/blutdruck/bmi/gewicht/schlaf/stimmung/
+medikamente/main insights page) should be lifted to `ctaSize="lg"`,
+or the prop should be revisited in v1.4.28 once consumers actually
+opt in. The plan-fix expected eight migrations; the diff has zero.
+
+### F-M4 — `useCoachLaunch().setOpen` exported but only one consumer
+
+`src/lib/insights/coach-launch-context.tsx:50` exposes `setOpen` on
+the context value, but the only consumer is
+`src/components/insights/layout-coach-mount.tsx` which forwards it to
+``. Every other consumer
+(`coach-launch-button.tsx`, the hero strip's suggested-prompt chips,
+and the main `/insights` page) reads only `open` + `askCoach`.
+Tighten the public surface to `{ open, prefill, askCoach }` and have
+the drawer mount read a sibling helper (`closeCoach()`).
+
+Less importantly: `CoachLaunchScope.metric` is doc-stamped "Reserved
+for v1.4.28" and is currently `void`-discarded in `askCoach`. Per the
+project's "no premature abstraction" principle, drop the parameter
+until v1.4.28 actually wires it; today every call site passes one
+argument, never two.
+
+### F-M5 — Seven insight-status test files duplicate the same mock prelude
+
+`src/lib/insights/__tests__/{blood-pressure,bmi,general,medication-compliance,mood,pulse,weight}-status.test.ts`
+each lead with the same 15-25 line `vi.mock()` block for `@/lib/db`,
+`@/lib/ai/provider`, `@/lib/insights/memory`, and (where applicable)
+`@/lib/medication-category`:
+
+```ts
+vi.mock("@/lib/db", () => ({ prisma: { user: { findUnique: vi.fn() }, … } }));
+vi.mock("@/lib/ai/provider", () => ({ resolveProvider: vi.fn() }));
+vi.mock("@/lib/insights/memory", () => ({
+ getPreviousInsightContext: vi.fn().mockResolvedValue(null),
+ formatPreviousContextForPrompt: vi.fn().mockReturnValue(""),
+}));
+```
+
+Extract `src/lib/insights/__tests__/_mocks.ts` with `setupInsightStatusMocks()`
+(or rely on Vitest's `setupFiles`) so adding the next status surface in
+v1.4.28 doesn't duplicate the prelude again.
+
+### F-M6 — `CoachDrawer` weighs in at ~560 LOC
+
+`src/components/insights/coach-panel/coach-drawer.tsx` carries the
+single bottom-sheet/right-sheet switcher, the prefill-reset hook,
+the window-pill `