diff --git a/src/app/globals.css b/src/app/globals.css
index 092e89a9d..ddbfffd56 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -410,6 +410,27 @@
--clinical-accent-soft: var(--primary-soft);
--clinical-accent-border: #c6dcf2;
--clinical-accent-contrast: #ffffff;
+
+ /* Brand-mark ink. The logo is identity, not UI state, so it carries its own
+ role rather than reading --clinical-accent directly: retuning the accent for
+ selection/evidence/send must not silently repaint the brand, and a surface
+ that needs the mark to hold its ground can ask for the emphasis role by name
+ instead of borrowing a pressed-state colour.
+
+ Why an emphasis role exists at all: the mark is drawn as two counter-turning
+ strokes whose lower one is the thinner read, so at chrome sizes (28-40px) on
+ a near-white band the resting accent lets the bottom of the S dissolve into
+ the ground while the dot-and-bowl cluster at the top stays solid — the mark
+ goes top-heavy. Emphasis is a full ramp step, not a nudge, and restores an
+ even read without changing the geometry.
+
+ All three are aliases, so light, dark and forced-colors are inherited from
+ the accent ramp with no second definition to keep in sync. `.dark` and the
+ forced-colors block both land on , the same element as `:root`, so the
+ var() substitution below resolves against whichever theme is winning. */
+ --brand-mark: var(--clinical-accent);
+ --brand-mark-emphasis: var(--clinical-accent-strong);
+ --brand-mark-contrast: var(--clinical-accent-contrast);
/* Tinted filter-count pill on the results band — quieter than a solid disc. */
--search-band-badge-bg: color-mix(in oklab, var(--clinical-accent) 16%, transparent);
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 9659cc5a6..3eae06dd4 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -11,6 +11,7 @@ import { MobileKeyboardProvider } from "@/components/use-mobile-keyboard";
import { AppAnnouncements } from "@/components/app-announcements";
import { OverlayRoot } from "@/components/ui/overlay-root";
import { PRIVATE_APP_ROBOTS_METADATA } from "@/lib/crawler-policy";
+import { BRAND_DESCRIPTION, BRAND_NAME } from "@/lib/brand";
import "./globals.css";
/**
@@ -58,13 +59,13 @@ const geistMono = localFont({
});
const baseMetadata: Metadata = {
- applicationName: "PsychSift",
- title: "PsychSift",
- description: "Private medical guideline RAG knowledge base",
+ applicationName: BRAND_NAME,
+ title: BRAND_NAME,
+ description: BRAND_DESCRIPTION,
robots: PRIVATE_APP_ROBOTS_METADATA,
appleWebApp: {
capable: true,
- title: "PsychSift",
+ title: BRAND_NAME,
statusBarStyle: "black-translucent",
},
};
diff --git a/src/app/manifest.ts b/src/app/manifest.ts
index 997d2d746..3528f4698 100644
--- a/src/app/manifest.ts
+++ b/src/app/manifest.ts
@@ -1,5 +1,7 @@
import type { MetadataRoute } from "next";
+import { BRAND_DESCRIPTION, BRAND_NAME } from "@/lib/brand";
+
// PWA manifest — makes the app installable with a proper icon. Icons derive from
// the single brand-mark source: the SVG for modern browsers, plus generated PNG
// "any" and "maskable" sets from app/icons/[variant]. Theme colours stay on
@@ -7,9 +9,9 @@ import type { MetadataRoute } from "next";
// so light/dark can update without a static PWA manifest colour lock.
export default function manifest(): MetadataRoute.Manifest {
return {
- name: "PsychSift",
- short_name: "PsychSift",
- description: "Private medical guideline RAG knowledge base",
+ name: BRAND_NAME,
+ short_name: BRAND_NAME,
+ description: BRAND_DESCRIPTION,
id: "/",
start_url: "/",
scope: "/",
@@ -35,7 +37,7 @@ export default function manifest(): MetadataRoute.Manifest {
],
shortcuts: [
{
- name: "Ask PsychSift",
+ name: `Ask ${BRAND_NAME}`,
short_name: "Ask",
description: "Open a source-backed clinical question",
url: "/?mode=answer&focus=1",
diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx
index 81b7300d4..0daf8b48a 100644
--- a/src/app/opengraph-image.tsx
+++ b/src/app/opengraph-image.tsx
@@ -1,8 +1,9 @@
import { ImageResponse } from "next/og";
+import { BRAND_CATCHPHRASE_BARE, BRAND_NAME, BRAND_OG_ALT } from "@/lib/brand";
import { brandMarkDataUri } from "@/lib/brand-image";
-export const alt = "PsychSift — private medical guideline knowledge base";
+export const alt = BRAND_OG_ALT;
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
@@ -23,8 +24,8 @@ export default function OpengraphImage() {
>
-
PsychSift
-
Private medical guideline knowledge base
+
{BRAND_NAME}
+
{BRAND_CATCHPHRASE_BARE}
,
{ ...size },
diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx
index 9a59c4bfc..73c2c298a 100644
--- a/src/components/clinical-dashboard/ClinicalSidebar.tsx
+++ b/src/components/clinical-dashboard/ClinicalSidebar.tsx
@@ -26,6 +26,7 @@ import {
} from "lucide-react";
import { appModeIcons } from "@/lib/app-mode-icons";
import { BrandMark } from "@/components/clinical-dashboard/brand";
+import { BRAND_CATCHPHRASE, BRAND_MENU_DESCRIPTION } from "@/lib/brand";
import {
cn,
fieldControlWithIcon,
@@ -133,6 +134,40 @@ const collapsedSidebarButton = `grid ${collapsedSidebarControl}`;
const collapsedSidebarActiveButton =
"border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]";
+/* Phone drawer header (ClinicalMobileSidebar).
+ *
+ * The stock Sheet header is built for a dialog that has to explain itself: a
+ * 5-unit pad, an 18px title, and a `text-sm leading-6` description paragraph.
+ * On this drawer that combination spent roughly 160px — a third of the first
+ * screenful above the notch on a 390px phone — restating what the drawer is,
+ * to a user who has just tapped the menu button and can see it. The description
+ * also wrapped to two lines, because the title column is squeezed between the
+ * brand mark and a 48px close button, which is why the block was so tall.
+ *
+ * The header now reads as a brand lockup rather than a dialog preamble: mark,
+ * wordmark, one strapline that cannot wrap, and a close control that keeps its
+ * full 48px tap target while giving up the boxed chrome that made it the
+ * loudest thing in the header. The functional sentence moves to `sr-only`,
+ * where it is still the accessible description.
+ *
+ * These are overrides on the shared Sheet rather than edits to it: every other
+ * dialog in the app uses that header, and the case for a compact brand header
+ * is specific to a navigation drawer that is already showing its own contents. */
+const drawerHeader = "gap-x-2.5 px-4 py-3 sm:px-5 sm:py-3.5";
+const drawerHeaderTitle = "text-base leading-6 tracking-tight sm:text-lg";
+const drawerHeaderStrapline = "block truncate text-2xs font-medium leading-4 text-[color:var(--text-muted)]";
+/* Ghost close control, matching the collapsed rail's idiom (transparent border
+ * that resolves on hover, so forced-colors still has an edge to paint) instead
+ * of the toolbar recipe's resting border, fill and inset shadow. The tap target
+ * is unchanged at h-tap/w-tap; only the chrome is quieter.
+ *
+ * The glyph is lifted to --spacing-icon-lg, the 20px "header / primary controls"
+ * step, via a child variant: Sheet hardcodes its X at 16px, which reads as a
+ * default-sized icon adrift once the surrounding box is removed. Scoped here
+ * rather than changed in Sheet, since that icon is shared by every dialog. */
+const drawerHeaderClose =
+ "grid h-tap w-tap shrink-0 place-items-center rounded-lg border border-transparent text-[color:var(--text-muted)] transition hover:border-[color:var(--border)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] [&>svg]:size-icon-lg";
+
function SidebarModesTrigger({
variant,
active,
@@ -587,10 +622,16 @@ export function ClinicalSidebarContent({
{showHeader ? (
-
-
- Clinical Guide
-
+
+ {/* Same lockup as the phone drawer header: wordmark over strapline,
+ so the two entry points to the same navigation read as one brand
+ rather than two headers that happen to share a title. */}
+