// Nav.jsx — GreySurface top navigation (v4: responsive, with mobile menu)
// Viewport hook — re-renders on resize across a breakpoint
const useIsMobile = (bp = 900) => {
const [m, setM] = React.useState(() => (typeof window !== 'undefined' ? window.innerWidth <= bp : false));
React.useEffect(() => {
const on = () => setM(window.innerWidth <= bp);
on();
window.addEventListener('resize', on);
return () => window.removeEventListener('resize', on);
}, [bp]);
return m;
};
// Global responsive stylesheet — injected once. Inline-style React can't hold
// media queries, so we collapse the site's column grids via attribute selectors.
const injectResponsiveCSS = () => {
if (typeof document === 'undefined' || document.getElementById('gs-responsive')) return;
const css = `
/* ── Shared keyframes (every page loads Nav, so declare once here) ── */
@keyframes gs-marquee { from { transform: translateX(0); } to { transform: translateX(-33.3333%); } }
@keyframes gs-marquee-rtl { from { transform: translateX(-33.3333%); } to { transform: translateX(0); } }
@keyframes gs-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.85); } }
@keyframes gs-blink { 50% { border-color: transparent; } }
@keyframes gs-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes gs-ticker { from { transform: translateY(0); } to { transform: translateY(-50%); } }
@keyframes gs-cve-fall { 0% { transform: translateY(-100%); } 100% { transform: translateY(0); } }
@keyframes gs-mega-in { from { opacity: 0; transform: translateY(-6px) scale(0.985); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes gs-mega-sweep { 0% { transform: translateX(-100%); } 100% { transform: translateX(200%); } }
@keyframes gs-mega-drift { from { transform: translateX(-40px); } to { transform: translateX(160px); } }
@keyframes gs-mega-item { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
.gs-pulse { animation: gs-pulse 1.6s ease-in-out infinite; }
/* ── GreySurface responsive layer ──
Inline-style React can't carry media queries, so we adapt the site's column
grids via attribute selectors, tiered widest→narrowest (narrower wins). */
/* Dense item grids reflow continuously by available width (handles any ratio). */
.gs-grid-auto { grid-template-columns: repeat(auto-fit, minmax(var(--gs-min, 150px), 1fr)) !important; }
/* ── Small-laptop / large-tablet: 3-up → 2-up, 6-up → 3-up ── */
@media (max-width: 1080px) {
[style*="repeat(3, 1fr)"]:not(.gs-grid-auto) { grid-template-columns: repeat(2, 1fr) !important; }
[style*="repeat(6, 1fr)"]:not(.gs-grid-auto) { grid-template-columns: repeat(3, 1fr) !important; }
/* Footer 5-col → 3-col */
[style*="2fr 1.4fr repeat(3, 1fr)"] { grid-template-columns: repeat(3, 1fr) !important; }
}
/* ── Tablet portrait: collapse all two-column feature / asymmetric rows ── */
@media (max-width: 880px) {
[style*="1fr 1fr"]:not(.gs-grid-auto),
[style*="1.1fr 1fr"], [style*="1fr 1.1fr"],
[style*="1.05fr 1fr"],
[style*="1.2fr 1fr"], [style*="1fr 1.2fr"],
[style*="1.3fr 1fr"], [style*="1fr 1.3fr"],
[style*="1.4fr 1fr"], [style*="1fr 1.4fr"],
[style*="1.5fr 1fr"], [style*="1.6fr 1fr"],
[style*="2fr 1.4fr"]:not([style*="repeat"]),
[style*="repeat(5, 1fr)"]:not(.gs-grid-auto) { grid-template-columns: 1fr !important; }
[style*="position: sticky"] { position: static !important; top: auto !important; }
/* Reset explicit cell placement so collapsed grids stack in source order */
[style*="grid-column"] { grid-column: auto !important; }
[style*="grid-row"] { grid-row: auto !important; }
/* Footer 5-col → 2-col */
[style*="2fr 1.4fr repeat(3, 1fr)"] { grid-template-columns: repeat(2, 1fr) !important; }
/* Tame oversized section padding from tablet down */
[style*="120px 32px"], [style*="110px 32px"], [style*="96px 32px"],
[style*="88px 32px"], [style*="72px 32px"] { padding-left: 24px !important; padding-right: 24px !important; }
}
/* ── Phones ── */
@media (max-width: 600px) {
[style*="repeat(3, 1fr)"]:not(.gs-grid-auto),
[style*="repeat(2, 1fr)"]:not(.gs-grid-auto) { grid-template-columns: 1fr !important; }
[style*="repeat(4, 1fr)"]:not(.gs-grid-auto),
[style*="repeat(6, 1fr)"]:not(.gs-grid-auto) { grid-template-columns: repeat(2, 1fr) !important; }
/* Compress vertical section rhythm so phones aren't all whitespace */
[style*="padding: 120px 32px"], [style*="padding: 110px 32px"] { padding-top: 84px !important; padding-bottom: 48px !important; }
[style*="padding: 96px 32px"] { padding-top: 56px !important; padding-bottom: 56px !important; }
[style*="padding: 96px 0"] { padding-top: 56px !important; padding-bottom: 56px !important; }
[style*="padding: 88px 32px"] { padding-top: 52px !important; padding-bottom: 52px !important; }
[style*="padding: 72px 32px"] { padding-top: 44px !important; padding-bottom: 44px !important; }
/* Smaller side gutters + lighter card padding */
[style*="120px 32px"], [style*="110px 32px"], [style*="96px 32px"], [style*="88px 32px"],
[style*="72px 32px"], [style*="64px 32px"], [style*="56px 32px"] { padding-left: 18px !important; padding-right: 18px !important; }
[style*="padding: 56px"]:not([style*="32px"]) { padding: 28px !important; }
[style*="padding: 48px"] { padding: 26px !important; }
/* Tighten big section gaps */
[style*="gap: 48px"] { gap: 28px !important; }
}
/* Very narrow phones: even the 2-up dense grids relax a touch */
@media (max-width: 380px) {
.gs-grid-auto { --gs-min: 120px; }
}
/* Fixed-column data tables / matrices scroll horizontally instead of clipping */
@media (max-width: 760px) {
.gs-scroll-x { overflow-x: auto !important; -webkit-overflow-scrolling: touch; }
.gs-scroll-x > * { min-width: 480px; }
}`;
const el = document.createElement('style');
el.id = 'gs-responsive';
el.textContent = css;
document.head.appendChild(el);
};
// Mobile slide-down menu — accordion over the same nav data
const MobileMenu = ({ navItems, onLink, onClose, ctaHref }) => {
const [openKey, setOpenKey] = React.useState(navItems[0] && navItems[0].key);
return (
{navItems.map(item => {
const open = openKey === item.key;
const items = item.mega ? item.mega.columns.flatMap(c => c.items) : [];
if (!item.mega) {
return (
);
}
return (
{open && (
{items.map(it => (
))}
)}
);
})}
);
};
const Nav = ({ onNav, currentPage = 'home' }) => {
const [scrolled, setScrolled] = React.useState(false);
const [openMenu, setOpenMenu] = React.useState(null);
const [anchorX, setAnchorX] = React.useState(0);
const closeTimer = React.useRef(null);
const cancelClose = () => { if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; } };
const scheduleClose = () => { cancelClose(); closeTimer.current = setTimeout(() => setOpenMenu(null), 150); };
const openMega = (key, el) => { cancelClose(); const r = el.getBoundingClientRect(); setAnchorX(r.left + r.width / 2); setOpenMenu(key); };
const [mobileOpen, setMobileOpen] = React.useState(false);
const isMobile = useIsMobile(900);
React.useEffect(() => { injectResponsiveCSS(); }, []);
// Escape closes any open menu
React.useEffect(() => {
const onKey = (e) => { if (e.key === 'Escape') { setOpenMenu(null); setMobileOpen(false); } };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
React.useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 8);
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
// Close mobile menu + lock body scroll while open
React.useEffect(() => { if (!isMobile && mobileOpen) setMobileOpen(false); }, [isMobile, mobileOpen]);
React.useEffect(() => {
document.body.style.overflow = (isMobile && mobileOpen) ? 'hidden' : '';
return () => { document.body.style.overflow = ''; };
}, [isMobile, mobileOpen]);
// Resolve URL based on where we are: site root, /services/, /blog/, or /legal/
const _path = location.pathname;
const inServices = _path.includes('/services/');
const inSub = /\/(services|blog|legal)\//.test(_path);
const svcHref = (slug) => `/services/${slug}/`;
const rootHref = (path) => path; // links are already root-relative
// Every link is root-relative and pages live at /name/index.html, so the
// address bar shows clean URLs like greysurface.com/about/ (never .html).
const homeRoot = '/';
const homeHref = (hash) => `/#${hash}`;
const services = {
title: 'Penetration testing services',
subtitle: 'Manual testing across every attack surface.',
columns: [
{
heading: 'Core testing',
items: [
{ label: 'Penetration Testing', desc: 'Full-scope manual testing across your stack', icon: 'shield', href: svcHref('vapt') },
{ label: 'Web Application Pentesting', desc: 'OWASP ASVS, business logic, auth flaws', icon: 'globe', href: svcHref('web') },
{ label: 'Mobile Application Pentesting', desc: 'iOS, Android, OWASP MASVS', icon: 'fingerprint', href: svcHref('mobile') },
],
},
{
heading: 'Programs and adversary simulation',
items: [
{ label: 'Cloud Pentesting', desc: 'AWS, Azure, GCP, Kubernetes, IAM', icon: 'cloud', href: svcHref('cloud') },
{ label: 'Continuous Pentesting', desc: 'Rolling testing on every release', icon: 'refresh', href: svcHref('ptaas') },
{ label: 'Red Teaming', desc: 'Objective-based adversary simulation', icon: 'target', href: svcHref('red-team') },
],
},
{
// TEMPORARY: IT Support is a short-term offering. Remove this column,
// the footer link, /it-support/ and /components/ITSupportPage.jsx together.
heading: 'Managed IT',
items: [
{ label: 'IT Support', desc: 'Helpdesk, endpoints, M365, network', icon: 'server', href: rootHref('/it-support/') },
],
},
],
feature: {
tag: 'Most requested',
title: 'Continuous Pentesting',
desc: 'Testing that keeps pace with your releases. Every meaningful deploy is reviewed by one of our testers.',
cta: 'Explore the program',
href: svcHref('ptaas'),
},
};
const resources = {
title: 'Resources',
subtitle: 'Research, reports, and pricing.',
columns: [
{
heading: 'Read',
items: [
{ label: 'Blog', desc: 'Research, write-ups, advisories', icon: 'code', href: rootHref('/blog/') },
{ label: 'Pentest Trends 2026', desc: 'Data from 600+ engagements', icon: 'flame', href: rootHref('/resources/#report') },
{ label: 'Sample report', desc: 'See exactly what you receive', icon: 'download', href: rootHref('/resources/#sample') },
],
},
{
heading: 'Programs',
items: [
{ label: 'Pricing', desc: 'Fixed-fee engagements, no surprises', icon: 'layers', href: rootHref('/pricing/') },
{ label: 'Customers', desc: 'Who we work with, and why', icon: 'globe', href: rootHref('/customers/') },
{ label: 'Partners', desc: 'MSP and MSSP partner program', icon: 'network', href: rootHref('/partners/') },
],
},
],
};
const company = {
title: 'Company',
subtitle: 'A specialist penetration testing practice.',
columns: [
{
heading: 'About us',
items: [
{ label: 'About', desc: 'Mission, story, the team', icon: 'sparkle', href: rootHref('/about/') },
{ label: 'Philosophy', desc: 'How we think about offense', icon: 'eye', href: rootHref('/philosophy/') },
{ label: 'Careers', desc: 'Join the team', icon: 'arrowUpRight', href: rootHref('/careers/') },
{ label: 'Trust center', desc: 'Compliance, attestations, security', icon: 'lock', href: rootHref('/about/#compliance') },
],
},
{
heading: 'Connect',
items: [
{ label: 'Contact', desc: 'Scoping, pricing, timing', icon: 'mail', href: rootHref('/contact/') },
{ label: 'Industries', desc: 'Sectors we specialize in', icon: 'globe', href: homeHref('industries') },
{ label: 'Responsible disclosure', desc: 'Report a vulnerability in our systems', icon: 'shield', href: rootHref('/contact/#disclose') },
],
},
],
};
const navItems = [
{ label: 'Services', key: 'services', mega: services },
{ label: 'Resources', key: 'resources', mega: resources },
{ label: 'Company', key: 'company', mega: company },
{ label: 'Pricing', key: 'pricing', href: rootHref('/pricing/') },
];
const onLink = (href) => {
if (!href) return;
if (href.startsWith('#')) onNav && onNav(href.slice(1));
else window.location.href = href;
};
return (
<>
{/* Mega menu — sibling of