diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 758330f517..8591331d90 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -141,7 +141,10 @@ While the sidebar is active on desktop/tablet project screens, Board and List wo
The active nav-item highlight and the resize-handle hover/focus accent track the active color theme's `--accent` token across all themes, so shadcn, forest, ocean, and other themes no longer show a fixed blue selected state. The Header retains the Fusion brand and project selector, keeps non-navigation controls, and hides duplicate desktop view-toggle entries while the sidebar is active.
-On mobile viewports (`<=768px`), the sidebar is not rendered even when the default-on setting is enabled. The existing bottom `MobileNavBar` remains the navigation surface, with mobile-only More-sheet entries for compact tools such as Git Manager, Terminal, Files, and **Import from GitHub**.
+On mobile viewports (`<=768px`), the sidebar is not rendered even when the default-on setting is enabled. The existing bottom `MobileNavBar` remains the navigation surface on project task screens, with mobile-only More-sheet entries for compact tools such as Git Manager, Terminal, Files, and **Import from GitHub**.
+
+
+On the **All Projects** overview, Fusion suppresses the mobile bottom nav because task tabs are not active there. It also clears `--mobile-nav-height`, so the overview does not reserve bottom-bar space. The overview reflows through 320px: headers, filters, stats, and card actions stack or wrap, while long project names and paths truncate rather than overflowing. Dashboard component styles use literal pixel values for media-query breakpoints (including `480px` and `380px`); declaration values within those blocks stay token-based.
## Right Dock (experimental, default on)
diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx
index f9deaba0f5..8b3663d4a9 100644
--- a/packages/dashboard/app/App.tsx
+++ b/packages/dashboard/app/App.tsx
@@ -877,6 +877,7 @@ function AppInner() {
/* FNXC:Navigation 2026-06-22-18:00: The right dock panel is no longer experimental or user-toggleable; tablet/desktop project screens always support it regardless of any stale persisted `rightDock` setting. */
const rightDockEnabled = true;
const executorFooterVisible = viewMode === "project" && !!currentProject;
+ const mobileNavVisible = viewMode === "project" && !!currentProject;
const rightDockActive = rightDockEnabled && !isMobile && executorFooterVisible;
const sidebarActive = leftSidebarNavEnabled && !isMobile && executorFooterVisible;
const agentOnboardingEnabled = experimentalFeatures.agentOnboarding === true;
@@ -1890,7 +1891,7 @@ function AppInner() {
/>
)}
@@ -1929,8 +1930,9 @@ function AppInner() {
)}
{}}
- footerVisible={viewMode === "project" && !!currentProject}
+ onChangeView={mobileNavVisible ? handleTaskViewChange : () => {}}
+ footerVisible={mobileNavVisible}
+ hidden={!mobileNavVisible}
modalOpen={modalManager.anyModalOpen}
keyboardOpen={mobileNavKeyboardOpen}
mobileNavPrimaryItems={mobileNavPrimaryItems}
diff --git a/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx b/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx
index 2752880afd..0873c4f33c 100644
--- a/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx
+++ b/packages/dashboard/app/__tests__/mobile-feature-access-regression.test.tsx
@@ -130,6 +130,7 @@ const createProjects = () => [
describe("Mobile Feature Access Regression Guard", () => {
beforeEach(() => {
mockViewport("mobile");
+ document.documentElement.style.removeProperty("--mobile-nav-height");
});
it("list view is accessible via mobile nav bar", () => {
@@ -261,11 +262,15 @@ describe("Mobile Feature Access Regression Guard", () => {
expect(screen.queryByTestId("mobile-more-item-chat")).toBeNull();
});
- it("mobile nav bar renders only on mobile viewport and hides for modal or desktop", () => {
- const mobileRender = render();
+ it("mobile nav bar renders only on mobile viewport and hides for modal, desktop, or project overview", () => {
+ const mobileRender = render();
expect(mobileRender.container.querySelector(".mobile-nav-bar")).not.toBeNull();
mobileRender.unmount();
+ const hiddenRender = render();
+ expect(hiddenRender.container.querySelector(".mobile-nav-bar")).toBeNull();
+ hiddenRender.unmount();
+
mockViewport("desktop");
const desktopRender = render();
expect(desktopRender.container.querySelector(".mobile-nav-bar")).toBeNull();
@@ -276,6 +281,19 @@ describe("Mobile Feature Access Regression Guard", () => {
expect(modalRender.container.querySelector(".mobile-nav-bar")).toBeNull();
});
+ it("clears the published mobile-nav height when the overview hides the bar", () => {
+ const hiddenRender = render();
+ expect(document.documentElement.style.getPropertyValue("--mobile-nav-height")).toBe("");
+ hiddenRender.unmount();
+
+ const { rerender, unmount } = render();
+ expect(document.documentElement.style.getPropertyValue("--mobile-nav-height")).toMatch(/^[1-9]\d*px$/);
+
+ rerender();
+ expect(document.documentElement.style.getPropertyValue("--mobile-nav-height")).toBe("");
+ unmount();
+ });
+
it("desktop and tablet header view navigation is suppressed when left sidebar is active", () => {
for (const tier of ["desktop", "tablet"] as const) {
mockViewport(tier);
diff --git a/packages/dashboard/app/__tests__/project-overview-small-screen.test.ts b/packages/dashboard/app/__tests__/project-overview-small-screen.test.ts
new file mode 100644
index 0000000000..d0a9fcf260
--- /dev/null
+++ b/packages/dashboard/app/__tests__/project-overview-small-screen.test.ts
@@ -0,0 +1,91 @@
+import { readFileSync } from "fs";
+import { resolve } from "path";
+import { describe, expect, it } from "vitest";
+
+const overviewCss = readFileSync(resolve(__dirname, "../components/ProjectOverview.css"), "utf8");
+const cardCss = readFileSync(resolve(__dirname, "../components/ProjectCard.css"), "utf8");
+const appSource = readFileSync(resolve(__dirname, "../App.tsx"), "utf8");
+
+function extractMediaBlock(css: string, breakpoint: number): string {
+ const match = new RegExp(`@media\\s*\\(max-width:\\s*${breakpoint}px\\)\\s*\\{`).exec(css);
+ expect(match, `missing max-width: ${breakpoint}px media block`).not.toBeNull();
+
+ const start = (match?.index ?? 0) + (match?.[0].length ?? 0);
+ let depth = 1;
+ let end = start;
+ while (depth > 0 && end < css.length) {
+ if (css[end] === "{") depth += 1;
+ if (css[end] === "}") depth -= 1;
+ end += 1;
+ }
+
+ expect(depth, `unterminated max-width: ${breakpoint}px media block`).toBe(0);
+ return css.slice(start, end - 1);
+}
+
+function extractRule(css: string, selector: string): string {
+ const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ return css.match(new RegExp(`${escaped}\\s*\\{([\\s\\S]*?)\\}`))?.[1] ?? "";
+}
+
+describe("project overview small-screen reflow", () => {
+ it("keeps literal dashboard breakpoints and a single-column grid through 320px", () => {
+ for (const css of [overviewCss, cardCss]) {
+ expect(css).toContain("@media (max-width: 480px)");
+ expect(css).toContain("@media (max-width: 380px)");
+ }
+
+ const overview480 = extractMediaBlock(overviewCss, 480);
+ const overview380 = extractMediaBlock(overviewCss, 380);
+ expect(extractRule(overview480, ".project-grid")).toContain("grid-template-columns: 1fr");
+ expect(overview480).toContain(".project-overview__header");
+ expect(overview480).toContain(".project-overview__filters");
+ expect(overview480).toContain(".project-filter-tabs");
+ expect(overview380).toContain(".project-overview__stats-row");
+ });
+
+ it("removes desktop card minimums and preserves truncation for populated long-name data", () => {
+ const card480 = extractMediaBlock(cardCss, 480);
+ const card380 = extractMediaBlock(cardCss, 380);
+
+ expect(extractRule(card480, ".project-card,\n .project-card-header,\n .project-card-title-section,\n .project-card-availability__row,\n .project-card-skeleton__header,\n .project-skeleton__text-group")).toContain("min-width: 0");
+ expect(card480).toMatch(/\.project-card-name,[\s\S]*?\.project-card-availability__path\s*\{[\s\S]*?overflow:\s*hidden;[\s\S]*?text-overflow:\s*ellipsis;/);
+ expect(card380).toContain(".project-card-footer");
+ expect(card380).toContain(".project-card-actions");
+ expect(card480).not.toMatch(/min-width:\s*(?:2[89]\d|[3-9]\d\d)px/);
+ });
+
+ it("keeps empty, filtered-empty, skeleton, health-empty, and availability surfaces shrinkable", () => {
+ const overview480 = extractMediaBlock(overviewCss, 480);
+ const card480 = extractMediaBlock(cardCss, 480);
+
+ expect(overview480).toContain(".project-empty-state");
+ expect(overview480).toContain(".project-overview__no-results");
+ expect(overview480).toContain(".project-card--skeleton");
+ expect(overview480).toContain(".project-overview__stat-skeleton");
+ expect(card480).toContain(".project-card-metric-empty");
+ expect(card480).toContain(".project-card-availability__path");
+ });
+
+ it("uses tokens in new declaration blocks while allowing literal media conditions", () => {
+ for (const css of [overviewCss, cardCss]) {
+ for (const breakpoint of [480, 380]) {
+ const block = extractMediaBlock(css, breakpoint);
+ expect(block).not.toMatch(/#[0-9a-f]{3,8}\b|\brgba?\(/i);
+ expect(block).not.toMatch(/\b\d+px\b/);
+ }
+ }
+
+ const overview480 = extractMediaBlock(overviewCss, 480);
+ const card480 = extractMediaBlock(cardCss, 480);
+ expect(overview480).toContain("var(--space-sm)");
+ expect(card480).toContain("var(--space-md)");
+ });
+
+ it("reserves project mobile-nav padding only when the shared nav predicate is visible", () => {
+ expect(appSource).toContain('const mobileNavVisible = viewMode === "project" && !!currentProject;');
+ expect(appSource).toContain('isMobile && mobileNavVisible && !mobileKeyboardOpen ? " project-content--with-mobile-nav" : ""');
+ expect(appSource).toContain("hidden={!mobileNavVisible}");
+ expect(appSource).toContain("footerVisible={mobileNavVisible}");
+ });
+});
diff --git a/packages/dashboard/app/components/MobileNavBar.tsx b/packages/dashboard/app/components/MobileNavBar.tsx
index 747553a044..489e0a344f 100644
--- a/packages/dashboard/app/components/MobileNavBar.tsx
+++ b/packages/dashboard/app/components/MobileNavBar.tsx
@@ -70,6 +70,11 @@ export interface MobileNavBarProps {
footerVisible: boolean;
/** Whether any full-screen modal is currently open (hides the tab bar) */
modalOpen?: boolean;
+ /*
+ FNXC:Navigation 2026-07-25-22:57:
+ The project overview has no task tabs to drive, so callers hide the mobile bar rather than rendering a non-functional navigation shell.
+ */
+ hidden?: boolean;
/** Whether the on-screen mobile keyboard is open */
keyboardOpen?: boolean;
// Navigation handlers
@@ -145,6 +150,7 @@ export function MobileNavBar({
onChangeView,
footerVisible,
modalOpen = false,
+ hidden = false,
keyboardOpen = false,
onOpenSettings,
onOpenActivityLog,
@@ -346,6 +352,15 @@ export function MobileNavBar({
}, [dismissMore, isMoreOpen]);
useLayoutEffect(() => {
+ /*
+ FNXC:Navigation 2026-07-25-22:57:
+ A hidden overview bar must remove its published height as well as its DOM shell; otherwise project content retains dead bottom space.
+ */
+ if (hidden) {
+ document.documentElement.style.removeProperty("--mobile-nav-height");
+ return;
+ }
+
const navEl = navRef.current;
if (!navEl || typeof document === "undefined") {
return;
@@ -377,9 +392,9 @@ export function MobileNavBar({
observer?.disconnect();
document.documentElement.style.removeProperty("--mobile-nav-height");
};
- }, []);
+ }, [hidden]);
- if (mode !== "mobile" || modalOpen) {
+ if (mode !== "mobile" || modalOpen || hidden) {
return null;
}
diff --git a/packages/dashboard/app/components/ProjectCard.css b/packages/dashboard/app/components/ProjectCard.css
index 9083c02302..a00d103443 100644
--- a/packages/dashboard/app/components/ProjectCard.css
+++ b/packages/dashboard/app/components/ProjectCard.css
@@ -285,3 +285,61 @@
display: none;
}
}
+
+/*
+FNXC:DashboardResponsive 2026-07-25-22:57:
+Project cards must not retain their desktop minimum at 320px. Literal pixel media conditions are the permitted dashboard breakpoint convention; declarations remain token-based.
+*/
+@media (max-width: 480px) {
+ .project-card,
+ .project-card-header,
+ .project-card-title-section,
+ .project-card-availability__row,
+ .project-card-skeleton__header,
+ .project-skeleton__text-group {
+ min-width: 0;
+ }
+
+ .project-card-name,
+ .project-card-path,
+ .project-card-availability__path {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .project-card-health,
+ .project-card-skeleton__health {
+ flex-wrap: wrap;
+ gap: var(--space-md);
+ }
+
+ .project-card-metric,
+ .project-card-metric-empty {
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+}
+
+/*
+FNXC:DashboardResponsive 2026-07-25-22:57:
+At the 320px floor, card actions and skeleton footer content stack instead of creating a horizontal overflow. Literal pixel media conditions are intentional; declarations use tokens.
+*/
+@media (max-width: 380px) {
+ .project-card-header,
+ .project-card-footer,
+ .project-card-skeleton__footer {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .project-card-actions {
+ width: 100%;
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .project-card-action {
+ width: 100%;
+ }
+}
diff --git a/packages/dashboard/app/components/ProjectOverview.css b/packages/dashboard/app/components/ProjectOverview.css
index a2be9dcb0c..3194ed2e19 100644
--- a/packages/dashboard/app/components/ProjectOverview.css
+++ b/packages/dashboard/app/components/ProjectOverview.css
@@ -663,3 +663,102 @@ Keep Dashboard on the canonical header model instead of changing every other vie
max-width: 100%;
}
}
+
+/*
+FNXC:DashboardResponsive 2026-07-25-22:57:
+The overview must stay usable down to 320px. Literal pixel media conditions are the permitted dashboard breakpoint convention; declaration values remain token-based.
+*/
+@media (max-width: 480px) {
+ .project-overview__body {
+ padding: var(--space-sm);
+ }
+
+ .project-overview__header,
+ .project-overview__filters {
+ flex-direction: column;
+ align-items: stretch;
+ min-width: 0;
+ }
+
+ .project-overview__stats,
+ .project-overview__stats-row {
+ gap: var(--space-sm);
+ min-width: 0;
+ }
+
+ .project-stat,
+ .project-overview__stat-skeleton {
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+
+ .project-filter-tabs,
+ .project-overview__filters-skeleton {
+ flex-wrap: wrap;
+ min-width: 0;
+ }
+
+ .project-filter-tab {
+ flex: 1 1 auto;
+ }
+
+ .project-sort,
+ .project-node-filter {
+ flex-direction: column;
+ align-items: stretch;
+ gap: var(--space-xs);
+ min-width: 0;
+ }
+
+ .project-sort-select,
+ .project-node-filter-select {
+ width: 100%;
+ min-width: 0;
+ margin-left: 0;
+ }
+
+ .project-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .project-empty-state,
+ .project-overview__no-results {
+ min-width: 0;
+ padding: var(--space-lg);
+ }
+
+ .project-card--skeleton,
+ .project-skeleton__text-group {
+ min-width: 0;
+ }
+}
+
+/*
+FNXC:DashboardResponsive 2026-07-25-22:57:
+At the 320px floor, stack compact stat and skeleton rows so fixed-width placeholders cannot force horizontal scrolling. Literal pixel media conditions are intentional; declarations use tokens.
+*/
+@media (max-width: 380px) {
+ .project-overview__stats,
+ .project-overview__stats-row {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .project-stat,
+ .project-overview__stat-skeleton {
+ width: 100%;
+ }
+
+ .project-overview__filters-skeleton {
+ gap: var(--space-xs);
+ }
+
+ .project-skeleton--tab {
+ flex: 1 1 auto;
+ }
+
+ .project-empty-state,
+ .project-overview__no-results {
+ padding: var(--space-md);
+ }
+}