diff --git a/apps/web/app/(dashboard)/layout.tsx b/apps/web/app/(dashboard)/layout.tsx
index 0072ec3..2140380 100644
--- a/apps/web/app/(dashboard)/layout.tsx
+++ b/apps/web/app/(dashboard)/layout.tsx
@@ -1,4 +1,5 @@
import { Logo } from '@/components/logo';
+import { MobileActionBar } from '@/components/mobile-action-bar';
import { FileClock, LayoutGrid, Package, Server, Settings } from 'lucide-react';
import Link from 'next/link';
@@ -29,13 +30,14 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
+ New server
- {children}
+ {children}
+
);
}
diff --git a/apps/web/components/mobile-action-bar.tsx b/apps/web/components/mobile-action-bar.tsx
new file mode 100644
index 0000000..ccce73c
--- /dev/null
+++ b/apps/web/components/mobile-action-bar.tsx
@@ -0,0 +1,35 @@
+'use client';
+
+import Link from 'next/link';
+import { usePathname } from 'next/navigation';
+
+// Routes where the global "+ New server" CTA makes no sense — the wizard is
+// itself the create flow and owns its own primary action.
+const HIDDEN_PATHS: readonly string[] = ['/servers/new'];
+
+/**
+ * Mobile-only sticky bottom action bar. On phones the dashboard top header is
+ * already tight with the nav icons; the primary action belongs in the thumb
+ * zone, not crammed top-right. Hidden from `sm:` upward where the header has
+ * room for the same button inline.
+ */
+export function MobileActionBar() {
+ const pathname = usePathname();
+ if (HIDDEN_PATHS.includes(pathname)) return null;
+
+ return (
+