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. */} +
+

+ Clinical Guide +

+

{BRAND_CATCHPHRASE}

+
{sharedHomeIdentity ? (
- + {/* The name leads and the strapline supports, which is a weight and a colour apart, not just a size. The wordmark takes the display diff --git a/src/components/privacy-quiet-signal-page.tsx b/src/components/privacy-quiet-signal-page.tsx index cdcd7d712..e9509c1c8 100644 --- a/src/components/privacy-quiet-signal-page.tsx +++ b/src/components/privacy-quiet-signal-page.tsx @@ -210,7 +210,7 @@ export function PrivacyQuietSignalPage() { }> - +

{privacyCopy.pageEyebrow}

diff --git a/src/components/pwa-lifecycle.tsx b/src/components/pwa-lifecycle.tsx index 5c2329858..d919733c4 100644 --- a/src/components/pwa-lifecycle.tsx +++ b/src/components/pwa-lifecycle.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import { BrandMark } from "@/components/clinical-dashboard/brand"; +import { BRAND_INSTALL_TAGLINE, BRAND_NAME } from "@/lib/brand"; import { createBrowserStore } from "@/lib/client-store-factory"; const SERVICE_WORKER_URL = "/sw.js"; @@ -167,7 +168,7 @@ function InstallHeader({ return (

- +

{title}

@@ -201,7 +202,7 @@ function InstallBenefits() { function InstallManualSteps() { return ( -
    +
    1. 1. Tap Share