diff --git a/src/components/medications/MedicationCardHeader.tsx b/src/components/medications/MedicationCardHeader.tsx
index df201dcb2..dc13fd51a 100644
--- a/src/components/medications/MedicationCardHeader.tsx
+++ b/src/components/medications/MedicationCardHeader.tsx
@@ -48,12 +48,27 @@ export function MedicationCardHeader({
{name} {dose}
-
+ {/* D-H6 — state badges (without-notification, paused-since,
+ inactive) used to share line 2 with the category badge
+ via `flex flex-wrap`. At 320 px any state badge pushed
+ the row to three lines and broke the FB-G1 "two-line,
+ no exceptions" contract for ~20 % of configured drugs.
+ State badges now ride their own row below the category
+ badge so the canonical row stays two lines on narrow
+ viewports. */}
+
{categoryLabel}
- {stateBadges}
+ {stateBadges ? (
+
+ {stateBadges}
+
+ ) : null}
{actions ? (
{actions}
diff --git a/src/components/medications/SideEffectsSection.tsx b/src/components/medications/SideEffectsSection.tsx
index 32ba14695..45fdec960 100644
--- a/src/components/medications/SideEffectsSection.tsx
+++ b/src/components/medications/SideEffectsSection.tsx
@@ -303,7 +303,14 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
{fmt.dateShort(new Date(row.occurredAt))}
diff --git a/src/components/medications/__tests__/MedicationCardHeader.test.tsx b/src/components/medications/__tests__/MedicationCardHeader.test.tsx
new file mode 100644
index 000000000..48a5617e8
--- /dev/null
+++ b/src/components/medications/__tests__/MedicationCardHeader.test.tsx
@@ -0,0 +1,91 @@
+import { describe, it, expect } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import { Badge } from "@/components/ui/badge";
+import { Card } from "@/components/ui/card";
+import { MedicationCardHeader } from "@/components/medications/MedicationCardHeader";
+
+/**
+ * v1.4.28 — narrow-viewport contract for the shared medication-list
+ * row primitive (D-H6).
+ *
+ * FB-G1 calls for a two-line shape: `{name} {dose}` on line 1 and the
+ * category badge on line 2. State badges (without-notification, paused,
+ * inactive) used to share line 2 via `flex flex-wrap`; on a 320 px
+ * viewport that pushed the row to three lines for ~20 % of configured
+ * drugs. State badges now ride their own row below the category badge
+ * so the canonical row stays two lines on narrow viewports.
+ *
+ * The tests assert the structural seams, not the visual measurement
+ * (no DOM measurement layer in SSR). The data-slot on the state-badges
+ * row plus the absence of state badges inside the category row are the
+ * load-bearing contracts.
+ */
+
+function render(node: React.ReactNode) {
+ return renderToStaticMarkup({node});
+}
+
+describe("", () => {
+ it("paints `{name} {dose}` on the title row", () => {
+ const html = render(
+ ,
+ );
+ expect(html).toContain("Ramipril 5 mg");
+ expect(html).toContain("Blood Pressure");
+ });
+
+ it("does not paint the state-badges row when no badges are supplied", () => {
+ const html = render(
+ ,
+ );
+ // Pin the absence so a future refactor can't paint an empty
+ // wrapper that eats vertical space.
+ expect(html).not.toContain(
+ 'data-slot="medication-card-header-state-badges"',
+ );
+ });
+
+ it("breaks state badges onto their own row below the category badge (D-H6)", () => {
+ // The narrow-viewport contract: state badges live on their own
+ // row so the canonical row stays two lines at 320 px even when
+ // a drug carries one of the state badges. Pinning the seam.
+ const html = render(
+
+
+ Without notification
+
+
+ Paused since 12.05
+
+ >
+ }
+ />,
+ );
+ expect(html).toContain(
+ 'data-slot="medication-card-header-state-badges"',
+ );
+ // Both badges land inside the state row, not adjacent to the
+ // category badge. Verifying via document order: the state-badges
+ // wrapper must paint AFTER the category badge.
+ const categoryIdx = html.indexOf("Blood Pressure");
+ const stateRowIdx = html.indexOf(
+ 'data-slot="medication-card-header-state-badges"',
+ );
+ expect(categoryIdx).toBeGreaterThan(-1);
+ expect(stateRowIdx).toBeGreaterThan(categoryIdx);
+ });
+});
diff --git a/src/components/medications/__tests__/SideEffectsSection.test.tsx b/src/components/medications/__tests__/SideEffectsSection.test.tsx
index a9103a29e..d33b133c9 100644
--- a/src/components/medications/__tests__/SideEffectsSection.test.tsx
+++ b/src/components/medications/__tests__/SideEffectsSection.test.tsx
@@ -194,6 +194,41 @@ describe(" — timeline rows", () => {
expect(html).toContain("Very severe");
});
+ it("narrows the date column to w-14 at narrow viewports (D-H7)", () => {
+ // The earlier `w-[5.5rem]` (88 px) date column overspec pushed
+ // the category badge + entry label + severity chip's left slot
+ // into a wrap-prone shape at 320 px. The narrowed `w-14` (56 px)
+ // fits the longest short-date variant ("15. Mai") with slack
+ // and recovers 32 px for the free-text notes.
+ const client = makeClient();
+ seedSideEffects(client, "med-1", [
+ {
+ id: "se-1",
+ category: "GI",
+ entry: "NAUSEA",
+ severity: 2,
+ occurredAt: "2026-05-13T08:00:00.000Z",
+ notes: null,
+ },
+ ]);
+ const html = render(, client);
+ // Class attribute order in SSR depends on JSX prop order; match
+ // either order so the assertion stays stable across React minor
+ // versions.
+ const dateCell = html.match(
+ /]*data-slot="side-effect-row-date"[^>]*>|
]*class="[^"]*w-14[^"]*"[^>]*data-slot="side-effect-row-date"/,
+ );
+ expect(dateCell).not.toBeNull();
+ // Source-of-truth check via the rendered HTML — pin both the
+ // adoption (`w-14`) and the absence (`w-[5.5rem]`).
+ expect(html).toContain('data-slot="side-effect-row-date"');
+ const tagWithSlot = html.match(
+ /
]*data-slot="side-effect-row-date"[^>]*>/,
+ );
+ expect(tagWithSlot?.[0]).toContain("w-14");
+ expect(tagWithSlot?.[0]).not.toContain("w-[5.5rem]");
+ });
+
it("renders German entry and category labels when locale=de", () => {
const client = makeClient();
seedSideEffects(client, "med-1", [