PageSourceSearch

https://veza.com/wp-content/themes/veza/assets/js/forminator-form-listener.js?ver=1790267423

js veza.com collected 2026-09-25 14:12:29 UTC 77,105 bytes, 1,459 lines download raw bytes

1/**
2 * Forminator Form Submission Listener
3 * Listens for form submissions and logs form ID to console
4 */
5
6(function() {
7    'use strict';
8
9    // Score mapping for legacy NIST-style Likert assessment (5 pts per question)
10    const scoreMapping = {
11        "Never": 1,
12        "Rarely": 2,
13        "Sometimes": 3,
14        "Mostly": 4,
15        "Comprehensively": 5,
16    };
17
18    /** RSA GenAI assessment (quiz.json): option slug → points */
19    const rsaGenaiOptionScores = {
20        'q1-yes': 0,
21        'q1-yes-stripped': 2,
22        'q1-yes-approved': 2.5,
23        'q1-never': 2.5,
24        'q2-aware': 2.5,
25        'q2-general-idea': 1.5,
26        'q2-assume-public': 0,
27        'q2-no': 0,
28        'q3-complete': 2,
29        'q3-none': 0,
30        'q3-unsure': 1,
31        'q4-allow': 0,
32        'q4-check-allow': 1,
33        'q4-deny-write': 1.5,
34        'q5-minimal': 0.5,
35        'q5-mild': 1,
36        'q5-significant': 1.5,
37    };
38
39    const RSA_GENAI_MAX_TOTAL = 10;
40
41    function isRsaGenaiAssessmentForm(form) {
42        if (!form || !form.querySelector) return false;
43        return Boolean(form.querySelector('input[type="radio"][value^="q1-"]'));
44    }
45
46    function getRsaGenaiQuestionMaxScore(questionText) {
47        if (!questionText) return 5;
48        const rules = [
49            ['pasted internal data into a GenAI', 2.5],
50            ['official policy regarding which Generative AI tools', 2.5],
51            ['How much visibility do you believe your security and IT teams', 2],
52            ['great GenAI task tracking app', 1.5],
53            ['disabled public LLM access tomorrow', 1.5],
54        ];
55        for (let i = 0; i < rules.length; i++) {
56            if (questionText.includes(rules[i][0])) return rules[i][1];
57        }
58        return 5;
59    }
60
61    function resolveRadioScore(input, selectedLabel) {
62        const value = input && input.value ? String(input.value) : '';
63        if (Object.prototype.hasOwnProperty.call(rsaGenaiOptionScores, value)) {
64            return rsaGenaiOptionScores[value];
65        }
66        const numeric = parseFloat(value);
67        if (value !== '' && !Number.isNaN(numeric) && /^-?[0-9]+(\.[0-9]+)?$/.test(value.trim())) {
68            return numeric;
69        }
70        if (selectedLabel) {
71            for (const [option, pts] of Object.entries(scoreMapping)) {
72                if (selectedLabel.includes(option)) return pts;
73            }
74        }
75        const fallback = parseFloat(value);
76        return Number.isNaN(fallback) ? 1 : fallback;
77    }
78
79    // Hide the hero/left column in the Marketo gated block
80    function hideMarketoHeroColumn(form) {
81        try {
82            const marketoBlock = form.closest('.wp-block-veza-blocks-vz-marketo-form-gated');
83            if (!marketoBlock) return;
84            const columnsContainer = marketoBlock.closest('.wp-block-columns');
85            if (!columnsContainer) return;
86            const firstColumn = columnsContainer.querySelector('.wp-block-column:first-child');
87            if (firstColumn && firstColumn.style.display !== 'none') {
88                firstColumn.style.display = 'none';
89            }
90        } catch (_) {}
91    }
92
93    // After submission, hide hero when the container resizes next
94    function installHideHeroOnResize(form) {
95        try {
96            const marketoBlock = form.closest('.wp-block-veza-blocks-vz-marketo-form-gated');
97            if (!marketoBlock) return;
98            const columnsContainer = marketoBlock.closest('.wp-block-columns');
99            if (!columnsContainer) return;
100
101            if (form._vzHideHeroObserver) return; // already installed
102
103            const observer = new ResizeObserver(() => {
104                if (form.dataset.hideHeroOnResize === 'true') {
105                    hideMarketoHeroColumn(form);
106                    observer.disconnect();
107                    delete form._vzHideHeroObserver;
108                }
109            });
110            observer.observe(columnsContainer);
111            form._vzHideHeroObserver = observer;
112        } catch (_) {}
113    }
114
115    // Function to capture form data before rows are hidden
116    function captureFormData(form) {
117        const formData = {
118            selectedInputs: [],
119            questionGroups: new Map(),
120            maxTotalScore: 0
121        };
122
123        const rsaGenaiForm = isRsaGenaiAssessmentForm(form);
124
125        // Get all radio inputs to calculate max possible scores
126        const allRadioInputs = form.querySelectorAll('input[type="radio"]');
127        const selectedInputs = form.querySelectorAll('input[type="radio"]:checked');
128        
129        console.log('Form structure debug:');
130        console.log('Total radio inputs found:', allRadioInputs.length);
131        console.log('Selected radio inputs found:', selectedInputs.length);
132        console.log('Form HTML:', form.outerHTML);
133        
134        // Debug: Log each radio input
135        allRadioInputs.forEach((input, index) => {
136            console.log(`Radio input ${index}:`, {
137                checked: input.checked,
138                value: input.value,
139                name: input.name,
140                parentLabel: input.closest('label')?.textContent?.trim(),
141                questionRow: input.closest('.forminator-row')?.outerHTML
142            });
143        });
144
145        // Group questions by their container structure with proper pillar mapping
146        allRadioInputs.forEach(input => {
147            const questionRow = input.closest('.forminator-row');
148            if (questionRow) {
149                // Try to get the actual question text from the forminator label
150                const questionText = questionRow.querySelector('.forminator-label')?.textContent?.trim() ||
151                                   questionRow.querySelector('.forminator-field-radio .forminator-field-label')?.textContent?.trim() ||
152                                   questionRow.querySelector('.forminator-field-label')?.textContent?.trim() ||
153                                   questionRow.querySelector('label')?.textContent?.trim();
154                
155                if (questionText) {
156                    // Map questions to their correct pillars based on the original HTML structure
157                    let pillar = 'Identity Security';
158                    let subArea = 'General';
159                    if (rsaGenaiForm) {
160                        pillar = 'GenAI Security';
161                        subArea = 'Assessment';
162                    } else if (questionText.includes('What risk dashboards do you use to monitor privilege concentration and toxic combinations?') ||
163                               questionText.includes('usage-based data') ||
164                               questionText.includes('anomalies in access usage') ||
165                               questionText.includes('refine and optimize roles') ||
166                               questionText.includes('simulate attack paths') ||
167                               questionText.includes('prioritizing critical access risk')) {
168                        pillar = 'Intelligence & Analytics';
169                        if (questionText.includes('What risk dashboards do you use to monitor privilege concentration and toxic combinations?')) {
170                            subArea = 'Access Dashboards';
171                        } else if (questionText.includes('usage-based') || questionText.includes('anomalies')) {
172                            subArea = 'Access Intelligence';
173                        } else if (questionText.includes('refine and optimize roles')) {
174                            subArea = 'Intelligent Role Optimization';
175                        } else if (questionText.includes('simulate attack paths')) {
176                            subArea = 'Proactive Risk Modeling';
177                        } else if (questionText.includes('prioritizing critical access risk')) {
178                            subArea = 'Risk Prioritization';
179                        }
180                    } else if (questionText.includes('inventory of all identities') || 
181                        questionText.includes('dormant accounts') || 
182                        questionText.includes('emergency/break glass accounts') ||
183                        questionText.includes('permission inheritance') ||
184                        questionText.includes('service accounts and system accounts') ||
185                        questionText.includes('business purpose and risk justification') ||
186                        questionText.includes('toxic combinations') ||
187                        questionText.includes('enterprise-wide permission catalog') ||
188                        questionText.includes('cross-platform permission relationships')) {
189                        pillar = 'Visibility';
190                        if (questionText.includes('inventory') || questionText.includes('dormant') || questionText.includes('emergency')) {
191                            subArea = 'Identity Hygiene';
192                        } else if (questionText.includes('permission inheritance') || questionText.includes('service accounts')) {
193                            subArea = 'Access Graph';
194                        } else if (questionText.includes('business purpose') || questionText.includes('toxic combinations')) {
195                            subArea = 'Priority Access';
196                        } else if (questionText.includes('enterprise-wide') || questionText.includes('cross-platform')) {
197                            subArea = 'Comprehensive Mapping';
198                        }
199                    } else if (questionText.includes('real-time alerts') ||
200                               questionText.includes('continuously monitor') ||
201                               questionText.includes('correlate access events') ||
202                               questionText.includes('blast radius') ||
203                               questionText.includes('compliance requirements in real time')) {
204                        pillar = 'Monitoring & Orchestration';
205                        if (questionText.includes('real-time alerts')) {
206                            subArea = 'Privilege Elevation Alerts';
207                        } else if (questionText.includes('continuously monitor')) {
208                            subArea = 'Access Monitoring';
209                        } else if (questionText.includes('correlate access events')) {
210                            subArea = 'Cross-Platform Correlation';
211                        } else if (questionText.includes('blast radius')) {
212                            subArea = 'Blast Radius';
213                        } else if (questionText.includes('compliance requirements')) {
214                            subArea = 'Compliance Monitoring';
215                        }
216                    } else if (questionText.includes('periodic access reviews') ||
217                               questionText.includes('usage-based and risk-prioritized') ||
218                               questionText.includes('self-service requests') ||
219                               questionText.includes('onboarding, transfers, and offboarding') ||
220                               questionText.includes('automated triggers to remove')) {
221                        pillar = 'Workflows';
222                        if (questionText.includes('periodic access reviews') || questionText.includes('usage-based and risk-prioritized')) {
223                            subArea = 'User Access Reviews';
224                        } else if (questionText.includes('self-service requests')) {
225                            subArea = 'Self-Service Access';
226                        } else if (questionText.includes('onboarding, transfers, and offboarding')) {
227                            subArea = 'Provisioning/Deprovisioning';
228                        } else if (questionText.includes('automated triggers to remove')) {
229                            subArea = 'Automated Remediation';
230                        }
231                    } else if (questionText.includes('report on access certifications') ||
232                               questionText.includes('unified privilege elevation controls') ||
233                               questionText.includes('Just-in-Time (JIT) access') ||
234                               questionText.includes('MFA coverage for privilege accounts') ||
235                               questionText.includes('role composition and SoC conflicts') ||
236                               questionText.includes('control effectiveness for audits')) {
237                        pillar = 'Controls';
238                        if (questionText.includes('report on access certifications')) {
239                            subArea = 'Compliance Reporting';
240                        } else if (questionText.includes('unified privilege elevation controls')) {
241                            subArea = 'Privileged Access Assurance';
242                        } else if (questionText.includes('Just-in-Time (JIT) access')) {
243                            subArea = 'JIT Access';
244                        } else if (questionText.includes('MFA coverage')) {
245                            subArea = 'MFA';
246                        } else if (questionText.includes('role composition and SoC conflicts')) {
247                            subArea = 'Role Assignment';
248                        } else if (questionText.includes('control effectiveness for audits')) {
249                            subArea = 'Compliance Assurance';
250                        }
251                    }
252                    
253                    if (!formData.questionGroups.has(pillar)) {
254                        formData.questionGroups.set(pillar, []);
255                    }
256                    
257                    // Check if this question is already added to avoid duplicates
258                    const existingQuestions = formData.questionGroups.get(pillar);
259                    const questionExists = existingQuestions.some(q => q.text === questionText);
260                    
261                    if (!questionExists) {
262                        existingQuestions.push({
263                            text: questionText,
264                            maxScore: rsaGenaiForm ? getRsaGenaiQuestionMaxScore(questionText) : 5,
265                            subArea: subArea
266                        });
267                        console.log(`Added question to ${pillar}: "${questionText}"`);
268                    } else {
269                        console.log(`Question already exists in ${pillar}: "${questionText}"`);
270                    }
271                }
272            }
273        });
274        
275        // Debug: Log what we found in question groups
276        console.log('Question groups found:', formData.questionGroups);
277        console.log('Question groups size:', formData.questionGroups.size);
278        console.log('Question groups type:', typeof formData.questionGroups);
279        console.log('Question groups constructor:', formData.questionGroups.constructor.name);
280        
281        // Test Map functionality
282        console.log('Testing Map functionality:');
283        console.log('formData.questionGroups instanceof Map:', formData.questionGroups instanceof Map);
284        console.log('formData.questionGroups.has("Visibility"):', formData.questionGroups.has("Visibility"));
285        console.log('formData.questionGroups.get("Visibility"):', formData.questionGroups.get("Visibility"));
286        
287        // Debug: Log each question and its mapped pillar
288        if (formData.questionGroups && typeof formData.questionGroups.forEach === 'function') {
289            formData.questionGroups.forEach((questions, pillar) => {
290                console.log(`Pillar: ${pillar}`);
291                questions.forEach(q => {
292                    console.log(`  - ${q.text} (${q.subArea})`);
293                });
294            });
295        } else {
296            console.log('Question groups is not iterable or empty');
297        }
298
299        // Capture selected responses
300        selectedInputs.forEach(input => {
301            const questionRow = input.closest('.forminator-row');
302            if (questionRow) {
303                // Try to get the actual question text from the forminator label
304                const questionText = questionRow.querySelector('.forminator-label')?.textContent?.trim() ||
305                                   questionRow.querySelector('.forminator-field-radio .forminator-field-label')?.textContent?.trim() ||
306                                   questionRow.querySelector('.forminator-field-label')?.textContent?.trim() ||
307                                   questionRow.querySelector('label')?.textContent?.trim();
308                
309                if (questionText) {
310                    // Use the same pillar mapping logic as above
311                    let pillar = 'Identity Security';
312                    let subArea = 'General';
313                    if (rsaGenaiForm) {
314                        pillar = 'GenAI Security';
315                        subArea = 'Assessment';
316                    } else if (questionText.includes('What risk dashboards do you use to monitor privilege concentration and toxic combinations?') ||
317                               questionText.includes('usage-based data') ||
318                               questionText.includes('anomalies in access usage') ||
319                               questionText.includes('refine and optimize roles') ||
320                               questionText.includes('simulate attack paths') ||
321                               questionText.includes('prioritizing critical access risk')) {
322                        pillar = 'Intelligence & Analytics';
323                        if (questionText.includes('What risk dashboards do you use to monitor privilege concentration and toxic combinations?')) {
324                            subArea = 'Access Dashboards';
325                        } else if (questionText.includes('usage-based') || questionText.includes('anomalies')) {
326                            subArea = 'Access Intelligence';
327                        } else if (questionText.includes('refine and optimize roles')) {
328                            subArea = 'Intelligent Role Optimization';
329                        } else if (questionText.includes('simulate attack paths')) {
330                            subArea = 'Proactive Risk Modeling';
331                        } else if (questionText.includes('prioritizing critical access risk')) {
332                            subArea = 'Risk Prioritization';
333                        }
334                    } else if (questionText.includes('inventory of all identities') || 
335                        questionText.includes('dormant accounts') || 
336                        questionText.includes('emergency/break glass accounts') ||
337                        questionText.includes('permission inheritance') ||
338                        questionText.includes('service accounts and system accounts') ||
339                        questionText.includes('business purpose and risk justification') ||
340                        questionText.includes('toxic combinations') ||
341                        questionText.includes('enterprise-wide permission catalog') ||
342                        questionText.includes('cross-platform permission relationships')) {
343                        pillar = 'Visibility';
344                        if (questionText.includes('inventory') || questionText.includes('dormant') || questionText.includes('emergency')) {
345                            subArea = 'Identity Hygiene';
346                        } else if (questionText.includes('permission inheritance') || questionText.includes('service accounts')) {
347                            subArea = 'Access Graph';
348                        } else if (questionText.includes('business purpose') || questionText.includes('toxic combinations')) {
349                            subArea = 'Priority Access';
350                        } else if (questionText.includes('enterprise-wide') || questionText.includes('cross-platform')) {
351                            subArea = 'Comprehensive Mapping';
352                        }
353                    } else if (questionText.includes('real-time alerts') ||
354                               questionText.includes('continuously monitor') ||
355                               questionText.includes('correlate access events') ||
356                               questionText.includes('blast radius') ||
357                               questionText.includes('compliance requirements in real time')) {
358                        pillar = 'Monitoring & Orchestration';
359                        if (questionText.includes('real-time alerts')) {
360                            subArea = 'Privilege Elevation Alerts';
361                        } else if (questionText.includes('continuously monitor')) {
362                            subArea = 'Access Monitoring';
363                        } else if (questionText.includes('correlate access events')) {
364                            subArea = 'Cross-Platform Correlation';
365                        } else if (questionText.includes('blast radius')) {
366                            subArea = 'Blast Radius';
367                        } else if (questionText.includes('compliance requirements')) {
368                            subArea = 'Compliance Monitoring';
369                        }
370                    } else if (questionText.includes('periodic access reviews') ||
371                               questionText.includes('usage-based and risk-prioritized') ||
372                               questionText.includes('self-service requests') ||
373                               questionText.includes('onboarding, transfers, and offboarding') ||
374                               questionText.includes('automated triggers to remove')) {
375                        pillar = 'Workflows';
376                        if (questionText.includes('periodic access reviews') || questionText.includes('usage-based and risk-prioritized')) {
377                            subArea = 'User Access Reviews';
378                        } else if (questionText.includes('self-service requests')) {
379                            subArea = 'Self-Service Access';
380                        } else if (questionText.includes('onboarding, transfers, and offboarding')) {
381                            subArea = 'Provisioning/Deprovisioning';
382                        } else if (questionText.includes('automated triggers to remove')) {
383                            subArea = 'Automated Remediation';
384                        }
385                    } else if (questionText.includes('report on access certifications') ||
386                               questionText.includes('unified privilege elevation controls') ||
387                               questionText.includes('Just-in-Time (JIT) access') ||
388                               questionText.includes('MFA coverage for privilege accounts') ||
389                               questionText.includes('role composition and SoC conflicts') ||
390                               questionText.includes('control effectiveness for audits')) {
391                        pillar = 'Controls';
392                        if (questionText.includes('report on access certifications')) {
393                            subArea = 'Compliance Reporting';
394                        } else if (questionText.includes('unified privilege elevation controls')) {
395                            subArea = 'Privileged Access Assurance';
396                        } else if (questionText.includes('Just-in-Time (JIT) access')) {
397                            subArea = 'JIT Access';
398                        } else if (questionText.includes('MFA coverage')) {
399                            subArea = 'MFA';
400                        } else if (questionText.includes('role composition and SoC conflicts')) {
401                            subArea = 'Role Assignment';
402                        } else if (questionText.includes('control effectiveness for audits')) {
403                            subArea = 'Compliance Assurance';
404                        }
405                    }
406                    
407                    const selectedLabel = input.closest('label')?.textContent?.trim();
408                    const score = resolveRadioScore(input, selectedLabel);
409                    
410                    formData.selectedInputs.push({
411                        questionText,
412                        pillar,
413                        subArea,
414                        score,
415                        selectedLabel
416                    });
417                    
418                    // Debug: Log the question text being captured
419                    console.log(`Captured question: "${questionText}" with score ${score} (${selectedLabel})`);
420                }
421            }
422        });
423
424        // Alternative approach: Try to get form data from FormData or form submission
425        console.log('Trying alternative form data capture...');
426        
427        // Check if there's a FormData object available
428        try {
429            const formDataObj = new FormData(form);
430            console.log('FormData entries:');
431            for (let [key, value] of formDataObj.entries()) {
432                console.log(`${key}: ${value}`);
433            }
434        } catch (e) {
435            console.log('FormData not available:', e);
436        }
437        
438        // Check if there are any hidden inputs with form data
439        const hiddenInputs = form.querySelectorAll('input[type="hidden"]');
440        console.log('Hidden inputs found:', hiddenInputs.length);
441        hiddenInputs.forEach((input, index) => {
442            console.log(`Hidden input ${index}:`, {
443                name: input.name,
444                value: input.value
445            });
446        });
447        
448        // Check if this is a test form or if the user actually selected options
449        if (formData.selectedInputs.length === 0) {
450            console.log('WARNING: No selected inputs found! This suggests:');
451            console.log('1. The form was submitted without user selections');
452            console.log('2. The form structure is different than expected');
453            console.log('3. The form is being submitted programmatically');
454            console.log('4. The user needs to actually select radio button options');
455            
456            // Try to find ANY form inputs that might contain data
457            const allInputs = form.querySelectorAll('input, select, textarea');
458            console.log('All form inputs found:', allInputs.length);
459            allInputs.forEach((input, index) => {
460                console.log(`Input ${index}:`, {
461                    type: input.type,
462                    name: input.name,
463                    value: input.value,
464                    checked: input.checked,
465                    tagName: input.tagName
466                });
467            });
468            
469            // Check if this might be a different type of form
470            const formType = form.getAttribute('data-form-type') || 'unknown';
471            console.log('Form type attribute:', formType);
472        }
473
474        // Calculate max scores
475        if (rsaGenaiForm) {
476            formData.maxTotalScore = RSA_GENAI_MAX_TOTAL;
477        } else {
478            formData.questionGroups.forEach((questions) => {
479                formData.maxTotalScore += questions.reduce((sum, q) => sum + (q.maxScore != null ? q.maxScore : 5), 0);
480            });
481        }
482        
483        // If no question groups were found, calculate based on unique questions
484        if (formData.maxTotalScore === 0) {
485            // Count unique questions by their names (q1, q2, q3, etc.)
486            const uniqueQuestions = new Set();
487            allRadioInputs.forEach(input => {
488                if (input.name) {
489                    uniqueQuestions.add(input.name);
490                }
491            });
492            formData.maxTotalScore = uniqueQuestions.size * 5;
493            console.log(`Calculated max score from unique questions: ${uniqueQuestions.size} questions × 5 = ${formData.maxTotalScore}`);
494        }
495
496        console.log('Captured form data:', formData);
497        return formData;
498    }
499
500    // Function to calculate scores from captured form data
501    function calculateScores(form, formData) {
502        console.log('Calculating scores from captured data:', formData);
503        
504        if (!formData || !formData.selectedInputs) {
505            console.log('No form data provided, returning default scores');
506            return {
507                totalScore: 0,
508                maxTotalScore: 0,
509                pillarScores: {},
510                maxPillarScores: {},
511                subAreaScores: {},
512                maxSubAreaScores: {}
513            };
514        }
515
516        let totalScore = 0;
517        const pillarScores = {};
518        const maxPillarScores = {};
519        const subAreaScores = {};
520        const maxSubAreaScores = {};
521
522        // Calculate max scores from question groups
523        // Handle both Map objects and plain objects (from JSON parsing)
524        const perQuestionMax = q => (q.maxScore != null ? q.maxScore : 5);
525
526        if (formData.questionGroups && typeof formData.questionGroups.forEach === 'function') {
527            // It's a Map object
528            formData.questionGroups.forEach((questions, pillar) => {
529                maxPillarScores[pillar] = questions.reduce((sum, q) => sum + perQuestionMax(q), 0);
530                pillarScores[pillar] = 0;
531
532                questions.forEach(q => {
533                    const subArea = q.subArea;
534                    maxSubAreaScores[subArea] = (maxSubAreaScores[subArea] || 0) + perQuestionMax(q);
535                    if (!subAreaScores[subArea]) subAreaScores[subArea] = 0;
536                });
537            });
538        } else if (formData.questionGroups && typeof formData.questionGroups === 'object') {
539            // It's a plain object (from JSON parsing)
540            Object.entries(formData.questionGroups).forEach(([pillar, questions]) => {
541                maxPillarScores[pillar] = questions.reduce((sum, q) => sum + perQuestionMax(q), 0);
542                pillarScores[pillar] = 0;
543
544                questions.forEach(q => {
545                    const subArea = q.subArea;
546                    maxSubAreaScores[subArea] = (maxSubAreaScores[subArea] || 0) + perQuestionMax(q);
547                    if (!subAreaScores[subArea]) subAreaScores[subArea] = 0;
548                });
549            });
550        } else {
551            // Fallback: create default pillar structure from selected inputs
552            console.log('No question groups found, creating default structure from selected inputs');
553            const uniquePillars = [...new Set(formData.selectedInputs.map(input => input.pillar))];
554            uniquePillars.forEach(pillar => {
555                const pillarQuestions = formData.selectedInputs.filter(input => input.pillar === pillar);
556                maxPillarScores[pillar] = pillarQuestions.length * 5;
557                pillarScores[pillar] = 0;
558            });
559        }
560        
561        // Debug: Log the pillar scores calculation
562        console.log('Max pillar scores calculated:', maxPillarScores);
563        console.log('Pillar scores initialized:', pillarScores);
564
565        // Calculate actual scores from captured responses
566        formData.selectedInputs.forEach(input => {
567            totalScore += input.score;
568            
569            if (pillarScores[input.pillar] !== undefined) {
570                pillarScores[input.pillar] += input.score;
571            } else {
572                pillarScores[input.pillar] = input.score;
573            }
574            
575            if (subAreaScores[input.subArea] !== undefined) {
576                subAreaScores[input.subArea] += input.score;
577            } else {
578                subAreaScores[input.subArea] = input.score;
579            }
580        });
581
582        const maxTotal = formData.maxTotalScore > 0 ? formData.maxTotalScore : 155;
583        const result = {
584            totalScore,
585            maxTotalScore: maxTotal,
586            pillarScores,
587            maxPillarScores,
588            subAreaScores,
589            maxSubAreaScores,
590            scoreScaleMax: maxTotal,
591        };
592
593        console.log('Calculated result:', result);
594        return result;
595    }
596
597    /**
598     * Risk bands: legacy assessment uses 0–155 scale; RSA GenAI quiz uses 0–10.
599     * @param {number} score
600     * @param {number} scaleMax
601     */
602    function getRiskCategory(score, scaleMax) {
603        const max = scaleMax > 0 ? scaleMax : 155;
604        const isShortForm = max <= 12;
605
606        if (isShortForm) {
607            if (score >= 8) {
608                return { name: 'Adaptive', color: '#40DB35', backgroundColor: '#E9FBE8', borderColor: '#40DB35' };
609            }
610            if (score >= 6) {
611                return { name: 'Repeatable', color: '#FFD728', backgroundColor: '#FFF8CC', borderColor: '#FFD728' };
612            }
613            if (score >= 4) {
614                return { name: 'Risk Informed', color: '#F29025', backgroundColor: '#FFE6C7', borderColor: '#F29025' };
615            }
616            return { name: 'Partial', color: '#F24C26', backgroundColor: '#FFE1DA', borderColor: '#F24C26' };
617        }
618
619        if (score >= 155) {
620            return {
621                name: 'Adaptive',
622                color: '#40DB35',
623                backgroundColor: '#E9FBE8',
624                borderColor: '#40DB35',
625            };
626        }
627        if (score >= 120) {
628            return {
629                name: 'Repeatable',
630                color: '#FFD728',
631                backgroundColor: '#FFF8CC',
632                borderColor: '#FFD728',
633            };
634        }
635        if (score >= 80) {
636            return {
637                name: 'Risk Informed',
638                color: '#F29025',
639                backgroundColor: '#FFE6C7',
640                borderColor: '#F29025',
641            };
642        }
643        return {
644            name: 'Partial',
645            color: '#F24C26',
646            backgroundColor: '#FFE1DA',
647            borderColor: '#F24C26',
648        };
649    }
650
651    // Function to create and display the scoring results
652    function displayScoringResults(form, formData) {
653        console.log('Displaying scoring results for form:', form);
654        
655        // Check if scoring results are already displayed to prevent duplicates
656        if (form.querySelector('.vz-scoring-results')) {
657            console.log('Scoring results already displayed, skipping');
658            return;
659        }
660        
661        const scores = calculateScores(form, formData);
662        console.log('Calculated scores:', scores);
663
664        const scaleMax = scores.scoreScaleMax != null ? scores.scoreScaleMax : scores.maxTotalScore;
665        const riskCategory = getRiskCategory(scores.totalScore, scaleMax);
666        const isShortForm = scaleMax > 0 && scaleMax <= 12;
667        const displayTotal = Math.round(scores.totalScore * 10) / 10;
668        const displayMax = scaleMax;
669        const scorePercentage = scaleMax > 0 ? Math.round((scores.totalScore / scaleMax) * 100) : 0;
670        const t = scores.totalScore;
671        const legendPartialActive = isShortForm ? t < 4 : t < 80;
672        const legendRiskInformedActive = isShortForm ? t >= 4 && t < 6 : t >= 80 && t < 120;
673        const legendRepeatableActive = isShortForm ? t >= 6 && t < 8 : t >= 120 && t < 155;
674        const legendAdaptiveActive = isShortForm ? t >= 8 : t >= 155;
675        const rangePartial = isShortForm ? '0-3.9' : '0-79';
676        const rangeRiskInformed = isShortForm ? '4-5.9' : '80-119';
677        const rangeRepeatable = isShortForm ? '6-7.9' : '120-154';
678        const rangeAdaptive = isShortForm ? '8-10' : '155+';
679        
680        const resp = form.querySelector('.forminator-response-message');
681        
682        if (!resp) {
683            console.log('No response message found, cannot display scoring results');
684            return;
685        }
686        
687        // Create the scoring results HTML with dashboard-style design
688        const scoringHtml = `
689            <div class="vz-scoring-results" style="margin-top: 2rem; padding: 0; background: #ffffff; border-radius: 1rem; border: 1px solid #e2e8f0; box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);">
690                <!-- Header Section -->
691                <div style="padding: 2rem 2rem 1rem; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-radius: 1rem 1rem 0 0; border-bottom: 1px solid #e2e8f0;">
692                    <h3 style="font-size: 1.75rem; font-weight: 800; color: #1e293b; margin: 0 0 0.5rem; text-align: center; letter-spacing: -0.025em;">
693                        Identity Security Assessment
694                </h3>
695                    <p style="font-size: 1rem; color: #64748b; margin: 0; text-align: center;">
696                        Your comprehensive security maturity evaluation
697                    </p>
698                </div>
699                
700                <!-- Main Score Dashboard -->
701                <div style="padding: 2rem; display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: center;">
702                    <!-- Score Display - Speedometer Style -->
703                    <div style="text-align: center;">
704                        <div style="position: relative; display: inline-block; margin-bottom: 1rem;">
705                            <!-- Modern Circular Progress -->
706                            <div style="position: relative; width: 200px; height: 200px; margin: 0 auto;">
707                                <!-- Background Circle -->
708                                <svg width="200" height="200" style="transform: rotate(-90deg);">
709                                    <circle cx="100" cy="100" r="80" 
710                                            stroke="#f1f5f9" 
711                                            stroke-width="8" 
712                                            fill="none"/>
713                                    
714                                    <!-- Progress Circle -->
715                                    <circle cx="100" cy="100" r="80" 
716                                            stroke="${riskCategory.color}" 
717                                            stroke-width="8" 
718                                            fill="none"
719                                            stroke-linecap="round"
720                                            stroke-dasharray="${2 * Math.PI * 80}"
721                                            stroke-dashoffset="${2 * Math.PI * 80 * (1 - scorePercentage / 100)}"
722                                            style="transition: stroke-dashoffset 2s cubic-bezier(0.4, 0, 0.2, 1);"/>
723                                </svg>
724                                
725                                <!-- Center Content -->
726                                <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;">
727                                    <div style="font-size: 3rem; font-weight: 800; color: ${riskCategory.color}; line-height: 1; transition: color 0.5s ease-in-out;">
728                                        ${displayTotal}
729                                    </div>
730                                    <div style="font-size: 1.125rem; color: #64748b; font-weight: 600; margin-bottom: 0.5rem;">
731                                        / ${displayMax}
732                                    </div>
733                                    <div style="font-size: 1rem; color: #64748b; font-weight: 500;">
734                                        ${scorePercentage}%
735                                    </div>
736                                </div>
737                            </div>
738                        </div>
739                    </div>
740                    
741                    <!-- Risk Category Card -->
742                    <div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 1rem; padding: 1.5rem; text-align: center;">
743                        <div style="margin-bottom: 1rem;">
744                            <div style="width: 10px; height: 10px; background: ${riskCategory.color}; border-radius: 50%; display: inline-block; margin-right: 0.5rem;"></div>
745                            <span style="font-size: 0.875rem; color: #64748b; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;">
746                                Risk Maturity Level
747                            </span>
748                        </div>
749                        <h4 style="font-size: 2rem; font-weight: 800; color: #1f2937; margin: 0 0 0.5rem; line-height: 1.1;">
750                            ${riskCategory.name}
751                        </h4>
752                        <p style="font-size: 0.875rem; color: #6b7280; margin: 0; line-height: 1.4;">
753                            ${riskCategory.name === 'Adaptive' ? 'Advanced security posture with continuous improvement' :
754                              riskCategory.name === 'Repeatable' ? 'Strong security controls with systematic management' :
755                              riskCategory.name === 'Risk Informed' ? 'Good security awareness with room for enhancement' :
756                              'Basic security measures, significant improvement opportunities'}
757                        </p>
758                    </div>
759                </div>
760
761                <!-- Risk Level Legend -->
762                <div style="padding: 0 2rem 2rem;">
763                    <div style="background: #f8fafc; border-radius: 0.75rem; padding: 1.5rem; border: 1px solid #e2e8f0;">
764                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
765                            <h5 style="font-size: 1.125rem; font-weight: 700; color: #1e293b; margin: 0;">
766                                Risk Maturity Scale
767                            </h5>
768                            <div style="display: flex; align-items: center; gap: 0.5rem;">
769                                <div style="width: 8px; height: 8px; background: ${riskCategory.color}; border-radius: 50%;"></div>
770                                <span style="font-size: 0.875rem; color: #64748b; font-weight: 600;">
771                                    Current: ${riskCategory.name}
772                                </span>
773                            </div>
774                        </div>
775                        
776                        <!-- Risk Level Indicators -->
777                        <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem;">
778                            <div style="text-align: center; padding: 1rem; background: #ffffff; border-radius: 0.5rem; border: 1px solid ${legendPartialActive ? riskCategory.borderColor : '#e5e7eb'};">
779                                <div style="width: 8px; height: 8px; background: #F24C26; border-radius: 50%; margin: 0 auto 0.5rem;"></div>
780                                <h6 style="font-size: 0.875rem; font-weight: 600; color: #1f2937; margin: 0 0 0.25rem;">Partial</h6>
781                                <p style="font-size: 0.75rem; color: #6b7280; margin: 0;">${rangePartial}</p>
782                            </div>
783                            <div style="text-align: center; padding: 1rem; background: #ffffff; border-radius: 0.5rem; border: 1px solid ${legendRiskInformedActive ? riskCategory.borderColor : '#e5e7eb'};">
784                                <div style="width: 8px; height: 8px; background: #F29025; border-radius: 50%; margin: 0 auto 0.5rem;"></div>
785                                <h6 style="font-size: 0.875rem; font-weight: 600; color: #1f2937; margin: 0 0 0.25rem;">Risk Informed</h6>
786                                <p style="font-size: 0.75rem; color: #6b7280; margin: 0;">${rangeRiskInformed}</p>
787                            </div>
788                            <div style="text-align: center; padding: 1rem; background: #ffffff; border-radius: 0.5rem; border: 1px solid ${legendRepeatableActive ? riskCategory.borderColor : '#e5e7eb'};">
789                                <div style="width: 8px; height: 8px; background: #FFD728; border-radius: 50%; margin: 0 auto 0.5rem;"></div>
790                                <h6 style="font-size: 0.875rem; font-weight: 600; color: #1f2937; margin: 0 0 0.25rem;">Repeatable</h6>
791                                <p style="font-size: 0.75rem; color: #6b7280; margin: 0;">${rangeRepeatable}</p>
792                            </div>
793                            <div style="text-align: center; padding: 1rem; background: #ffffff; border-radius: 0.5rem; border: 1px solid ${legendAdaptiveActive ? riskCategory.borderColor : '#e5e7eb'};">
794                                <div style="width: 8px; height: 8px; background: #40DB35; border-radius: 50%; margin: 0 auto 0.5rem;"></div>
795                                <h6 style="font-size: 0.875rem; font-weight: 600; color: #1f2937; margin: 0 0 0.25rem;">Adaptive</h6>
796                                <p style="font-size: 0.75rem; color: #6b7280; margin: 0;">${rangeAdaptive}</p>
797                            </div>
798                        </div>
799                    </div>
800                </div>
801
802                <!-- Detailed Pillar Breakdown -->
803                <div style="padding: 0 2rem 2rem;">
804                    <h4 style="font-size: 1.25rem; font-weight: 700; color: #1e293b; margin: 0 0 1.5rem; text-align: center;">
805                        Security Pillar Breakdown
806                    </h4>
807                    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem;">
808                    ${Object.entries(scores.pillarScores).length > 0 ? 
809                        Object.entries(scores.pillarScores).map(([pillar, score]) => {
810                            const maxScore = scores.maxPillarScores[pillar] || 5;
811                            const displayPillarScore = Math.round(score * 10) / 10;
812                            const percentage = maxScore > 0 ? (score / maxScore) * 100 : 0;
813                            let maturity = '';
814                                let statusColor = '#3b82f6';
815                                
816                                if (percentage > 80) {
817                                    maturity = "Excellent maturity in this area.";
818                                    statusColor = '#10b981';
819                                } else if (percentage > 60) {
820                                    maturity = "Good progress, but some areas for optimization.";
821                                    statusColor = '#f59e0b';
822                                } else if (percentage > 40) {
823                                    maturity = "Moderate maturity, significant room for improvement.";
824                                    statusColor = '#f97316';
825                                } else {
826                                    maturity = "Early stages, focus on foundational enhancements.";
827                                    statusColor = '#ef4444';
828                                }
829                            
830                            return `
831                                    <div style="background: #ffffff; border: 1px solid #e2e8f0; border-radius: 0.75rem; padding: 1.5rem; box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);">
832                                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
833                                            <h5 style="font-size: 1.125rem; font-weight: 700; color: #1e293b; margin: 0;">${pillar}</h5>
834                                            <div style="display: flex; align-items: center; gap: 0.5rem;">
835                                                <div style="width: 8px; height: 8px; background: ${statusColor}; border-radius: 50%;"></div>
836                                                <span style="font-size: 0.875rem; color: #64748b; font-weight: 600;">${Math.round(percentage)}%</span>
837                                    </div>
838                                        </div>
839                                        <div style="width: 100%; height: 6px; background: #e2e8f0; border-radius: 3px; margin-bottom: 1rem; overflow: hidden;">
840                                            <div style="height: 100%; background: ${statusColor}; width: ${percentage}%; transition: width 0.5s ease-in-out; border-radius: 3px;"></div>
841                                        </div>
842                                        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
843                                            <span style="font-size: 1.5rem; font-weight: 800; color: #1e293b;">
844                                                ${displayPillarScore}
845                                            </span>
846                                            <span style="font-size: 0.875rem; color: #64748b; font-weight: 500;">
847                                                / ${maxScore} points
848                                            </span>
849                                        </div>
850                                        <p style="font-size: 0.75rem; color: #475569; margin: 0; line-height: 1.4;">${maturity}</p>
851                                </div>
852                            `;
853                        }).join('') :
854                            `<div style="grid-column: 1 / -1; text-align: center; padding: 2rem; color: #64748b; background: #f8fafc; border-radius: 0.75rem; border: 1px solid #e2e8f0;">
855                                <p style="margin: 0; font-size: 1rem;">Assessment completed successfully!</p>
856                        </div>`
857                    }
858                    </div>
859                </div>
860
861                <!-- Footer Section -->
862                <div style="padding: 2rem; background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%); border-radius: 0 0 1rem 1rem; border-top: 1px solid #e2e8f0; text-align: center;">
863                    <div style="max-width: 600px; margin: 0 auto;">
864                        <h5 style="font-size: 1.125rem; font-weight: 700; color: #1e293b; margin: 0 0 0.75rem;">
865                            Ready to strengthen your security posture?
866                        </h5>
867                        <p style="font-size: 0.875rem; color: #64748b; margin: 0; line-height: 1.5;">
868                            Understanding your score is the first step. Our experts can help you develop a comprehensive strategy to enhance your identity security program and move toward a more adaptive security posture.
869                        </p>
870                    </div>
871                </div>
872            </div>
873        `;
874
875        // Insert the scoring results after the thank you message
876        resp.insertAdjacentHTML('afterend', scoringHtml);
877    }
878
879    // Function to handle form submission
880    function handleFormSubmit(event) {
881        const form = event.target;
882        
883        // Check if this is a Forminator form
884        if (form.classList.contains('forminator-custom-form')) {
885            // Get form ID from data attribute
886            const formId = form.getAttribute('data-form-id');
887            
888            if (formId) {
889                console.log('Forminator form submitted - Form ID:', formId);
890                
891                // Immediately hide the left column to prevent flashing
892                const marketoBlock = form.closest('.wp-block-veza-blocks-vz-marketo-form-gated');
893                if (marketoBlock) {
894                    const columnsContainer = marketoBlock.closest('.wp-block-columns');
895                    if (columnsContainer) {
896                        const firstColumn = columnsContainer.querySelector('.wp-block-column:first-child');
897                        if (firstColumn) {
898                            firstColumn.style.display = 'none';
899                            console.log('Immediately hidden first column to prevent flashing');
900                        }
901                    }
902                }
903
904                // After submission, mark that we want to hide hero on next resize
905                form.dataset.hideHeroOnResize = 'true';
906                installHideHeroOnResize(form);
907                
908                // Capture form data immediately when form is submitted
909                console.log('Capturing form data on submission...');
910                const immediateFormData = captureFormData(form);
911                console.log('Immediate form data:', immediateFormData);
912                
913                // Store this data for later use
914                // Convert Map to array of entries for JSON serialization
915        const serializableFormData = {
916            ...immediateFormData,
917            questionGroups: Array.from(immediateFormData.questionGroups.entries())
918        };
919        console.log('Serializing questionGroups:', Array.from(immediateFormData.questionGroups.entries()));
920        form.setAttribute('data-captured-form-data', JSON.stringify(serializableFormData));
921            }
922        }
923    }
924
925    // --- Pagination + Progress --------------------
926    function ensureSubmitButtonLabel(form) {
927        const submitBtn = form.querySelector('.forminator-row-last .forminator-button.forminator-button-submit');
928        if (!submitBtn) return;
929        if (!submitBtn.textContent || !submitBtn.textContent.trim()) {
930            submitBtn.textContent = 'Submit';
931        }
932        if (!submitBtn.getAttribute('aria-label')) {
933            submitBtn.setAttribute('aria-label', 'Submit');
934        }
935    }
936
937    function setupPaginationForForm(form) {
938        // Always ensure fallback label exists, even when pagination is skipped.
939        ensureSubmitButtonLabel(form);
940
941        // Collect radio-question rows to consider as questions
942        const questionRows = Array.from(form.querySelectorAll('.forminator-row'))
943            .filter(row => row.querySelector('.forminator-field-radio'));
944        const totalQuestions = questionRows.length;
945
946        // Helper that runs after submission (also used by global observer)
947        function finalizeAfterSubmit() {
948            // Try to get stored form data first, then capture if not available
949            let formData = null;
950            const storedData = form.getAttribute('data-captured-form-data');
951            
952            if (storedData) {
953                try {
954                    formData = JSON.parse(storedData);
955                    
956                    // Convert array of entries back to Map
957                    if (formData.questionGroups && Array.isArray(formData.questionGroups)) {
958                        console.log('Converting questionGroups back to Map from:', formData.questionGroups);
959                        formData.questionGroups = new Map(formData.questionGroups);
960                        console.log('Converted questionGroups to Map:', formData.questionGroups);
961                    }
962                    
963                    console.log('Using stored form data:', formData);
964                } catch (e) {
965                    console.log('Failed to parse stored form data:', e);
966                }
967            }
968            
969            // If no stored data, capture it now
970            if (!formData) {
971                console.log('No stored data, capturing form data now...');
972                formData = captureFormData(form);
973            }
974            
975            // Hide navigation and progress if present
976            const nav = form.querySelector('.vz-form-nav');
977            const progressWrap = form.querySelector('.vz-form-progress');
978            if (nav) nav.style.display = 'none';
979            if (progressWrap) progressWrap.style.display = 'none';
980            // Hide all question rows
981            const allRows = form.querySelectorAll('.forminator-row');
982            allRows.forEach(row => (row.style.display = 'none'));
983            const submitRow = form.querySelector('.forminator-row-last');
984            if (submitRow) submitRow.style.display = 'none';
985            // Style the thank-you message
986            const resp = form.querySelector('.forminator-response-message');
987            if (resp) {
988                resp.classList.add('vz-thanks');
989                // Cancel Forminator fade-out if applied
990                resp.style.removeProperty('display');
991                resp.style.display = 'block';
992                resp.style.opacity = '1';
993                resp.style.visibility = 'visible';
994                try {
995                    resp.scrollIntoView({ behavior: 'smooth', block: 'start' });
996                } catch (_) {}
997                
998                // Display scoring results using captured data
999                setTimeout(() => {
1000                    displayScoringResults(form, formData);
1001                }, 500);
1002            }
1003        }
1004
1005        // Attach observer to handle post-submit for this form
1006        installSubmissionObserver(form, finalizeAfterSubmit);
1007
1008        if (totalQuestions < 2) return; // no need to paginate further
1009
1010        // Compute groups: aim for ~7 questions per group by default
1011        const QUESTIONS_PER_GROUP = 7;
1012        const groupCount = Math.ceil(totalQuestions / QUESTIONS_PER_GROUP);
1013        const groupSize = Math.ceil(totalQuestions / groupCount);
1014
1015        // Build grouped step arrays
1016        const groups = [];
1017        for (let i = 0; i < groupCount; i++) {
1018            const start = i * groupSize;
1019            const end = Math.min(start + groupSize, totalQuestions);
1020            if (start < end) groups.push(questionRows.slice(start, end));
1021        }
1022        if (groups.length < 2) return; // safety
1023
1024        // Hide default submit row and capture the real submit button
1025        const submitRow = form.querySelector('.forminator-row-last');
1026        let submitBtn = null;
1027        if (submitRow) {
1028            submitBtn = submitRow.querySelector('.forminator-button.forminator-button-submit');
1029            // Keep row hidden at all times; we'll use the real button in the nav
1030            submitRow.style.display = 'none';
1031        }
1032        // Mark form as paginated for CSS overrides
1033        form.classList.add('vz-paginated');
1034
1035        // Inject progress bar (track + bar + label)
1036        const progressWrap = document.createElement('div');
1037        progressWrap.className = 'vz-form-progress';
1038        progressWrap.style.cssText = 'width:100%;max-width:760px;margin:0 auto 12px;';
1039        const progressTrack = document.createElement('div');
1040        progressTrack.style.cssText = 'height:6px;background:#e9ecef;border-radius:6px;overflow:hidden;';
1041        const progressBar = document.createElement('div');
1042        progressBar.style.cssText = 'height:100%;width:0;background:#0E61FF;transition:width 200ms ease;';
1043        progressTrack.appendChild(progressBar);
1044        const progressLabel = document.createElement('div');
1045        progressLabel.style.cssText = 'margin:6px 0 0;text-align:center;font-size:12px;color:#565c6d;';
1046        progressWrap.appendChild(progressTrack);
1047        progressWrap.appendChild(progressLabel);
1048        form.insertBefore(progressWrap, questionRows[0]);
1049
1050        // Inject nav controls (prev/next) and move submit button here
1051        const nav = document.createElement('div');
1052        nav.className = 'vz-form-nav';
1053        nav.style.cssText = 'display:flex;justify-content:space-between;gap:12px;width:100%;max-width:760px;margin:12px auto 0;';
1054        const prevBtn = document.createElement('button');
1055        prevBtn.type = 'button';
1056        prevBtn.textContent = 'Previous';
1057        prevBtn.className = 'vz-form-nav-prev forminator-button forminator-button-submit';
1058        const nextBtn = document.createElement('button');
1059        nextBtn.type = 'button';
1060        nextBtn.textContent = 'Next';
1061        nextBtn.className = 'vz-form-nav-next forminator-button forminator-button-submit';
1062        nav.appendChild(prevBtn);
1063        nav.appendChild(nextBtn);
1064        // Move real submit button into nav (if present)
1065        if (submitBtn) {
1066            if (!submitBtn.textContent.trim()) submitBtn.textContent = 'Submit';
1067            submitBtn.style.setProperty('display', 'none', 'important');
1068            nav.appendChild(submitBtn);
1069        }
1070        form.insertBefore(nav, submitRow || null);
1071
1072        // State
1073        let current = 0;
1074
1075        function scrollToProgress() {
1076            const offset = 80; // approximate sticky header height
1077            const y = progressWrap.getBoundingClientRect().top + window.pageYOffset - offset;
1078            window.scrollTo({ top: Math.max(0, y), behavior: 'smooth' });
1079        }
1080
1081        function showStep(index) {
1082            current = Math.max(0, Math.min(index, groups.length - 1));
1083            // Toggle visibility for all question rows
1084            questionRows.forEach(row => (row.style.display = 'none'));
1085            groups[current].forEach(row => (row.style.display = ''));
1086
1087            // Controls visibility
1088            prevBtn.style.visibility = current === 0 ? 'hidden' : 'visible';
1089            // On last step, hide Next and show the submit button in nav
1090            const isLast = current === groups.length - 1;
1091            nextBtn.style.setProperty('display', isLast ? 'none' : 'inline-flex', 'important');
1092            if (submitBtn) submitBtn.style.setProperty('display', isLast ? 'inline-flex' : 'none', 'important');
1093
1094            // Progress
1095            const pct = Math.round(((current + 1) / groups.length) * 100);
1096            progressBar.style.width = pct + '%';
1097            progressLabel.textContent = 'Step ' + (current + 1) + ' of ' + groups.length;
1098        }
1099
1100        function currentGroupAnswered() {
1101            // Every question (radio group) in this step must have a selection
1102            return groups[current].every(row => row.querySelector('input[type="radio"]:checked'));
1103        }
1104
1105        prevBtn.addEventListener('click', function() {
1106            showStep(current - 1);
1107            scrollToProgress();
1108        });
1109        nextBtn.addEventListener('click', function() {
1110            if (!currentGroupAnswered()) {
1111                // Highlight only unanswered cards and keep error state until selection
1112                // Clear old errors first
1113                groups[current].forEach(row => {
1114                    const card = row.querySelector('.forminator-field-radio');
1115                    if (card) card.classList.remove('vz-error');
1116                });
1117                // Mark unanswered
1118                const unanswered = groups[current].filter(row => !row.querySelector('input[type="radio"]:checked'));
1119                unanswered.forEach(row => {
1120                    const card = row.querySelector('.forminator-field-radio') || row;
1121                    card.classList.add('vz-error');
1122                    // Attach one-time change listener to clear the error when answered
1123                    const radios = row.querySelectorAll('input[type="radio"]');
1124                    radios.forEach(r => {
1125                        r.addEventListener('change', function onChange() {
1126                            card.classList.remove('vz-error');
1127                            // remove listener after first change to avoid leaks
1128                            radios.forEach(rr => rr.removeEventListener('change', onChange));
1129                        });
1130                    });
1131                });
1132                return;
1133            }
1134            showStep(current + 1);
1135            scrollToProgress();
1136        });
1137
1138        // Initialize
1139        showStep(0);
1140    }
1141
1142    function installSubmissionObserver(form, onSuccess) {
1143        const resp = form.querySelector('.forminator-response-message');
1144        if (!resp) return;
1145        const run = () => {
1146            const shown = resp.classList.contains('forminator-show') || resp.style.display === 'block';
1147            const success = resp.classList.contains('forminator-success');
1148            if (shown && success) {
1149                onSuccess && onSuccess();
1150                return true;
1151            }
1152            return false;
1153        };
1154        // If it's already visible (e.g., AJAX completed), apply immediately
1155        if (run()) return;
1156        const observer = new MutationObserver(() => {
1157            if (run()) observer.disconnect();
1158        });
1159        observer.observe(resp, { attributes: true, attributeFilter: ['class', 'style'] });
1160    }
1161
1162    function setupPagination() {
1163        const forms = document.querySelectorAll('.forminator-custom-form');
1164        forms.forEach(form => {
1165            // Always install submission observer so non-paginated forms are handled
1166            installSubmissionObserver(form, function() {
1167                // Generic finalize for simple forms
1168                // Try to get stored form data first, then capture if not available
1169                let formData = null;
1170                const storedData = form.getAttribute('data-captured-form-data');
1171                
1172                if (storedData) {
1173                    try {
1174                        formData = JSON.parse(storedData);
1175                        
1176                        // Convert array of entries back to Map
1177                        if (formData.questionGroups && Array.isArray(formData.questionGroups)) {
1178                            console.log('Converting questionGroups back to Map from (generic):', formData.questionGroups);
1179                            formData.questionGroups = new Map(formData.questionGroups);
1180                            console.log('Converted questionGroups to Map (generic):', formData.questionGroups);
1181                        }
1182                        
1183                        console.log('Using stored form data (generic):', formData);
1184                    } catch (e) {
1185                        console.log('Failed to parse stored form data (generic):', e);
1186                    }
1187                }
1188                
1189                // If no stored data, capture it now
1190                if (!formData) {
1191                    console.log('No stored data, capturing form data now (generic)...');
1192                    formData = captureFormData(form);
1193                }
1194                
1195                // Enable hide-on-resize after success; do not hide immediately
1196                form.dataset.hideHeroOnResize = 'true';
1197                installHideHeroOnResize(form);
1198
1199                const nav = form.querySelector('.vz-form-nav');
1200                const progressWrap = form.querySelector('.vz-form-progress');
1201                if (nav) nav.style.display = 'none';
1202                if (progressWrap) progressWrap.style.display = 'none';
1203                const rows = form.querySelectorAll('.forminator-row');
1204                rows.forEach(r => (r.style.display = 'none'));
1205                const resp = form.querySelector('.forminator-response-message');
1206                if (resp) {
1207                    resp.classList.add('vz-thanks');
1208                    // Display scoring results after a short delay
1209                    setTimeout(() => {
1210                        displayScoringResults(form, formData);
1211                    }, 500);
1212                }
1213            });
1214            // Then, optionally, pagination
1215            setupPaginationForForm(form);
1216        });
1217    }
1218
1219    // Function to handle AJAX responses
1220    function handleAjaxResponse(event) {
1221        try {
1222            const response = event.detail;
1223            
1224            // Check if this is a Forminator response
1225            if (response && response.success && response.data && response.data.form_id) {
1226                const formId = response.data.form_id;
1227                console.log('Forminator form submission response - Form ID:', formId);
1228            }
1229        } catch (error) {
1230            // Silently handle any parsing errors
1231        }
1232    }
1233
1234    // Function to fetch submission ID from custom endpoint
1235    function fetchSubmissionId(formId) {
1236        // First test the endpoint
1237        const testFormData = new FormData();
1238        testFormData.append('action', 'test_endpoint');
1239        testFormData.append('form_id', formId);
1240        
1241        fetch(ajaxurl || '/wp-admin/admin-ajax.php', {
1242            method: 'POST',
1243            body: testFormData
1244        })
1245        .then(response => response.text())
1246        .then(text => {
1247            try {
1248                const data = JSON.parse(text);
1249                if (data.success) {
1250                    // Now try to get the submission ID
1251                    return fetchSubmissionIdReal(formId);
1252                }
1253            } catch (e) {
1254                // Silently handle parsing errors
1255            }
1256        })
1257        .catch(error => {
1258            // Silently handle errors
1259        });
1260    }
1261    
1262    // Function to actually fetch the submission ID
1263    function fetchSubmissionIdReal(formId) {
1264        const formData = new FormData();
1265        formData.append('action', 'get_forminator_submission_id');
1266        formData.append('form_id', formId);
1267        
1268        return fetch(ajaxurl || '/wp-admin/admin-ajax.php', {
1269            method: 'POST',
1270            body: formData
1271        })
1272        .then(response => response.text())
1273        .then(text => {
1274            try {
1275                const data = JSON.parse(text);
1276                if (data.success && data.data.submission_id) {
1277                    console.log('Submission ID:', data.data.submission_id);
1278                    
1279                    // Trigger Marketo form submission when we get the submission ID
1280                    triggerMarketoSubmission(data.data.submission_id);
1281                }
1282            } catch (e) {
1283                // Silently handle parsing errors
1284            }
1285        })
1286        .catch(error => {
1287            // Silently handle errors
1288        });
1289    }
1290
1291    // Function to trigger Marketo form submission
1292    function triggerMarketoSubmission(submissionId) {
1293        // Find the Marketo form gated block that contains this Forminator form
1294        const forminatorForms = document.querySelectorAll('.forminator-custom-form');
1295        let targetMarketoBlock = null;
1296        
1297        // Find which Forminator form was submitted and locate its parent Marketo block
1298        forminatorForms.forEach(form => {
1299            if (form.querySelector('.forminator-response-message.forminator-success')) {
1300                // This form was successfully submitted
1301                targetMarketoBlock = form.closest('.wp-block-veza-blocks-vz-marketo-form-gated');
1302                if (targetMarketoBlock) {
1303                    console.log('Found Marketo gated block, triggering submission');
1304                    
1305                    // Get the stored form data from the form's data attribute
1306                    let formData = null;
1307                    const storedData = form.getAttribute('data-captured-form-data');
1308                    
1309                    if (storedData) {
1310                        try {
1311                            formData = JSON.parse(storedData);
1312                            
1313                            // Convert array of entries back to Map
1314                            if (formData.questionGroups && Array.isArray(formData.questionGroups)) {
1315                                console.log('Converting questionGroups back to Map from triggerMarketoSubmission:', formData.questionGroups);
1316                                formData.questionGroups = new Map(formData.questionGroups);
1317                                console.log('Converted questionGroups to Map in triggerMarketoSubmission:', formData.questionGroups);
1318                            }
1319                            
1320                            console.log('Using stored form data in triggerMarketoSubmission:', formData);
1321                        } catch (e) {
1322                            console.log('Failed to parse stored form data in triggerMarketoSubmission:', e);
1323                        }
1324                    }
1325                    
1326                    // If no stored data, capture it now
1327                    if (!formData) {
1328                        console.log('No stored data, capturing form data now in triggerMarketoSubmission...');
1329                        formData = captureFormData(form);
1330                    }
1331                    
1332                    // Get the score data from the form
1333                    const scoreData = calculateScores(form, formData);
1334                    
1335                    // Dispatch custom event to trigger Marketo submission
1336                    targetMarketoBlock.dispatchEvent(new CustomEvent('forminatorSubmissionComplete', {
1337                        detail: { 
1338                            submissionId, 
1339                            formId: form.getAttribute('data-form-id'),
1340                            score: scoreData.totalScore,
1341                            maxScore: scoreData.maxTotalScore,
1342                            pillarScores: scoreData.pillarScores
1343                        }
1344                    }));
1345                }
1346            }
1347        });
1348    }
1349
1350    // Function to intercept AJAX requests to capture Forminator responses
1351    function interceptAjaxResponses() {
1352        // Override XMLHttpRequest to intercept responses
1353        const originalXHROpen = XMLHttpRequest.prototype.open;
1354        const originalXHRSend = XMLHttpRequest.prototype.send;
1355        
1356        XMLHttpRequest.prototype.open = function(method, url, ...args) {
1357            this._url = url;
1358            return originalXHROpen.apply(this, [method, url, ...args]);
1359        };
1360        
1361        XMLHttpRequest.prototype.send = function(...args) {
1362            const xhr = this;
1363            
1364            // Add response listener
1365            xhr.addEventListener('load', function() {
1366                try {
1367                    // Check if this is a Forminator AJAX response
1368                    if (xhr._url && xhr._url.includes('admin-ajax.php')) {
1369                        const responseText = xhr.responseText;
1370                        const response = JSON.parse(responseText);
1371                        
1372                        if (response && response.success && response.data && response.data.form_id) {
1373                            const formId = response.data.form_id;
1374                            
1375                            // Look for submission ID or other identifiers
1376                            const submissionId = response.data.submission_id || response.data.entry_id || response.data.id;
1377                            if (submissionId) {
1378                                console.log('Submission ID found in AJAX response:', submissionId);
1379                                // Trigger Marketo submission immediately when submission ID is found
1380                                triggerMarketoSubmission(submissionId);
1381                            } else {
1382                                // If no submission ID in response, try to fetch it
1383                                fetchSubmissionId(formId);
1384                            }
1385                        }
1386                    }
1387                } catch (error) {
1388                    // Silently handle parsing errors
1389                }
1390            });
1391            
1392            return originalXHRSend.apply(this, args);
1393        };
1394    }
1395
1396    // Function to initialize the listener
1397    function init() {
1398        // Listen for form submissions
1399        document.addEventListener('submit', handleFormSubmit, true);
1400        
1401        // Listen for Forminator AJAX responses (custom event)
1402        document.addEventListener('forminator_form_submitted', handleAjaxResponse);
1403        
1404        // Alternative: Listen for all AJAX responses
1405        document.addEventListener('ajax_response', handleAjaxResponse);
1406        
1407        // Listen for common Forminator events
1408        document.addEventListener('forminator_success', function(e) {
1409            if (e.detail && e.detail.form_id) {
1410                // Silently handle success events
1411            }
1412        });
1413        
1414        document.addEventListener('forminator_complete', function(e) {
1415            if (e.detail && e.detail.form_id) {
1416                // Silently handle complete events
1417            }
1418        });
1419        
1420        // Intercept AJAX responses to capture Forminator form submissions
1421        interceptAjaxResponses();
1422        
1423        // Setup pagination/progress
1424        setupPagination();
1425        
1426        // Listen for DOM changes to catch dynamically added forms
1427        const observer = new MutationObserver(function(mutations) {
1428            mutations.forEach(function(mutation) {
1429                if (mutation.type === 'childList') {
1430                    mutation.addedNodes.forEach(function(node) {
1431                        if (node.nodeType === Node.ELEMENT_NODE) {
1432                            // When new Forminator forms appear, paginate them
1433                            const newForms = node.matches && node.matches('.forminator-custom-form')
1434                                ? [node]
1435                                : (node.querySelectorAll ? node.querySelectorAll('.forminator-custom-form') : []);
1436                            if (newForms && newForms.length) {
1437                                newForms.forEach(setupPaginationForForm);
1438                            }
1439                        }
1440                    });
1441                }
1442            });
1443        });
1444        
1445        // Start observing
1446        observer.observe(document.body, {
1447            childList: true,
1448            subtree: true
1449        });
1450    }
1451
1452    // Initialize when DOM is ready
1453    if (document.readyState === 'loading') {
1454        document.addEventListener('DOMContentLoaded', init);
1455    } else {
1456        init();
1457    }
1458
1459})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.