// 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