/* Shared chrome: header, footer, page shell and the desktop/phone switch. MegaNav and SiteFooter render their links with href="#", so the shell patches hrefs by link text once the DOM exists — one map, every page. */ const { MegaNav, SiteFooter, Eyebrow, Button, Card, Tag, SectionHeader } = window.SoftVerxDesignSystem_6fd95b; const SX = window.SX_DATA; const DS = "_ds/softverx-design-system-6fd95bfe-4e47-4a86-bc31-c3b705051205/"; const LINKS = { SoftVerx: "/", "SoftVerx — home": "/", Practices: "/practices", Solutions: "/solutions", Work: "/work", Industries: "/industries", About: "/about", Technology: "/technology", "Technology stack": "/technology", Contact: "/contact", "Contact us": "/contact", "Tell us what is not working": "/contact", FAQs: "/faq", "Frequently asked questions": "/faq", Team: "/about", Careers: "/about", "Book a call": "/contact", "Send us a message": "/contact", "See our work": "/work", "See what we've shipped": "/work", }; SX.practices.forEach((p) => { LINKS[p.title] = p.slug === "document-intelligence" ? "/document-intelligence" : "/practices#" + p.slug; }); /* Practice names win over solution names where the two collide. */ SX.solutions.forEach((s) => { if (!LINKS[s.name]) LINKS[s.name] = "/solutions#" + s.id; }); SX.industries.forEach((i) => { if (!LINKS[i.name]) LINKS[i.name] = "/industries#" + i.id; }); /** Footer link columns: the practice map, plus the two indexes buyers hunt for. */ const FOOTER_GROUPS = SX.practiceGroups.concat([ { id: "solutions", label: "Solutions", practices: ["mdm", "workflow-agents", "doc-intelligence", "custom-ocr", "utility-cis", "agriculture", "iot-platforms", "object-detection"].map((id) => ({ title: SX.solutionsById[id].name })) }, { id: "sectors", label: "Sectors", practices: SX.industries.map((i) => ({ title: i.name })) }, ]); /* Phones get one column of practices instead of three, and the six most-asked solutions — the full map is one tap away on the practices page. */ const MOBILE_FOOTER_GROUPS = [ { id: "practices", label: "Practices", practices: SX.practiceGroups.flatMap((g) => g.practices).slice(0, 7).map((p) => ({ title: p.title })) }, { id: "solutions", label: "Solutions", practices: ["mdm", "workflow-agents", "doc-intelligence", "custom-ocr", "utility-cis", "agriculture"].map((id) => ({ title: SX.solutionsById[id].name })) }, ]; function patchLinks(root) { if (!root) return; root.querySelectorAll("a").forEach((a) => { const existing = a.getAttribute("href"); /* Anything that already points somewhere real is left alone — only the design system's placeholder "#" links are patched. */ if (a.dataset.sxPatched || (existing && existing !== "#")) return; const t = (a.textContent || "").replace(/[[\]]/g, "").trim(); /* MegaNav and the footer render two-line links (title + note) inside one anchor, so fall back to the first line before giving up. */ const first = (a.firstElementChild?.textContent || "").trim(); let href = LINKS[t] || LINKS[first]; if (!href && /case study/i.test(t)) href = "/lesco"; if (!href && a.getAttribute("aria-label")) href = LINKS[a.getAttribute("aria-label")]; if (href) { a.setAttribute("href", href); a.dataset.sxPatched = "1"; } }); } function useLinkPatch() { const ref = React.useRef(null); React.useEffect(() => { const el = ref.current; if (!el) return; patchLinks(el); const mo = new MutationObserver(() => patchLinks(el)); mo.observe(el, { childList: true, subtree: true }); return () => mo.disconnect(); }, []); return ref; } /* Clean URLs. Every page is a flat route so a shell's relative asset paths ("assets/…", "site.css") still resolve from the root — a nested route like /work/lesco would send them to /work/assets and 404. `serve.mjs` and the host rewrite config map each route back to its .html file. */ const ROUTES = { "/": "index.html", "/practices": "practices.html", "/document-intelligence": "practice-document-intelligence.html", "/solutions": "solutions.html", "/work": "projects.html", "/lesco": "project-lesco.html", "/case": "case.html", "/industries": "industries.html", "/technology": "technology.html", "/about": "about.html", "/faq": "faq.html", "/contact": "contact.html", }; /* Which nav item lights up on which route. The home page deliberately lights nothing: it is the logo's job, not a section's. */ const NAV_FOR_ROUTE = { "/practices": "Practices", "/document-intelligence": "Practices", "/solutions": "Solutions", "/work": "Work", "/lesco": "Work", "/case": "Work", "/industries": "Industries", "/technology": "Technology", "/about": "About", "/faq": "About", }; function routeOf() { if (typeof location === "undefined") return "/"; let p = location.pathname.replace(/\/index\.html$/, "/").replace(/\.html$/, ""); if (p.length > 1) p = p.replace(/\/$/, ""); return p === "" ? "/" : p; } const NARROW = "(max-width: 900px)"; /* The narrow layout follows the real viewport. `?preview=phone` frames it at 390px on a wide screen so the phone composition can be reviewed on desktop. */ function usePreviewFlag() { return typeof location !== "undefined" && /(?:^|[?&])preview=phone(?:&|$)/.test(location.search.slice(1) + "&" + location.hash); } /** Page shell: chrome plus footer. Narrow layout is viewport-driven. */ function Shell({ active, children, footerNote }) { /* The URL is the source of truth for which nav item is current. The `active` prop is only a fallback for the hash-routed single-file build. Every shell used to hardcode it, which is why the home page highlighted "Practices". */ const current = NAV_FOR_ROUTE[routeOf()] ?? (ROUTES[routeOf()] ? null : active); const preview = usePreviewFlag(); const [narrow, setNarrow] = React.useState(() => typeof matchMedia !== "undefined" && matchMedia(NARROW).matches); React.useEffect(() => { if (typeof matchMedia === "undefined") return; const mq = matchMedia(NARROW); const on = (e) => setNarrow(e.matches); setNarrow(mq.matches); mq.addEventListener("change", on); return () => mq.removeEventListener("change", on); }, []); const mobile = narrow || preview; const ref = useLinkPatch(); const page = (
{window.SiteSearch && React.createElement(window.SiteSearch, { mobile, key: String(mobile) })} {typeof children === "function" ? children(mobile) : children}
); return preview && !narrow ?
{page}
: page; } /** Standard section: hairline top, container, 80/56px rhythm. */ function Section({ children, mobile, bordered = true, id, style, well }) { return (
{children}
); } /** Interior page header — eyebrow, display line, lead, optional actions and meta rail. */ function PageHero({ mobile, eyebrow, title, lead, actions, meta, breadcrumb }) { return (
{breadcrumb} {eyebrow && {eyebrow}}

{title}

{lead &&

{lead}

} {actions &&
{actions}
} {meta && (
{meta.map((m) => (
{m.l}{m.href && }
{m.href ? {m.v} : m.v}
))}
)}
); } /** Lowercase mono fig caption used under every figure that is not in a ChartFrame. */ function FigNote({ children, style }) { return

{children}

; } function Grid({ children, cols = 3, mobile, gap = 16, style }) { return
{children}
; } Object.assign(window, { Shell, Section, PageHero, FigNote, Grid, SX, DS, LINKS, ROUTES, routeOf, patchLinks }); /* The ref-scoped observer misses links rendered outside the shell subtree on some pages, so patch the whole document too — cheap, idempotent, and it keeps every placeholder href honest. */ const sxPatchAll = () => patchLinks(document.body); if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", sxPatchAll); else setTimeout(sxPatchAll, 0); new MutationObserver(() => sxPatchAll()).observe(document.documentElement, { childList: true, subtree: true });