diff --git a/src/components/medications/card-parts/medication-compliance-bars.tsx b/src/components/medications/card-parts/medication-compliance-bars.tsx
index cd613b19d..4d6de452a 100644
--- a/src/components/medications/card-parts/medication-compliance-bars.tsx
+++ b/src/components/medications/card-parts/medication-compliance-bars.tsx
@@ -77,3 +77,38 @@ export function MedicationComplianceBars({
);
}
+
+/**
+ * Constant-height placeholder for the compliance block, shown while the
+ * per-card `/compliance` query is in flight or returns null. Without it a
+ * card whose compliance has not yet resolved is ~5rem shorter than a sibling
+ * whose compliance loaded, so the two cards in a 2-col grid row jump to
+ * unequal heights (transiently on first paint, persistently for any med
+ * whose endpoint returns null). The skeleton mirrors the two-bar layout's
+ * footprint so the card body keeps a constant inventory and the action row
+ * pins to the same baseline across the row.
+ *
+ * The streak-flame row height is intentionally NOT reserved — the flame is
+ * gated on `streak > 0` on the loaded card too, so reserving it would make
+ * every streak-less card taller than its own loaded self.
+ */
+export function MedicationComplianceSkeleton() {
+ return (
+
+ );
+}
diff --git a/src/components/medications/card-parts/medication-next-last-slot.tsx b/src/components/medications/card-parts/medication-next-last-slot.tsx
new file mode 100644
index 000000000..f2f6e6ea2
--- /dev/null
+++ b/src/components/medications/card-parts/medication-next-last-slot.tsx
@@ -0,0 +1,106 @@
+import { useTranslations } from "@/lib/i18n/context";
+
+/**
+ * Shared "next / last intake" middle slot for the medication cards.
+ *
+ * Extracted from the generic and GLP-1 cards so the slot is structurally
+ * identical rather than hand-synced. Before extraction the two cards
+ * diverged on every axis the eye reads in a 2-col grid row:
+ * - order: generic showed next-then-last; GLP-1 showed last-then-next;
+ * - colour: generic next-line was `text-muted-foreground`; GLP-1 was the
+ * brighter `text-foreground/85`;
+ * - spacing: generic `space-y-3.5`; GLP-1 `space-y-1`;
+ * - gating: generic only rendered the next-line when a schedule was due
+ * and the window was not currently open; GLP-1 always rendered it and
+ * printed a literal "—" placeholder.
+ *
+ * This part fixes the order (next then last), the colour
+ * (`text-muted-foreground` on both lines), the spacing (`space-y-3.5`), and
+ * the gating (the caller only passes `next` when it should render, and the
+ * placeholder branch is gone). The reserved `min-h-[2.75rem]` keeps a card
+ * whose last-dose line is absent the same vertical footprint as a sibling
+ * that renders both lines, so the dose rows line up across the grid.
+ *
+ * Each card still builds the line *content* itself (the generic card emits a
+ * day-label + window-range; the GLP-1 card emits a weekly-cadence prose
+ * string), because the two cadences read differently — but the wrapper,
+ * order, colour, and gating are now guaranteed identical.
+ */
+interface MedicationNextLastSlotProps {
+ /**
+ * The upcoming-intake line content, or null when there is nothing to show
+ * (no schedule, the window is currently open, a one-shot already taken,
+ * etc.). The caller owns the gating; this part never prints a placeholder.
+ */
+ next: React.ReactNode | null;
+ /** The last-intake line content, or null when the med has never been taken. */
+ last: React.ReactNode | null;
+ /**
+ * When true (the generic card) the part prepends the bold
+ * "Next intake" / "Last intake" labels. The GLP-1 card passes its own
+ * site-aware prose (whose i18n strings already read "Next injection …" /
+ * "Last injection …"), so it opts out and supplies the full line content.
+ * Either way the wrapper, order, colour, spacing, gating and reserved
+ * min-height are identical.
+ */
+ labelled?: boolean;
+}
+
+export function MedicationNextLastSlot({
+ next,
+ last,
+ labelled = true,
+}: MedicationNextLastSlotProps) {
+ const { t } = useTranslations();
+
+ return (
+
+ {next && (
+
+ {labelled && (
+ <>
+ {t("medications.nextIntake")}{" "}
+ >
+ )}
+ {next}
+
+ )}
+ {last && (
+
+ {labelled && (
+ <>
+ {t("medications.lastIntake")}{" "}
+ >
+ )}
+ {last}
+
+ )}
+
+ );
+}
+
+const WEEKDAY_KEYS = [
+ "medications.daysSun",
+ "medications.daysMon",
+ "medications.daysTue",
+ "medications.daysWed",
+ "medications.daysThu",
+ "medications.daysFri",
+ "medications.daysSat",
+] as const;
+
+/**
+ * Single weekday-label helper shared by both medication cards. Before this
+ * the generic card used the `medications.weekday{Monday…}` (full-name) key
+ * family while the GLP-1 card used `medications.days{Mon…}` (abbreviated),
+ * so the same weekday rendered "Monday" on one card and "Mon" on the other
+ * in the same grid. Both now resolve to the abbreviated form, which keeps
+ * the slot tight and consistent across types.
+ *
+ * @returns a function mapping a JS `Date.getDay()` value (0 = Sunday …
+ * 6 = Saturday) to its localised abbreviated weekday name.
+ */
+export function useWeekdayLabel(): (dayIndex: number) => string {
+ const { t } = useTranslations();
+ return (dayIndex: number) => t(WEEKDAY_KEYS[dayIndex]);
+}