diff --git a/docs/site-map.md b/docs/site-map.md
index f957fc9462..1f53342d9b 100644
--- a/docs/site-map.md
+++ b/docs/site-map.md
@@ -988,6 +988,13 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/mockups/calculators-popup-sheet` - Route discovered from app directory Source: `src/app/mockups/calculators-popup-sheet/page.tsx`.
- `/mockups/calculators-search` - Route discovered from app directory Source: `src/app/mockups/calculators-search/page.tsx`.
- `/mockups/calculators-search-page` - Route discovered from app directory Source: `src/app/mockups/calculators-search-page/page.tsx`.
+- `/mockups/document-navigation-contract` - Route discovered from app directory Source: `src/app/mockups/document-navigation-contract/page.tsx`.
+- `/mockups/document-navigation-final` - Route discovered from app directory Source: `src/app/mockups/document-navigation-final/page.tsx`.
+- `/mockups/document-navigation-pane` - Route discovered from app directory Source: `src/app/mockups/document-navigation-pane/page.tsx`.
+- `/mockups/document-phone-fused-directions` - Route discovered from app directory Source: `src/app/mockups/document-phone-fused-directions/page.tsx`.
+- `/mockups/document-phone-title` - Route discovered from app directory Source: `src/app/mockups/document-phone-title/page.tsx`.
+- `/mockups/document-phone-title-refined` - Route discovered from app directory Source: `src/app/mockups/document-phone-title-refined/page.tsx`.
+- `/mockups/document-phone-zero-chrome` - Route discovered from app directory Source: `src/app/mockups/document-phone-zero-chrome/page.tsx`.
- `/mockups/document-search` - Route discovered from app directory Source: `src/app/mockups/document-search/page.tsx`.
- `/mockups/document-search-evidence-lens` - Route discovered from app directory Source: `src/app/mockups/document-search-evidence-lens/page.tsx`.
- `/mockups/document-search-triage-board` - Route discovered from app directory Source: `src/app/mockups/document-search-triage-board/page.tsx`.
diff --git a/src/app/mockups/document-navigation-contract/page.tsx b/src/app/mockups/document-navigation-contract/page.tsx
new file mode 100644
index 0000000000..3739903c59
--- /dev/null
+++ b/src/app/mockups/document-navigation-contract/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+
+import { DocumentNavigationContractMockups } from "@/components/document-navigation-contract-mockups";
+
+export const metadata: Metadata = {
+ title: "Document navigation against the chrome contract - Clinical KB",
+ description:
+ "Two complete document-navigation candidates at desktop, tablet, and phone, built to the shipped header and footer behaviour.",
+};
+
+export default function DocumentNavigationContractMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/document-navigation-final/page.tsx b/src/app/mockups/document-navigation-final/page.tsx
new file mode 100644
index 0000000000..79a9cd0470
--- /dev/null
+++ b/src/app/mockups/document-navigation-final/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+
+import { DocumentNavigationFinalMockups } from "@/components/document-navigation-final-mockups";
+
+export const metadata: Metadata = {
+ title: "Document navigation, final design - Clinical KB",
+ description:
+ "The final document navigation design: segment-track header row and a two-column section grid, at desktop, tablet, and phone.",
+};
+
+export default function DocumentNavigationFinalMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/document-navigation-pane/page.tsx b/src/app/mockups/document-navigation-pane/page.tsx
new file mode 100644
index 0000000000..f3638f10b5
--- /dev/null
+++ b/src/app/mockups/document-navigation-pane/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+
+import { DocumentNavigationPaneMockups } from "@/components/document-navigation-pane-mockups";
+
+export const metadata: Metadata = {
+ title: "Document navigation pane mockups - Clinical KB",
+ description:
+ "Three navigation panes anchored to the universal header for the document page, shown on desktop, tablet, and phone.",
+};
+
+export default function DocumentNavigationPaneMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/document-phone-fused-directions/page.tsx b/src/app/mockups/document-phone-fused-directions/page.tsx
new file mode 100644
index 0000000000..89fac1545a
--- /dev/null
+++ b/src/app/mockups/document-phone-fused-directions/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+
+import { DocumentPhoneFusedDirectionsMockups } from "@/components/document-phone-fused-directions-mockups";
+
+export const metadata: Metadata = {
+ title: "Document phone fused row directions - Clinical KB",
+ description: "Three directions built on the fused phone title row for the document page.",
+};
+
+export default function DocumentPhoneFusedDirectionsMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/document-phone-title-refined/page.tsx b/src/app/mockups/document-phone-title-refined/page.tsx
new file mode 100644
index 0000000000..bb5945ab76
--- /dev/null
+++ b/src/app/mockups/document-phone-title-refined/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+
+import { DocumentPhoneTitleRefinedMockups } from "@/components/document-phone-title-refined-mockups";
+
+export const metadata: Metadata = {
+ title: "Document phone title refined mockups - Clinical KB",
+ description: "Three corrected variants of the fused phone title row for the document page.",
+};
+
+export default function DocumentPhoneTitleRefinedMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/document-phone-title/page.tsx b/src/app/mockups/document-phone-title/page.tsx
new file mode 100644
index 0000000000..32fe1c767a
--- /dev/null
+++ b/src/app/mockups/document-phone-title/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+
+import { DocumentPhoneTitleMockups } from "@/components/document-phone-title-mockups";
+
+export const metadata: Metadata = {
+ title: "Document phone title mockups - Clinical KB",
+ description: "Three ways to keep the document title visible on a phone alongside the navigation pane.",
+};
+
+export default function DocumentPhoneTitleMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/document-phone-zero-chrome/page.tsx b/src/app/mockups/document-phone-zero-chrome/page.tsx
new file mode 100644
index 0000000000..404350d273
--- /dev/null
+++ b/src/app/mockups/document-phone-zero-chrome/page.tsx
@@ -0,0 +1,13 @@
+import type { Metadata } from "next";
+
+import { DocumentPhoneZeroChromeMockups } from "@/components/document-phone-zero-chrome-mockups";
+
+export const metadata: Metadata = {
+ title: "Document phone navigation, zero new chrome - Clinical KB",
+ description:
+ "Section navigation on the document page that adds no permanent phone chrome and no second scroll owner.",
+};
+
+export default function DocumentPhoneZeroChromeMockupPage() {
+ return ;
+}
diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx
index a30dc493b3..8b0cec5bfe 100644
--- a/src/app/mockups/mockups-layout-client.tsx
+++ b/src/app/mockups/mockups-layout-client.tsx
@@ -11,6 +11,12 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
const isFavouritesPageMockup = pathname.startsWith("/mockups/favourites-");
const isDocumentSearchMockup = pathname.startsWith("/mockups/document-search");
const isDocumentTopNavigationMockup = pathname === "/mockups/document-top-navigation";
+ const isDocumentNavigationPaneMockup = pathname === "/mockups/document-navigation-pane";
+ const isDocumentPhoneTitleMockup = pathname.startsWith("/mockups/document-phone-");
+ // Draws its own universal top bar and document composer inside every frame, so
+ // the shared composer would read as a second, real search bar over the study.
+ const isDocumentNavigationContractMockup =
+ pathname === "/mockups/document-navigation-contract" || pathname === "/mockups/document-navigation-final";
const isSourceOverlayRedesignMockup = pathname === "/mockups/document-search/source-overlays";
const isStandaloneDocumentFlow = pathname === "/mockups/document-search";
const isUniversalSearchRedesignMockup = pathname === "/mockups/universal-search-redesign";
@@ -28,7 +34,11 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
? "tools"
: isFavouritesPageMockup
? "favourites"
- : isDocumentSearchMockup || isDocumentTopNavigationMockup
+ : isDocumentSearchMockup ||
+ isDocumentTopNavigationMockup ||
+ isDocumentNavigationPaneMockup ||
+ isDocumentPhoneTitleMockup ||
+ isDocumentNavigationContractMockup
? "documents"
: "answer"
}
@@ -37,6 +47,9 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) {
!isFavouritesPageMockup &&
!isStandaloneDocumentFlow &&
!isDocumentTopNavigationMockup &&
+ !isDocumentNavigationPaneMockup &&
+ !isDocumentPhoneTitleMockup &&
+ !isDocumentNavigationContractMockup &&
!isUniversalSearchRedesignMockup &&
!isCalculatorsSearchPageMockup &&
!isPhoneInPageNavigationMockup
diff --git a/src/components/document-navigation-contract-mockups.tsx b/src/components/document-navigation-contract-mockups.tsx
new file mode 100644
index 0000000000..69847a8d83
--- /dev/null
+++ b/src/components/document-navigation-contract-mockups.tsx
@@ -0,0 +1,773 @@
+"use client";
+
+import {
+ ArrowLeft,
+ ChevronDown,
+ Compass,
+ FileImage,
+ FileSearch,
+ FileText,
+ ListTree,
+ Menu,
+ MessageSquarePlus,
+ Plus,
+ Quote,
+ Search,
+ ShieldCheck,
+ Sparkles,
+ type LucideIcon,
+} from "lucide-react";
+import { useId } from "react";
+
+import { cn } from "@/components/ui-primitives";
+
+type CandidateId = "index" | "rail";
+type Device = "desktop" | "tablet" | "phone";
+
+const documentTitle = "Clinical practice guideline for schizophrenia";
+
+const sections: Array<{ label: string; short: string; detail: string; icon: LucideIcon; collapsible: boolean }> = [
+ { label: "Overview", short: "Overview", detail: "84 pages", icon: Compass, collapsible: false },
+ { label: "High-yield summary", short: "Summary", detail: "8 points", icon: Sparkles, collapsible: true },
+ { label: "PDF preview", short: "PDF", detail: "p. 12 / 84", icon: FileText, collapsible: false },
+ { label: "Pinned evidence", short: "Evidence", detail: "27 passages", icon: Quote, collapsible: false },
+ { label: "Indexed source text", short: "Text", detail: "312 chunks", icon: FileSearch, collapsible: true },
+ { label: "Tables and diagrams", short: "Tables", detail: "6 visuals", icon: FileImage, collapsible: true },
+ { label: "Indexing details", short: "Indexing", detail: "v3 · OCR", icon: ShieldCheck, collapsible: true },
+];
+
+const activeIndex = 3;
+const activeSection = sections[activeIndex];
+
+const contractFindings: Array<{ area: string; finding: string; consequence: string }> = [
+ {
+ area: "Phone header",
+ finding:
+ "The document header is not its own sticky bar. `PhoneHeaderCollapsePortal` moves it into the universal collapse row below `sm`, and the contract allows exactly one collapse owner per phone header.",
+ consequence:
+ "A second sticky or fixed phone header is out of bounds. Anything added lands inside the collapse row.",
+ },
+ {
+ area: "Collapse budget",
+ finding:
+ "`readChromeCollapseMetrics` charges the collapse row's controls, the released top safe-area, and the dock reserve against the remaining scroll runway.",
+ consequence: "Every extra row of header height makes hide-on-scroll fail to fire on short documents.",
+ },
+ {
+ area: "Phone footer",
+ finding:
+ "`DocumentViewer` already owns the bottom: a 56 px floating composer pill with `+` actions, portalled through `PhoneFooterLayerPortal`, reserve `9rem + safe-area + keyboard`, and `0rem` once hidden.",
+ consequence:
+ "The bottom edge is taken. Navigation cannot claim a persistent bar there without a second footer owner.",
+ },
+ {
+ area: "Sheets",
+ finding:
+ "The contract states plainly that modal and sheet headers are not viewport chrome and keep their own scroll context.",
+ consequence: "A bottom sheet is the one overlay that adds no chrome, no reserve, and no scroll owner.",
+ },
+ {
+ area: "Tablet",
+ finding: "The top bar hides and reveals, and tablet search stays pinned beneath it as the second sticky layer.",
+ consequence:
+ "A sticky nav pane would be a third layer. Tablet navigation should sit in page flow or share the pinned stack.",
+ },
+ {
+ area: "Desktop",
+ finding:
+ "The top bar is the only sticky desktop chrome; desktop search is page-flow. `DocumentViewer` already runs `lg:grid-cols-[minmax(0,1fr)_480px]`.",
+ consequence:
+ "A third column is real crowding under 1440 px, and any sticky rail must drop its offset to 0 when the top bar hides or it leaves a dead band.",
+ },
+];
+
+const candidates: Array<{
+ id: CandidateId;
+ title: string;
+ verdict: string;
+ summary: string;
+ perDevice: Record;
+}> = [
+ {
+ id: "index",
+ title: "Candidate A — Section index, no new chrome",
+ verdict: "Contract-safest",
+ summary:
+ "Navigation borrows surfaces that already exist. Desktop puts the index at the top of the 480 px column that is already there, tablet keeps it in page flow, and phone makes the existing title a disclosure for a bottom sheet.",
+ perDevice: {
+ desktop: "Sticky index card at the top of the existing right column. No third column, no new grid.",
+ tablet: "In-flow index card above the document. Scrolls away with content — no third sticky layer.",
+ phone:
+ "Existing header title becomes the disclosure; sections open in a bottom sheet. Zero added collapse height.",
+ },
+ },
+ {
+ id: "rail",
+ title: "Candidate B — Anchored rail, sheet on phone",
+ verdict: "Most spatial",
+ summary:
+ "A persistent rail gives the document a fixed sense of place on the wide breakpoints. Its sticky offset is bound to the top-bar hide state, and below `sm` it degrades to the same sheet as Candidate A because the collapse contract leaves no other option.",
+ perDevice: {
+ desktop: "17 rem labelled rail, sticky. Offset follows the top bar: header height when shown, 0 when hidden.",
+ tablet: "Compact icon rail sharing the pinned stack rather than adding a layer beneath it.",
+ phone: "Identical to Candidate A — the rail has nowhere to live inside a single collapse row.",
+ },
+ },
+];
+
+const perfected: Array<{ title: string; body: string }> = [
+ {
+ title: "Phone: title disclosure plus bottom sheet",
+ body: "No added collapse height, so the hide budget is untouched. The sheet is not chrome, so it adds no reserve and no scroll owner.",
+ },
+ {
+ title: "Sheet opens with the composer blurred",
+ body: "The contract requires blurring the focused composer so hide-on-scroll can reclaim both edges. Opening the sheet blurs the document search input first.",
+ },
+ {
+ title: "Sheet sits above the footer layer",
+ body: "The composer pill is `z-40` on the phone footer layer. The sheet takes a higher layer and its scrim covers the pill, so there is never a live control under an open sheet.",
+ },
+ {
+ title: "Desktop index rides the existing column",
+ body: "No third column below 1440 px. The index is the first card in the 480 px column the viewer already renders.",
+ },
+ {
+ title: "Sticky offset follows the top bar",
+ body: "Any sticky index or rail switches from `top: header + safe-area` to `top: 0` while the top bar is hidden, so no dead band is left behind it.",
+ },
+ {
+ title: "Jump opens the accordion first",
+ body: 'Target sections are `` — exclusive. The handler opens the target, waits a frame, then scrolls.',
+ },
+ {
+ title: "Anchor offset is measured, not fixed",
+ body: "`scroll-mt-24` assumes a constant header. Offsets read the live collapse-row height so a hidden header does not leave headings floating mid-viewport.",
+ },
+ {
+ title: "Reduced motion keeps the release",
+ body: "The sheet and any collapse animate normally, and under reduced motion they snap — but the full edge release still happens, per the contract.",
+ },
+];
+
+const focusRing =
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
+
+/* ------------------------------------------------------------------ */
+/* Shared chrome stand-ins */
+/* ------------------------------------------------------------------ */
+
+function UniversalTopBar({ device }: { device: Device }) {
+ const phone = device === "phone";
+
+ return (
+
+ Documents · Navigation against the chrome contract
+
+
+ Two complete candidates, built to the header and footer rules
+
+
+ Both candidates are drawn on top of the chrome that actually ships: the universal top bar, the document
+ header that portals into the phone collapse row, and the 56-pixel document composer that already owns the
+ phone footer. The findings below are what the contract permits — the candidates differ only where it leaves
+ a choice.
+
+ Candidate A everywhere, Candidate B's rail only above 1440 px
+
+
+ Candidate A never introduces a surface the contract has to make room for, which is why it is the default.
+ The rail from Candidate B is worth having, but only where a third column genuinely fits — below that it
+ takes width from the document to say what the index already says.
+
+
+ {perfected.map((item) => (
+
+
{item.title}
+
{item.body}
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/src/components/document-navigation-final-mockups.tsx b/src/components/document-navigation-final-mockups.tsx
new file mode 100644
index 0000000000..d6d80f8ad3
--- /dev/null
+++ b/src/components/document-navigation-final-mockups.tsx
@@ -0,0 +1,576 @@
+"use client";
+
+import {
+ ArrowLeft,
+ ChevronDown,
+ Compass,
+ FileImage,
+ FileSearch,
+ FileText,
+ ListTree,
+ Menu,
+ MessageSquarePlus,
+ Plus,
+ Quote,
+ Search,
+ ShieldCheck,
+ Sparkles,
+ type LucideIcon,
+} from "lucide-react";
+import { useId } from "react";
+
+import { cn } from "@/components/ui-primitives";
+
+type Device = "desktop" | "tablet" | "phone";
+type PhoneState = "rest" | "sheet" | "scrolled";
+
+const documentTitle = "Clinical practice guideline for schizophrenia";
+
+const sections: Array<{ label: string; short: string; detail: string; icon: LucideIcon; collapsible: boolean }> = [
+ { label: "Overview", short: "Overview", detail: "84 pages", icon: Compass, collapsible: false },
+ { label: "High-yield summary", short: "Summary", detail: "8 points", icon: Sparkles, collapsible: true },
+ { label: "PDF preview", short: "PDF", detail: "p. 12 / 84", icon: FileText, collapsible: false },
+ { label: "Pinned evidence", short: "Evidence", detail: "27 passages", icon: Quote, collapsible: false },
+ { label: "Indexed source text", short: "Text", detail: "312 chunks", icon: FileSearch, collapsible: true },
+ { label: "Tables and diagrams", short: "Tables", detail: "6 visuals", icon: FileImage, collapsible: true },
+ { label: "Indexing details", short: "Indexing", detail: "v3 · OCR", icon: ShieldCheck, collapsible: true },
+];
+
+const activeIndex = 3;
+const activeSection = sections[activeIndex];
+
+const lineage: Array<{ from: string; kept: string; changed: string }> = [
+ {
+ from: "Direction 02 — segment track",
+ kept: "The seven-segment hairline on the row's bottom edge is the position indicator at every breakpoint.",
+ changed:
+ "It replaces the `· 4 of 7` numerals, not the section label. The row keeps both lines; only the text that squeezed the title is gone.",
+ },
+ {
+ from: "Direction 03 — two-column pane",
+ kept: "Seven compact cards in two columns, whole structure visible at once, never scrolls internally.",
+ changed: "The same grid is now the shared surface: desktop index card, tablet index card, and phone sheet body.",
+ },
+ {
+ from: "Chrome contract review",
+ kept: "One collapse owner on phone, the composer keeps the footer, sheets stay outside chrome.",
+ changed:
+ "The phone pane drops from under the row and becomes a bottom sheet, so nothing is attached to a row that hides.",
+ },
+];
+
+const spec: Array<{ title: string; body: string }> = [
+ {
+ title: "Two lines, and the numerals are gone",
+ body: "Line one is the title at full width, line two names the section. Only `· 4 of 7` was dropped — the track already carries position, and the numerals were what squeezed the title.",
+ },
+ {
+ title: "The track places, the label names",
+ body: "A hairline can show where you are but cannot say what you are in. Keeping both means a first-time reader is never asked to decode the segments.",
+ },
+ {
+ title: "One pane shape at every size",
+ body: "The two-column grid is the index card on desktop and tablet and the sheet body on phone. One component, three placements.",
+ },
+ {
+ title: "Phone pane is a sheet",
+ body: "Anchored to the viewport, not to a row that scroll-hides. Opening it blurs the document composer and the scrim covers the pill.",
+ },
+ {
+ title: "Sticky offset follows the top bar",
+ body: "The desktop index card switches from `top: header + safe-area` to `top: 0` while the top bar is hidden, so no dead band appears.",
+ },
+ {
+ title: "Track hides with its row",
+ body: "It is part of the header, so it leaves with the header and leaves zero reserve. Position is still readable from the content itself.",
+ },
+ {
+ title: "Jump opens the accordion first",
+ body: 'Targets are ``. The handler opens the target, waits a frame, then scrolls by the measured row height.',
+ },
+ {
+ title: "Non-colour cue for the active segment",
+ body: "Under forced-colors the filled segments lose their hue, so the active one is also taller — 3 px against 2 px.",
+ },
+];
+
+const focusRing =
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
+
+/* ------------------------------------------------------------------ */
+/* Shared pieces */
+/* ------------------------------------------------------------------ */
+
+function UniversalTopBar({ device }: { device: Device }) {
+ const phone = device === "phone";
+
+ return (
+
+ );
+}
+
+const phoneCopy: Record = {
+ rest: "Title, section label, and the track on the bottom edge. Composer pill owns the footer, as it does today.",
+ sheet: "Grid opens as a sheet: composer blurred, scrim over the pill, nothing attached to a row that hides.",
+ scrolled: "Header and track leave together on the scroll signal and release the reserve to zero.",
+};
+
+export function DocumentNavigationFinalMockups() {
+ return (
+
+
+
+
+ Documents · Final navigation design
+
+
+ Segment track on the row, two-column grid for the pane
+
+
+ The two pieces that worked, composed and reconciled with the shipped chrome. The row keeps both lines —
+ title, then the section it names — and the two-pixel hairline replaces the numerals rather than the label.
+ The same two-column grid is the index on desktop and tablet and the sheet body on phone.
+
+
+
+
+ {/* The scroll container starts immediately below the universal header,
+ so anything `sticky top-0` inside it is anchored to that header. */}
+
+ A navigation pane anchored to the universal header
+
+
+ Three interactive options for moving between the document page's seven components — overview,
+ high-yield summary, PDF preview, pinned evidence, indexed source text, tables and diagrams, and indexing
+ details. Each option anchors to the underside of the universal header, keeps 44-pixel targets and a clear
+ active state, and is shown on desktop, tablet, and phone. Scroll inside any frame to see the pane hold its
+ anchor.
+
+
+
+
+ {concepts.map((concept, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/document-phone-fused-directions-mockups.tsx b/src/components/document-phone-fused-directions-mockups.tsx
new file mode 100644
index 0000000000..ad74d72b96
--- /dev/null
+++ b/src/components/document-phone-fused-directions-mockups.tsx
@@ -0,0 +1,481 @@
+"use client";
+
+import {
+ ArrowLeft,
+ ChevronDown,
+ Compass,
+ FileImage,
+ FileSearch,
+ FileText,
+ ListTree,
+ Quote,
+ Search,
+ ShieldCheck,
+ Sparkles,
+ type LucideIcon,
+} from "lucide-react";
+import { useId } from "react";
+
+import { cn } from "@/components/ui-primitives";
+
+type DirectionId = "chips" | "track" | "grid";
+type FrameState = "closed" | "open";
+
+const documentTitle = "Clinical practice guideline for schizophrenia";
+
+const sections: Array<{ label: string; short: string; detail: string; icon: LucideIcon; collapsible: boolean }> = [
+ { label: "Overview", short: "Overview", detail: "84 pages", icon: Compass, collapsible: false },
+ { label: "High-yield summary", short: "Summary", detail: "8 points", icon: Sparkles, collapsible: true },
+ { label: "PDF preview", short: "PDF", detail: "p. 12 / 84", icon: FileText, collapsible: false },
+ { label: "Pinned evidence", short: "Evidence", detail: "27 passages", icon: Quote, collapsible: false },
+ { label: "Indexed source text", short: "Text", detail: "312 chunks", icon: FileSearch, collapsible: true },
+ { label: "Tables and diagrams", short: "Tables", detail: "6 visuals", icon: FileImage, collapsible: true },
+ { label: "Indexing details", short: "Indexing", detail: "v3 · OCR", icon: ShieldCheck, collapsible: true },
+];
+
+const activeIndex = 3;
+const activeSection = sections[activeIndex];
+
+const directions: Array<{
+ id: DirectionId;
+ title: string;
+ verdict: string;
+ description: string;
+ notes: string[];
+}> = [
+ {
+ id: "chips",
+ title: "Section chips on line two",
+ verdict: "Navigate without opening",
+ description:
+ "Line two stops being a label and becomes a swipeable strip of section chips. Most moves happen without the pane ever opening; the chevron is still there for a full, ordered list.",
+ notes: [
+ "Row height is unchanged — the chip strip occupies the line the section label used.",
+ "Chips scroll horizontally, so the pane stays the only place the full order is visible.",
+ "Active chip carries the accent; the strip auto-scrolls to keep it in view.",
+ "Off-screen chips are the known cost — the chevron exists precisely for that.",
+ ],
+ },
+ {
+ id: "track",
+ title: "Segment track",
+ verdict: "Quietest",
+ description:
+ "Position stops being words. A seven-segment hairline sits on the bottom edge of the row, filled to the active section, so the title keeps the whole width and nothing numeric competes with it.",
+ notes: [
+ "The `· 4 of 7` text disappears — the track carries it in 2 px of height.",
+ "Track segments are tap targets too, but the row remains the primary control.",
+ "Reads as a progress hairline, so it never looks like a second toolbar.",
+ "Needs a non-colour cue for the active segment under forced-colors.",
+ ],
+ },
+ {
+ id: "grid",
+ title: "Two-column pane",
+ verdict: "Whole list at a glance",
+ description:
+ "The row you liked, untouched. Only the pane changes: seven compact cards in two columns, so the entire document structure is visible at once with no internal scrolling.",
+ notes: [
+ "Pane is roughly half the height of the seven-row list and never scrolls.",
+ "Counts stay on every card — the density comes from layout, not from dropping information.",
+ "Odd count leaves one full-width card, which the current section takes.",
+ "Card labels truncate at two words on the narrowest phones.",
+ ],
+ },
+];
+
+const focusRing =
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
+
+function UniversalHeader() {
+ return (
+
+ All three keep the row you preferred: full-width title on line one, section on line two, chevron on the
+ right, and a pane that pushes the document down rather than covering it. They differ in what line two does
+ and what the pane looks like. Chrome stays one row, so it still hides and reveals with the header rather
+ than owning a second scroll behaviour.
+
+
+ );
+}
diff --git a/src/components/document-phone-title-mockups.tsx b/src/components/document-phone-title-mockups.tsx
new file mode 100644
index 0000000000..564513871c
--- /dev/null
+++ b/src/components/document-phone-title-mockups.tsx
@@ -0,0 +1,438 @@
+"use client";
+
+import {
+ ArrowLeft,
+ ChevronDown,
+ ChevronRight,
+ Compass,
+ FileImage,
+ FileSearch,
+ FileText,
+ ListTree,
+ Quote,
+ Search,
+ ShieldCheck,
+ Sparkles,
+ X,
+ type LucideIcon,
+} from "lucide-react";
+import { useId } from "react";
+
+import { cn } from "@/components/ui-primitives";
+
+type ApproachId = "fused" | "breadcrumb" | "sheet";
+
+const documentTitle = "Clinical practice guideline for schizophrenia";
+const documentEyebrow = "NICE · Clinical guideline";
+
+const sections: Array<{ label: string; short: string; detail: string; icon: LucideIcon }> = [
+ { label: "Overview", short: "Overview", detail: "84 pages", icon: Compass },
+ { label: "High-yield summary", short: "Summary", detail: "8 points", icon: Sparkles },
+ { label: "PDF preview", short: "PDF", detail: "p. 12 / 84", icon: FileText },
+ { label: "Pinned evidence", short: "Evidence", detail: "27 passages", icon: Quote },
+ { label: "Indexed source text", short: "Text", detail: "312 chunks", icon: FileSearch },
+ { label: "Tables and diagrams", short: "Tables", detail: "6 visuals", icon: FileImage },
+ { label: "Indexing details", short: "Indexing", detail: "v3 · OCR", icon: ShieldCheck },
+];
+
+const activeIndex = 3;
+const activeSection = sections[activeIndex];
+
+const approaches: Array<{
+ id: ApproachId;
+ title: string;
+ verdict: string;
+ description: string;
+ chromeHeight: string;
+ tradeoff: string;
+}> = [
+ {
+ id: "fused",
+ title: "Fused title row",
+ verdict: "Recommended",
+ description:
+ "Title and pane trigger are one control. Line one is the document, line two is the active section, and the pane drops below the row instead of over it — so the title never disappears at the moment you are orienting.",
+ chromeHeight: "96 px of chrome (48 header + 48 fused row)",
+ tradeoff: "Two-line row is denser to read; section name competes with a long title for the same width.",
+ },
+ {
+ id: "breadcrumb",
+ title: "Title in the universal header",
+ verdict: "Cleanest reading area",
+ description:
+ "The document title replaces the mode chip in the universal header while you are on a document route. The row beneath carries only position and the pane trigger, so it can stay slim.",
+ chromeHeight: "92 px of chrome (48 header + 44 section bar)",
+ tradeoff:
+ "Edits shared chrome for one route — the biggest blast radius of the three, and the title loses its back affordance.",
+ },
+ {
+ id: "sheet",
+ title: "Fixed title, pane as bottom sheet",
+ verdict: "Most stable identity",
+ description:
+ "The title row is a single line and never moves. Section navigation is a compact pill that opens a bottom sheet, matching the phone sheet pattern already used for document actions.",
+ chromeHeight: "104 px of chrome (48 header + 56 title row)",
+ tradeoff:
+ "The pane is furthest from the header anchor, and the sheet covers the lower half of the document while open.",
+ },
+];
+
+const focusRing =
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
+
+function UniversalHeader({ title }: { title?: string }) {
+ return (
+
+ Three ways to show the document title on a 390-pixel viewport without stacking a third bar under the
+ universal header. Each approach is shown with the navigation pane closed and open, because the open state is
+ where the title is normally lost.
+
+
+ );
+}
diff --git a/src/components/document-phone-title-refined-mockups.tsx b/src/components/document-phone-title-refined-mockups.tsx
new file mode 100644
index 0000000000..b55000098f
--- /dev/null
+++ b/src/components/document-phone-title-refined-mockups.tsx
@@ -0,0 +1,568 @@
+"use client";
+
+import {
+ ArrowLeft,
+ ChevronDown,
+ ChevronUp,
+ Compass,
+ FileImage,
+ FileSearch,
+ FileText,
+ ListTree,
+ Plus,
+ Quote,
+ Search,
+ ShieldCheck,
+ Sparkles,
+ type LucideIcon,
+} from "lucide-react";
+import { useId } from "react";
+
+import { cn } from "@/components/ui-primitives";
+
+type VariantId = "refined" | "stepper" | "collapse";
+type FrameState = "rest" | "open" | "compact";
+
+const documentTitle = "Clinical practice guideline for schizophrenia";
+
+/**
+ * `collapsible` marks the sections that render as ``
+ * in the real viewer. That accordion is exclusive, so a jump has to open the target
+ * details before scrolling — the pane shows that state instead of hiding it.
+ */
+const sections: Array<{
+ label: string;
+ short: string;
+ detail: string;
+ icon: LucideIcon;
+ collapsible: boolean;
+}> = [
+ { label: "Overview", short: "Overview", detail: "84 pages", icon: Compass, collapsible: false },
+ { label: "High-yield summary", short: "Summary", detail: "8 points", icon: Sparkles, collapsible: true },
+ { label: "PDF preview", short: "PDF", detail: "p. 12 / 84", icon: FileText, collapsible: false },
+ { label: "Pinned evidence", short: "Evidence", detail: "27 passages", icon: Quote, collapsible: false },
+ { label: "Indexed source text", short: "Text", detail: "312 chunks", icon: FileSearch, collapsible: true },
+ { label: "Tables and diagrams", short: "Tables", detail: "6 visuals", icon: FileImage, collapsible: true },
+ { label: "Indexing details", short: "Indexing", detail: "v3 · OCR", icon: ShieldCheck, collapsible: true },
+];
+
+const activeIndex = 3;
+const activeSection = sections[activeIndex];
+
+const variants: Array<{
+ id: VariantId;
+ title: string;
+ verdict: string;
+ description: string;
+ changes: string[];
+ states: FrameState[];
+}> = [
+ {
+ id: "refined",
+ title: "Fused row, corrected",
+ verdict: "Straight fix",
+ description:
+ "Approach 01 with every issue from the review closed: the position becomes a discrete pill, document actions stay reachable, the pane is height-capped so the canvas never fully disappears, and collapsible sections are labelled as such.",
+ changes: [
+ "Position moved off the title line entirely — line one is the document, line two is section plus a `4/7` pill.",
+ "Document actions (+) kept in the row, so the existing actions sheet is not lost to the pane.",
+ "Pane capped at 60% of the viewport with internal scroll; a sliver of document stays visible.",
+ "Accordion sections carry a chevron marker — a jump opens that `` before scrolling.",
+ ],
+ states: ["rest", "open"],
+ },
+ {
+ id: "stepper",
+ title: "Fused row with section stepper",
+ verdict: "Fewest taps",
+ description:
+ "Same corrected row, plus previous and next section controls. Sequential reading — the common case — costs one tap and never opens the pane at all; the pane becomes the jump-anywhere escape hatch.",
+ changes: [
+ "Previous / next section steppers at 44 px each; disabled at the ends rather than wrapping.",
+ "Pane keeps the Read / Evidence / Provenance grouping as inline dividers, not extra header rows.",
+ "Pane trigger uses a list glyph, not a chevron — three chevrons in one row read as a single control.",
+ "Stepper and pane share one active-section state, so the row and list can never disagree.",
+ ],
+ states: ["rest", "open"],
+ },
+ {
+ id: "collapse",
+ title: "Progressive collapse",
+ verdict: "Most reading area",
+ description:
+ "The row is two lines at the top of the document and collapses to a single 40 px line once you scroll, keeping identity on screen while giving the document back its height. Header and row hide and reveal from the same scroll signal.",
+ changes: [
+ "Rest: 56 px two-line row. Scrolled: 40 px single line, section reduced to its pill.",
+ "Header and row share one scroll signal — never one hidden while the other floats.",
+ "Hidden means zero reserve; no phantom padding is left behind the collapsed row.",
+ "Section anchors need their scroll offset tied to the live row height, not a fixed value.",
+ ],
+ states: ["rest", "compact", "open"],
+ },
+];
+
+const focusRing =
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
+
+function UniversalHeader({ compact = false }: { compact?: boolean }) {
+ return (
+
+ Every variant below fixes the same four defects in the original fused row: position competing with a long
+ title, document actions displaced by the pane, the canvas disappearing when the pane opens, and section
+ jumps landing on a collapsed accordion. They differ only in what they add on top.
+
+
+ {[
+ ["Shared by all three", "44 px targets, `aria-expanded` on the trigger, single active-section source"],
+ ["Scroll contract", "Header and row hide and reveal together; hidden leaves zero reserve"],
+ ["Motion", "Pane height and row collapse animate; both drop to instant under reduced motion"],
+ ].map(([term, detail]) => (
+
+
+ {term}
+
+
{detail}
+
+ ))}
+
+
+
+
+ {variants.map((variant, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/document-phone-zero-chrome-mockups.tsx b/src/components/document-phone-zero-chrome-mockups.tsx
new file mode 100644
index 0000000000..915cfaed89
--- /dev/null
+++ b/src/components/document-phone-zero-chrome-mockups.tsx
@@ -0,0 +1,455 @@
+"use client";
+
+import {
+ ArrowLeft,
+ ChevronDown,
+ Compass,
+ Download,
+ ExternalLink,
+ FileImage,
+ FileSearch,
+ FileText,
+ ListTree,
+ Plus,
+ Quote,
+ Search,
+ ShieldCheck,
+ Sparkles,
+ Target,
+ type LucideIcon,
+} from "lucide-react";
+import { useId, type ReactNode } from "react";
+
+import { cn } from "@/components/ui-primitives";
+
+type FrameState = "rest" | "scrolled" | "sheet" | "actions";
+
+const documentTitle = "Clinical practice guideline for schizophrenia";
+
+/**
+ * Mirrors the real viewer sections. `collapsible` marks the ones that render as
+ * `` — an exclusive accordion, so a jump
+ * has to open the target before scrolling to it.
+ */
+const sections: Array<{ label: string; detail: string; icon: LucideIcon; collapsible: boolean }> = [
+ { label: "Overview", detail: "84 pages", icon: Compass, collapsible: false },
+ { label: "High-yield summary", detail: "8 points", icon: Sparkles, collapsible: true },
+ { label: "PDF preview", detail: "p. 12 / 84", icon: FileText, collapsible: false },
+ { label: "Pinned evidence", detail: "27 passages", icon: Quote, collapsible: false },
+ { label: "Indexed source text", detail: "312 chunks", icon: FileSearch, collapsible: true },
+ { label: "Tables and diagrams", detail: "6 visuals", icon: FileImage, collapsible: true },
+ { label: "Indexing details", detail: "v3 · OCR", icon: ShieldCheck, collapsible: true },
+];
+
+const activeIndex = 3;
+const activeSection = sections[activeIndex];
+
+const frameCopy: Record = {
+ rest: {
+ label: "At rest",
+ note: "Exactly the header that ships today. The only change is a chevron after the title.",
+ },
+ scrolled: {
+ label: "Scrolled — chrome hidden",
+ note: "Both headers hide on the shared signal and leave zero reserve. Nothing floats over the document.",
+ },
+ sheet: {
+ label: "Sections sheet",
+ note: "The existing Sheet component. Costs no permanent height and never touches the scroll signal.",
+ },
+ actions: {
+ label: "Actions sheet — second entry",
+ note: "Jump to section added above the actions that already live here. Still zero new chrome.",
+ },
+};
+
+const budget = [
+ { label: "Ships today", header: 48, row: 48, extra: 0, tone: "neutral" as const },
+ { label: "This proposal", header: 48, row: 48, extra: 0, tone: "good" as const },
+ { label: "Fused row (Approach 01)", header: 48, row: 56, extra: 0, tone: "bad" as const },
+ { label: "Fused row + status bar", header: 48, row: 48, extra: 44, tone: "bad" as const },
+];
+
+const focusRing =
+ "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
+
+function UniversalHeader() {
+ return (
+
+
+ KB
+
+
+
+ Documents
+
+
+
+
+
+
+ );
+}
+
+/**
+ * The document header exactly as it ships: back, title, scope, actions — 48 px.
+ * The single change is that the title is now a disclosure for the sections sheet.
+ */
+function DocumentHeader({ expanded, sheetId }: { expanded: boolean; sheetId: string }) {
+ return (
+
+ The title stays in the header row that already ships, and becomes the disclosure for the section list. The
+ pane is the bottom sheet the viewer already uses for document actions. No new bar, no second scroll owner,
+ no floating control that survives the hide.
+