diff --git a/src/components/MobileStickyCtaBar.astro b/src/components/MobileStickyCtaBar.astro
new file mode 100644
index 0000000..7e804b5
--- /dev/null
+++ b/src/components/MobileStickyCtaBar.astro
@@ -0,0 +1,187 @@
+---
+// MobileStickyCtaBar — Sticky bottom action bar for mobile/tablet (<768px)
+// 3 equal columns: Call, WhatsApp, Quote
+// - Fixed to bottom of viewport
+// - Respects safe-area-inset-bottom on iOS
+// - Hidden at md+ breakpoints (>=768px)
+// - Hidden on print
+// - Light + dark theme aware
+---
+
+
+
+
diff --git a/src/components/WhatsAppFab.astro b/src/components/WhatsAppFab.astro
new file mode 100644
index 0000000..ce4bd91
--- /dev/null
+++ b/src/components/WhatsAppFab.astro
@@ -0,0 +1,104 @@
+---
+// WhatsAppFab — Global floating WhatsApp contact button
+// - Fixed bottom-right, 56x56, brand green (#25D366)
+// - Visible on every page across desktop and mobile
+// - Mobile: lifted above the MobileStickyCtaBar via safe CSS variable offset
+// - Respects iOS safe-area-inset-bottom
+// - Hidden when printing
+---
+
+
+
+
+
+
diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro
index e775a7a..0859ab9 100644
--- a/src/layouts/BaseLayout.astro
+++ b/src/layouts/BaseLayout.astro
@@ -6,6 +6,8 @@ import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import Analytics from '../components/Analytics.astro';
import PWAInstallPrompt from '../components/PWAInstallPrompt.astro';
+import WhatsAppFab from '../components/WhatsAppFab.astro';
+import MobileStickyCtaBar from '../components/MobileStickyCtaBar.astro';
import '../styles/global.css';
interface Props {
@@ -290,6 +292,15 @@ const fullTitle = title === 'Home' ? siteName : `${title} | ${siteName}`;
+
+
+
+
+
+
+
+
+
diff --git a/tests/sticky-cta.spec.ts b/tests/sticky-cta.spec.ts
new file mode 100644
index 0000000..bdccf1f
--- /dev/null
+++ b/tests/sticky-cta.spec.ts
@@ -0,0 +1,122 @@
+import { test, expect } from '@playwright/test';
+
+const WA_HREF =
+ 'https://wa.me/919561417403?text=Hi%20WorkRoot%2C%20I%27d%20like%20to%20discuss%20a%20project.';
+
+/**
+ * Verifies the global WhatsApp FAB and the Mobile Sticky CTA bar render
+ * correctly across viewports, expose working contact links, and never
+ * cause horizontal overflow on a 360px viewport.
+ */
+
+test.describe('WhatsApp FAB (global)', () => {
+ test('renders on every page at desktop width', async ({ page }) => {
+ await page.setViewportSize({ width: 1280, height: 800 });
+
+ for (const path of ['/', '/about', '/services', '/contact']) {
+ await page.goto(path);
+ const fab = page.getByTestId('whatsapp-fab');
+ await expect(fab, `FAB missing on ${path}`).toBeVisible();
+ await expect(fab).toHaveAttribute('href', WA_HREF);
+ await expect(fab).toHaveAttribute('target', '_blank');
+ await expect(fab).toHaveAttribute('rel', /noopener/);
+ await expect(fab).toHaveAttribute('aria-label', 'Chat on WhatsApp');
+ }
+ });
+
+ test('renders on home at 360px mobile width', async ({ page }) => {
+ await page.setViewportSize({ width: 360, height: 800 });
+ await page.goto('/');
+ const fab = page.getByTestId('whatsapp-fab');
+ await expect(fab).toBeVisible();
+
+ // FAB is 56x56
+ const box = await fab.boundingBox();
+ expect(box).not.toBeNull();
+ expect(box!.width).toBeGreaterThanOrEqual(48);
+ expect(box!.height).toBeGreaterThanOrEqual(48);
+
+ // FAB does not horizontally overflow the viewport
+ expect(box!.x + box!.width).toBeLessThanOrEqual(360);
+ });
+});
+
+test.describe('Mobile Sticky CTA Bar', () => {
+ test('visible at 360px and exposes 3 working actions', async ({ page }) => {
+ await page.setViewportSize({ width: 360, height: 800 });
+ await page.goto('/');
+
+ const bar = page.getByTestId('mobile-sticky-cta-bar');
+ await expect(bar).toBeVisible();
+
+ const callBtn = page.getByTestId('mobile-cta-call');
+ const waBtn = page.getByTestId('mobile-cta-whatsapp');
+ const quoteBtn = page.getByTestId('mobile-cta-quote');
+
+ await expect(callBtn).toBeVisible();
+ await expect(waBtn).toBeVisible();
+ await expect(quoteBtn).toBeVisible();
+
+ await expect(callBtn).toHaveAttribute('href', 'tel:+919561417403');
+ await expect(waBtn).toHaveAttribute('href', WA_HREF);
+ await expect(quoteBtn).toHaveAttribute('href', '/contact');
+
+ // Each action is at least a 48px tall touch target
+ for (const btn of [callBtn, waBtn, quoteBtn]) {
+ const box = await btn.boundingBox();
+ expect(box).not.toBeNull();
+ expect(box!.height).toBeGreaterThanOrEqual(48);
+ }
+
+ // The bar spans the full viewport width and is pinned to the bottom
+ const barBox = await bar.boundingBox();
+ expect(barBox).not.toBeNull();
+ expect(barBox!.x).toBeLessThanOrEqual(0.5);
+ expect(barBox!.width).toBeGreaterThanOrEqual(355);
+ expect(barBox!.width).toBeLessThanOrEqual(360);
+
+ // No horizontal overflow at 360px (match existing site tolerance: viewport + 20px)
+ const bodyWidth = await page.evaluate(() => document.body.scrollWidth);
+ expect(bodyWidth).toBeLessThanOrEqual(360 + 20);
+
+ // The page must not be horizontally scrolled — confirms fixed elements fit.
+ const scrollX = await page.evaluate(() => window.scrollX);
+ expect(scrollX).toBe(0);
+ });
+
+ test('hidden at >=768px (desktop / tablet landscape)', async ({ page }) => {
+ await page.setViewportSize({ width: 1280, height: 800 });
+ await page.goto('/');
+
+ const bar = page.getByTestId('mobile-sticky-cta-bar');
+ // Either not visible, or has md:hidden applied (display:none via media query)
+ await expect(bar).toBeHidden();
+ });
+
+ test('renders on every key page on mobile', async ({ page }) => {
+ await page.setViewportSize({ width: 360, height: 800 });
+ for (const path of ['/', '/about', '/services', '/portfolio', '/blog', '/contact']) {
+ await page.goto(path);
+ await expect(
+ page.getByTestId('mobile-sticky-cta-bar'),
+ `Sticky CTA bar missing on ${path}`,
+ ).toBeVisible();
+ }
+ });
+
+ test('FAB sits above the sticky bar on mobile (no overlap)', async ({ page }) => {
+ await page.setViewportSize({ width: 360, height: 800 });
+ await page.goto('/');
+
+ const fab = page.getByTestId('whatsapp-fab');
+ const bar = page.getByTestId('mobile-sticky-cta-bar');
+
+ const fabBox = await fab.boundingBox();
+ const barBox = await bar.boundingBox();
+ expect(fabBox).not.toBeNull();
+ expect(barBox).not.toBeNull();
+
+ // The bottom of the FAB sits above the top of the sticky bar.
+ expect(fabBox!.y + fabBox!.height).toBeLessThanOrEqual(barBox!.y);
+ });
+});