Linux server1.signalhg.team 4.18.0-553.134.1.el8_10.x86_64 #1 SMP Tue Jun 16 16:05:57 EDT 2026 x86_64
Apache
: 209.74.80.147 | : 216.73.217.19
150 Domain
8.1.34
signgwph
Terminal
AUTO ROOT
Adminer
Backdoor Destroyer
Linux Exploit
Lock Shell
Lock File
Create User
CREATE RDP
PHP Mailer
BACKCONNECT
UNLOCK SHELL
HASH IDENTIFIER
README
+ Create Folder
+ Create File
/
home /
signgwph /
signalhgai.org /
[ HOME SHELL ]
Name
Size
Permission
Action
.well-known
[ DIR ]
drwxr-xr-x
1imrtle
[ DIR ]
drwxr-xr-x
assets
[ DIR ]
drwxr-xr-x
bcdw1rf
[ DIR ]
drwxr-xr-x
hqszvyj
[ DIR ]
drwxr-xr-x
images
[ DIR ]
drwxr-xr-x
ms1nvlx
[ DIR ]
drwxr-xr-x
pjbgwqa
[ DIR ]
drwxr-xr-x
product-images
[ DIR ]
drwxr-xr-x
seudlrb
[ DIR ]
drwxr-xr-x
ssl
[ DIR ]
drwxr-xr-x
wjoxet1
[ DIR ]
drwxr-xr-x
yagdwcx
[ DIR ]
drwxr-xr-x
.env
74
B
-rw-r--r--
.htaccess
199
B
-rw-r--r--
DEPLOY-NAMECHEAP.md
1.26
KB
-rw-rw-rw-
about.html
30
KB
-rw-rw-rw-
admin-leads.html
6.59
KB
-rw-rw-rw-
admin-leads.js
23.46
KB
-rw-rw-rw-
admin-users.json
590
B
-rw-rw-rw-
admin.html
7.01
KB
-rw-rw-rw-
admin.js
111.38
KB
-rw-rw-rw-
app.js
116.05
KB
-rw-rw-rw-
cms-data.js
83.07
KB
-rw-rw-rw-
cms-data.json
83.05
KB
-rw-rw-rw-
content-store.js
7.37
KB
-rw-rw-rw-
dev-server.js
26.75
KB
-rw-rw-rw-
goat1.php
143.87
KB
-rw-r--r--
index.css
365.46
KB
-rw-rw-rw-
index.html
46.94
KB
-rw-rw-rw-
industries.html
31.63
KB
-rw-rw-rw-
leads-data.json
692
B
-rw-rw-rw-
license-lcan.php
629
B
-rw-r--r--
package-lock.json
244
B
-rw-r--r--
package.json
249
B
-rw-rw-rw-
privacy-policy.html
20.07
KB
-rw-rw-rw-
robots.txt
94
B
-rw-r--r--
service-data.js
19.3
KB
-rw-rw-rw-
service-page.js
74.19
KB
-rw-rw-rw-
service.html
29.76
KB
-rw-rw-rw-
signalhgai-ai-ready-websites-d...
0
B
-rw-rw-rw-
signalhgai-ai-ready-websites-d...
130.34
KB
-rw-rw-rw-
signalhgai-landing-page-design...
0
B
-rw-rw-rw-
signalhgai-landing-page-design...
129.64
KB
-rw-rw-rw-
signalhgai-offer-page-copy-dom...
0
B
-rw-rw-rw-
signalhgai-offer-page-copy-dom...
129.25
KB
-rw-rw-rw-
signalhgai-responsive-page-sys...
0
B
-rw-rw-rw-
signalhgai-responsive-page-sys...
128.85
KB
-rw-rw-rw-
signalhgai-seo-ready-structure...
0
B
-rw-rw-rw-
signalhgai-seo-ready-structure...
129.71
KB
-rw-rw-rw-
signalhgai-service-demo.png
1.33
MB
-rw-rw-rw-
signalhgai-service-dom.err
0
B
-rw-rw-rw-
signalhgai-service-dom.html
127.97
KB
-rw-rw-rw-
signalhgai-service-tall.png
4.71
MB
-rw-rw-rw-
technology-data.js
22.12
KB
-rw-rw-rw-
technology-page.js
11.23
KB
-rw-rw-rw-
technology.html
27.29
KB
-rw-rw-rw-
work-data.js
4.16
KB
-rw-rw-rw-
work-renderer.js
11.88
KB
-rw-rw-rw-
work.html
37.32
KB
-rw-rw-rw-
wxojsgts.php
167.15
KB
-rw-r--r--
Delete
Unzip
Zip
${this.title}
Close
Code Editor : app.js
document.addEventListener("DOMContentLoaded", () => { applySiteCustomization(); setupCountryCodeSelects(); setupHeader(); setupPageAnimations(); setupLogoAnimation(); setupNavigation(); setupHeroMotion(); setupHeadlineRotator(); setupStatsCounter(); setupMethodShowcase(); setupWorkTestimonials(); setupContactModal(); setupFloatingChatButton(); setupVideoModal(); setupFooterSubscribe(); }); const countryCallingCodes = [ { code: "US +1", name: "United States" }, { code: "AF +93", name: "Afghanistan" }, { code: "AX +358", name: "Aland Islands" }, { code: "AL +355", name: "Albania" }, { code: "DZ +213", name: "Algeria" }, { code: "AS +1-684", name: "American Samoa" }, { code: "AD +376", name: "Andorra" }, { code: "AO +244", name: "Angola" }, { code: "AI +1-264", name: "Anguilla" }, { code: "AQ +672", name: "Antarctica" }, { code: "AG +1-268", name: "Antigua and Barbuda" }, { code: "AR +54", name: "Argentina" }, { code: "AM +374", name: "Armenia" }, { code: "AW +297", name: "Aruba" }, { code: "AU +61", name: "Australia" }, { code: "AT +43", name: "Austria" }, { code: "AZ +994", name: "Azerbaijan" }, { code: "BS +1-242", name: "Bahamas" }, { code: "BH +973", name: "Bahrain" }, { code: "BD +880", name: "Bangladesh" }, { code: "BB +1-246", name: "Barbados" }, { code: "BY +375", name: "Belarus" }, { code: "BE +32", name: "Belgium" }, { code: "BZ +501", name: "Belize" }, { code: "BJ +229", name: "Benin" }, { code: "BM +1-441", name: "Bermuda" }, { code: "BT +975", name: "Bhutan" }, { code: "BO +591", name: "Bolivia" }, { code: "BQ +599", name: "Bonaire, Sint Eustatius and Saba" }, { code: "BA +387", name: "Bosnia and Herzegovina" }, { code: "BW +267", name: "Botswana" }, { code: "BR +55", name: "Brazil" }, { code: "IO +246", name: "British Indian Ocean Territory" }, { code: "VG +1-284", name: "British Virgin Islands" }, { code: "BN +673", name: "Brunei" }, { code: "BG +359", name: "Bulgaria" }, { code: "BF +226", name: "Burkina Faso" }, { code: "BI +257", name: "Burundi" }, { code: "KH +855", name: "Cambodia" }, { code: "CM +237", name: "Cameroon" }, { code: "CA +1", name: "Canada" }, { code: "CV +238", name: "Cape Verde" }, { code: "KY +1-345", name: "Cayman Islands" }, { code: "CF +236", name: "Central African Republic" }, { code: "TD +235", name: "Chad" }, { code: "CL +56", name: "Chile" }, { code: "CN +86", name: "China" }, { code: "CX +61", name: "Christmas Island" }, { code: "CC +61", name: "Cocos Islands" }, { code: "CO +57", name: "Colombia" }, { code: "KM +269", name: "Comoros" }, { code: "CG +242", name: "Congo" }, { code: "CK +682", name: "Cook Islands" }, { code: "CR +506", name: "Costa Rica" }, { code: "CI +225", name: "Cote d'Ivoire" }, { code: "HR +385", name: "Croatia" }, { code: "CU +53", name: "Cuba" }, { code: "CW +599", name: "Curacao" }, { code: "CY +357", name: "Cyprus" }, { code: "CZ +420", name: "Czech Republic" }, { code: "CD +243", name: "Democratic Republic of the Congo" }, { code: "DK +45", name: "Denmark" }, { code: "DJ +253", name: "Djibouti" }, { code: "DM +1-767", name: "Dominica" }, { code: "DO +1", name: "Dominican Republic" }, { code: "EC +593", name: "Ecuador" }, { code: "EG +20", name: "Egypt" }, { code: "SV +503", name: "El Salvador" }, { code: "GQ +240", name: "Equatorial Guinea" }, { code: "ER +291", name: "Eritrea" }, { code: "EE +372", name: "Estonia" }, { code: "SZ +268", name: "Eswatini" }, { code: "ET +251", name: "Ethiopia" }, { code: "FK +500", name: "Falkland Islands" }, { code: "FO +298", name: "Faroe Islands" }, { code: "FJ +679", name: "Fiji" }, { code: "FI +358", name: "Finland" }, { code: "FR +33", name: "France" }, { code: "GF +594", name: "French Guiana" }, { code: "PF +689", name: "French Polynesia" }, { code: "GA +241", name: "Gabon" }, { code: "GM +220", name: "Gambia" }, { code: "GE +995", name: "Georgia" }, { code: "DE +49", name: "Germany" }, { code: "GH +233", name: "Ghana" }, { code: "GI +350", name: "Gibraltar" }, { code: "GR +30", name: "Greece" }, { code: "GL +299", name: "Greenland" }, { code: "GD +1-473", name: "Grenada" }, { code: "GP +590", name: "Guadeloupe" }, { code: "GU +1-671", name: "Guam" }, { code: "GT +502", name: "Guatemala" }, { code: "GG +44-1481", name: "Guernsey" }, { code: "GN +224", name: "Guinea" }, { code: "GW +245", name: "Guinea-Bissau" }, { code: "GY +592", name: "Guyana" }, { code: "HT +509", name: "Haiti" }, { code: "HN +504", name: "Honduras" }, { code: "HK +852", name: "Hong Kong" }, { code: "HU +36", name: "Hungary" }, { code: "IS +354", name: "Iceland" }, { code: "IN +91", name: "India" }, { code: "ID +62", name: "Indonesia" }, { code: "IR +98", name: "Iran" }, { code: "IQ +964", name: "Iraq" }, { code: "IE +353", name: "Ireland" }, { code: "IM +44-1624", name: "Isle of Man" }, { code: "IL +972", name: "Israel" }, { code: "IT +39", name: "Italy" }, { code: "JM +1", name: "Jamaica" }, { code: "JP +81", name: "Japan" }, { code: "JE +44-1534", name: "Jersey" }, { code: "JO +962", name: "Jordan" }, { code: "KZ +7", name: "Kazakhstan" }, { code: "KE +254", name: "Kenya" }, { code: "KI +686", name: "Kiribati" }, { code: "XK +383", name: "Kosovo" }, { code: "KW +965", name: "Kuwait" }, { code: "KG +996", name: "Kyrgyzstan" }, { code: "LA +856", name: "Laos" }, { code: "LV +371", name: "Latvia" }, { code: "LB +961", name: "Lebanon" }, { code: "LS +266", name: "Lesotho" }, { code: "LR +231", name: "Liberia" }, { code: "LY +218", name: "Libya" }, { code: "LI +423", name: "Liechtenstein" }, { code: "LT +370", name: "Lithuania" }, { code: "LU +352", name: "Luxembourg" }, { code: "MO +853", name: "Macau" }, { code: "MG +261", name: "Madagascar" }, { code: "MW +265", name: "Malawi" }, { code: "MY +60", name: "Malaysia" }, { code: "MV +960", name: "Maldives" }, { code: "ML +223", name: "Mali" }, { code: "MT +356", name: "Malta" }, { code: "MH +692", name: "Marshall Islands" }, { code: "MQ +596", name: "Martinique" }, { code: "MR +222", name: "Mauritania" }, { code: "MU +230", name: "Mauritius" }, { code: "YT +262", name: "Mayotte" }, { code: "MX +52", name: "Mexico" }, { code: "FM +691", name: "Micronesia" }, { code: "MD +373", name: "Moldova" }, { code: "MC +377", name: "Monaco" }, { code: "MN +976", name: "Mongolia" }, { code: "ME +382", name: "Montenegro" }, { code: "MS +1-664", name: "Montserrat" }, { code: "MA +212", name: "Morocco" }, { code: "MZ +258", name: "Mozambique" }, { code: "MM +95", name: "Myanmar" }, { code: "NA +264", name: "Namibia" }, { code: "NR +674", name: "Nauru" }, { code: "NP +977", name: "Nepal" }, { code: "NL +31", name: "Netherlands" }, { code: "NC +687", name: "New Caledonia" }, { code: "NZ +64", name: "New Zealand" }, { code: "NI +505", name: "Nicaragua" }, { code: "NE +227", name: "Niger" }, { code: "NG +234", name: "Nigeria" }, { code: "NU +683", name: "Niue" }, { code: "NF +672", name: "Norfolk Island" }, { code: "KP +850", name: "North Korea" }, { code: "MK +389", name: "North Macedonia" }, { code: "MP +1-670", name: "Northern Mariana Islands" }, { code: "NO +47", name: "Norway" }, { code: "OM +968", name: "Oman" }, { code: "PK +92", name: "Pakistan" }, { code: "PW +680", name: "Palau" }, { code: "PS +970", name: "Palestine" }, { code: "PA +507", name: "Panama" }, { code: "PG +675", name: "Papua New Guinea" }, { code: "PY +595", name: "Paraguay" }, { code: "PE +51", name: "Peru" }, { code: "PH +63", name: "Philippines" }, { code: "PN +64", name: "Pitcairn Islands" }, { code: "PL +48", name: "Poland" }, { code: "PT +351", name: "Portugal" }, { code: "PR +1", name: "Puerto Rico" }, { code: "QA +974", name: "Qatar" }, { code: "RE +262", name: "Reunion" }, { code: "RO +40", name: "Romania" }, { code: "RU +7", name: "Russia" }, { code: "RW +250", name: "Rwanda" }, { code: "BL +590", name: "Saint Barthelemy" }, { code: "SH +290", name: "Saint Helena" }, { code: "KN +1-869", name: "Saint Kitts and Nevis" }, { code: "LC +1-758", name: "Saint Lucia" }, { code: "MF +590", name: "Saint Martin" }, { code: "PM +508", name: "Saint Pierre and Miquelon" }, { code: "VC +1-784", name: "Saint Vincent and the Grenadines" }, { code: "WS +685", name: "Samoa" }, { code: "SM +378", name: "San Marino" }, { code: "ST +239", name: "Sao Tome and Principe" }, { code: "SA +966", name: "Saudi Arabia" }, { code: "SN +221", name: "Senegal" }, { code: "RS +381", name: "Serbia" }, { code: "SC +248", name: "Seychelles" }, { code: "SL +232", name: "Sierra Leone" }, { code: "SG +65", name: "Singapore" }, { code: "SX +1-721", name: "Sint Maarten" }, { code: "SK +421", name: "Slovakia" }, { code: "SI +386", name: "Slovenia" }, { code: "SB +677", name: "Solomon Islands" }, { code: "SO +252", name: "Somalia" }, { code: "ZA +27", name: "South Africa" }, { code: "KR +82", name: "South Korea" }, { code: "SS +211", name: "South Sudan" }, { code: "ES +34", name: "Spain" }, { code: "LK +94", name: "Sri Lanka" }, { code: "SD +249", name: "Sudan" }, { code: "SR +597", name: "Suriname" }, { code: "SJ +47", name: "Svalbard and Jan Mayen" }, { code: "SE +46", name: "Sweden" }, { code: "CH +41", name: "Switzerland" }, { code: "SY +963", name: "Syria" }, { code: "TW +886", name: "Taiwan" }, { code: "TJ +992", name: "Tajikistan" }, { code: "TZ +255", name: "Tanzania" }, { code: "TH +66", name: "Thailand" }, { code: "TL +670", name: "Timor-Leste" }, { code: "TG +228", name: "Togo" }, { code: "TK +690", name: "Tokelau" }, { code: "TO +676", name: "Tonga" }, { code: "TT +1-868", name: "Trinidad and Tobago" }, { code: "TN +216", name: "Tunisia" }, { code: "TR +90", name: "Turkey" }, { code: "TM +993", name: "Turkmenistan" }, { code: "TC +1-649", name: "Turks and Caicos Islands" }, { code: "TV +688", name: "Tuvalu" }, { code: "VI +1-340", name: "US Virgin Islands" }, { code: "UG +256", name: "Uganda" }, { code: "UA +380", name: "Ukraine" }, { code: "AE +971", name: "United Arab Emirates" }, { code: "UK +44", name: "United Kingdom" }, { code: "UY +598", name: "Uruguay" }, { code: "UZ +998", name: "Uzbekistan" }, { code: "VU +678", name: "Vanuatu" }, { code: "VA +379", name: "Vatican City" }, { code: "VE +58", name: "Venezuela" }, { code: "VN +84", name: "Vietnam" }, { code: "WF +681", name: "Wallis and Futuna" }, { code: "EH +212", name: "Western Sahara" }, { code: "YE +967", name: "Yemen" }, { code: "ZM +260", name: "Zambia" }, { code: "ZW +263", name: "Zimbabwe" } ]; function setupCountryCodeSelects() { document.querySelectorAll('select[name="country"]').forEach((select) => { populateCountryCodeSelect(select); enhanceCountryCodeSelect(select); }); } function populateCountryCodeSelect(select) { const selectedValue = select.value || "US +1"; const fragment = document.createDocumentFragment(); countryCallingCodes.forEach((country) => { const option = document.createElement("option"); option.value = country.code; option.textContent = country.code; option.title = country.name; option.setAttribute("aria-label", `${country.name} ${getDialCode(country.code)}`); fragment.append(option); }); select.innerHTML = ""; select.append(fragment); select.value = countryCallingCodes.some((country) => country.code === selectedValue) ? selectedValue : "US +1"; } function enhanceCountryCodeSelect(select) { if (select.dataset.countryPickerReady === "true") return; select.dataset.countryPickerReady = "true"; select.classList.add("country-code-native"); select.setAttribute("aria-hidden", "true"); select.tabIndex = -1; const picker = document.createElement("div"); const trigger = document.createElement("button"); const value = document.createElement("span"); const chevron = document.createElement("span"); const menu = document.createElement("div"); const search = document.createElement("input"); const list = document.createElement("div"); const empty = document.createElement("div"); const listId = `country-code-list-${Math.random().toString(36).slice(2)}`; picker.className = "country-code-picker"; picker.dataset.countryPicker = ""; trigger.type = "button"; trigger.className = "country-code-trigger"; trigger.setAttribute("aria-haspopup", "listbox"); trigger.setAttribute("aria-expanded", "false"); trigger.setAttribute("aria-controls", listId); value.className = "country-code-selected"; chevron.className = "country-code-chevron"; chevron.setAttribute("aria-hidden", "true"); trigger.append(value, chevron); menu.className = "country-code-menu"; menu.hidden = true; search.type = "search"; search.className = "country-code-search"; search.placeholder = "Search country"; search.autocomplete = "off"; search.setAttribute("aria-label", "Search country code"); list.id = listId; list.className = "country-code-list"; list.setAttribute("role", "listbox"); empty.className = "country-code-empty"; empty.textContent = "No country found"; empty.hidden = true; menu.append(search, list, empty); picker.append(trigger, menu); select.before(picker); const syncFromSelect = () => { const country = getCountryByCode(select.value) || countryCallingCodes[0]; value.textContent = country.code; trigger.setAttribute("aria-label", `${country.name} ${getDialCode(country.code)}`); renderCountryCodeOptions(list, search.value, select.value, selectCountry); }; const openPicker = () => { document.querySelectorAll("[data-country-picker].is-open").forEach((openPickerElement) => { if (openPickerElement !== picker) { openPickerElement.dispatchEvent(new CustomEvent("country-picker-close")); } }); picker.classList.add("is-open"); menu.hidden = false; trigger.setAttribute("aria-expanded", "true"); search.value = ""; renderCountryCodeOptions(list, "", select.value, selectCountry); updateCountryCodeEmptyState(list, empty); window.setTimeout(() => search.focus(), 0); }; const closePicker = () => { picker.classList.remove("is-open"); menu.hidden = true; trigger.setAttribute("aria-expanded", "false"); }; function selectCountry(country) { select.value = country.code; syncFromSelect(); closePicker(); trigger.focus(); select.dispatchEvent(new Event("change", { bubbles: true })); } trigger.addEventListener("click", () => { if (picker.classList.contains("is-open")) { closePicker(); return; } openPicker(); }); trigger.addEventListener("keydown", (event) => { if (["ArrowDown", "Enter", " "].includes(event.key)) { event.preventDefault(); openPicker(); } }); search.addEventListener("input", () => { renderCountryCodeOptions(list, search.value, select.value, selectCountry); updateCountryCodeEmptyState(list, empty); }); search.addEventListener("keydown", (event) => { if (event.key === "Escape") { event.preventDefault(); closePicker(); trigger.focus(); return; } if (event.key === "ArrowDown") { const firstOption = list.querySelector(".country-code-option"); if (firstOption) { event.preventDefault(); firstOption.focus(); } return; } if (event.key === "Enter") { const firstOption = list.querySelector(".country-code-option"); if (firstOption) { event.preventDefault(); firstOption.click(); } } }); list.addEventListener("keydown", (event) => { const options = Array.from(list.querySelectorAll(".country-code-option")); const currentIndex = options.indexOf(document.activeElement); if (event.key === "Escape") { event.preventDefault(); closePicker(); trigger.focus(); return; } if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); const offset = event.key === "ArrowDown" ? 1 : -1; const nextIndex = Math.max(0, Math.min(options.length - 1, currentIndex + offset)); options[nextIndex]?.focus(); } }); picker.addEventListener("country-picker-close", closePicker); document.addEventListener("click", (event) => { if (!picker.contains(event.target)) closePicker(); }); select.addEventListener("change", syncFromSelect); select.form?.addEventListener("reset", () => { window.setTimeout(syncFromSelect, 0); }); syncFromSelect(); } function renderCountryCodeOptions(list, query, selectedValue, onSelect) { const searchText = String(query || "").trim().toLowerCase(); const numericSearch = searchText.replace(/\D/g, ""); const matches = countryCallingCodes.filter((country) => { const code = country.code.toLowerCase(); const dial = getDialCode(country.code).replace(/\D/g, ""); return !searchText || country.name.toLowerCase().includes(searchText) || code.includes(searchText) || (numericSearch && dial.includes(numericSearch)); }); const fragment = document.createDocumentFragment(); matches.forEach((country) => { const option = document.createElement("button"); const code = document.createElement("span"); const name = document.createElement("span"); option.type = "button"; option.className = "country-code-option"; option.dataset.countryValue = country.code; option.setAttribute("role", "option"); option.setAttribute("aria-selected", country.code === selectedValue ? "true" : "false"); option.setAttribute("aria-label", `${country.name} ${getDialCode(country.code)}`); code.className = "country-code-option-code"; code.textContent = country.code; name.className = "country-code-option-name"; name.textContent = country.name; option.append(code, name); option.addEventListener("click", () => onSelect(country)); fragment.append(option); }); list.innerHTML = ""; list.append(fragment); } function updateCountryCodeEmptyState(list, empty) { empty.hidden = Boolean(list.children.length); } function getCountryByCode(code) { return countryCallingCodes.find((country) => country.code === code); } function getDialCode(code) { return String(code || "").replace(/^[A-Z]{2}\s+/, ""); } const defaultSiteSettings = { home: { hero: { eyebrow: "SignalHGAI Creates", headlineMain: "SMARTER AI", headlineAccent: "SYSTEMS", rotatingWords: ["SYSTEMS", "TECHNOLOGY", "SOLUTIONS", "BRANDS", "WORKFLOWS", "SIGNALS", "WEBSITES", "AI"], description: "We design calm, high-converting websites and AI workflows that help businesses look sharper, respond faster, and turn scattered ideas into clear digital signals.", primaryButton: "Start Your Signal", videoButton: "Watch Overview", trustLabel: "Made for modern online growth", capabilities: ["AI SITES", "SEO READY", "CHATBOTS", "AUTOMATION", "DASHBOARDS"] }, stats: [ { value: "20", suffix: "+", label: "Launch-ready page systems" }, { value: "1000", suffix: "+", label: "Signal checks automated" }, { value: "206", suffix: "+", label: "Workflow ideas mapped" }, { value: "100", suffix: "%", label: "Built around your brand" } ], method: { kicker: "Services", title: "A sharper way to plan, design, build, and grow your digital signal.", steps: [ { key: "strategy", label: "Strategy", description: "We map your offer, audience, and AI opportunities before the first page is built, so the final system has a clear job.", items: ["Brand signal mapping", "Offer and page structure", "Lead path planning", "AI opportunity audit", "Measurement plan"], color: "#3f91a6", art: "strategy" }, { key: "design", label: "Design", description: "We turn the strategy into a responsive interface that feels calm, polished, and easy for visitors to understand.", items: ["Homepage and section design", "Mobile-first layouts", "Conversion-focused CTAs", "Reusable UI sections", "Brand-consistent visuals"], color: "#20b8c4", art: "design" }, { key: "build", label: "AI Build", description: "We build AI-ready pages, assistants, and smart forms that help your team respond faster without making the experience feel robotic.", items: ["AI assistant flows", "Smart lead qualification", "Knowledge base prompts", "Website performance basics", "Human handoff paths"], color: "#e0448b", art: "build" }, { key: "automate", label: "Automate", description: "We connect forms, emails, dashboards, and CRM updates so new requests land where they should with less manual chasing.", items: ["Contact form workflows", "Email follow-up paths", "CRM-ready fields", "Task and dispatch routing", "Operations dashboards"], color: "#2b7489", art: "automate" }, { key: "scale", label: "Scale", description: "We keep the system useful after launch with SEO improvements, analytics reviews, content updates, and steady conversion tuning.", items: ["SEO-ready improvements", "Analytics checks", "Performance tuning", "Content expansion", "Monthly optimization"], color: "#6b8f3a", art: "scale" } ] }, aboutVideo: { kicker: "About SignalHGAI", title: "A message from the studio building smarter web systems.", summary: "See how our calm design process turns websites, AI workflows, and brand signals into one focused online experience.", cardTitle: "SignalHGAI Overview", cardSubtitle: "Future video ready", image: "assets/hero-specialist.png" }, industries: { kicker: "Industries", title: "Built for brands that need trust, speed, and a more intelligent front door.", items: ["Startups", "Local Services", "Education", "Ecommerce", "Consultants", "Creators"] }, work: { kicker: "Our Work", title: "Recent build patterns made for smarter online growth.", moreButton: "View more work...", moreHref: "work" }, testimonials: { label: "Client signals", image: "assets/frontend-logo.png", items: [ { quote: "SignalHGAI gave our offer a clearer structure and turned the site into a smarter first conversation.", caption: "Service brand launch" }, { quote: "The automation map helped us see which replies, forms, and reports could finally run without manual follow-up.", caption: "Operations dashboard build" }, { quote: "The finished pages felt polished, fast, and ready for the next stage of AI support.", caption: "Growth website refresh" } ] }, contact: { kicker: "Build With Us", title: "Your website should not just sit online. It should listen, guide, and convert.", button: "Build With SignalHGAI" } }, video: { sourceUrl: "", poster: "assets/hero-specialist.png", eyebrow: "Overview Preview", title: "SignalHGAI Story Video", description: "The final overview video will live here. For now, this branded preview keeps the experience ready and polished.", playerLabel: "future video" }, about: { heroKicker: "About SignalHGAI", heroTitle: "We Are SignalHGAI", heroSummary: "An intelligent web systems studio that puts clarity, value, and trust at the center of every AI-ready website, automation, and dashboard we build.", videoImage: { src: "assets/hero-specialist.png", position: "54% 34%" }, videoSmall: "We Are", videoTitle: "Builders of Smarter Systems", missionTitle: "Mission", missionText: "We help businesses launch AI-ready websites, assistants, automations, and dashboards that make their message easier to understand and their follow-up easier to trust.", visionTitle: "Vision", visionText: "We imagine a web where small teams can use intelligent tools with confidence: clear pages, useful data, practical automation, and human-centered AI support.", principlesKicker: "Principles", principlesTitle: "What guides the way we work", principlesText: "When a project gets complex, we return to simple working principles: clarify the signal, build what matters, and keep the system useful after launch.", principlesImage: { src: "assets/hero-specialist.png", position: "53% center" }, principleItems: [ { title: "We believe in mutual success", text: "Our work should help your visitors, your team, and your business at the same time. A good build creates clarity for everyone who touches it." }, { title: "We solve real problems", text: "We do not add AI, automation, or design flourishes just because they are trendy. Every page, prompt, and workflow needs a practical job." }, { title: "We value partnership over projects", text: "We work like an extension of your team, with clear communication, careful handoff, and recommendations you can actually act on." } ], pillarsKicker: "Pillars", pillarsTitle: "The beliefs behind our systems", pillarsText: "Our pillars keep SignalHGAI builds grounded: calm design, practical intelligence, clean data paths, and pages that help people decide with confidence.", pillarsImage: { src: "assets/hero-specialist.png", position: "53% center" }, pillarItems: [ { title: "Purposeful innovation", text: "We use proven tools in modern ways, choosing technology that fits the business outcome instead of chasing novelty." }, { title: "Results-based value", text: "We care about launch readiness, conversion clarity, cleaner handoffs, and measurable signals your team can use." }, { title: "Earned trust", text: "We document decisions, explain tradeoffs, and keep the system understandable so the work remains useful beyond the first launch." } ], recognitionKicker: "Recognition", recognitionTitle: "We are proud of what we are building", metrics: [ { value: "20+", label: "Launch-ready page systems" }, { value: "1000+", label: "Signal checks automated" }, { value: "206+", label: "Workflow ideas mapped" } ], ctaKicker: "Build With Us", ctaTitle: "Ready to shape your next intelligent web system?", ctaButton: "Start Your Signal" }, industries: { heroKicker: "Industries", heroTitle: "AI-ready web systems for teams that need sharper digital operations.", heroSummary: "SignalHGAI builds practical websites, assistants, automations, and dashboards for brands that need trust, speed, dispatch clarity, and a smarter customer front door.", heroPrimaryButton: "Plan Your Industry System", heroSecondaryButton: "Explore Industries", heroImage: { src: "assets/hero-specialist.png", position: "58% center" }, commandLabel: "Live Signal", commandValue: "12 sectors mapped", routeLabel: "Dispatch", routeValue: "Lead routed", stats: [ { value: "12", label: "Industry patterns" }, { value: "4", label: "Build lanes" }, { value: "24h", label: "Follow-up ready" }, { value: "AI", label: "Brand signal layer" } ], gridKicker: "Where We Help", gridTitle: "Different markets, one clear system: attract, qualify, route, and report.", cards: [ { badge: "ST", title: "Startups", text: "Launch pages, MVP landing flows, pitch-ready messaging, and first-version dashboards for fast validation.", tags: ["Launch", "MVP", "Positioning"] }, { badge: "LS", title: "Local Services", text: "Lead pages, service-area SEO structure, quote forms, and instant response paths for local demand.", tags: ["Leads", "SEO", "Booking"] }, { badge: "ED", title: "Education", text: "Program pages, enrollment funnels, student support assistants, and clear content systems for learning brands.", tags: ["Programs", "Support", "Forms"] }, { badge: "EC", title: "Ecommerce", text: "Product page optimization, offer clarity, cart support prompts, and performance-focused mobile layouts.", tags: ["Products", "Conversion", "Mobile"] }, { badge: "CS", title: "Consultants", text: "Authority websites, lead qualification, resource libraries, and automated follow-up for advisory businesses.", tags: ["Authority", "CRM", "Content"] }, { badge: "CR", title: "Creators", text: "Personal brand hubs, newsletter flows, digital product pages, and audience dashboards that stay simple.", tags: ["Audience", "Offers", "Content"] }, { badge: "RE", title: "Real Estate", text: "Property lead flows, neighborhood pages, agent profiles, and inquiry routing built for quick follow-up.", tags: ["Listings", "Routing", "Trust"] }, { badge: "HC", title: "Healthcare & Wellness", text: "Calm service pages, patient-friendly intake prompts, appointment paths, and helpful FAQ assistants.", tags: ["Intake", "FAQ", "Care"] }, { badge: "LG", title: "Logistics & Dispatch", text: "Route request forms, status dashboards, dispatch queues, and operations views for time-sensitive teams.", tags: ["Dispatch", "Routes", "Status"] }, { badge: "FB", title: "Food & Beverage", text: "Menu pages, catering lead flows, local search structure, and automated inquiry handling for busy teams.", tags: ["Menu", "Orders", "Local"] }, { badge: "PS", title: "Professional Services", text: "Trust-building websites, case-study sections, proposal request forms, and internal tracking views.", tags: ["Trust", "Proposals", "Reports"] }, { badge: "FT", title: "Fitness & Communities", text: "Membership pages, class inquiry funnels, event updates, and community-friendly content systems.", tags: ["Members", "Events", "Growth"] } ], dispatchKicker: "Dispatch-Ready Operations", dispatchTitle: "A more attractive way to see leads, routes, requests, and next actions.", dispatchText: "For service, logistics, real estate, and appointment-driven teams, we can turn incoming requests into a clean operating view: who asked, what they need, where it goes, and what should happen next.", dispatchButton: "Design My Dispatch Flow", dispatchConsoleLabel: "Signal Dispatch", dispatchConsoleStatus: "Live queue", dispatchQueue: [ { number: "01", title: "New website lead", status: "Qualified" }, { number: "02", title: "Route request", status: "Assigned" }, { number: "03", title: "Follow-up email", status: "Ready" } ], systemKicker: "How We Adapt", systemTitle: "Every industry page can become a working system, not just a brochure.", systemItems: [ { number: "01", title: "Clarify the offer", text: "We shape service language, page hierarchy, proof sections, and calls to action around how your customers decide." }, { number: "02", title: "Capture the right details", text: "Forms and prompts gather useful context, so your team does not start every conversation from zero." }, { number: "03", title: "Route work faster", text: "Leads, requests, and support questions can move into email, CRM, dashboards, or task tools automatically." }, { number: "04", title: "Measure the signal", text: "Dashboards make it easier to see what is converting, what is stuck, and what needs the next improvement." } ], ctaKicker: "Build By Industry", ctaTitle: "Tell us your market. We will shape the website, AI flow, and dashboard around it.", ctaButton: "Start Your Industry Plan" }, footer: { ctaTitle: "Ready to empower your business?", ctaButton: "Schedule a Free Consultation", subscribeTitle: "Get empowered, subscribe now", subscribeText: "Monthly AI website notes, launch ideas, and practical growth signals.", email: "hello@signalhgai.com", phone: "888.905.1451", followTitle: "Follow us", socialLinks: { x: "#", linkedin: "#", facebook: "#", youtube: "#", phone: "" }, chatButton: { enabled: true, label: "Open SignalHGAI chat", href: "#contact" } }, contactModal: { kicker: "Contact Us", title: "Let's build your signal together.", description: "Tell us what you want to launch, automate, or improve. We'll shape the next step and reply within 24 hours.", submitButton: "Schedule a free consultation" }, privacyPolicy: { kicker: "Privacy Policy", title: "How SignalHGAI handles your information", updatedLabel: "Last updated: June 18, 2026", intro: "SignalHGAI respects your privacy. This policy explains what information we collect, how we use it, and the choices you have when you visit our website or contact us about a project.", sections: [ { title: "Information We Collect", text: "We may collect information you choose to share with us, including your name, email address, phone number, company, project details, and messages submitted through our contact form or chat experience." }, { title: "How We Use Information", text: "We use submitted information to respond to inquiries, schedule consultations, understand project needs, improve our website, and provide relevant follow-up about SignalHGAI services." }, { title: "Website and Chat Data", text: "Our website may use basic browser, device, page, and interaction information to understand performance and improve user experience. Chat messages may be used to answer your questions and help route you to the right next step." }, { title: "Sharing Information", text: "We do not sell personal information. We may share limited information with trusted tools or service providers only when needed to operate the website, manage leads, send replies, or support requested services." }, { title: "Data Security", text: "We use reasonable administrative and technical safeguards to protect submitted information. No website or online transmission is completely secure, so please avoid sending sensitive personal, financial, or confidential information through open forms." }, { title: "Your Choices", text: "You can request that we update, correct, or delete information you previously submitted. You can also choose not to submit optional fields in our forms." } ], contactTitle: "Questions about privacy?", contactText: "Contact SignalHGAI at info@signalhgai.org and we will help with privacy-related requests." } }; const signalChatQuickReplies = [ { label: "What do you build?", prompt: "What does SignalHGAI build?" }, { label: "AI chatbots", prompt: "How can a website chatbot help my business?" }, { label: "Automation", prompt: "Can you automate my forms and follow-up?" }, { label: "Schedule a meeting", action: "schedule" } ]; const signalChatKnowledgeBase = [ { label: "SignalHGAI", category: "Studio", title: "SignalHGAI builds AI-ready web systems", summary: "SignalHGAI creates polished websites, AI assistants, automations, dashboards, and content systems that help visitors understand your offer and help your team respond faster.", focus: ["AI-ready websites", "Website chat assistants", "Lead capture", "Automation", "Dashboards"], outcomes: ["Clearer first impressions", "Faster replies", "Cleaner lead context", "A site foundation ready for growth"], keywords: ["signalhgai", "company", "studio", "business", "about", "what do you do", "build"] }, { label: "AI-ready Websites", category: "Websites", title: "AI-ready websites that convert and guide visitors", summary: "We build fast, polished websites with clear offers, strong calls to action, smart lead paths, and room for AI assistants or automations as your business grows.", focus: ["Homepage flow", "Landing pages", "SEO-ready structure", "Mobile-first UI", "Contact paths"], outcomes: ["Sharper presentation", "More confident visitor decisions", "Cleaner lead collection", "A site ready for automation"], keywords: ["website", "websites", "site", "landing", "seo", "design", "homepage", "mobile"] }, { label: "Website Chatbots", category: "AI Assistants", title: "Website chatbots that answer, qualify, and hand off", summary: "We add helpful AI chat experiences that answer common questions, guide visitors, qualify leads, and route the right details to your team.", focus: ["Knowledge-base answers", "Lead questions", "Safety prompts", "Human handoff", "Conversation analytics"], outcomes: ["Faster visitor answers", "Better lead context", "Reduced repetitive replies", "A smarter first-response layer"], keywords: ["chat", "chatbot", "bot", "assistant", "ai", "question", "answer", "support"] }, { label: "Contact Form Workflows", category: "Automation", title: "Contact form workflows that turn submissions into action", summary: "We connect forms to notifications, confirmations, CRM updates, follow-up emails, and task routing so leads do not sit still after they submit.", focus: ["Form routing", "Email confirmations", "CRM-ready fields", "Team notifications", "Task updates"], outcomes: ["Faster lead response", "Cleaner form data", "Less manual copying", "A professional first reply"], keywords: ["form", "forms", "contact", "lead", "crm", "email", "follow up", "notification", "notifications"] }, { label: "Automation", category: "Automation", title: "Automations that connect your daily tools", summary: "We map repeatable steps and connect tools like forms, email, CRM, Airtable, Zapier, Make, and dashboards so requests move without manual chasing.", focus: ["Zapier and Make flows", "CRM updates", "Task routing", "Email follow-up", "Data formatting"], outcomes: ["Fewer repetitive tasks", "Cleaner handoffs", "Quicker response loops", "More reliable operations"], keywords: ["automation", "automate", "zapier", "make", "workflow", "workflows", "task", "routing"] }, { label: "Dashboards", category: "Dashboards", title: "Dashboards for leads, reports, and operations", summary: "We shape lead dashboards, report views, Airtable systems, and analytics setups so your team can see what is happening and what needs attention.", focus: ["Lead dashboards", "Report dashboards", "Airtable views", "Analytics setup", "Shared visibility"], outcomes: ["Clearer pipeline health", "Less spreadsheet hunting", "Better decision meetings", "More useful sales reviews"], keywords: ["dashboard", "dashboards", "analytics", "reports", "reporting", "airtable", "data", "metrics"] }, { label: "Process", category: "Planning", title: "A simple path from idea to working system", summary: "We clarify your offer, design the interface, build the web or AI layer, connect the follow-up path, and keep improving after launch.", focus: ["Strategy", "Design", "AI build", "Automation", "Optimization"], outcomes: ["A clearer roadmap", "A smoother launch", "Better handoff", "Room for steady improvement"], keywords: ["process", "timeline", "how long", "steps", "roadmap", "start", "launch", "plan"] }, { label: "Pricing", category: "Consultation", title: "Pricing depends on the system you want to build", summary: "Small updates, landing pages, chat assistants, automations, and dashboards each need a different scope. The best next step is a short consultation so we can map the right path.", focus: ["Project goals", "Current tools", "Pages or workflows", "Timeline", "Launch priority"], outcomes: ["Clearer scope", "Better estimate", "Less guesswork", "A useful first roadmap"], keywords: ["price", "pricing", "cost", "budget", "quote", "estimate", "how much"] } ]; function applySiteCustomization() { const site = getSiteSettings(); applyHomeSettings(site); applyAboutSettings(site); applyIndustrySettings(site); applyPrivacyPolicySettings(site); applyFooterSettings(site); applyContactModalSettings(site); applyVideoSettings(site); } function getSiteSettings() { const saved = window.SignalCMS?.read("site") || {}; return normalizeSiteSettings(saved); } function applyHomeSettings(site) { const home = site.home; setText(".hero-copy .eyebrow", home.hero.eyebrow); setText(".headline-main", home.hero.headlineMain); setText(".headline-accent", home.hero.headlineAccent); setText(".hero-description", home.hero.description); setText(".hero-primary", home.hero.primaryButton); setText(".video-button span", home.hero.videoButton); setText(".trusted-clients > p", home.hero.trustLabel); const accent = document.querySelector(".headline-accent"); if (accent) { accent.dataset.words = home.hero.rotatingWords.join("|"); } const logos = document.querySelector(".client-logos"); if (logos) { logos.innerHTML = home.hero.capabilities.map((item) => `<span class="client-logo signal">${escapeSiteHtml(item)}</span>`).join(""); } const statsGrid = document.querySelector(".stats-grid"); if (statsGrid) { statsGrid.innerHTML = home.stats.map((stat) => ` <article class="stat-item"> <strong class="stat-number" data-count="${escapeSiteHtml(stat.value)}" data-suffix="${escapeSiteHtml(stat.suffix)}">0</strong> <span>${escapeSiteHtml(stat.label)}</span> </article> `).join(""); } renderHomeMethod(home.method); setText(".video-story-heading p", home.aboutVideo.kicker); setText(".video-story-heading h2", home.aboutVideo.title); setText(".video-story-heading span", home.aboutVideo.summary); setImage(".story-video-card img", home.aboutVideo.image); setText(".video-caption strong", home.aboutVideo.cardTitle); setText(".video-caption small", home.aboutVideo.cardSubtitle); setText(".proof-grid .section-kicker", home.industries.kicker); setText(".proof-grid h2", home.industries.title); const industryList = document.querySelector(".industry-list"); if (industryList) { industryList.innerHTML = home.industries.items.map((item) => `<span>${escapeSiteHtml(item)}</span>`).join(""); } setText(".home-work-band .work-heading p", home.work.kicker); setText(".home-work-band .work-heading h2", home.work.title); setText(".work-more-button", home.work.moreButton); setHref(".work-more-button", home.work.moreHref); renderHomeTestimonials(home.testimonials); setText(".contact-inner .section-kicker", home.contact.kicker); setText(".contact-inner h2", home.contact.title); setText(".contact-button", home.contact.button); } function renderHomeMethod(method) { const normalized = normalizeMethodSettings(method); const showcase = document.querySelector("[data-method-showcase]"); const heading = document.querySelector(".method-heading"); if (!showcase || !heading) return; setText(".method-heading p", normalized.kicker); setText("#method-title", normalized.title); const steps = normalized.steps; showcase.innerHTML = ` <div class="method-steps" role="tablist" aria-label="SignalHGAI service stages"> ${steps.map((step, index) => { const active = index === 0; const number = String(index + 1).padStart(2, "0"); return ` <button class="method-step${active ? " is-active" : ""}" id="method-tab-${escapeSiteHtml(step.key)}" type="button" role="tab" aria-selected="${active ? "true" : "false"}" aria-controls="method-panel-${escapeSiteHtml(step.key)}" data-method-tab="${escapeSiteHtml(step.key)}" data-method-color="${escapeSiteHtml(step.color)}"> <span>${number}</span> <strong>${escapeSiteHtml(step.label)}</strong> </button> `; }).join("")} </div> <div class="method-frame"> ${steps.map((step, index) => { const active = index === 0; return ` <article class="method-panel${active ? " is-active" : ""}" id="method-panel-${escapeSiteHtml(step.key)}" role="tabpanel" aria-labelledby="method-tab-${escapeSiteHtml(step.key)}" data-method-panel="${escapeSiteHtml(step.key)}"${active ? "" : " hidden"}> <div class="method-copy"> <p>${escapeSiteHtml(step.description)}</p> <ul> ${step.items.map((item) => `<li><span></span>${escapeSiteHtml(item)}</li>`).join("")} </ul> </div> ${renderMethodArt(step.art)} </article> `; }).join("")} </div> `; } function renderMethodArt(art) { const style = normalizeMethodArt(art); const pieces = { strategy: ` <span class="method-art-ring"></span> <span class="art-board"></span> <span class="art-card art-card-one"></span> <span class="art-card art-card-two"></span> <span class="art-lens"></span> `, design: ` <span class="method-art-ring"></span> <span class="art-browser"></span> <span class="art-device"></span> <span class="art-palette"></span> `, build: ` <span class="method-art-ring"></span> <span class="art-code-window"></span> <span class="art-ai-node art-node-one"></span> <span class="art-ai-node art-node-two"></span> <span class="art-ai-node art-node-three"></span> `, automate: ` <span class="method-art-ring"></span> <span class="art-flow-line"></span> <span class="art-flow-node flow-a"></span> <span class="art-flow-node flow-b"></span> <span class="art-flow-node flow-c"></span> `, scale: ` <span class="method-art-ring"></span> <span class="art-growth-panel"></span> <span class="art-chart-bar bar-one"></span> <span class="art-chart-bar bar-two"></span> <span class="art-chart-bar bar-three"></span> ` }; return `<div class="method-art art-${escapeSiteHtml(style)}" aria-hidden="true">${pieces[style]}</div>`; } function renderHomeTestimonials(testimonials) { const normalized = normalizeHomeTestimonials(testimonials); const slider = document.querySelector("[data-testimonials]"); if (!slider) return; slider.innerHTML = ` <div class="testimonial-mark"> <img src="${escapeSiteHtml(normalized.image)}" alt=""> <span>${escapeSiteHtml(normalized.label)}</span> </div> <div class="testimonial-stage"> ${normalized.items.map((item, index) => ` <figure class="testimonial-quote${index === 0 ? " is-active" : ""}"> <blockquote>"${escapeSiteHtml(item.quote)}"</blockquote> <figcaption>${escapeSiteHtml(item.caption)}</figcaption> </figure> `).join("")} </div> <div class="testimonial-controls" aria-label="Work testimonials"> <button class="testimonial-arrow" type="button" data-testimonial-prev aria-label="Previous testimonial"> <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 18 9 12l6-6" /></svg> </button> <div class="testimonial-dots"> ${normalized.items.map((item, index) => ` <button class="${index === 0 ? "is-active" : ""}" type="button" data-testimonial-dot aria-label="Show testimonial ${index + 1}"></button> `).join("")} </div> <button class="testimonial-arrow" type="button" data-testimonial-next aria-label="Next testimonial"> <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m9 18 6-6-6-6" /></svg> </button> </div> `; } function applyAboutSettings(site) { const about = site.about; setText(".about-hero .about-kicker", about.heroKicker); setText("#about-title", about.heroTitle); setText(".about-hero-inner > p:not(.about-kicker)", about.heroSummary); setImage(".about-video-card img", about.videoImage); setText(".about-video-copy small", about.videoSmall); setText(".about-video-copy strong", about.videoTitle); const statements = document.querySelectorAll(".about-statement"); setTextIn(statements[0], "span", about.missionTitle); setTextIn(statements[0], "p", about.missionText); setTextIn(statements[1], "span", about.visionTitle); setTextIn(statements[1], "p", about.visionText); setText(".about-principles-copy .about-kicker", about.principlesKicker); setText(".about-principles-copy h2", about.principlesTitle); setText(".about-principles-copy > p:not(.about-kicker)", about.principlesText); setImage(".about-photo-badge:not(.about-photo-badge-alt) img", about.principlesImage); renderAboutInfoPanel(".about-principle-panel", about.principleItems); setText(".about-pillars-copy .about-kicker", about.pillarsKicker); setText(".about-pillars-copy h2", about.pillarsTitle); setText(".about-pillars-copy > p:not(.about-kicker)", about.pillarsText); setImage(".about-photo-badge-alt img", about.pillarsImage); renderAboutInfoPanel(".about-pillar-panel", about.pillarItems); setText(".about-awards-inner .about-kicker", about.recognitionKicker); setText(".about-awards-inner h2", about.recognitionTitle); renderAboutMetrics(about.metrics); setText(".about-cta-inner .about-kicker", about.ctaKicker); setText(".about-cta-inner h2", about.ctaTitle); setText(".about-cta-inner .pill-button", about.ctaButton); } function renderAboutInfoPanel(selector, items) { const panel = document.querySelector(selector); if (!panel) return; panel.innerHTML = (items || []).map((item) => ` <article> <span></span> <div> <h3>${escapeSiteHtml(item.title)}</h3> <p>${escapeSiteHtml(item.text)}</p> </div> </article> `).join(""); } function renderAboutMetrics(metrics) { const container = document.querySelector(".about-metrics"); if (!container) return; container.innerHTML = (metrics || []).map((metric) => ` <article> <strong>${escapeSiteHtml(metric.value)}</strong> <span>${escapeSiteHtml(metric.label)}</span> </article> `).join(""); } function applyIndustrySettings(site) { const industries = site.industries; setText(".industry-eyebrow", industries.heroKicker); setText("#industry-title", industries.heroTitle); setText(".industry-hero-copy > p:not(.industry-eyebrow)", industries.heroSummary); setText(".industry-hero-actions .pill-button", industries.heroPrimaryButton); setText(".industry-secondary-link", industries.heroSecondaryButton); setImage(".industry-hero-visual img", industries.heroImage); setText(".industry-command-card span", industries.commandLabel); setText(".industry-command-card strong", industries.commandValue); setText(".industry-route-card span", industries.routeLabel); setText(".industry-route-card strong", industries.routeValue); renderIndustryStats(industries.stats); setText(".industry-grid-section .industry-section-heading p", industries.gridKicker); setText(".industry-grid-section .industry-section-heading h2", industries.gridTitle); renderIndustryCards(industries.cards); setText(".industry-dispatch-copy p", industries.dispatchKicker); setText(".industry-dispatch-copy h2", industries.dispatchTitle); setText(".industry-dispatch-copy > span", industries.dispatchText); setText(".industry-dispatch-copy .pill-button", industries.dispatchButton); setText(".dispatch-console-top span", industries.dispatchConsoleLabel); setText(".dispatch-console-top strong", industries.dispatchConsoleStatus); renderDispatchQueue(industries.dispatchQueue); setText(".industry-system-band .industry-section-heading p", industries.systemKicker); setText(".industry-system-band .industry-section-heading h2", industries.systemTitle); renderIndustrySystemItems(industries.systemItems); setText(".industry-contact-band .section-kicker", industries.ctaKicker); setText(".industry-contact-band h2", industries.ctaTitle); setText(".industry-contact-band .contact-button", industries.ctaButton); } function renderIndustryStats(stats) { const container = document.querySelector(".industry-snapshot-grid"); if (!container) return; container.innerHTML = (stats || []).map((stat) => ` <article> <strong>${escapeSiteHtml(stat.value)}</strong> <span>${escapeSiteHtml(stat.label)}</span> </article> `).join(""); } function renderIndustryCards(cards) { const container = document.querySelector(".industry-card-grid"); if (!container) return; container.innerHTML = (cards || []).map((card) => ` <article class="industry-card"> <span class="industry-badge">${escapeSiteHtml(card.badge)}</span> <h3>${escapeSiteHtml(card.title)}</h3> <p>${escapeSiteHtml(card.text)}</p> <div>${(card.tags || []).map((tag) => `<span>${escapeSiteHtml(tag)}</span>`).join("")}</div> </article> `).join(""); } function renderDispatchQueue(items) { const container = document.querySelector(".dispatch-queue"); if (!container) return; container.innerHTML = (items || []).map((item) => ` <article> <span>${escapeSiteHtml(item.number)}</span> <strong>${escapeSiteHtml(item.title)}</strong> <small>${escapeSiteHtml(item.status)}</small> </article> `).join(""); } function renderIndustrySystemItems(items) { const container = document.querySelector(".industry-system-grid"); if (!container) return; container.innerHTML = (items || []).map((item) => ` <article> <span>${escapeSiteHtml(item.number)}</span> <h3>${escapeSiteHtml(item.title)}</h3> <p>${escapeSiteHtml(item.text)}</p> </article> `).join(""); } function applyFooterSettings(site) { const footer = site.footer; const phoneHref = formatPhoneHref(footer.phone); setText(".footer-signal-inner h2", footer.ctaTitle); setText(".footer-consult-button", footer.ctaButton); setText("#footer-subscribe-title", footer.subscribeTitle); setText(".footer-subscribe-wrap p", footer.subscribeText); setText("#footer-social-title", footer.followTitle || "Follow us"); document.querySelectorAll(".footer-bottom a").forEach((link) => { const label = link.textContent.trim().toLowerCase(); if (label === "privacy policy") link.href = "privacy-policy"; if (label === "sitemap") link.href = "/#home"; }); document.querySelectorAll('a[href^="mailto:"]').forEach((link) => { if (link.closest(".footer-social-links")) return; link.textContent = footer.email; link.href = `mailto:${footer.email}`; }); document.querySelectorAll('a[href^="tel:"]').forEach((link) => { if (link.closest(".footer-social-links")) return; link.href = `tel:${phoneHref}`; const span = link.querySelector("span"); if (span) span.textContent = footer.phone; if (!span) link.textContent = footer.phone; }); renderFooterIconLinks(footer, phoneHref); applyFloatingChatSettings(footer); } function renderFooterIconLinks(footer, phoneHref) { const container = document.querySelector(".footer-social-links"); if (!container) return; const social = footer.socialLinks || {}; const telHref = normalizeFooterHref(social.phone, phoneHref ? `tel:${phoneHref}` : ""); const links = [ { key: "x", label: "SignalHGAI on X", href: normalizeFooterHref(social.x), icon: "x" }, { key: "linkedin", label: "SignalHGAI on LinkedIn", href: normalizeFooterHref(social.linkedin), icon: "linkedin" }, { key: "facebook", label: "SignalHGAI on Facebook", href: normalizeFooterHref(social.facebook), icon: "facebook" }, { key: "youtube", label: "SignalHGAI on YouTube", href: normalizeFooterHref(social.youtube), icon: "youtube" }, { key: "phone", label: `Call ${footer.phone || "SignalHGAI"}`, href: telHref, icon: "phone" } ].filter((link) => link.href); container.innerHTML = links.map((link) => { const action = isContactHref(link.href) ? " data-contact-open" : ""; const external = isExternalHref(link.href) ? ' target="_blank" rel="noopener noreferrer"' : ""; return ` <a class="social-${escapeSiteHtml(link.key)}" href="${escapeSiteHtml(link.href)}" aria-label="${escapeSiteHtml(link.label)}"${action}${external}> ${footerIconSvg(link.icon)} </a> `; }).join(""); } function applyFloatingChatSettings(footer) { const button = document.querySelector(".chat-fab"); if (!button) return; const chat = footer.chatButton || {}; button.hidden = false; button.setAttribute("aria-label", chat.label || "Open SignalHGAI chat"); button.removeAttribute("data-contact-open"); delete button.dataset.footerChatHref; } function setupFloatingChatButton() { const button = document.querySelector(".chat-fab"); if (!button) return; const panel = ensureSignalChatPanel(); const form = panel.querySelector("[data-signal-chat-form]"); const input = panel.querySelector("[data-signal-chat-input]"); const messages = panel.querySelector("[data-signal-chat-messages]"); let greeted = false; button.setAttribute("aria-expanded", "false"); button.setAttribute("aria-controls", panel.id); button.innerHTML = signalChatOpenSvg(); const setOpen = (isOpen) => { setSignalChatOpen(panel, button, isOpen); if (isOpen && !greeted) { greeted = true; appendSignalChatMessage(messages, "bot", "Hi, I am the SignalHGAI assistant. Ask me about websites, AI chatbots, automations, dashboards, pricing, or next steps."); appendSignalChatMessage(messages, "bot", "If you want to talk with the team, I can open the schedule form for you."); appendSignalChatActions(messages, signalChatQuickReplies); } if (isOpen) { window.setTimeout(() => input?.focus(), 120); } }; button.addEventListener("click", () => { setOpen(!panel.classList.contains("is-open")); }); panel.addEventListener("click", (event) => { const closeButton = event.target.closest("[data-signal-chat-close]"); if (closeButton) { setOpen(false); return; } const actionButton = event.target.closest("[data-signal-chat-action]"); if (actionButton?.dataset.signalChatAction === "schedule") { appendSignalChatMessage(messages, "user", actionButton.textContent.trim() || "Schedule a meeting"); appendSignalChatMessage(messages, "bot", "Absolutely. I will open the contact form so you can share your details."); openContactFormFromChat(panel, button); return; } const promptButton = event.target.closest("[data-signal-chat-prompt]"); if (promptButton) { handleSignalChatPrompt(promptButton.dataset.signalChatPrompt, messages, input); } }); form?.addEventListener("submit", (event) => { event.preventDefault(); handleSignalChatPrompt(input?.value, messages, input); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && panel.classList.contains("is-open")) { setOpen(false); } }); } function ensureSignalChatPanel() { let panel = document.getElementById("signal-chat-panel"); if (panel) return panel; panel = document.createElement("section"); panel.className = "signal-chat-panel"; panel.id = "signal-chat-panel"; panel.setAttribute("aria-label", "SignalHGAI chat assistant"); panel.hidden = true; panel.innerHTML = ` <div class="signal-chat-header"> <button class="signal-chat-back" type="button" data-signal-chat-close aria-label="Close chat"> <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m15 18-6-6 6-6" /></svg> </button> <span class="signal-chat-avatar" aria-hidden="true"> <img src="assets/frontend-logo.png" alt=""> </span> <div class="signal-chat-title"> <strong>SignalHGAI Assistant</strong> <span>AI website guide</span> </div> <button class="signal-chat-close" type="button" data-signal-chat-close aria-label="Close chat"> <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg> </button> </div> <div class="signal-chat-messages" data-signal-chat-messages></div> <form class="signal-chat-composer" data-signal-chat-form> <input type="text" data-signal-chat-input autocomplete="off" placeholder="Ask about your project"> <button type="submit" aria-label="Send message"> <svg viewBox="0 0 24 24" aria-hidden="true"><path d="m22 2-7 20-4-9-9-4 20-7Z" /></svg> </button> </form> `; document.body.append(panel); return panel; } function setSignalChatOpen(panel, button, isOpen) { if (isOpen) { panel.hidden = false; button.classList.add("is-open"); button.setAttribute("aria-expanded", "true"); button.setAttribute("aria-controls", panel.id); button.setAttribute("aria-label", "Close SignalHGAI chat"); button.innerHTML = signalChatCloseSvg(); const scheduleFrame = window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : ((callback) => window.setTimeout(callback, 0)); scheduleFrame(() => { panel.classList.add("is-open"); }); return; } panel.classList.remove("is-open"); button.classList.remove("is-open"); button.setAttribute("aria-expanded", "false"); button.setAttribute("aria-controls", panel.id); button.setAttribute("aria-label", "Open SignalHGAI chat"); button.innerHTML = signalChatOpenSvg(); window.setTimeout(() => { if (!panel.classList.contains("is-open")) { panel.hidden = true; } }, 180); } function handleSignalChatPrompt(value, messages, input) { const prompt = String(value || "").trim(); if (!prompt) return; appendSignalChatMessage(messages, "user", prompt); if (input) input.value = ""; appendSignalChatTyping(messages); window.setTimeout(() => { removeSignalChatTyping(messages); const response = getSignalChatResponse(prompt); appendSignalChatMessage(messages, "bot", response.text); appendSignalChatActions(messages, response.actions || defaultSignalChatActions(prompt)); }, 360); } function appendSignalChatMessage(container, role, text) { if (!container) return; const message = document.createElement("article"); message.className = `signal-chat-message is-${role}`; const bubble = document.createElement("div"); bubble.className = "signal-chat-bubble"; const copy = document.createElement("p"); copy.textContent = text; const time = document.createElement("time"); time.textContent = formatSignalChatTime(); bubble.append(copy, time); message.append(bubble); container.append(message); scrollSignalChat(container); } function appendSignalChatActions(container, actions) { if (!container || !actions?.length) return; const wrap = document.createElement("div"); wrap.className = "signal-chat-actions"; actions.forEach((action) => { const button = document.createElement("button"); button.type = "button"; button.className = action.action === "schedule" ? "signal-chat-action is-primary" : "signal-chat-action"; button.textContent = action.label; if (action.action) button.dataset.signalChatAction = action.action; if (action.prompt) button.dataset.signalChatPrompt = action.prompt; wrap.append(button); }); container.append(wrap); scrollSignalChat(container); } function appendSignalChatTyping(container) { if (!container) return; const typing = document.createElement("div"); typing.className = "signal-chat-typing"; typing.dataset.signalChatTyping = ""; typing.setAttribute("aria-label", "SignalHGAI Assistant is typing"); typing.innerHTML = "<span></span><span></span><span></span>"; container.append(typing); scrollSignalChat(container); } function removeSignalChatTyping(container) { container?.querySelector("[data-signal-chat-typing]")?.remove(); } function scrollSignalChat(container) { container.scrollTop = container.scrollHeight; } function getSignalChatResponse(prompt) { const normalized = normalizeSignalChatText(prompt); if (hasSignalChatIntent(normalized, ["schedule", "meeting", "meet", "call", "contact", "consult", "consultation", "book", "appointment", "get started"])) { return { text: "Yes. The best next step is a free consultation. Share your name, email, and project notes in the form, and the team will reply within 24 hours.", actions: [{ label: "Schedule a meeting", action: "schedule" }] }; } if (hasSignalChatIntent(normalized, ["hello", "hi", "hey"])) { return { text: "Hi. I can help you understand SignalHGAI services, AI website assistants, automation workflows, dashboards, and the best next step for your project.", actions: signalChatQuickReplies }; } const match = findSignalChatKnowledge(prompt); if (match) { const focus = listSignalChatItems(match.focus, "The work usually focuses on"); const outcomes = listSignalChatItems(match.outcomes, "Useful outcomes include"); return { text: `${match.title}. ${match.summary}${focus}${outcomes}`, actions: defaultSignalChatActions(prompt) }; } return { text: "SignalHGAI can help with AI-ready websites, website chatbots, lead qualification, contact-form workflows, follow-up automation, dashboards, SEO-ready structure, and ongoing optimization. Tell me what you want to launch or improve, and I will point you to the right path.", actions: signalChatQuickReplies }; } function defaultSignalChatActions(prompt) { const normalized = normalizeSignalChatText(prompt); if (hasSignalChatIntent(normalized, ["price", "pricing", "cost", "budget", "quote", "estimate"])) { return [{ label: "Schedule a meeting", action: "schedule" }]; } return [ { label: "Schedule a meeting", action: "schedule" }, { label: "Ask about AI chatbots", prompt: "How can a website chatbot help my business?" }, { label: "Ask about automation", prompt: "Can you automate my forms and follow-up?" } ]; } function findSignalChatKnowledge(prompt) { const tokens = tokenizeSignalChat(prompt); if (!tokens.length) return null; const catalog = getSignalChatKnowledge(); let best = null; let bestScore = 0; catalog.forEach((item) => { const haystack = normalizeSignalChatText([ item.label, item.category, item.title, item.summary, ...(item.focus || []), ...(item.outcomes || []), ...(item.tools || []), ...(item.keywords || []) ].join(" ")); const score = tokens.reduce((total, token) => { if (haystack.includes(token)) return total + (token.length > 5 ? 2 : 1); return total; }, 0); if (score > bestScore) { bestScore = score; best = item; } }); return bestScore >= 1 ? best : null; } function getSignalChatKnowledge() { const serviceItems = Array.isArray(window.signalServiceCatalog) ? window.signalServiceCatalog : []; const technologyItems = Array.isArray(window.signalTechnologyCatalog) ? window.signalTechnologyCatalog : []; const catalogItems = [...serviceItems, ...technologyItems].map((item) => ({ label: item.label, category: item.category, title: item.title || item.label, summary: item.summary || item.description || "", focus: item.focus || item.signals || [], outcomes: item.outcomes || [], tools: item.tools || [], keywords: [item.slug, item.label, item.category] })); return [...catalogItems, ...signalChatKnowledgeBase]; } function hasSignalChatIntent(text, keywords) { return keywords.some((keyword) => { const normalized = normalizeSignalChatText(keyword).trim(); if (!normalized) return false; if (normalized.includes(" ")) return text.includes(normalized); return new RegExp(`\\b${escapeRegExp(normalized)}\\b`).test(text); }); } function normalizeSignalChatText(value) { return String(value || "").toLowerCase().replace(/[^a-z0-9+ ]+/g, " "); } function escapeRegExp(value) { return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } function tokenizeSignalChat(value) { const ignored = new Set(["about", "what", "with", "your", "does", "have", "help", "need", "want", "this", "that", "and", "the", "for", "can", "you", "our"]); return normalizeSignalChatText(value) .split(/\s+/) .map((token) => token.trim()) .filter((token) => token.length > 2 && !ignored.has(token)); } function listSignalChatItems(items, prefix) { const list = (Array.isArray(items) ? items : []).filter(Boolean).slice(0, 3); return list.length ? ` ${prefix} ${list.join(", ")}.` : ""; } function formatSignalChatTime() { return new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); } function openContactFormFromChat(panel, button) { setSignalChatOpen(panel, button, false); window.setTimeout(() => { const existingOpener = document.querySelector("[data-contact-open]"); if (existingOpener) { existingOpener.click(); return; } const modal = document.getElementById("contact-modal"); if (!modal) return; modal.classList.add("is-open"); modal.setAttribute("aria-hidden", "false"); document.body.classList.add("modal-open"); }, 160); } function signalChatOpenSvg() { return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v8Z" /></svg>'; } function signalChatCloseSvg() { return '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>'; } function navigateFooterHref(href) { if (!href || href === "#") return; if (isExternalHref(href)) { window.open(href, "_blank", "noopener,noreferrer"); return; } window.location.href = href; } function normalizeFooterHref(value, fallback = "") { return String(value || fallback || "").trim(); } function formatPhoneHref(value) { return String(value || "").replace(/[^\d+]/g, ""); } function isContactHref(value) { const href = String(value || "").trim().toLowerCase(); return href === "#contact" || href.endsWith("/#contact"); } function isExternalHref(value) { return /^https?:\/\//i.test(String(value || "").trim()); } function footerIconSvg(icon) { const icons = { x: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18.24 2.25h3.31l-7.23 8.26 8.5 11.24h-6.66l-5.21-6.82-5.96 6.82H1.68l7.73-8.84L1.25 2.25h6.99l4.71 6.23 5.29-6.23Zm-1.16 17.52h1.83L7.08 4.13H5.12l11.96 15.64Z" /></svg>', linkedin: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6.94 20H3.56V9.24h3.38V20ZM5.25 7.78a1.96 1.96 0 1 1 0-3.92 1.96 1.96 0 0 1 0 3.92ZM20.44 20h-3.37v-5.23c0-1.25-.02-2.86-1.74-2.86-1.74 0-2.01 1.36-2.01 2.77V20H9.95V9.24h3.24v1.47h.05c.45-.86 1.56-1.76 3.2-1.76 3.42 0 4.05 2.25 4.05 5.18V20h-.05Z" /></svg>', facebook: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14.2 8.15h2.48V4.32c-.43-.06-1.9-.19-3.62-.19-3.57 0-6.02 2.18-6.02 6.18v3.47H3v4.28h4.04V24H12v-5.94h3.88l.62-4.28H12v-3.05c0-1.24.34-2.58 2.2-2.58Z" /></svg>', youtube: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M23.5 6.2a3 3 0 0 0-2.1-2.12c-1.85-.5-9.4-.5-9.4-.5s-7.55 0-9.4.5A3 3 0 0 0 .5 6.2 31.4 31.4 0 0 0 0 12c0 1.93.17 3.87.5 5.8a3 3 0 0 0 2.1 2.12c1.85.5 9.4.5 9.4.5s7.55 0 9.4-.5a3 3 0 0 0 2.1-2.12c.33-1.93.5-3.87.5-5.8s-.17-3.87-.5-5.8Z" /><path fill="#ffffff" d="M9.55 15.57V8.43L15.82 12l-6.27 3.57Z" /></svg>', chat: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 15a4 4 0 0 1-4 4H8l-5 3V7a4 4 0 0 1 4-4h10a4 4 0 0 1 4 4v8Z" /></svg>', email: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16v12H4V6Z" /><path d="m4 7 8 6 8-6" /></svg>', phone: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.8 19.8 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6A19.8 19.8 0 0 1 2.12 4.18 2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.13.96.35 1.89.66 2.78a2 2 0 0 1-.45 2.11L8.09 9.84a16 16 0 0 0 6.07 6.07l1.23-1.23a2 2 0 0 1 2.11-.45c.89.31 1.82.53 2.78.66A2 2 0 0 1 22 16.92Z" /></svg>' }; return icons[icon] || ""; } function applyPrivacyPolicySettings(site) { const policy = site.privacyPolicy; const sections = document.querySelector("[data-privacy-sections]"); if (!sections || !policy) return; setText("[data-privacy-kicker]", policy.kicker); setText("[data-privacy-title]", policy.title); setText("[data-privacy-updated]", policy.updatedLabel); setText("[data-privacy-intro]", policy.intro); setText("[data-privacy-contact-title]", policy.contactTitle); setText("[data-privacy-contact-text]", policy.contactText); sections.innerHTML = (policy.sections || []).map((section) => ` <article class="privacy-section"> <span></span> <div> <h2>${escapeSiteHtml(section.title)}</h2> ${renderPrivacyText(section.text)} </div> </article> `).join(""); } function renderPrivacyText(value) { const paragraphs = String(value || "") .split(/\n{2,}/) .map((item) => item.trim()) .filter(Boolean); return paragraphs.map((paragraph) => `<p>${escapeSiteHtml(paragraph)}</p>`).join(""); } function applyContactModalSettings(site) { const contact = site.contactModal; setText(".contact-kicker", contact.kicker); setText("#contact-title", contact.title); setText(".contact-info-card > p", contact.description); setText(".submit-contact", contact.submitButton); } function applyVideoSettings(site) { const video = site.video; setImage(".modal-video-still", video.poster); setText(".video-copy span", video.eyebrow); setText(".video-copy h2", video.title); setText(".video-copy p", video.description); setText(".fake-player-bar strong", video.playerLabel); renderCustomVideo(video.sourceUrl, video.title); } function renderCustomVideo(sourceUrl, title) { const frame = document.querySelector(".video-frame"); if (!frame) return; let slot = frame.querySelector("[data-custom-video]"); if (!sourceUrl) { frame.classList.remove("has-custom-video"); slot?.remove(); return; } if (!slot) { slot = document.createElement("div"); slot.className = "custom-video-embed"; slot.dataset.customVideo = ""; frame.prepend(slot); } const embedUrl = toEmbedUrl(sourceUrl); frame.classList.add("has-custom-video"); if (embedUrl) { slot.innerHTML = `<iframe src="${escapeSiteHtml(embedUrl)}" title="${escapeSiteHtml(title || "Overview video")}" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>`; return; } slot.innerHTML = `<video src="${escapeSiteHtml(sourceUrl)}" controls playsinline preload="metadata"></video>`; } function toEmbedUrl(url) { try { const parsed = new URL(url, window.location.href); const host = parsed.hostname.replace(/^www\./, ""); if (host === "youtu.be") { return `https://www.youtube.com/embed/${parsed.pathname.replace("/", "")}`; } if (host.endsWith("youtube.com")) { const id = parsed.searchParams.get("v") || parsed.pathname.split("/").filter(Boolean).pop(); return id ? `https://www.youtube.com/embed/${id}` : ""; } if (host.endsWith("vimeo.com")) { const id = parsed.pathname.split("/").filter(Boolean).pop(); return id ? `https://player.vimeo.com/video/${id}` : ""; } } catch (error) { return ""; } return ""; } function normalizeSiteSettings(value) { const site = deepMerge(defaultSiteSettings, value && typeof value === "object" ? value : {}); site.home.hero.rotatingWords = listifySite(site.home.hero.rotatingWords); site.home.hero.capabilities = listifySite(site.home.hero.capabilities); site.home.industries.items = listifySite(site.home.industries.items); site.home.stats = normalizeSiteStats(site.home.stats); site.home.method = normalizeMethodSettings(site.home.method); site.home.testimonials = normalizeHomeTestimonials(site.home.testimonials); site.about = normalizeAboutSettings(site.about); site.industries = normalizeIndustrySettings(site.industries); site.privacyPolicy = normalizePrivacyPolicySettings(site.privacyPolicy); return site; } function normalizeSiteStats(stats) { const source = Array.isArray(stats) && stats.length ? stats : defaultSiteSettings.home.stats; return source.slice(0, 4).map((stat, index) => ({ value: String(stat?.value ?? defaultSiteSettings.home.stats[index]?.value ?? "0"), suffix: String(stat?.suffix ?? defaultSiteSettings.home.stats[index]?.suffix ?? ""), label: String(stat?.label ?? defaultSiteSettings.home.stats[index]?.label ?? "Metric") })); } function normalizeMethodSettings(method) { const defaults = defaultSiteSettings.home.method; const source = method && typeof method === "object" ? method : {}; const savedSteps = Array.isArray(source.steps) ? source.steps : []; return { kicker: String(source.kicker ?? defaults.kicker), title: String(source.title ?? defaults.title), steps: defaults.steps.map((defaultStep, index) => { const savedStep = savedSteps.find((step) => step?.key === defaultStep.key) || savedSteps[index] || {}; return { key: defaultStep.key, label: String(savedStep.label ?? defaultStep.label), description: String(savedStep.description ?? defaultStep.description), items: listifySite(savedStep.items ?? defaultStep.items), color: normalizeMethodColor(savedStep.color, defaultStep.color), art: normalizeMethodArt(savedStep.art, defaultStep.art) }; }) }; } function normalizeMethodColor(value, fallback) { const color = String(value || "").trim(); return /^#[0-9a-f]{3}([0-9a-f]{3})?$/i.test(color) ? color : fallback; } function normalizeMethodArt(value, fallback = "strategy") { const art = String(value || "").trim().toLowerCase(); return ["strategy", "design", "build", "automate", "scale"].includes(art) ? art : fallback; } function normalizeHomeTestimonials(testimonials) { const defaults = defaultSiteSettings.home.testimonials; const source = testimonials && typeof testimonials === "object" ? testimonials : {}; const sourceItems = Array.isArray(source.items) && source.items.length ? source.items : defaults.items; return { label: String(source.label ?? defaults.label), image: String(source.image ?? defaults.image), items: defaults.items.map((defaultItem, index) => { const sourceItem = sourceItems[index] || {}; return { quote: String(sourceItem.quote ?? defaultItem.quote), caption: String(sourceItem.caption ?? defaultItem.caption) }; }) }; } function normalizeAboutSettings(about) { const defaults = defaultSiteSettings.about; const source = about && typeof about === "object" ? about : {}; source.videoImage = normalizeSiteImage(source.videoImage, defaults.videoImage); source.principlesImage = normalizeSiteImage(source.principlesImage, defaults.principlesImage); source.pillarsImage = normalizeSiteImage(source.pillarsImage, defaults.pillarsImage); source.principleItems = normalizeTitleTextItems(source.principleItems, defaults.principleItems); source.pillarItems = normalizeTitleTextItems(source.pillarItems, defaults.pillarItems); source.metrics = normalizeMetricItems(source.metrics, defaults.metrics); return source; } function normalizeIndustrySettings(industries) { const defaults = defaultSiteSettings.industries; const source = industries && typeof industries === "object" ? industries : {}; source.heroImage = normalizeSiteImage(source.heroImage, defaults.heroImage); source.stats = normalizeMetricItems(source.stats, defaults.stats); source.cards = normalizeIndustryCards(source.cards, defaults.cards); source.dispatchQueue = normalizeDispatchItems(source.dispatchQueue, defaults.dispatchQueue); source.systemItems = normalizeNumberedTextItems(source.systemItems, defaults.systemItems); return source; } function normalizePrivacyPolicySettings(policy) { const defaults = defaultSiteSettings.privacyPolicy; const source = policy && typeof policy === "object" ? policy : {}; const sourceSections = Array.isArray(source.sections) && source.sections.length ? source.sections : defaults.sections; return { kicker: String(source.kicker ?? defaults.kicker), title: String(source.title ?? defaults.title), updatedLabel: String(source.updatedLabel ?? defaults.updatedLabel), intro: String(source.intro ?? defaults.intro), sections: defaults.sections.map((defaultSection, index) => { const savedSection = sourceSections[index] || {}; return { title: String(savedSection.title ?? defaultSection.title), text: String(savedSection.text ?? defaultSection.text) }; }), contactTitle: String(source.contactTitle ?? defaults.contactTitle), contactText: String(source.contactText ?? defaults.contactText) }; } function normalizeSiteImage(image, fallback) { const defaultImage = normalizeRawImage(fallback, { src: "assets/hero-specialist.png", position: "center" }); return normalizeRawImage(image, defaultImage); } function normalizeRawImage(image, fallback) { if (typeof image === "string") { return { src: image.trim() || fallback.src, position: fallback.position }; } return { src: String(image?.src || fallback.src).trim() || fallback.src, position: String(image?.position || fallback.position).trim() || fallback.position }; } function normalizeTitleTextItems(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ title: String(item?.title ?? fallback.title), text: String(item?.text ?? item?.description ?? fallback.text) })); } function normalizeMetricItems(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ value: String(item?.value ?? fallback.value), label: String(item?.label ?? fallback.label) })); } function normalizeIndustryCards(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ badge: String(item?.badge ?? fallback.badge), title: String(item?.title ?? fallback.title), text: String(item?.text ?? item?.description ?? fallback.text), tags: listifySite(item?.tags ?? fallback.tags) })); } function normalizeDispatchItems(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ number: String(item?.number ?? fallback.number), title: String(item?.title ?? fallback.title), status: String(item?.status ?? fallback.status) })); } function normalizeNumberedTextItems(items, defaults) { return normalizeArrayItems(items, defaults, (item, fallback) => ({ number: String(item?.number ?? fallback.number), title: String(item?.title ?? fallback.title), text: String(item?.text ?? item?.description ?? fallback.text) })); } function normalizeArrayItems(items, defaults, mapper) { const source = Array.isArray(items) && items.length ? items : defaults; return source.map((item, index) => mapper(item || {}, defaults[index] || defaults[defaults.length - 1] || {})); } function deepMerge(defaults, value) { if (Array.isArray(defaults)) { return Array.isArray(value) ? JSON.parse(JSON.stringify(value)) : JSON.parse(JSON.stringify(defaults)); } if (!defaults || typeof defaults !== "object") { return value == null ? defaults : value; } const merged = JSON.parse(JSON.stringify(defaults)); Object.keys(value || {}).forEach((key) => { merged[key] = deepMerge(defaults[key], value[key]); }); return merged; } function listifySite(value) { if (Array.isArray(value)) return value.map((item) => String(item).trim()).filter(Boolean); return String(value || "").split(/\r?\n|,/).map((item) => item.trim()).filter(Boolean); } function setText(selector, value) { const node = document.querySelector(selector); if (node && value != null) node.textContent = value; } function setTextIn(root, selector, value) { const node = root?.querySelector(selector); if (node && value != null) node.textContent = value; } function setHref(selector, value) { const node = document.querySelector(selector); if (node && value) node.setAttribute("href", value); } function setImage(selector, value) { const node = document.querySelector(selector); if (!node || !value) return; if (typeof value === "object") { const image = normalizeSiteImage(value, { src: node.getAttribute("src") || "assets/hero-specialist.png", position: "" }); node.setAttribute("src", image.src); if (image.position) node.style.objectPosition = image.position; return; } node.setAttribute("src", value); } function escapeSiteHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function setupHeader() { const header = document.getElementById("site-header"); if (!header) return; const update = () => { header.classList.toggle("is-scrolled", window.scrollY > 12); }; update(); window.addEventListener("scroll", update, { passive: true }); } function setupPageAnimations() { const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); const selectors = [ ".hero-copy > *", ".hero-visual", ".stats-band .stat-item", ".video-story-heading", ".story-video-card", ".method-heading", ".method-step", ".method-frame", ".proof-grid > *", ".home-work-band .work-heading", ".home-work-band .case-card", ".work-testimonial", ".contact-inner > *", ".about-hero-inner > *", ".about-video-card", ".about-statement", ".about-principle-panel article", ".about-pillar-panel article", ".about-photo-badge", ".about-awards-inner > *", ".about-metrics article", ".about-cta-inner > *", ".service-hero-copy > *", ".service-hero-art", ".service-anchor-nav a", ".service-section-heading", ".service-detail-card", ".service-case-inner > *", ".service-process-grid article", ".service-insight-card", ".technology-code-card", ".technology-use-card", ".technology-loop", ".work-hero-inner > *", ".work-tabs a", ".work-select", ".case-featured", ".work-band .case-grid .case-card", ".work-trusted h2", ".work-logo-row span", ".industry-hero-copy > *", ".industry-hero-visual", ".industry-snapshot article", ".industry-section-heading", ".industry-card", ".industry-dispatch-copy > *", ".dispatch-console", ".industry-system-grid article", ".footer-signal-inner > *", ".footer-main-grid > *", ]; const targets = Array.from(new Set(document.querySelectorAll(selectors.join(",")))) .filter((target) => !target.closest(".mega-menu, .contact-modal, .video-modal")); if (targets.length === 0) return; const parentCounts = new WeakMap(); targets.forEach((target) => { const parent = target.parentElement || document.body; const order = parentCounts.get(parent) || 0; parentCounts.set(parent, order + 1); target.style.setProperty("--motion-order", Math.min(order, 8)); target.classList.add("motion-ready"); }); const revealAll = () => { targets.forEach((target) => target.classList.add("motion-revealed")); }; if (reduceMotion.matches || !("IntersectionObserver" in window)) { revealAll(); return; } const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add("motion-revealed"); observer.unobserve(entry.target); }); }, { rootMargin: "0px 0px -9% 0px", threshold: 0.14, }); targets.forEach((target) => observer.observe(target)); reduceMotion.addEventListener?.("change", () => { if (!reduceMotion.matches) return; revealAll(); observer.disconnect(); }); } function setupLogoAnimation() { const brand = document.querySelector(".brand"); if (!brand) return; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); let timeoutId; const finish = () => { brand.classList.remove("is-logo-animating"); }; const play = () => { if (reduceMotion.matches) { finish(); return; } window.clearTimeout(timeoutId); brand.classList.remove("is-logo-animating"); void brand.offsetWidth; brand.classList.add("is-logo-animating"); timeoutId = window.setTimeout(finish, 1100); }; if (brand.classList.contains("is-logo-animating")) { timeoutId = window.setTimeout(finish, 1100); } else { window.setTimeout(play, 180); } window.addEventListener("hashchange", play); document.addEventListener("click", (event) => { const link = event.target.closest('a[href^="#"]'); if (!link) return; play(); }); } function setupNavigation() { const header = document.getElementById("site-header"); const toggle = document.getElementById("mobile-toggle"); const dropdowns = document.querySelectorAll(".has-dropdown"); const navReadyDelay = 1100; document.body.classList.remove("nav-dropdown-open"); closeDropdowns(); window.setTimeout(() => { document.body.classList.add("nav-ready"); }, navReadyDelay); if (toggle && header) { toggle.addEventListener("click", () => { const isOpen = header.classList.toggle("nav-open"); document.body.classList.toggle("nav-menu-open", isOpen); toggle.setAttribute("aria-expanded", String(isOpen)); toggle.setAttribute("aria-label", isOpen ? "Close navigation" : "Open navigation"); if (!isOpen) { closeDropdowns(); } }); } dropdowns.forEach((item) => { const button = item.querySelector(".nav-button"); const menu = item.querySelector(".mega-menu"); if (!button || !menu) return; let closeTimer = 0; let openedByClick = false; const cancelClose = () => { if (!closeTimer) return; window.clearTimeout(closeTimer); closeTimer = 0; }; const open = (event) => { if (event?.type === "mouseenter" && !document.body.classList.contains("nav-ready")) return; if (event?.type !== "click" && !item.classList.contains("is-open")) { openedByClick = false; } cancelClose(); closeDropdowns(item); item.classList.add("is-open"); button.setAttribute("aria-expanded", "true"); menu.setAttribute("aria-hidden", "false"); document.body.classList.add("nav-dropdown-open"); }; const close = () => { cancelClose(); openedByClick = false; item.classList.remove("is-open"); button.setAttribute("aria-expanded", "false"); menu.setAttribute("aria-hidden", "true"); syncDropdownLayer(); }; const scheduleClose = (event) => { if (openedByClick && event?.currentTarget === item) return; const nextTarget = event?.relatedTarget; if (nextTarget instanceof Node && item.contains(nextTarget)) return; cancelClose(); closeTimer = window.setTimeout(() => { closeTimer = 0; if (item.matches(":hover") || menu.matches(":hover") || item.contains(document.activeElement)) return; close(); }, 300); }; item.addEventListener("mouseenter", open); item.addEventListener("mouseleave", scheduleClose); menu.addEventListener("mouseenter", open); menu.addEventListener("mouseleave", scheduleClose); item.addEventListener("focusout", scheduleClose); button.addEventListener("click", (event) => { event.stopPropagation(); document.body.classList.add("nav-ready"); cancelClose(); if (item.classList.contains("is-open")) { close(); } else { openedByClick = true; open(event); } }); }); document.addEventListener("click", (event) => { if (header?.classList.contains("nav-open") && !event.target.closest("#site-header")) { header.classList.remove("nav-open"); document.body.classList.remove("nav-menu-open"); toggle?.setAttribute("aria-expanded", "false"); toggle?.setAttribute("aria-label", "Open navigation"); } if (!event.target.closest("#site-header")) { closeDropdowns(); } }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeDropdowns(); header?.classList.remove("nav-open"); document.body.classList.remove("nav-menu-open"); toggle?.setAttribute("aria-expanded", "false"); toggle?.setAttribute("aria-label", "Open navigation"); } }); document.querySelectorAll(".main-nav a").forEach((link) => { link.addEventListener("click", () => { header?.classList.remove("nav-open"); document.body.classList.remove("nav-menu-open"); toggle?.setAttribute("aria-expanded", "false"); closeDropdowns(); }); }); } function closeDropdowns(except = null) { document.querySelectorAll(".has-dropdown").forEach((item) => { if (item === except) return; const button = item.querySelector(".nav-button"); const menu = item.querySelector(".mega-menu"); item.classList.remove("is-open"); button?.setAttribute("aria-expanded", "false"); menu?.setAttribute("aria-hidden", "true"); }); syncDropdownLayer(); } function syncDropdownLayer() { document.body.classList.toggle("nav-dropdown-open", Boolean(document.querySelector(".has-dropdown.is-open"))); } function setupHeroMotion() { const hero = document.querySelector(".hero-section"); if (!hero) return; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); let ticking = false; const update = () => { if (reduceMotion.matches) { hero.style.setProperty("--scroll-shift", "0px"); hero.style.setProperty("--scroll-ratio", "0"); ticking = false; return; } const rect = hero.getBoundingClientRect(); const distance = Math.min(hero.offsetHeight, Math.max(0, -rect.top)); const ratio = distance / Math.max(hero.offsetHeight, 1); hero.style.setProperty("--scroll-shift", `${Math.round(ratio * 170)}px`); hero.style.setProperty("--scroll-ratio", ratio.toFixed(3)); ticking = false; }; const requestUpdate = () => { if (ticking) return; ticking = true; requestAnimationFrame(update); }; update(); window.addEventListener("scroll", requestUpdate, { passive: true }); window.addEventListener("resize", requestUpdate); reduceMotion.addEventListener?.("change", requestUpdate); } function setupHeadlineRotator() { const accent = document.querySelector(".headline-accent"); if (!accent) return; const configuredWords = String(accent.dataset.words || "") .split("|") .map((word) => word.trim()) .filter(Boolean); const words = configuredWords.length ? configuredWords : ["SYSTEMS", "TECHNOLOGY", "SOLUTIONS", "BRANDS", "WORKFLOWS", "SIGNALS", "WEBSITES", "AI"]; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); let index = Math.max(0, words.indexOf(accent.textContent.trim())); let intervalId; const setWord = () => { index = (index + 1) % words.length; if (reduceMotion.matches) { accent.textContent = words[index]; return; } accent.classList.add("is-changing"); window.setTimeout(() => { accent.textContent = words[index]; accent.classList.remove("is-changing"); }, 260); }; const start = () => { window.clearInterval(intervalId); intervalId = window.setInterval(setWord, 3000); }; start(); reduceMotion.addEventListener?.("change", start); } function setupStatsCounter() { const statsBand = document.querySelector(".stats-band"); const numbers = document.querySelectorAll(".stat-number"); if (!statsBand || numbers.length === 0) return; const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); let hasRun = false; const formatNumber = (value, suffix) => `${Math.round(value)}${suffix}`; const setFinalValues = () => { numbers.forEach((number) => { const target = Number(number.dataset.count || 0); const suffix = number.dataset.suffix || ""; number.textContent = formatNumber(target, suffix); }); statsBand.classList.add("is-counting"); }; const animate = () => { if (hasRun) return; hasRun = true; if (reduceMotion.matches) { setFinalValues(); return; } statsBand.classList.add("is-counting"); const duration = 1700; const startTime = performance.now(); const tick = (now) => { const progress = Math.min((now - startTime) / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); numbers.forEach((number) => { const target = Number(number.dataset.count || 0); const suffix = number.dataset.suffix || ""; number.textContent = formatNumber(target * eased, suffix); }); if (progress < 1) { requestAnimationFrame(tick); } else { setFinalValues(); } }; requestAnimationFrame(tick); }; if (!("IntersectionObserver" in window)) { animate(); return; } const observer = new IntersectionObserver((entries) => { if (!entries.some((entry) => entry.isIntersecting)) return; animate(); observer.disconnect(); }, { threshold: 0.35 }); observer.observe(statsBand); } function setupMethodShowcase() { const showcase = document.querySelector("[data-method-showcase]"); if (!showcase) return; const tabs = Array.from(showcase.querySelectorAll("[data-method-tab]")); const panels = Array.from(showcase.querySelectorAll("[data-method-panel]")); const defaultColors = { strategy: "#3f91a6", design: "#20b8c4", build: "#e0448b", automate: "#2b7489", scale: "#6b8f3a", }; const colors = tabs.reduce((map, tab) => { const key = tab.dataset.methodTab; const color = normalizeMethodColor(tab.dataset.methodColor, defaultColors[key] || defaultColors.strategy); if (key) map[key] = color; return map; }, { ...defaultColors }); if (tabs.length === 0 || panels.length === 0) return; const show = (key) => { showcase.style.setProperty("--active-method", colors[key] || colors.strategy); tabs.forEach((tab) => { const isActive = tab.dataset.methodTab === key; tab.classList.toggle("is-active", isActive); tab.setAttribute("aria-selected", String(isActive)); }); panels.forEach((panel) => { const isActive = panel.dataset.methodPanel === key; panel.classList.toggle("is-active", isActive); panel.hidden = !isActive; }); }; tabs.forEach((tab, index) => { tab.addEventListener("click", () => show(tab.dataset.methodTab)); tab.addEventListener("keydown", (event) => { if (!["ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight"].includes(event.key)) return; event.preventDefault(); const direction = event.key === "ArrowUp" || event.key === "ArrowLeft" ? -1 : 1; const nextIndex = (index + direction + tabs.length) % tabs.length; tabs[nextIndex].focus(); show(tabs[nextIndex].dataset.methodTab); }); }); const activeTab = tabs.find((tab) => tab.classList.contains("is-active")) || tabs[0]; show(activeTab.dataset.methodTab); } function setupWorkTestimonials() { const slider = document.querySelector("[data-testimonials]"); if (!slider) return; const slides = Array.from(slider.querySelectorAll(".testimonial-quote")); const dots = Array.from(slider.querySelectorAll("[data-testimonial-dot]")); const prevButton = slider.querySelector("[data-testimonial-prev]"); const nextButton = slider.querySelector("[data-testimonial-next]"); const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); if (slides.length === 0) return; let index = slides.findIndex((slide) => slide.classList.contains("is-active")); let timerId; index = index < 0 ? 0 : index; const show = (nextIndex) => { slides[index]?.classList.remove("is-active"); dots[index]?.classList.remove("is-active"); index = (nextIndex + slides.length) % slides.length; slides[index]?.classList.add("is-active"); dots[index]?.classList.add("is-active"); }; const start = () => { window.clearInterval(timerId); if (reduceMotion.matches || slides.length < 2) return; timerId = window.setInterval(() => show(index + 1), 5200); }; prevButton?.addEventListener("click", () => { show(index - 1); start(); }); nextButton?.addEventListener("click", () => { show(index + 1); start(); }); dots.forEach((dot, dotIndex) => { dot.addEventListener("click", () => { show(dotIndex); start(); }); }); slider.addEventListener("mouseenter", () => window.clearInterval(timerId)); slider.addEventListener("mouseleave", start); reduceMotion.addEventListener?.("change", start); show(index); start(); } function setupContactModal() { const modal = document.getElementById("contact-modal"); const closeButton = document.getElementById("contact-close"); const backdrop = document.getElementById("contact-backdrop"); const form = document.getElementById("contact-form"); if (!modal || !closeButton || !backdrop) return; const contactNotice = form ? ensureContactNotice(form) : null; const submitButton = form?.querySelector(".submit-contact"); const defaultSubmitText = submitButton?.textContent || "Schedule a free consultation"; const clearSubmissionFeedback = () => { hideContactNotice(contactNotice); form?.classList.remove("is-submitted"); if (submitButton && !submitButton.disabled) { submitButton.textContent = defaultSubmitText; } }; const open = (event) => { event?.preventDefault(); clearSubmissionFeedback(); modal.classList.add("is-open"); modal.setAttribute("aria-hidden", "false"); document.body.classList.add("modal-open"); }; const close = () => { modal.classList.remove("is-open"); modal.setAttribute("aria-hidden", "true"); document.body.classList.remove("modal-open"); }; document.addEventListener("click", (event) => { const opener = event.target.closest("[data-contact-open]"); if (opener) open(event); }); closeButton.addEventListener("click", close); backdrop.addEventListener("click", close); form?.addEventListener("input", clearSubmissionFeedback); form?.addEventListener("change", clearSubmissionFeedback); form?.addEventListener("submit", async (event) => { event.preventDefault(); let submitted = false; hideContactNotice(contactNotice); form.classList.remove("is-submitted"); if (submitButton) { submitButton.disabled = true; submitButton.textContent = "Sending request..."; } try { await submitContactLead(form); submitted = true; form.classList.add("is-submitted"); form.reset(); showContactNotice( contactNotice, "success", "Your information has been sent.", "Thanks for reaching out. We'll review your request and reply within 24 hours." ); if (submitButton) { submitButton.textContent = "Information sent"; } } catch (error) { showContactNotice( contactNotice, "error", "We couldn't send that yet.", "Please check your connection and try again." ); if (submitButton) { submitButton.textContent = "Try again"; } } finally { window.setTimeout(() => { if (submitButton) { submitButton.disabled = false; submitButton.textContent = submitted ? "Send another request" : defaultSubmitText; } }, 1800); } }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && modal.classList.contains("is-open")) { close(); } }); } function ensureContactNotice(form) { const existing = form.querySelector("[data-contact-notice]"); if (existing) return existing; const notice = document.createElement("div"); notice.className = "contact-submit-notice"; notice.dataset.contactNotice = ""; notice.setAttribute("aria-live", "polite"); notice.setAttribute("aria-atomic", "true"); notice.hidden = true; const icon = document.createElement("span"); icon.className = "contact-submit-notice-icon"; icon.setAttribute("aria-hidden", "true"); const copy = document.createElement("span"); copy.className = "contact-submit-notice-copy"; const title = document.createElement("strong"); title.dataset.contactNoticeTitle = ""; const message = document.createElement("span"); message.dataset.contactNoticeMessage = ""; copy.append(title, message); notice.append(icon, copy); const footer = form.querySelector(".form-footer"); form.insertBefore(notice, footer || null); return notice; } function showContactNotice(notice, type, title, message) { if (!notice) return; const titleEl = notice.querySelector("[data-contact-notice-title]"); const messageEl = notice.querySelector("[data-contact-notice-message]"); if (titleEl) titleEl.textContent = title; if (messageEl) messageEl.textContent = message; notice.hidden = false; notice.classList.remove("is-success", "is-error", "is-visible"); notice.classList.add(type === "error" ? "is-error" : "is-success"); notice.setAttribute("role", type === "error" ? "alert" : "status"); const scheduleFrame = window.requestAnimationFrame ? window.requestAnimationFrame.bind(window) : ((callback) => window.setTimeout(callback, 0)); scheduleFrame(() => { notice.classList.add("is-visible"); }); } function hideContactNotice(notice) { if (!notice || notice.hidden) return; notice.classList.remove("is-visible"); notice.hidden = true; notice.removeAttribute("role"); } async function submitContactLead(form) { const formData = new FormData(form); const payload = { name: formData.get("name"), email: formData.get("email"), company: formData.get("company"), country: formData.get("country"), phone: formData.get("phone"), message: formData.get("message"), sourcePage: document.title, sourceUrl: window.location.href, userAgent: navigator.userAgent }; if (!window.fetch || !/^https?:$/i.test(window.location.protocol)) { saveLeadFallback(payload); return; } try { const response = await fetch("api/leads", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!response.ok) { throw new Error("Lead submission failed"); } } catch (error) { saveLeadFallback(payload); } } function saveLeadFallback(payload) { const key = "signalhgai.local.leads.v1"; const leads = JSON.parse(window.localStorage.getItem(key) || "[]"); leads.unshift({ ...payload, id: `local-${Date.now()}`, status: "active", createdAt: new Date().toISOString() }); window.localStorage.setItem(key, JSON.stringify(leads)); } function setupVideoModal() { const modal = document.getElementById("video-modal"); const openButtons = document.querySelectorAll("[data-video-open]"); const closeButton = document.getElementById("modal-close"); const backdrop = document.getElementById("modal-backdrop"); if (!modal || openButtons.length === 0 || !closeButton || !backdrop) return; const open = (event) => { event?.preventDefault(); modal.classList.add("is-open"); modal.setAttribute("aria-hidden", "false"); document.body.classList.add("modal-open"); }; const close = () => { modal.classList.remove("is-open"); modal.setAttribute("aria-hidden", "true"); document.body.classList.remove("modal-open"); }; openButtons.forEach((button) => { button.addEventListener("click", open); }); closeButton.addEventListener("click", close); backdrop.addEventListener("click", close); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && modal.classList.contains("is-open")) { close(); } }); } function setupFooterSubscribe() { document.querySelectorAll("[data-footer-subscribe]").forEach((form) => { const note = form.querySelector("[data-footer-subscribe-note]"); const button = form.querySelector("button"); form.addEventListener("submit", (event) => { event.preventDefault(); if (note) { note.textContent = "Thanks. Your monthly signal is on the list."; } if (button) { button.textContent = "Sent"; } }); }); }
Close