diff --git a/src/app/globals.css b/src/app/globals.css index 88624cfc7d..1d282af1d0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -543,24 +543,17 @@ summary::-webkit-details-marker { isolation: isolate; padding-left: max(0.75rem, var(--safe-area-left)); padding-right: max(0.75rem, var(--safe-area-right)); - border-bottom: 1px solid var(--border); - background: var(--surface); + /* Translucent glass bar: the header's backdrop-blur utilities frost the + content scrolling beneath it (they were inert over the old opaque + var(--surface)). The .edge-glass-header-backdrop scrim supersedes the + old hard border-bottom and below-header ::after gradient. Keep this + value in lock-step with .universal-header below (chrome parity). */ + background: color-mix(in srgb, var(--surface) 72%, transparent); box-shadow: none; } - .edge-glass-header::after { - pointer-events: none; - position: absolute; - inset-inline: 0; - bottom: -1.25rem; - z-index: -1; - height: 1.25rem; - background: linear-gradient(180deg, color-mix(in srgb, var(--background) 54%, transparent), transparent); - content: ""; - } - .universal-header { - background: var(--surface); + background: color-mix(in srgb, var(--surface) 72%, transparent); } .universal-header-mode-button { @@ -663,6 +656,71 @@ summary::-webkit-details-marker { } } +/* Top progressive-blur scrim behind the glass header — the vertical mirror of + the bottom dock's .answer-footer-search-backdrop three-pass blur below. + Strongest blur/tint hugs the physical top edge (status-bar/notch band via + safe-area-inset-top) and fades downward into content, so scrolling text + frosts as it slides under the bar. Lives INSIDE
(first child) so it + translates away with the header on scroll-hide. Unlayered on purpose, like + the rest of the positioning chrome (see the layering note above). */ +.edge-glass-header-backdrop { + pointer-events: none; + position: absolute; + inset-inline: 0; + top: 0; + z-index: -1; + height: max(6.5rem, calc(var(--safe-area-top) + 5.5rem)); + overflow: hidden; + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--background) 42%, transparent) 0%, + color-mix(in srgb, var(--background) 32%, transparent) 12%, + color-mix(in srgb, var(--background) 18%, transparent) 32%, + color-mix(in srgb, var(--background) 8%, transparent) 62%, + transparent 100% + ); + backdrop-filter: blur(2px) saturate(130%); + -webkit-backdrop-filter: blur(2px) saturate(130%); + mask-image: linear-gradient(180deg, black 0%, black 45%, rgb(0 0 0 / 45%) 72%, rgb(0 0 0 / 20%) 100%); + -webkit-mask-image: linear-gradient(180deg, black 0%, black 45%, rgb(0 0 0 / 45%) 72%, rgb(0 0 0 / 20%) 100%); +} + +.edge-glass-header-backdrop::before, +.edge-glass-header-backdrop::after { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; +} + +.edge-glass-header-backdrop::before { + backdrop-filter: blur(8px) saturate(130%); + -webkit-backdrop-filter: blur(8px) saturate(130%); + mask-image: linear-gradient( + 180deg, + black 0%, + black 20%, + rgb(0 0 0 / 55%) 45%, + rgb(0 0 0 / 35%) 65%, + transparent 100% + ); + -webkit-mask-image: linear-gradient( + 180deg, + black 0%, + black 20%, + rgb(0 0 0 / 55%) 45%, + rgb(0 0 0 / 35%) 65%, + transparent 100% + ); +} + +.edge-glass-header-backdrop::after { + backdrop-filter: blur(22px) saturate(140%); + -webkit-backdrop-filter: blur(22px) saturate(140%); + mask-image: linear-gradient(180deg, black 0%, black 32%, transparent 50%, transparent 100%); + -webkit-mask-image: linear-gradient(180deg, black 0%, black 32%, transparent 50%, transparent 100%); +} + .answer-footer-search-backdrop { pointer-events: none; position: fixed; @@ -787,6 +845,30 @@ summary::-webkit-details-marker { color-mix(in srgb, var(--background) 92%, transparent) 100% ); } + + /* No blur available: the glass header goes opaque so text never sits on + un-frosted scrolled content, and the top scrim swaps to a stronger tint. */ + .edge-glass-header-backdrop, + .edge-glass-header-backdrop::before, + .edge-glass-header-backdrop::after { + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + .edge-glass-header-backdrop { + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--background) 92%, transparent) 0%, + color-mix(in srgb, var(--background) 78%, transparent) 40%, + color-mix(in srgb, var(--background) 45%, transparent) 72%, + transparent 100% + ); + } + + .edge-glass-header, + .universal-header { + background: var(--surface); + } } @media (prefers-reduced-transparency: reduce) { @@ -806,6 +888,29 @@ summary::-webkit-details-marker { color-mix(in srgb, var(--background) 94%, transparent) 100% ); } + + /* Reduced transparency: same opaque-bar treatment as the no-blur fallback. */ + .edge-glass-header-backdrop, + .edge-glass-header-backdrop::before, + .edge-glass-header-backdrop::after { + backdrop-filter: none; + -webkit-backdrop-filter: none; + } + + .edge-glass-header-backdrop { + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--background) 94%, transparent) 0%, + color-mix(in srgb, var(--background) 82%, transparent) 42%, + color-mix(in srgb, var(--background) 50%, transparent) 74%, + transparent 100% + ); + } + + .edge-glass-header, + .universal-header { + background: var(--surface); + } } @layer components { @@ -2002,6 +2107,23 @@ summary::-webkit-details-marker { select { forced-color-adjust: auto; } + + /* Glass header: color-mix(Canvas, transparent) would thin system-color + contrast, so force a solid Canvas bar and drop the scrim blur entirely. */ + .edge-glass-header, + .universal-header { + background: Canvas; + } + + .edge-glass-header-backdrop, + .edge-glass-header-backdrop::before, + .edge-glass-header-backdrop::after { + background: Canvas; + backdrop-filter: none; + -webkit-backdrop-filter: none; + mask-image: none; + -webkit-mask-image: none; + } } @media print { diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 218fa3b409..83a5386815 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -709,9 +709,6 @@ export function ClinicalDashboard({ mainRef.current = node; setMainScrollRoot(node); }, []); - const phoneScrollHide = useScrollHideReporter(); - const reportPhoneScrollHideRef = useRef(phoneScrollHide.reportScroll); - reportPhoneScrollHideRef.current = phoneScrollHide.reportScroll; const [bottomSearchScrollHidden, setBottomSearchScrollHidden] = useState(false); const composerInputRef = useRef(null); const scrollFrameRef = useRef(null); @@ -737,6 +734,12 @@ export function ClinicalDashboard({ const [answerThreadBootstrapped, setAnswerThreadBootstrapped] = useState(false); const [query, setQuery] = useState(initialQuery); const [searchMode, setSearchMode] = useState(initialSearchMode); + // Answer mode hides the glass header at every breakpoint (all-breakpoints + // overlay); other modes keep the phone-only collapse, so the reporter only + // widens past the phone media gate while in answer mode. + const phoneScrollHide = useScrollHideReporter(false, searchMode === "answer"); + const reportPhoneScrollHideRef = useRef(phoneScrollHide.reportScroll); + reportPhoneScrollHideRef.current = phoneScrollHide.reportScroll; const [modeSearchSubmitted, setModeSearchSubmitted] = useState(() => Boolean(autoRunSearch && initialQuery.trim() && initialSearchMode !== "tools"), ); @@ -3465,7 +3468,7 @@ export function ClinicalDashboard({ onPrefetchApplications={prefetchApplications} /> -
+
, - // so hiding must collapse its layout space to hand it to content. - hideOnScroll={{ strategy: "collapse", scrollHidden: phoneScrollHide.hidden }} + // Answer view: the header overlays the scrolling
at every width + // (main reserves matching top padding) so content frosts under the + // glass bar, and it slides away/returns with scroll direction. Other + // modes keep the phone-only collapse (their sm+ composer renders + // in-flow below the header, which an absolute header would bury). + hideOnScroll={ + searchMode === "answer" + ? { strategy: "overlay", allBreakpoints: true, scrollHidden: phoneScrollHide.hidden } + : { strategy: "collapse", scrollHidden: phoneScrollHide.hidden } + } onBottomComposerScrollHiddenChange={setBottomSearchScrollHidden} /> - {privateScopeStatus === "unavailable" ? ( -
-

- The original private document scope is unavailable. Choose the documents again or confirm a broader - search. -

-
- - -
-
- ) : null} -
reserves its exact height as top padding (72px borderless + // bar = 4rem content/padding + the max(0.5rem, safe-area) top inset — + // measured; must stay 1:1 with the rendered #search height so all the + // dvh-based section floors below keep their meaning). Padding, not + // margin: padding scrolls with content, which is what lets it slide + // up and frost beneath the bar. Kept constant when the header + // scroll-hides — the reserve lives at scroll-start, already off-screen + // whenever the header is hidden, so reclaiming it would only jump + // the content. + searchMode === "answer" && + "pt-[calc(4rem+max(0.5rem,env(safe-area-inset-top)))] [scroll-padding-top:calc(4.5rem+max(0.5rem,env(safe-area-inset-top)))]", searchMode === "answer" ? compactMobileModeHome ? "mb-0" @@ -3584,6 +3585,29 @@ export function ClinicalDashboard({ )} >

Clinical Guide

+ {privateScopeStatus === "unavailable" ? ( + // Lives inside
(not as a header sibling): in the answer view + // the header is absolute, so a sibling alert would reflow to the + // column top and hide behind the glass bar. +
+

+ The original private document scope is unavailable. Choose the documents again or confirm a broader + search. +

+
+ + +
+
+ ) : null}