diff --git a/docs/process-hardening.md b/docs/process-hardening.md
index 4b7d68ce1d..d53cf10292 100644
--- a/docs/process-hardening.md
+++ b/docs/process-hardening.md
@@ -68,3 +68,10 @@ This document turns the current process review into phased, durable repo practic
- **Known follow-up debts (documented, not actioned):**
- Live migration history has duplicate-version churn (two each of `api_rate_limits`, `audit_logs`, `rag_queries_retention`, `audit_logs_service_role_policy`, `indexing_reliability_recovery`) from the same raw-apply habit. Do not rewrite history; treat as a caution for future applies.
- Auth server is capped at 10 absolute DB connections (Supabase advisor); switch to percentage-based allocation in the dashboard before scaling instance size (not settable via SQL/MCP).
+
+## CSS cascade layering (2026-07-02)
+
+- The custom component classes in `src/app/globals.css` predate cascade layers, so they sat unlayered and silently beat Tailwind v4 utilities (which live in `@layer utilities`) on the same element. This caused three shipped UI bugs: the header source ledger ignoring responsive `hidden`, the composer clear button covering typed text (`pr-*` defeated), and the standalone-home status chips sliding under the mode pill.
+- Conflict-free helper classes (`app-edge-backdrop`, `mobile-app-shell`, `mobile-popover-scroll`, `citation-link`, `animate-skeleton-shimmer`, `focus-ring-premium`, `source-capsule-hover`, `polished-scroll`) now live in `@layer components`, so utilities override them normally. Their call sites were audited for same-property utility collisions before the move.
+- **Remaining debt:** the chrome classes (`edge-glass-header`, `universal-header-*`, `answer-footer-search-*`, `*-composer-edge`, `desktop-home-search-*`, `document-mobile-search-*`) stay intentionally unlayered because call sites stack utilities that set the same properties and today rely on the class winning (e.g. footer input font-size/padding, pill min-height, header shadow). Layering them requires reconciling each call site so rendered output is unchanged. Until then: when adding a utility to an element carrying one of these classes, check the class body first — the class wins.
+- `tests/ui-overlap.spec.ts` is the standing regression guard for the visible symptom (overlapping header controls, composer clear-button geometry) across 640-1536px widths.
diff --git a/playwright.config.ts b/playwright.config.ts
index 73b3ee1e4b..b219750d9c 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -5,7 +5,7 @@ const baseURL = getPlaywrightBaseUrl();
export default defineConfig({
testDir: "./tests",
- testMatch: /.*ui-(smoke|stress|accessibility|tools)\.spec\.ts/,
+ testMatch: /.*ui-(smoke|stress|accessibility|tools|overlap)\.spec\.ts/,
timeout: 60_000,
retries: process.env.CI ? 1 : 0,
expect: {
diff --git a/src/app/globals.css b/src/app/globals.css
index a9effb3d22..763a8a958f 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -430,11 +430,12 @@ summary::-webkit-details-marker {
color: #ecfeff;
}
-/* Layout utilities */
-.app-edge-backdrop {
- background: var(--background);
-}
-
+/*
+ * Layout chrome — INTENTIONALLY UNLAYERED: these classes beat Tailwind
+ * utilities on the same element (unlayered CSS wins over all layers). Their
+ * call sites stack utilities that set the same properties and rely on the
+ * class winning; the conflict-free helpers live in @layer components below.
+ */
.edge-glass-header {
isolation: isolate;
padding-left: max(0.75rem, var(--safe-area-left));
@@ -538,13 +539,6 @@ summary::-webkit-details-marker {
opacity: 1;
}
-.answer-footer-search-mic {
- height: 2.75rem;
- width: 2.75rem;
- min-width: 44px;
- color: var(--text-muted);
-}
-
.answer-footer-search-divider {
display: none;
height: 2.25rem;
@@ -591,11 +585,6 @@ summary::-webkit-details-marker {
color: var(--text-muted);
}
-.mobile-app-shell {
- min-height: 100svh;
- height: 100svh;
-}
-
.dashboard-composer-edge {
left: max(0.75rem, var(--safe-area-left));
right: max(0.75rem, var(--safe-area-right));
@@ -644,21 +633,6 @@ summary::-webkit-details-marker {
font-weight: 560;
}
-.mobile-popover-scroll {
- max-height: min(70svh, 28rem);
-}
-
-@supports (height: 100dvh) {
- .mobile-app-shell {
- min-height: 100dvh;
- height: 100dvh;
- }
-
- .mobile-popover-scroll {
- max-height: min(70dvh, 28rem);
- }
-}
-
@media (min-width: 640px) {
.edge-glass-header {
padding-left: max(1rem, var(--safe-area-left));
@@ -693,11 +667,6 @@ summary::-webkit-details-marker {
width: 3.3rem;
}
- .answer-footer-search-mic {
- height: 3.3rem;
- width: 3.3rem;
- }
-
.answer-footer-search-divider {
display: block;
}
@@ -855,72 +824,108 @@ summary::-webkit-details-marker {
}
}
-/* Scroll and print helpers */
-.citation-link {
- position: relative;
-}
+/*
+ * Helper classes with no same-property utility conflicts at any call site
+ * live in @layer components so Tailwind utilities can override them.
+ * Audited 2026-07-02: the chrome classes above (edge-glass-header,
+ * universal-header-*, answer-footer-search-*, *-composer-edge,
+ * desktop-home-search-*) DO conflict with call-site utilities and stay
+ * unlayered deliberately — layering them changes rendered pixels. When
+ * adding a utility to an element carrying one of those classes, check the
+ * class body first; the class wins.
+ */
+@layer components {
+ .app-edge-backdrop {
+ background: var(--background);
+ }
-.citation-link::after {
- content: "";
- position: absolute;
- top: -10px;
- bottom: -10px;
- left: -10px;
- right: -10px;
-}
+ .mobile-app-shell {
+ min-height: 100svh;
+ height: 100svh;
+ }
-/* Premium Skeleton Shimmer animation with custom easing */
-.animate-skeleton-shimmer {
- animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
-}
+ .mobile-popover-scroll {
+ max-height: min(70svh, 28rem);
+ }
-@keyframes skeleton-pulse {
- 0%,
- 100% {
- opacity: 1;
+ @supports (height: 100dvh) {
+ .mobile-app-shell {
+ min-height: 100dvh;
+ height: 100dvh;
+ }
+
+ .mobile-popover-scroll {
+ max-height: min(70dvh, 28rem);
+ }
}
- 50% {
- opacity: 0.35;
+
+ /* Scroll and print helpers */
+ .citation-link {
+ position: relative;
}
-}
-/* Premium Double-Ring Focus style */
-.focus-ring-premium {
- outline: none;
-}
-.focus-ring-premium:focus-visible {
- outline: 2px solid var(--focus) !important;
- outline-offset: 2px !important;
- box-shadow: 0 0 0 4px color-mix(in srgb, var(--focus) 25%, transparent) !important;
-}
+ .citation-link::after {
+ content: "";
+ position: absolute;
+ top: -10px;
+ bottom: -10px;
+ left: -10px;
+ right: -10px;
+ }
-/* Premium Hover Transitions for Source Capsules and Action row chips */
-.source-capsule-hover {
- transition: all 180ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
-}
+ /* Premium Skeleton Shimmer animation with custom easing */
+ .animate-skeleton-shimmer {
+ animation: skeleton-pulse 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
+ }
-.source-capsule-hover:hover {
- transform: translateY(-1px) scale(1.015) !important;
- box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 8%, transparent) !important;
-}
+ /* Premium Double-Ring Focus style */
+ .focus-ring-premium {
+ outline: none;
+ }
+ .focus-ring-premium:focus-visible {
+ outline: 2px solid var(--focus) !important;
+ outline-offset: 2px !important;
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--focus) 25%, transparent) !important;
+ }
-.polished-scroll {
- scrollbar-color: color-mix(in srgb, var(--border-strong) 80%, transparent) transparent;
- scrollbar-width: thin;
-}
+ /* Premium Hover Transitions for Source Capsules and Action row chips */
+ .source-capsule-hover {
+ transition: all 180ms cubic-bezier(0.34, 1.56, 0.64, 1) !important;
+ }
-.polished-scroll::-webkit-scrollbar {
- height: 0.5rem;
- width: 0.5rem;
-}
+ .source-capsule-hover:hover {
+ transform: translateY(-1px) scale(1.015) !important;
+ box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 8%, transparent) !important;
+ }
-.polished-scroll::-webkit-scrollbar-thumb {
- background: color-mix(in srgb, var(--border-strong) 72%, transparent);
- border-radius: 999px;
+ .polished-scroll {
+ scrollbar-color: color-mix(in srgb, var(--border-strong) 80%, transparent) transparent;
+ scrollbar-width: thin;
+ }
+
+ .polished-scroll::-webkit-scrollbar {
+ height: 0.5rem;
+ width: 0.5rem;
+ }
+
+ .polished-scroll::-webkit-scrollbar-thumb {
+ background: color-mix(in srgb, var(--border-strong) 72%, transparent);
+ border-radius: 999px;
+ }
+
+ .polished-scroll::-webkit-scrollbar-track {
+ background: transparent;
+ }
}
-.polished-scroll::-webkit-scrollbar-track {
- background: transparent;
+@keyframes skeleton-pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.35;
+ }
}
/* User preference and alternate output modes */
diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx
index 79d6a77e08..b2ffbf54ea 100644
--- a/src/components/DocumentViewer.tsx
+++ b/src/components/DocumentViewer.tsx
@@ -19,7 +19,6 @@ import {
Maximize2,
Menu,
Minimize2,
- Mic,
Minus,
Plus,
Quote,
@@ -362,7 +361,9 @@ function DocumentImage({ image }: { image: ImageRow }) {
const showImageCaptionLine = cleanCaption && cleanCaption !== tableCaption;
const displayLabels = smartEvidenceTags(
image.labels,
- [tableHeading, cleanCaption, image.tableTextSnippet ? sourceTextForCompactDisplay(image.tableTextSnippet) : null].filter(Boolean).join(" "),
+ [tableHeading, cleanCaption, image.tableTextSnippet ? sourceTextForCompactDisplay(image.tableTextSnippet) : null]
+ .filter(Boolean)
+ .join(" "),
);
return (
@@ -2967,13 +2968,6 @@ export function DocumentViewer({
className="min-h-[44px] min-w-0 flex-1 bg-transparent px-2 text-base font-medium text-[color:var(--text)] outline-none placeholder:text-[color:var(--text-soft)]"
/>
-
-