diff --git a/docs/site-map.md b/docs/site-map.md
index b8098c8514..55ea7e389e 100644
--- a/docs/site-map.md
+++ b/docs/site-map.md
@@ -1042,6 +1042,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir
- `/mockups/tools-action-workbench` - Route discovered from app directory Source: `src/app/mockups/tools-action-workbench/page.tsx`.
- `/mockups/tools-clinical-lanes` - Route discovered from app directory Source: `src/app/mockups/tools-clinical-lanes/page.tsx`.
- `/mockups/tools-command-center` - Route discovered from app directory Source: `src/app/mockups/tools-command-center/page.tsx`.
+- `/mockups/tools-search-directions` - Route discovered from app directory Source: `src/app/mockups/tools-search-directions/page.tsx`.
- `/mockups/tools-search-mode` - Route discovered from app directory Source: `src/app/mockups/tools-search-mode/page.tsx`.
- `/mockups/tools-split-clinical-brief` - Route discovered from app directory Source: `src/app/mockups/tools-split-clinical-brief/page.tsx`.
- `/mockups/tools-split-compact-sheet` - Route discovered from app directory Source: `src/app/mockups/tools-split-compact-sheet/page.tsx`.
diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx
index 12b548ae66..7c4b8ac0f6 100644
--- a/src/app/mockups/mockups-layout-client.tsx
+++ b/src/app/mockups/mockups-layout-client.tsx
@@ -67,6 +67,10 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
// at the bottom on phones), so the shared universal composer is suppressed here
// to avoid a second, floating search bar.
const isCalculatorsSearchPageMockup = pathname === "/mockups/calculators-search-page";
+ // Draws its own top bar, composer and results band inside every device frame, because two of
+ // the three directions restructure that band. Shared chrome above them would read as a second,
+ // real header and a second real composer over the study.
+ const isToolsSearchDirectionsMockup = pathname === "/mockups/tools-search-directions";
return (
;
+}
diff --git a/src/components/tools-search-directions-mockups.tsx b/src/components/tools-search-directions-mockups.tsx
new file mode 100644
index 0000000000..253b459ae0
--- /dev/null
+++ b/src/components/tools-search-directions-mockups.tsx
@@ -0,0 +1,860 @@
+"use client";
+
+// Design-scratch: the Tools search RESULTS state (`/tools?q=…`), three directions.
+//
+// Grounds `docs/outstanding-issues.md` `#162`, which rates this page an urgent redesign and
+// picked "Compact Results Instrument" from the static comps in
+// `public/mockups/mode-page-redesign-2026-07/tools-search/`. Those comps were never built, so
+// the pick had never been read against the real 13-record catalogue. These directions are
+// fresh, informed by that study rather than tracing it.
+//
+// Every frame draws its own top bar, composer and results band, so
+// `src/app/mockups/mockups-layout-client.tsx` suppresses the shared app chrome for this route
+// (`chromeVisible={false}`) — otherwise the real header reads as a second, live header sitting
+// over the study. Per `mockups/README.md`, shared chrome is inherited or suppressed, never
+// forked into the page, which is why the in-frame band here is a deliberate replica and the
+// real `SearchResultsHeaderBand` is not mounted: directions 01 and 03 exist to restructure
+// that band, and it reads its count noun from the live mode registry.
+//
+// Results state only. The pre-query home state and the Tools launcher redesign are out of
+// scope (`#162`'s own stop rule).
+
+import {
+ ArrowLeft,
+ Brain,
+ Calculator,
+ ChevronDown,
+ ClipboardCheck,
+ ClipboardList,
+ FileCheck2,
+ FileText,
+ Menu,
+ Pill,
+ Plus,
+ Search,
+ Send,
+ ShieldAlert,
+ ShieldCheck,
+ Sparkles,
+ Star,
+ TriangleAlert,
+ Users,
+ Waves,
+ Wrench,
+ X,
+ type LucideIcon,
+} from "lucide-react";
+
+import { cn } from "@/components/ui-primitives";
+import { normalizeSearchText } from "@/lib/catalog-search";
+import {
+ rankToolRecords,
+ toolCatalogRecordsForSession,
+ toolSearchText,
+ type ToolCatalogArea,
+ type ToolCatalogRecord,
+} from "@/lib/tools-catalog";
+
+type DirectionId = "instrument" | "brief" | "deck";
+type FrameQuery = "monitoring" | "compare";
+type FrameDevice = "phone" | "desktop";
+
+/* ------------------------------------------------------------------ *
+ * Data — the real catalogue, not fixtures.
+ * ------------------------------------------------------------------ */
+
+const areaLabels: Record = {
+ assessment: "Assess",
+ reference: "Evidence",
+ care: "Treat",
+ coordination: "Coordinate",
+ saved: "Saved",
+};
+
+// Presentation-only mapping, mirroring `applications-launcher-page.tsx`: the shared catalogue
+// is icon-free so server code (universal search) can import it.
+const iconById: Record = {
+ "clinical-kb-search": Search,
+ differentials: Brain,
+ documents: FileText,
+ guidelines: ShieldCheck,
+ "risk-safety": ShieldCheck,
+ "medication-prescribing": Pill,
+ services: Users,
+ forms: FileCheck2,
+ "care-plans": ClipboardCheck,
+ "safety-plan": ClipboardList,
+ calculators: Calculator,
+ monitoring: Waves,
+ favourites: Star,
+};
+
+/** Catalogue record with its icon already attached, so no component is resolved during render. */
+type MockupTool = ToolCatalogRecord & { icon: LucideIcon };
+
+const withIcon = (record: ToolCatalogRecord): MockupTool => ({
+ ...record,
+ icon: iconById[record.id] ?? Sparkles,
+});
+
+const session = { authenticated: true, demoMode: false } as const;
+const catalogue: MockupTool[] = toolCatalogRecordsForSession(session).map(withIcon);
+
+/**
+ * The matcher used by the submitted /tools results route. Reuse its normalizer and shared
+ * catalogue search text so the study cannot drift back to the retired launcher matcher.
+ */
+function submittedMatches(query: string): MockupTool[] {
+ const normalized = normalizeSearchText(query);
+ return catalogue.filter(
+ (tool) => (!normalized || toolSearchText(tool).includes(normalized)) && tool.id !== "favourites",
+ );
+}
+
+/** Focused contract surface: demo-query IDs must stay aligned with the production results route. */
+export function submittedToolIdsForMockup(query: string): string[] {
+ return submittedMatches(query).map((tool) => tool.id);
+}
+
+/** Reason labels as `rankToolRecords` emits them, mapped to reader-facing copy. */
+const reasonLabels: Record = {
+ title: "Name",
+ keywords: "Keywords",
+ description: "Description",
+ content: "Detail",
+ phrase: "Exact phrase",
+};
+
+const rankedMatches = (query: string) =>
+ rankToolRecords(query, 8, [], session).map((match) => ({ ...match, tool: withIcon(match.tool) }));
+
+const queryCopy: Record = {
+ monitoring: {
+ label: "monitoring",
+ note: "Submitted search finds 4 and puts the exactly-named tool last. Ranked keeps 4, Monitoring first.",
+ },
+ compare: {
+ label: "compare",
+ note: "One match — the state in the reported screenshot, where the only result fell below the fold.",
+ },
+};
+
+/* ------------------------------------------------------------------ *
+ * Direction annotations
+ * ------------------------------------------------------------------ */
+
+const directions: Array<{
+ id: DirectionId;
+ title: string;
+ verdict: string;
+ description: string;
+ changes: string[];
+}> = [
+ {
+ id: "instrument",
+ title: "Compact Results Instrument",
+ verdict: "Smallest change that fixes it",
+ description:
+ "The results state stops pretending to be the home page. The mode hero drops on submit, the band becomes the page title with the query as the only h1, and the filter row collapses onto that same line so one control no longer occupies a whole shelf. Results become dense rows whose primary button launches the tool.",
+ changes: [
+ "Mode hero and the 7-chip quick-action grid are gone once a query exists — one h1, and it is the query.",
+ "Count, mode and area filters share the title row; the second band shelf disappears. Phone lists only the areas the result set contains, so the row stays one line.",
+ "Each row's primary button is the tool's own actionLabel (Ask / Compare / Prescribe / Open); Details demotes to a desktop-only secondary, and the row itself opens detail on phone.",
+ "A filter is only drawn when there is something to filter — at one match the row disappears entirely rather than offering All 1.",
+ "Cross-mode escape added as a collapsed disclosure at the foot, so a miss is recoverable without retyping.",
+ "The colour-coding reference link leaves the results flow — it is settings, not a result.",
+ ],
+ },
+ {
+ id: "brief",
+ title: "Ranked Clinical Brief",
+ verdict: "Best decision support",
+ description:
+ "Everything in 01, plus the page finally calls rankToolRecords — which the catalogue already exports and this page has never used. The top match is promoted into an inline brief built from the four catalogue fields the results state currently hides, so the likely answer needs no dialog at all.",
+ changes: [
+ "rankToolRecords replaces submitted-order ranking: `monitoring` keeps all four matches and moves Monitoring from last to first.",
+ "Rows carry why they matched, from the ranker's own reasons array, as neutral metadata chips.",
+ "The top match expands inline into Best for / Check first / You'll need / Output — real per-tool catalogue fields, authored for all 13 tools and currently invisible.",
+ "Match-reason chips deliberately use categorical --type-* tones, never the semantic source-backed treatment: a keyword hit must not be able to read as a grounding claim.",
+ "Remaining matches stay as 01's dense rows, so density is only spent where it buys a decision.",
+ ],
+ },
+ {
+ id: "deck",
+ title: "Launch-First Triage Deck",
+ verdict: "Fewest taps to the tool",
+ description:
+ "An interaction-model change: no dialog anywhere. Every match is a disclosure that opens its detail inline, so nothing is hidden behind a modal, and the area filter folds into the band as a segmented control that doubles as the count. Safety-first tools are marked permanently rather than one tap deep.",
+ changes: [
+ "The detail dialog is removed. Rows are native , so detail costs no modal and no JavaScript.",
+ "Filter and count become one segmented control; the phone filter sheet and its trigger both disappear.",
+ "Areas with no matches are present but inert, so the filter still reports the shape of the catalogue.",
+ "safetyFirst tools (Risk & Safety, Medication Prescribing, Safety plan) carry a persistent warning edge and chip.",
+ "Trade-off to judge: at desktop width the inline detail makes rows tall, and no modal means no room for anything longer.",
+ ],
+ },
+];
+
+/* ------------------------------------------------------------------ *
+ * Shared in-frame chrome (replicas — see the header note)
+ * ------------------------------------------------------------------ */
+
+const focusRing =
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
+
+const iconTone: Record = {
+ assessment:
+ "border-[color:var(--type-service-border)] bg-[color:var(--type-service-soft)] text-[color:var(--type-service)]",
+ reference: "border-[color:var(--type-table-border)] bg-[color:var(--type-table-soft)] text-[color:var(--type-table)]",
+ care: "border-[color:var(--type-document-border)] bg-[color:var(--type-document-soft)] text-[color:var(--type-document)]",
+ coordination:
+ "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]",
+ saved: "border-[color:var(--type-search-border)] bg-[color:var(--type-search-soft)] text-[color:var(--type-search)]",
+ safety: "border-[color:var(--danger-border)] bg-[color:var(--danger-soft)] text-[color:var(--danger)]",
+ medication: "border-[color:var(--type-form-border)] bg-[color:var(--type-form-soft)] text-[color:var(--type-form)]",
+ differentials:
+ "border-[color:var(--type-source-border)] bg-[color:var(--type-source-soft)] text-[color:var(--type-source)]",
+};
+
+function toneFor(tool: MockupTool) {
+ if (tool.safetyFirst && tool.id !== "medication-prescribing") return iconTone.safety;
+ if (tool.id === "medication-prescribing") return iconTone.medication;
+ if (tool.id === "differentials") return iconTone.differentials;
+ return iconTone[tool.area];
+}
+
+function ToolGlyph({ tool, size = "md" }: { tool: MockupTool; size?: "sm" | "md" | "lg" }) {
+ return (
+
+
+
+ );
+}
+
+function FrameTopBar({ device }: { device: FrameDevice }) {
+ return (
+
+
+ KB
+
+
+
+
+
+ Tools
+
+
+
+
+
+
+ );
+}
+
+/** Back affordance to the Tools home — every direction keeps a single return path. */
+function BackRow({ label = "Tools" }: { label?: string }) {
+ return (
+
+
+ Do not enter patient-identifiable information.
+ Privacy and data processing
+
+
+ );
+}
+
+/** The query as the page's only heading — the change every direction shares. */
+function QueryTitle({
+ query,
+ countLabel,
+ device,
+ trailing,
+}: {
+ query: FrameQuery;
+ countLabel: string;
+ device: FrameDevice;
+ trailing?: React.ReactNode;
+}) {
+ return (
+
+
+
+ {queryCopy[query].label}
+
+
{countLabel}
+
+ {trailing}
+
+ );
+}
+
+/**
+ * Phone shows only the areas the result set actually contains, so the row stays one line at
+ * 390 px; desktop has room to show every area with its count, empty ones included.
+ */
+function AreaFilterRow({ tools, device }: { tools: MockupTool[]; device: FrameDevice }) {
+ const present = new Set(tools.map((tool) => tool.area));
+ const allAreas: ToolCatalogArea[] = ["assessment", "reference", "care", "coordination"];
+ const areas = device === "phone" ? allAreas.filter((area) => present.has(area)) : allAreas;
+ return (
+
+ Direction {String(number).padStart(2, "0")} · {direction.verdict}
+
+
+ {direction.title}
+
+
+
+ {direction.description}
+
+
+
+
+ {direction.changes.map((change) => (
+
+ {change}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const currentDefects: string[] = [
+ "Two competing page titles — the mode hero’s “Tools” h1 and the band’s query heading.",
+ "The home hero and the 7-chip quick-action grid survive submission, pushing a single result below the phone fold.",
+ "Those chips read as refinements beside results, but each one opens a detail dialog instead of re-searching.",
+ "A two-row band shelf carrying one lone Filter control.",
+ "Card-grid density for a 13-item catalogue where most queries return 1–4 matches.",
+ "The primary affordance on a result is “Details” — a dialog — not launching the tool you searched for.",
+ "A “Colour coding reference” link rendered inside the results flow as though it were a result.",
+ "No cross-mode escape when the 13-tool catalogue misses the query.",
+ "Relevance thrown away: a plain substring filter, while rankToolRecords already computes scores and match reasons.",
+];
+
+export function ToolsSearchDirectionsMockups() {
+ return (
+
+
+
+
+ Tools · Search results state
+
+
+ Three directions for the Tools results page
+
+
+ All three fix the same nine defects in today’s /tools?q=…; they differ in how far they go.
+ Every frame is built from the real 13-record catalogue in src/lib/tools-catalog.ts, so the
+ match counts, orderings, badges and per-tool copy below are the genuine data — not fixtures. Design scratch:
+ this route 404s in production and changes no production surface.
+
+
+ {[
+ [
+ "Shared by all three",
+ "Query is the only h1; hero and quick-action grid drop on submit; launch beats dialog",
+ ],
+ [
+ "Queries shown",
+ "“monitoring” (multi-match, exposes the ranking defect) and “compare” (the reported screenshot)",
+ ],
+ ["Read in light theme", "Frames use role tokens, so set the app to Clinical White before judging"],
+ ].map(([term, detail]) => (
+