1<!DOCTYPE html><html lang="en" data-astro-cid-sckkx6r4> <head><!-- Google tag (gtag.js) -->
vendor: 63 bytes, line 1
1<script async src="https://www.googletagmanager.com/gtag/js?id=
1G-TRGYDLTX5X
vendor: 11 bytes, line 1
1"></script>
1<script> 2
vendor: 158 bytes, lines 2-6
2window.dataLayer = window.dataLayer || []; 3 function gtag(){dataLayer.push(arguments);} 4 gtag('js', new Date()); 5 6 gtag('config', '
6G-TRGYDLTX5X
vendor: 12 bytes, lines 6-7
6'); 7
7</script>
7<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><link rel="icon" type="image/png" href="/assets/img/kurukalogoMTmap.png"><meta name="generator" content="Astro v5.9.1"><!-- Standard SEO Meta Tags --><title>About - Kuruka LLC</title><meta name="description" content="Kuruka LLC offers expert custom GIS web application development and geospatial consulting services. We build tailored solutions to provide actionable insights from your spatial data. Contact us for innovative mapping and data analysis."><meta name="keywords" content="Custom GIS web applications,Geospatial consulting,Spatial data analysis,Mapping solutions, GIS automation,Location intelligence,Astro site development"><meta name="author" content="Mike Peckham"><link rel="canonical" href="https://www.kurukasolutions.com/about"><!-- Favicon (already present, added apple-touch-icon) --><link rel="apple-touch-icon" href="/assets/img/kurukalogoMTmap.png"><!-- Open Graph Meta Tags (for Facebook, LinkedIn, Pinterest, etc.) --><meta property="og:title" content="About - Kuruka LLC"><meta property="og:description" content="Kuruka LLC offers expert custom GIS web application development and geospatial consulting services. We build tailored solutions to provide actionable insights from your spatial data. Contact us for innovative mapping and data analysis."><meta property="og:image" content="https://www.kurukasolutions.com/assets/img/kurukalogoMTmap.png"><meta property="og:image:alt" content="Kuruka LLC Logo"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta property="og:url" content="https://www.kurukasolutions.com/about"><meta property="og:type" content="website"><meta property="og:site_name" content="Kuruka LLC"><meta property="og:locale" content="en_US"><!-- Twitter Card Meta Tags --><meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="About - Kuruka LLC"><meta name="twitter:description" content="Kuruka LLC offers expert custom GIS web application development and geospatial consulting services. We build tailored solutions to provide actionable insights from your spatial data. Contact us for innovative mapping and data analysis."><meta name="twitter:image" content="https://www.kurukasolutions.com/assets/img/kurukalogoMTmap.png"><meta name="twitter:image:alt" content="Kuruka LLC Logo"><!-- Inject Mapbox token as global variable -->
7<script>(function(){const MAPBOX_TOKEN = "pk.eyJ1Ijoia3VydWthIiwiYSI6ImNtMW8zeHljeDEwYTIybnB6bnU0OWlneHMifQ.o_Kt9TRzzBV3_haBp8ahZg"; 8 9 window.MAPBOX_TOKEN = MAPBOX_TOKEN; 10 })();</script>
10<!-- TODO: Add Google Fonts --><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet"><!-- Site styles --><link rel="stylesheet" href="/styles/main.css"><link rel="stylesheet" href="/styles/shared-hero.css"><!-- Mapbox GL JS -->
10<script src="https://api.mapbox.com/mapbox-gl-js/v3.12.0/mapbox-gl.js"></script>
10<link href="https://api.mapbox.com/mapbox-gl-js/v3.12.0/mapbox-gl.css" rel="stylesheet"><!-- Phosphor Icons -->
10<script type="module">const o=document.getElementById("mobile-menu"),t=document.querySelector(".nav-links"),c=document.body;o&&t&&(o.addEventListener("click",()=>{t.classList.toggle("active"),o.classList.toggle("active"),t.classList.contains("active")?c.style.overflow="hidden":c.style.overflow=""}),t.querySelectorAll("a").forEach(n=>{n.addEventListener("click",()=>{t.classList.remove("active"),o.classList.remove("active"),c.style.overflow=""})}));const s=document.querySelector(".floating-nav");let l=window.scrollY;function d(){if(!s)return;const e=window.scrollY,n=document.documentElement.scrollHeight,i=window.innerHeight,r=n-i,a=e/r*100>85;e<50?s.classList.remove("hidden"):(e>l&&e>100||a)&&s.classList.add("hidden"),l=e}s&&window.addEventListener("scroll",d);</script>
10<!-- Plotly.js (modern v2 bundle to avoid window.document error) -->
10<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
10<link rel="stylesheet" href="/_astro/about.BRuAyPVK.css"> 11<link rel="stylesheet" href="/_astro/about.CH-pxmiW.css"> 12<link rel="stylesheet" href="/_astro/index.BH3rEWXN.css"></head> <body class="site-wrapper" data-astro-cid-sckkx6r4> <!-- Floating navigation for home page --> <header class="floating-nav" data-astro-cid-sckkx6r4> <nav data-astro-cid-sckkx6r4> <a href="/" class="logo" data-astro-cid-sckkx6r4> <img src="/assets/img/kurukalogoMTmap.png" alt="Kuruka Logo" data-astro-cid-sckkx6r4> <span class="logo-text" data-astro-cid-sckkx6r4>Kuruka</span> </a> <div class="menu-toggle" id="mobile-menu" data-astro-cid-sckkx6r4> <span data-astro-cid-sckkx6r4></span> <span data-astro-cid-sckkx6r4></span> <span data-astro-cid-sckkx6r4></span> </div> <ul class="nav-links" data-astro-cid-sckkx6r4> <li data-astro-cid-sckkx6r4><a href="/map-challenge-2025" data-astro-cid-sckkx6r4>Maps</a></li> <li data-astro-cid-sckkx6r4><a href="/services" data-astro-cid-sckkx6r4>Services</a></li> <li data-astro-cid-sckkx6r4><a href="/products" data-astro-cid-sckkx6r4>Products</a></li> <li data-astro-cid-sckkx6r4><a href="/about" data-astro-cid-sckkx6r4>About</a></li> <li data-astro-cid-sckkx6r4><a href="/contact" data-astro-cid-sckkx6r4>Contact</a></li> </ul> </nav> </header> <main data-astro-cid-sckkx6r4> <section id="about-hero" class="hero-60" data-astro-cid-kh7btl4r> <style>astro-island,astro-slot,astro-static-slot{display:contents}</style>
12<script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).load=e;window.dispatchEvent(new Event("astro:load"));})();</script>
12<script>(()=>{var A=Object.defineProperty;var g=(i,o,a)=>o in i?A(i,o,{enumerable:!0,configurable:!0,writable:!0,value:a}):i[o]=a;var d=(i,o,a)=>g(i,typeof o!="symbol"?o+"":o,a);{let i={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t),11:t=>1/0*t},o=t=>{let[l,e]=t;return l in i?i[l](e):void 0},a=t=>t.map(o),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([l,e])=>[l,o(e)]));class y extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var b;if(!this.hydrator||!this.isConnected)return;let e=(b=this.parentElement)==null?void 0:b.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let c=this.querySelectorAll("astro-slot"),n={},h=this.querySelectorAll("template[data-astro-template]");for(let r of h){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("data-astro-template")||"default"]=r.innerHTML,r.remove())}for(let r of c){let s=r.closest(this.tagName);s!=null&&s.isSameNode(this)&&(n[r.getAttribute("name")||"default"]=r.innerHTML)}let p;try{p=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(r){let s=this.getAttribute("component-url")||"<unknown>",v=this.getAttribute("component-export");throw v&&(s+=` (export ${v})`),console.error(`[hydrate] Error parsing props for component ${s}`,this.getAttribute("props"),r),r}let u;await this.hydrator(this)(this.Component,p,n,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),c.disconnect(),this.childrenConnectedCallback()},c=new MutationObserver(()=>{var n;((n=this.lastChild)==null?void 0:n.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});c.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),c=this.getAttribute("client");if(Astro[c]===void 0){window.addEventListener(`astro:${c}`,()=>this.start(),{once:!0});return}try{await Astro[c](async()=>{let n=this.getAttribute("renderer-url"),[h,{default:p}]=await Promise.all([import(this.getAttribute("component-url")),n?import(n):()=>()=>{}]),u=this.getAttribute("component-export")||"default";if(!u.includes("."))this.Component=h[u];else{this.Component=h;for(let f of u.split("."))this.Component=this.Component[f]}return this.hydrator=p,this.hydrate},e,this)}catch(n){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,n)}}attributeChangedCallback(){this.hydrate()}}d(y,"observedAttributes",["props"]),customElements.get("astro-island")||customElements.define("astro-island",y)}})();</script>
12<astro-island uid="s9V54" prefix="r1" component-url="/_astro/HeroTerrainAbout.CIv2UeuP.js" component-export="default" renderer-url="/_astro/client.BPIbHqJh.js" props="{"data-astro-cid-kh7btl4r":[0,true]}" ssr client="load" opts="{"name":"HeroTerrainAbout","value":true}" await-children><div class="hero-terrain-container "><div class="mapbox-container"></div><div class="terrain-gradient-overlay"></div></div><!--astro:end--></astro-island> <div class="hero-overlay" data-astro-cid-kh7btl4r></div> <div class="copy" data-astro-cid-kh7btl4r> <h1 data-astro-cid-kh7btl4r>About Kuruka</h1> <p data-astro-cid-kh7btl4r>Geospatial expertise with a boots-on-the-ground mindset.</p> <button type="button" class="cta-button" id="open-discovery-popup-btn" data-astro-cid-kh7btl4r>Let's Connect</button> </div> </section> <section id="about-content" class="image-position-left" data-astro-cid-kh7btl4r> <div class="about-image-container" data-astro-cid-kh7btl4r> <img src="/assets/img/Mike-mtns.png" alt="Mike Peckham" class="about-image" data-astro-cid-kh7btl4r> <div class="gradient-overlay" data-astro-cid-kh7btl4r></div> </div> <div class="about-text" data-astro-cid-kh7btl4r> <h2 data-astro-cid-kh7btl4r>Mike Peckham</h2> <p data-astro-cid-kh7btl4r> 13I started Kuruka Solutions in 2021 because I kept seeing the 14 same gap, the realities in the field weren't making it into the 15 stategies that drove mutli-million-dollar decisions. My early 16 years in the military drilled in the idea that geography isn't 17 background scenery, it's what decides outcomes. Working on 18 wildfire assignments, I learned how quickly bad or delayed data 19 turns into real-world risk. 20</p> <p data-astro-cid-kh7btl4r> 21Drones gave me a way to capture that ground truth. I wanted to 22 translate it into something strategic decision makers could act 23 on without extra meetings. So I went back to school for 24 geography with a GIS focus, sharpening the technical side while 25 keeping the "boots-on-the-ground" mindset. Kuruka now blends 26 those threads together with a mix of mission discipline, 27 emergency-response urgency, and data science all into lean 28 geoprocessing tools and AI-powered workflows. The goal is 29 simple: deliver clear, defensible solutions for teams, fast 30 enough to keep projects moving and grounded enough to trust when 31 you're standing on site. 32</p> <div class="social-links" data-astro-cid-kh7btl4r> <a href="https://www.linkedin.com/in/mike-peckham-kuruka/" class="social-link" target="_blank" rel="noopener noreferrer" data-astro-cid-kh7btl4r> <i class="ph-bold ph-linkedin-logo" data-astro-cid-kh7btl4r></i> </a> <button type="button" class="social-link" id="email-popup-btn" aria-label="Contact via email" data-astro-cid-kh7btl4r> <i class="ph-bold ph-envelope" data-astro-cid-kh7btl4r></i> </button> </div> </div> </section> <section id="our-services" class="services-section" data-astro-cid-kh7btl4r> <div class="services-container" data-astro-cid-kh7btl4r> <i class="ph-bold ph-globe-hemisphere-west service-icon section-title-icon" data-astro-cid-kh7btl4r></i> <h2 data-astro-cid-kh7btl4r>Our Services</h2> <p class="services-intro" data-astro-cid-kh7btl4r> 33We offer a comprehensive range of geospatial services to meet 34 your needs. From data analysis to custom web applications, our 35 expertise is at your service. 36</p> <div class="services-grid" data-astro-cid-kh7btl4r> <div class="service-card" data-astro-cid-kh7btl4r> <p class="service-card-description" data-astro-cid-kh7btl4r> 37Here are some general categories of what we do on a 38 weekly basis. 39</p> <div class="service-items" data-astro-cid-kh7btl4r> <a href="/services" class="service-item" data-content="constraint-analysis" data-astro-cid-kh7btl4r> <i class="ph-bold ph-selection-background" data-astro-cid-kh7btl4r></i> <span data-astro-cid-kh7btl4r>Constraint Analysis</span> </a> <a href="/services" class="service-item" data-content="geo-comparison" data-astro-cid-kh7btl4r> <i class="ph-bold ph-chart-pie-slice" data-astro-cid-kh7btl4r></i> <span data-astro-cid-kh7btl4r>Geospatial Comparisons</span> </a> <a href="/services" class="service-item" data-content="data-acquisition" data-astro-cid-kh7btl4r> <i class="ph-bold ph-tree-structure" data-astro-cid-kh7btl4r></i> <span data-astro-cid-kh7btl4r>Data Acquisition & ETL</span> </a> <a href="/services" class="service-item" data-content="map-production" data-astro-cid-kh7btl4r> <i class="ph-bold ph-map-trifold" data-astro-cid-kh7btl4r></i> <span data-astro-cid-kh7btl4r>Cartography & Map Production</span> </a> <a href="/services" class="service-item" data-content="geodatabase-design" data-astro-cid-kh7btl4r> <i class="ph-bold ph-database" data-astro-cid-kh7btl4r></i> <span data-astro-cid-kh7btl4r>Geodatabase Design</span> </a> <a href="/services" class="service-item" data-content="interactive-mapping" data-astro-cid-kh7btl4r> <i class="ph-bold ph-monitor-play" data-astro-cid-kh7btl4r></i> <span data-astro-cid-kh7btl4r>Interactive Mapping & Dashboards</span> </a> <a href="/services" class="service-item" data-content="automated-workflows" data-astro-cid-kh7btl4r> <i class="ph-bold ph-robot" data-astro-cid-kh7btl4r></i> <span data-astro-cid-kh7btl4r>Automated Workflows & APIs</span> </a> </div> </div> </div> <div class="services-cta" data-astro-cid-kh7btl4r> <a href="/services" class="services-button" data-astro-cid-kh7btl4r>Explore All Services</a> </div> </div> </section> </main> <footer class="site-footer" data-astro-cid-sz7xmlte> <div class="footer-container" data-astro-cid-sz7xmlte> <div class="footer-section footer-branding" data-astro-cid-sz7xmlte> <div class="footer-logo" data-astro-cid-sz7xmlte> <img src="/assets/img/kurukalogoMTmap.png" alt="Kuruka Logo" data-astro-cid-sz7xmlte> <span data-astro-cid-sz7xmlte>Kuruka</span> </div> <p class="footer-tagline" data-astro-cid-sz7xmlte>
40Geospatial solutions for complex challenges 41</p> </div> <div class="footer-section footer-sitemap" data-astro-cid-sz7xmlte> <h3 data-astro-cid-sz7xmlte>Site Map</h3> <ul class="footer-links" data-astro-cid-sz7xmlte> <li data-astro-cid-sz7xmlte><a href="/map-challenge-2025" data-astro-cid-sz7xmlte>Maps</a></li> <li data-astro-cid-sz7xmlte><a href="/services" data-astro-cid-sz7xmlte>Services</a></li> <li data-astro-cid-sz7xmlte><a href="/products" data-astro-cid-sz7xmlte>Products</a></li> <li data-astro-cid-sz7xmlte><a href="/about" data-astro-cid-sz7xmlte>About</a></li> <li data-astro-cid-sz7xmlte><a href="/contact" data-astro-cid-sz7xmlte>Contact</a></li> </ul> </div> <div class="footer-section footer-contact-info" data-astro-cid-sz7xmlte> <h3 data-astro-cid-sz7xmlte>Contact</h3> <address class="footer-contact" data-astro-cid-sz7xmlte> <div class="contact-item" data-astro-cid-sz7xmlte> <i class="ph-bold ph-user" data-astro-cid-sz7xmlte></i> <span data-astro-cid-sz7xmlte>Mike Peckham</span> </div> <div class="contact-item" data-astro-cid-sz7xmlte> <i class="ph-bold ph-envelope" data-astro-cid-sz7xmlte></i> <a href="mailto:[email protected]" data-astro-cid-sz7xmlte>[email protected]</a> </div> <div class="contact-item" data-astro-cid-sz7xmlte> <i class="ph-bold ph-phone" data-astro-cid-sz7xmlte></i> <a href="tel:510-671-0875" data-astro-cid-sz7xmlte>510-671-0875</a> </div> <div class="contact-item" data-astro-cid-sz7xmlte> <i class="ph-bold ph-map-pin" data-astro-cid-sz7xmlte></i> <span data-astro-cid-sz7xmlte>El Cerrito, CA</span> </div> <div class="contact-item" data-astro-cid-sz7xmlte> <i class="ph-bold ph-linkedin-logo" data-astro-cid-sz7xmlte></i> <a href="https://www.linkedin.com/in/mike-peckham-kuruka/" target="_blank" rel="noopener noreferrer" data-astro-cid-sz7xmlte>LinkedIn</a> </div> </address> </div> </div> <div class="footer-bottom" data-astro-cid-sz7xmlte> <p data-astro-cid-sz7xmlte>© 2026 Kuruka LLC. All rights reserved.</p> </div> </footer> <!-- TODO: Add mobile menu script -->
41<script type="module" src="/_astro/Layout.astro_astro_type_script_index_1_lang.DyH8HZto.js"></script>
41 <div id="discovery-popup-overlay" class="discovery-popup-overlay hidden" data-astro-cid-yxfxwmyh> <div id="discovery-popup" class="discovery-popup" data-astro-cid-yxfxwmyh> <button id="discovery-popup-close" class="discovery-popup-close" data-astro-cid-yxfxwmyh>×</button> <div class="popup-logo-container" data-astro-cid-yxfxwmyh> <img src="/assets/img/kurukalogoMTmap.png" alt="Kuruka Logo" class="popup-logo" data-astro-cid-yxfxwmyh> </div> <p class="popup-subtitle" data-astro-cid-yxfxwmyh> <strong data-astro-cid-yxfxwmyh>Fill out the form below, and we'll get back to you shortly.</strong> </p> <form id="discovery-form" data-astro-cid-yxfxwmyh> <div class="form-group" data-astro-cid-yxfxwmyh> <label for="discovery-name" data-astro-cid-yxfxwmyh>Full Name <span class="required" data-astro-cid-yxfxwmyh>*</span></label> <input type="text" id="discovery-name" name="name" required data-astro-cid-yxfxwmyh> </div> <div class="form-group" data-astro-cid-yxfxwmyh> <label for="discovery-email" data-astro-cid-yxfxwmyh>Email Address <span class="required" data-astro-cid-yxfxwmyh>*</span></label> <input type="email" id="discovery-email" name="email" required data-astro-cid-yxfxwmyh> </div> <div class="form-group" data-astro-cid-yxfxwmyh> <label for="discovery-phone" data-astro-cid-yxfxwmyh>Phone Number (Optional)</label> <input type="tel" id="discovery-phone" name="phone" data-astro-cid-yxfxwmyh> </div> <div class="form-group" data-astro-cid-yxfxwmyh> <label for="discovery-notes" data-astro-cid-yxfxwmyh>Notes/Comments (Optional)</label> <textarea id="discovery-notes" name="notes" rows="3" data-astro-cid-yxfxwmyh></textarea> </div> <button type="submit" id="discovery-submit-btn" class="discovery-submit-btn" data-astro-cid-yxfxwmyh> <span class="btn-text" data-astro-cid-yxfxwmyh>Submit Request</span> <span class="btn-spinner hidden" data-astro-cid-yxfxwmyh>â³</span> </button> </form> <div id="discovery-toast" class="discovery-toast discovery-toast--hidden" data-astro-cid-yxfxwmyh> <span id="discovery-toast-message" data-astro-cid-yxfxwmyh></span> </div> </div> </div>
41<script> 42 document.addEventListener("DOMContentLoaded", () => { 43 const overlay = document.getElementById("discovery-popup-overlay"); 44 const popup = document.getElementById("discovery-popup"); 45 const closeButton = document.getElementById("discovery-popup-close"); 46 const form = document.getElementById("discovery-form"); 47 const submitBtn = document.getElementById("discovery-submit-btn"); 48 const btnText = submitBtn.querySelector(".btn-text"); 49 const btnSpinner = submitBtn.querySelector(".btn-spinner"); 50 const toast = document.getElementById("discovery-toast"); 51 const toastMessage = document.getElementById("discovery-toast-message"); 52 const nameInput = document.getElementById("discovery-name"); 53 const emailInput = document.getElementById("discovery-email"); 54 55 // Initial state for submit button: disabled 56 if (submitBtn) { 57 submitBtn.disabled = true; 58 } 59 60 const validateEmail = (email) => { 61 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 62 return emailRegex.test(String(email).toLowerCase()); 63 }; 64 65 const updateSubmitButtonState = () => { 66 if (!nameInput || !emailInput || !submitBtn) return; 67 68 const isNameValid = nameInput.value.trim() !== ""; 69 const isEmailValid = validateEmail(emailInput.value); 70 71 submitBtn.disabled = !(isNameValid && isEmailValid); 72 73 // Visual feedback for email input 74 if (emailInput.value.trim() !== "") { 75 if (isEmailValid) { 76 emailInput.style.borderColor = "#475569"; // Match CSS default border 77 } else { 78 emailInput.style.borderColor = "#d9534f"; // Error border (red) 79 } 80 } else { 81 emailInput.style.borderColor = "#475569"; // Match CSS default border when empty 82 } 83 }; 84 85 // Function to show the popup 86 window.openDiscoveryPopup = () => { 87 if (!overlay) return; 88 overlay.classList.remove("hidden"); // Make it part of the layout (display:flex) but still invisible (opacity:0) 89 90 // Use a double requestAnimationFrame to ensure the 'display' change is processed 91 // before adding the class that triggers the transition. 92 requestAnimationFrame(() => { 93 requestAnimationFrame(() => { 94 overlay.classList.add("is-visible"); 95 }); 96 }); 97 98 if (nameInput) { 99 nameInput.focus(); 100 } 101 updateSubmitButtonState(); // Check button state when popup opens 102 }; 103 104 // Function to hide the popup 105 const closeDiscoveryPopup = () => { 106 if (!overlay) return; 107 overlay.classList.remove("is-visible"); // This will trigger the fade-out transition 108 109 const onTransitionEnd = (event) => { 110 // Important: Check if the transition that ended is for the overlay and for the 'opacity' property. 111 if ( 112 event.target === overlay && 113 event.propertyName === "opacity" 114 ) { 115 overlay.classList.add("hidden"); // Now set display:none 116 overlay.removeEventListener( 117 "transitionend", 118 onTransitionEnd, 119 ); // Clean up listener 120 121 // Reset form and other states 122 form.reset(); 123 hideDiscoveryToast(); 124 if (nameInput) nameInput.style.borderColor = "#475569"; // Match CSS default border 125 if (emailInput) emailInput.style.borderColor = "#475569"; // Match CSS default border 126 updateSubmitButtonState(); 127 } 128 }; 129 overlay.addEventListener("transitionend", onTransitionEnd); 130 }; 131 132 // Event listeners 133 if (closeButton) { 134 closeButton.addEventListener("click", closeDiscoveryPopup); 135 } 136 137 // Close popup if overlay (outside the popup content) is clicked 138 if (overlay) { 139 overlay.addEventListener("click", (event) => { 140 if (event.target === overlay) { 141 closeDiscoveryPopup(); 142 } 143 }); 144 } 145 146 // Close popup with Escape key 147 document.addEventListener("keydown", (event) => { 148 if ( 149 event.key === "Escape" && 150 !overlay.classList.contains("hidden") 151 ) { 152 closeDiscoveryPopup(); 153 } 154 }); 155 156 // Event listeners for input fields to update submit button state 157 if (nameInput) { 158 nameInput.addEventListener("input", updateSubmitButtonState); 159 } 160 if (emailInput) { 161 emailInput.addEventListener("input", updateSubmitButtonState); 162 } 163 164 const showDiscoveryToast = ( 165 message, 166 type = "info", 167 duration = 4000, 168 ) => { 169 if (!toast || !toastMessage) return; 170 toastMessage.textContent = message; 171 toast.className = `discovery-toast ${type}`; // Reset classes and add type 172 173 setTimeout(() => { 174 hideDiscoveryToast(); 175 }, duration); 176 }; 177 178 const hideDiscoveryToast = () => { 179 if (toast) { 180 toast.classList.add("discovery-toast--hidden"); 181 } 182 }; 183 184 if (form) { 185 form.addEventListener("submit", async (event) => { 186 event.preventDefault(); 187 188 submitBtn.disabled = true; 189 btnText.classList.add("hidden"); 190 btnSpinner.classList.remove("hidden"); 191 hideDiscoveryToast(); 192 193 const formData = new FormData(form); 194 const data = { 195 name: formData.get("name"), 196 email: formData.get("email"), 197 phone: formData.get("phone") || null, // Send null if empty 198 notes: formData.get("notes") || null, // Send null if empty 199 }; 200 201 try { 202 const response = await fetch( 203 "/api/sendDiscoveryCallEmail", 204 { 205 method: "POST", 206 headers: { 207 "Content-Type": "application/json", 208 }, 209 body: JSON.stringify(data), 210 }, 211 ); 212 213 const result = await response.json(); 214 215 if (response.ok && result.success) { 216 showDiscoveryToast( 217 result.message || "Request submitted successfully!", 218 "success", 219 ); 220 form.reset(); 221 setTimeout(closeDiscoveryPopup, 2000); // Close popup after a short delay 222 } else { 223 showDiscoveryToast( 224 result.error || 225 "Submission failed. Please try again.", 226 "error", 227 ); 228 } 229 } catch (error) { 230 console.error("Form submission error:", error); 231 showDiscoveryToast( 232 "An unexpected error occurred. Please try again.", 233 "error", 234 ); 235 } finally { 236 submitBtn.disabled = false;
237 btnText.classList.remove("hidden"); 238 btnSpinner.classList.add("hidden"); 239 } 240 }); 241 } 242 }); 243</script>
243 </body></html>
243<script> 244 document.addEventListener("DOMContentLoaded", () => { 245 // Add click handlers for service items to navigate to specific tabs on services page 246 const serviceItems = document.querySelectorAll(".service-item"); 247 248 serviceItems.forEach((item) => { 249 item.addEventListener("click", (e) => { 250 e.preventDefault(); 251 252 // Get the base URL and the content identifier 253 const href = item.getAttribute("href"); 254 const content = item.getAttribute("data-content"); 255 256 // Create the URL with both the section and the specific tab content 257 let targetUrl = href; 258 if (content) { 259 // Store the content ID in sessionStorage so the services page can activate it 260 sessionStorage.setItem("activateContent", content); 261 } 262 263 // Navigate to the services page 264 window.location.href = targetUrl; 265 }); 266 }); 267 268 // Add click handlers for the discovery call buttons 269 const popupButtons = [ 270 document.getElementById("open-discovery-popup-btn"), 271 document.getElementById("email-popup-btn"), 272 ]; 273 274 popupButtons.forEach((button) => { 275 if (button) { 276 button.addEventListener("click", () => { 277 if (window.openDiscoveryPopup) { 278 window.openDiscoveryPopup(); 279 } else { 280 console.error("Discovery popup function not found."); 281 } 282 }); 283 } 284 }); 285 }); 286</script>
286
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.