diff --git a/src/components/clinical-dashboard/medication-nav-header.tsx b/src/components/clinical-dashboard/medication-nav-header.tsx
index 6fa1ab6b4..40fe2fae4 100644
--- a/src/components/clinical-dashboard/medication-nav-header.tsx
+++ b/src/components/clinical-dashboard/medication-nav-header.tsx
@@ -177,7 +177,10 @@ export function MedicationNavHeader({
if (isMedicationTabId(id)) onSelectTab(id);
}}
// Every slot carries a section count, so the four-slot band clips.
- rail={{ label: "Medication sections", countedLabels: true }}
+ // `extended-counted`, named rather than inherited. Its four slots each
+ // carry an icon, a label and a count badge, so they need the wider bands
+ // this profile was calibrated for — not Therapy's wordmark bands.
+ rail={{ label: "Medication sections", density: "extended-counted", countedLabels: true }}
/>
);
}
diff --git a/src/components/in-page-nav/in-page-nav-header.tsx b/src/components/in-page-nav/in-page-nav-header.tsx
index cf03617e8..d234556aa 100644
--- a/src/components/in-page-nav/in-page-nav-header.tsx
+++ b/src/components/in-page-nav/in-page-nav-header.tsx
@@ -139,8 +139,12 @@ export type InPageNavHeaderProps =
*/
rail?: {
label: string;
- /** Calibrated label family — moves only the band widths. */
- density?: ModeNavDensityProfile;
+ /**
+ * Calibrated label family, moving only the band widths. Required: see
+ * the note on `InPageSectionRail`'s own `density` for why inheriting a
+ * default here shipped a regression.
+ */
+ density: ModeNavDensityProfile;
/** `true` when every slot carries a count badge beside its label. */
countedLabels?: boolean;
};
diff --git a/src/components/in-page-nav/in-page-section-rail.tsx b/src/components/in-page-nav/in-page-section-rail.tsx
index d94bfcf59..1fef2e80f 100644
--- a/src/components/in-page-nav/in-page-section-rail.tsx
+++ b/src/components/in-page-nav/in-page-section-rail.tsx
@@ -35,7 +35,7 @@ export function InPageSectionRail({
sectionSheetOpen,
label,
testIdPrefix,
- density = "extended-counted",
+ density,
countedLabels = false,
}: {
sections: readonly PageSection[];
@@ -50,8 +50,20 @@ export function InPageSectionRail({
* Which calibrated label family this rail's labels belong to. It only moves
* the container width at which each band becomes active — never the order,
* and never which item folds first.
+ *
+ * Required, with no default, and that is the point. It used to default to
+ * `extended`, so medication's rail inherited Therapy's profile without ever
+ * naming it — invisible to `registryModeNavDensityProfiles`, which lists only
+ * the modes that render the top bar. Retuning `extended` for Therapy in
+ * PR #2686 therefore silently unfolded medication's counted labels early, and
+ * `ui-smoke`'s prescribing journey was the only thing that caught it.
+ *
+ * A default cannot be safe here: the value is a calibration against one
+ * label family's measured widths, so inheriting someone else's is always a
+ * guess. Making it required costs each call site one line and makes the
+ * consumer set answerable by `git grep "density:"`.
*/
- density?: ModeNavDensityProfile;
+ density: ModeNavDensityProfile;
/**
* `true` when every slot carries a count badge beside its label, which needs
* roughly a third more width per slot.
diff --git a/src/components/therapy-compass/pathway-step-stack.tsx b/src/components/therapy-compass/pathway-step-stack.tsx
index e0d75464e..15e801107 100644
--- a/src/components/therapy-compass/pathway-step-stack.tsx
+++ b/src/components/therapy-compass/pathway-step-stack.tsx
@@ -9,6 +9,7 @@ import { cn } from "@/components/ui-primitives";
import type { Pathway, PathwayStep, Therapy } from "./data/types";
import { pathwayLinkedStepCount } from "./pathway-review-label";
+import { StatusBadge } from "./ui";
type PathwayStepStackProps = {
steps: PathwayStep[];
@@ -61,6 +62,11 @@ function StepCard({
{roleLabel}
+ {/* Only when the step resolves to a record. `description` falls
+ back to that record's `bestUsedFor`, so a linked step quotes
+ its clinical prose and owes its review state; an unlinked step
+ names no record and has none to state. */}
+ {therapy ? : null}
{description}
@@ -106,7 +112,10 @@ function StepCard({
-
{title}
+
+ {title}
+ {therapy ? : null}
+
{description}
diff --git a/src/components/therapy-compass/record/related-therapies.tsx b/src/components/therapy-compass/record/related-therapies.tsx
index bc628d07f..9da73a7f3 100644
--- a/src/components/therapy-compass/record/related-therapies.tsx
+++ b/src/components/therapy-compass/record/related-therapies.tsx
@@ -8,6 +8,7 @@ import { InteractiveRow } from "@/components/ui/interactive-row";
import { cardPreviewText } from "../data/select";
import type { RelatedTherapy } from "../data/related";
+import { StatusBadge } from "../ui";
/**
* Nearest neighbours, each carrying the reason it is here.
@@ -44,6 +45,14 @@ export function RelatedTherapies({ related, onOpen }: { related: RelatedTherapy[
{reason}
+ {/* The row prints a sentence of another record's clinical
+ prose, so it owes that record's review state — the same
+ obligation the card, detail, brief and sheet already meet.
+ It matters more since the catalogue-wide notice went
+ (PR #2686): the per-record badge is now the whole
+ disclosure, and this listing was one of two surfaces
+ quoting a record without it. */}
+
{cardPreviewText(therapy.bestUsedFor ?? therapy.clinicalSummary, {
diff --git a/src/components/therapy-compass/ui.tsx b/src/components/therapy-compass/ui.tsx
index 8c41e6a68..5504362a0 100644
--- a/src/components/therapy-compass/ui.tsx
+++ b/src/components/therapy-compass/ui.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import { ShieldCheck, TriangleAlert, type LucideIcon } from "lucide-react";
-import { Chip, type ChipAppearance } from "@/components/ui/chip";
+import { Chip, type ChipAppearance, type ChipSize } from "@/components/ui/chip";
import { missingValuePhrase } from "@/components/ui/missing-value";
import { cn, EmptyState as SharedEmptyState, LoadingPanel } from "@/components/ui-primitives";
@@ -86,12 +86,12 @@ export function TagRow({
// ---- review status badge ------------------------------------------------
-export function StatusBadge({ status }: { status: string }) {
+export function StatusBadge({ status, size = "standard" }: { status: string; size?: ChipSize }) {
const meta = reviewStatusMeta(status);
const tone = meta.tone === "success" ? "success" : meta.tone === "warning" ? "warning" : "neutral";
const Icon = meta.tone === "success" ? ShieldCheck : TriangleAlert;
return (
-
+
{meta.label}
);
diff --git a/tests/in-page-nav-header.dom.test.tsx b/tests/in-page-nav-header.dom.test.tsx
index 1add79845..c36c98622 100644
--- a/tests/in-page-nav-header.dom.test.tsx
+++ b/tests/in-page-nav-header.dom.test.tsx
@@ -149,7 +149,7 @@ describe("InPageNavHeader", () => {
});
it("keeps a phone gap between the action row and the section rail", () => {
- renderHeader({ rail: { label: "Service sections" } });
+ renderHeader({ rail: { label: "Service sections", density: "balanced-four" } });
expect(screen.getByTestId("service-section-rail")).toHaveClass("mt-2");
});
@@ -201,7 +201,7 @@ describe("InPageNavHeader", () => {
// closes after the viewport crosses `sm` must not restore to a display:none
// button (focus then falls to the page body).
const user = userEvent.setup();
- renderHeader({ rail: { label: "Service sections" } });
+ renderHeader({ rail: { label: "Service sections", density: "balanced-four" } });
const trigger = screen.getByTestId("service-section-trigger");
await user.click(trigger);
diff --git a/tests/mode-nav-contract.test.ts b/tests/mode-nav-contract.test.ts
index aa64e9a79..80be3c4da 100644
--- a/tests/mode-nav-contract.test.ts
+++ b/tests/mode-nav-contract.test.ts
@@ -207,6 +207,32 @@ describe("ModeNav item contract", () => {
});
});
+describe("ModeNav density is chosen, never inherited", () => {
+ const railSource = read("src/components/in-page-nav/in-page-section-rail.tsx");
+ const headerSource = read("src/components/in-page-nav/in-page-nav-header.tsx");
+
+ it("requires every rail to name its own density profile", () => {
+ // `density` defaulted to a profile, so medication's rail inherited
+ // Therapy's without naming it. `registryModeNavDensityProfiles` lists only
+ // the modes that render the top bar, so the coupling was invisible there,
+ // and retuning `extended` for Therapy in PR #2686 unfolded medication's
+ // counted labels early. A default cannot be safe: the value is calibrated
+ // against one label family's measured widths.
+ expect(railSource).not.toMatch(/density\s*=\s*"/);
+ expect(railSource).toMatch(/^\s*density: ModeNavDensityProfile;/m);
+ expect(headerSource).toMatch(/^\s*density: ModeNavDensityProfile;/m);
+ });
+
+ it("keeps every rail call site naming a profile, so the consumer set is greppable", () => {
+ for (const path of [
+ "src/components/clinical-dashboard/medication-nav-header.tsx",
+ "src/components/therapy-compass/therapy-record-nav-header.tsx",
+ ]) {
+ expect(read(path), `${path} must name its density profile`).toMatch(/density: "/);
+ }
+ });
+});
+
describe("ModeNav overflow slot", () => {
const moreSlot = modeNavSource.slice(
modeNavSource.indexOf("plan.moreUntil !== null ? ("),
diff --git a/tests/therapy-review-regressions.test.ts b/tests/therapy-review-regressions.test.ts
index 134ac85ce..2f3368319 100644
--- a/tests/therapy-review-regressions.test.ts
+++ b/tests/therapy-review-regressions.test.ts
@@ -80,6 +80,14 @@ describe("Therapy review regression contracts", () => {
"src/components/therapy-compass/screens/sheets-screen.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
"src/components/therapy-compass/screens/pathways-screen.tsx",
+ // The two secondary listings. Both quote another record's clinical prose
+ // (`bestUsedFor` / `clinicalSummary`) beside its name, and both shipped
+ // without its review state — the gap the 2026-09-02 audit recorded under
+ // L03c. Harmless while a catalogue-wide notice also stated the caveat;
+ // load-bearing once PR #2686 removed it and left the per-record badge as
+ // the whole disclosure.
+ "src/components/therapy-compass/record/related-therapies.tsx",
+ "src/components/therapy-compass/pathway-step-stack.tsx",
]) {
expect(source(path), `${path} must still surface reviewStatus`).toContain("reviewStatus");
}