PageSourceSearch

https://meltmethod.ca/wp-content/plugins/WP-Assessment/assets/js/frontend.js?ver=1.0.0

js meltmethod.ca collected 2026-10-09 01:40:12 UTC 86,235 bytes, 2,145 lines download raw bytes

1/**
2 * Frontend JavaScript for WP Assessment
3 * Functional approach for step navigation and answer collection
4 */
5
6(function() {
7    'use strict';
8
9    // Get settings from WordPress
10    const settings = window.wpAssessmentSettings || {
11        animation_type: 'fade',
12        animation_duration: 300
13    };
14
15    // Initialize global debug object
16    window.wpAssessmentDebug = {
17        state: null,
18        settings: settings,
19        localStorage: {},
20        history: [],
21        events: [],
22
23        // Helper functions
24        printState: function() {
25            console.group('%c WP Assessment - Current State', 'color: #0073aa; font-weight: bold; font-size: 14px;');
26            console.log('%cState Object:', 'font-weight: bold;');
27            console.table(this.state);
28            console.log('%cAnswers:', 'font-weight: bold;');
29            console.table(this.state?.answers || []);
30            console.groupEnd();
31            return this.state;
32        },
33
34        printHistory: function() {
35            console.group('%c WP Assessment - State History', 'color: #0073aa; font-weight: bold; font-size: 14px;');
36            console.log(`Total transitions: ${this.history.length}`);
37            console.table(this.history);
38            console.groupEnd();
39            return this.history;
40        },
41
42        printEvents: function() {
43            console.group('%c WP Assessment - Events Log', 'color: #0073aa; font-weight: bold; font-size: 14px;');
44            console.log(`Total events: ${this.events.length}`);
45            console.table(this.events);
46            console.groupEnd();
47            return this.events;
48        },
49
50        export: function() {
51            const data = {
52                state: this.state,
53                settings: this.settings,
54                localStorage: this.localStorage,
55                history: this.history,
56                events: this.events,
57                timestamp: new Date().toISOString()
58            };
59
60            const json = JSON.stringify(data, null, 2);
61
62            // Try to copy to clipboard
63            if (navigator.clipboard) {
64                navigator.clipboard.writeText(json).then(() => {
65                    console.log('%c✓ Debug data copied to clipboard!', 'color: #46b450; font-weight: bold;');
66                }).catch(() => {
67                    console.log('%cDebug data:', 'font-weight: bold;');
68                    console.log(json);
69                });
70            } else {
71                console.log('%cDebug data:', 'font-weight: bold;');
72                console.log(json);
73            }
74
75            return data;
76        },
77
78        clear: function() {
79            this.history = [];
80            this.events = [];
81            console.log('%c✓ Debug history cleared', 'color: #46b450;');
82        }
83    };
84
85    /**
86     * Send log to server (file)
87     */
88    const sendLogToServer = (type, message, data = null) => {
89        if (!settings.debug_mode || !settings.ajax_url) return;
90
91        const formData = new FormData();
92        formData.append('action', 'wp_assessment_log_js');
93        formData.append('nonce', settings.nonce);
94        formData.append('type', type);
95        formData.append('message', message);
96        if (data) {
97            formData.append('data', JSON.stringify(data, null, 2));
98        }
99
100        // Send async (don't wait for response)
101        fetch(settings.ajax_url, {
102            method: 'POST',
103            body: formData
104        }).catch(() => {
105            // Silently fail - don't block user experience
106        });
107    };
108
109    /**
110     * Log debug event
111     */
112    const logDebugEvent = (type, message, data = null) => {
113        const event = {
114            timestamp: new Date().toISOString(),
115            type: type,
116            message: message,
117            data: data
118        };
119
120        window.wpAssessmentDebug.events.push(event);
121
122        // Log to console if debug mode enabled
123        if (settings.debug_mode) {
124            const color = type === 'ERROR' ? '#dc3232' : type === 'WARNING' ? '#f0b849' : '#0073aa';
125            console.log(`%c[${type}] ${message}`, `color: ${color};`, data || '');
126        }
127
128        // Send to server log file
129        sendLogToServer(type, message, data);
130    };
131
132    /**
133     * Log state transition
134     */
135    const logStateTransition = (fromState, toState, action) => {
136        const transition = {
137            timestamp: new Date().toISOString(),
138            action: action,
139            from: {
140                currentStep: fromState?.currentStep,
141                answerCount: fromState?.answers?.length || 0
142            },
143            to: {
144                currentStep: toState.currentStep,
145                answerCount: toState.answers.length
146            }
147        };
148
149        window.wpAssessmentDebug.history.push(transition);
150        window.wpAssessmentDebug.state = toState;
151
152        logDebugEvent('INFO', `State transition: ${action}`, transition);
153    };
154
155    /**
156     * Update container height to match the active step
157     * This ensures .wp-assessment-steps always matches the active step's height
158     */
159    const updateContainerHeight = (container, targetStepEl) => {
160        const stepsContainer = container.querySelector('.wp-assessment-steps');
161        if (!stepsContainer) return;
162
163        // If no target provided, find the active step
164        const stepEl = targetStepEl || container.querySelector('.wp-assessment-step.active');
165        if (!stepEl) return;
166
167        // Use max of offsetHeight and scrollHeight to ensure all content is visible
168        const stepHeight = Math.max(stepEl.offsetHeight, stepEl.scrollHeight);
169
170        // Apply height to container
171        stepsContainer.style.height = `${stepHeight}px`;
172    };
173
174    /**
175     * Create initial state
176     */
177    const createInitialState = (steps) => ({
178        steps: steps,
179        currentStep: 0,
180        answers: [],
181        answerCache: {}, // All answers ever given, indexed by stepName
182        pathTaken: [],   // Steps visited on the final path
183        totalSteps: steps.length
184    });
185
186    /**
187     * Initialize assessment
188     */
189    const init = () => {
190        const containers = document.querySelectorAll('.wp-assessment-container');
191
192        // Store container and state references for popstate handler
193        const assessments = [];
194
195        containers.forEach(container => {
196            const dataElement = container.querySelector('.wp-assessment-data');
197            if (!dataElement) return;
198
199            try {
200                const rawJson = dataElement.textContent;
201                const steps = JSON.parse(rawJson);
202                const state = createInitialState(steps);
203
204                const assessment = setupAssessment(container, state);
205                assessments.push(assessment);
206            } catch (error) {
207                console.error('Failed to initialize assessment:', error);
208            }
209        });
210
211        // Handle browser back/forward buttons (global handler)
212        window.addEventListener('popstate', (e) => {
213            if (e.state && e.state.step !== undefined) {
214                assessments.forEach(({ container, getState, setState }) => {
215                    const targetStep = e.state.step;
216                    const currentState = getState();
217                    const newState = { ...currentState, currentStep: targetStep };
218
219                    navigateToStep(container, newState, targetStep);
220                    updateProgress(container, newState);
221                    saveStateToLocalStorage(newState);
222                    broadcastStateToDebug(newState);
223                    setState(newState);
224                });
225            }
226        });
227
228        // Handle window resize - update container heights
229        let resizeTimeout;
230        window.addEventListener('resize', () => {
231            // Debounce resize events
232            clearTimeout(resizeTimeout);
233            resizeTimeout = setTimeout(() => {
234                assessments.forEach(({ container, getState }) => {
235                    const activeStep = container.querySelector('.wp-assessment-step.active');
236                    if (activeStep) {
237                        updateContainerHeight(container, activeStep);
238                    }
239                });
240            }, 100);
241        });
242
243        // Initialize debug panel if present
244        initDebugPanel();
245    };
246
247    /**
248     * Setup assessment with event listeners
249     */
250    const setupAssessment = (container, initialState) => {
251        let state = initialState;
252
253        // Add assessment ID to state
254        const assessmentId = container.dataset.assessmentId;
255        state = { ...state, assessmentId };
256
257        // Check URL for initial step
258        const urlStep = getStepFromURL();
259
260        // If no query string, clear localStorage and start fresh
261        if (urlStep === null) {
262            clearLocalStorage(assessmentId);
263            logDebugEvent('INFO', 'No query string - starting fresh assessment', { assessmentId });
264            state = { ...state, currentStep: 0, answers: [] };
265            navigateToStep(container, state, 0);
266        } else if (urlStep >= 0 && urlStep < state.totalSteps) {
267            // URL has a step - try to restore from localStorage
268            const savedState = loadStateFromLocalStorage(assessmentId);
269            if (savedState) {
270                state = { ...state, ...savedState };
271            }
272            state = { ...state, currentStep: urlStep };
273            navigateToStep(container, state, urlStep);
274        } else {
275            // Invalid step in URL - start fresh
276            clearLocalStorage(assessmentId);
277            state = { ...state, currentStep: 0, answers: [] };
278            navigateToStep(container, state, 0);
279        }
280
281        // Set animation duration CSS variable
282        container.style.setProperty('--wp-assessment-duration', settings.animation_duration + 'ms');
283
284        // Radio buttons handle their own selection state natively
285
286        // Next button (for multi-field steps)
287        container.addEventListener('click', (e) => {
288            if (e.target.classList.contains('wp-assessment-next-btn')) {
289                e.preventDefault();
290                logDebugEvent('CLICK', 'Next button clicked', {
291                    currentStep: state.currentStep,
292                    pathTaken: state.pathTaken
293                });
294                state = handleNextButton(container, state);
295            }
296            // Back button
297            else if (e.target.classList.contains('wp-assessment-back-btn')) {
298                e.preventDefault();
299                logDebugEvent('CLICK', 'Back button clicked', {
300                    currentStep: state.currentStep,
301                    pathTaken: state.pathTaken
302                });
303                state = handleBackButton(container, state);
304            }
305            // Restart button
306            else if (e.target.classList.contains('wp-assessment-restart-btn')) {
307                e.preventDefault();
308                logDebugEvent('CLICK', 'Restart button clicked', {
309                    currentStep: state.currentStep
310                });
311                state = resetAssessment(container, state);
312            }
313            // Radio button clicks
314            else if (e.target.classList.contains('wp-assessment-radio-input')) {
315                logDebugEvent('CLICK', 'Radio button clicked', {
316                    value: e.target.value,
317                    fieldName: e.target.name,
318                    currentStep: state.currentStep
319                });
320
321                // Check for auto-advance
322                const currentStepData = state.steps[state.currentStep];
323                const fields = currentStepData.fields || [];
324
325                // Only auto-advance if step has exactly one field
326                if (fields.length === 1) {
327                    const field = fields[0];
328
329                    // Check if field is radio type and has autoAdvance enabled (handle both boolean and string values)
330                    if (field.type === 'radio' && (field.autoAdvance === true || field.autoAdvance === '1' || field.autoAdvance === 1)) {
331                        // Check if an animation is currently in progress
332                        const hasActiveAnimation = container.querySelector('.wp-assessment-step.fade-out, .wp-assessment-step.fade-in');
333
334                        logDebugEvent('AUTO-ADVANCE', 'Auto-advance triggered', {
335                            fieldName: field.name,
336                            value: e.target.value,
337                            autoAdvanceValue: field.autoAdvance,
338                            currentStep: state.currentStep,
339                            animationInProgress: !!hasActiveAnimation
340                        });
341
342                        if (!hasActiveAnimation) {
343                            // Auto-advance immediately if no animation in progress
344                            state = handleNextButton(container, state);
345                        } else {
346                            logDebugEvent('AUTO-ADVANCE', 'Auto-advance blocked - animation in progress', {
347                                currentStep: state.currentStep
348                            });
349                        }
350                    }
351                }
352            }
353        });
354
355        // Initial render
356        updateProgress(container, state);
357        updateURL(state.currentStep, true); // Replace initial state
358        broadcastStateToDebug(state);
359
360        // Recalculate height after content settles (for shortcodes, forms, images)
361        // Multiple timeouts to catch different loading scenarios
362        [200, 500, 1000].forEach(delay => {
363            setTimeout(() => {
364                const activeStep = container.querySelector('.wp-assessment-step.active');
365                if (activeStep) {
366                    updateContainerHeight(container, activeStep);
367                }
368            }, delay);
369        });
370
371        // Also recalculate on window load (for images/iframes)
372        window.addEventListener('load', () => {
373            const activeStep = container.querySelector('.wp-assessment-step.active');
374            if (activeStep) {
375                updateContainerHeight(container, activeStep);
376            }
377        });
378
379        // Listen for custom event to force height recalculation
380        document.addEventListener('wp-assessment:recalculate-height', () => {
381            const activeStep = container.querySelector('.wp-assessment-step.active');
382            if (activeStep) {
383                updateContainerHeight(container, activeStep);
384            }
385        });
386
387        // Return state getter/setter for popstate handler
388        return {
389            container,
390            getState: () => state,
391            setState: (newState) => { state = newState; }
392        };
393    };
394
395    // Radio buttons manage their own selection state natively
396
397    /**
398     * Handle Back button click
399     * Goes back to the last step in the path taken (not just previous step index)
400     */
401    const handleBackButton = (container, state) => {
402        // Find current step name in pathTaken
403        const currentStepData = state.steps[state.currentStep];
404        const currentStepName = currentStepData.name || state.currentStep.toString();
405        const currentPathIndex = state.pathTaken.indexOf(currentStepName);
406
407        // If current step not in path or at beginning, can't go back
408        if (currentPathIndex <= 0) {
409            logDebugEvent('INFO', 'Cannot go back - at beginning of path', {
410                currentStep: state.currentStep,
411                currentStepName: currentStepName,
412                currentPathIndex: currentPathIndex,
413                pathTaken: state.pathTaken
414            });
415            return state;
416        }
417
418        // Get the previous step name from path taken
419        const previousStepName = state.pathTaken[currentPathIndex - 1];
420
421        // Find that step's index in steps array
422        const previousStepIndex = state.steps.findIndex(step => step.name === previousStepName);
423
424        if (previousStepIndex === -1) {
425            logDebugEvent('ERROR', 'Could not find previous step in steps array', {
426                previousStepName: previousStepName,
427                pathTaken: state.pathTaken
428            });
429            return state;
430        }
431
432        logDebugEvent('INFO', 'Going back to previous step in path', {
433            from: state.currentStep,
434            fromName: currentStepName,
435            to: previousStepIndex,
436            toName: previousStepName,
437            currentPathIndex: currentPathIndex,
438            pathTaken: state.pathTaken
439        });
440
441        // Trim pathTaken to only include steps up to the target step
442        const newPathTaken = state.pathTaken.slice(0, currentPathIndex);
443
444        // Update state
445        const newState = {
446            ...state,
447            currentStep: previousStepIndex,
448            pathTaken: newPathTaken
449        };
450
451        // Start navigation animation FIRST, update UI AFTER animation completes
452        navigateToStep(container, newState, previousStepIndex, () => {
453            // Callback: runs after animation finishes
454            updateProgress(container, newState);
455            updateURL(previousStepIndex);
456            saveStateToLocalStorage(newState);
457            broadcastStateToDebug(newState);
458        });
459
460        return newState;
461    };
462
463    /**
464     * Handle Next button click (multi-field steps)
465     */
466    const handleNextButton = (container, state) => {
467        const currentStepEl = container.querySelector(`[data-step="${state.currentStep}"]`);
468        if (!currentStepEl) return state;
469
470        // Check if this step has fields
471        const hasFields = currentStepEl.dataset.hasFields === '1';
472
473        // For steps without fields, skip validation and just continue
474        if (!hasFields) {
475            return handleAnswer(container, state, 'continue');
476        }
477
478        // Validate required fields
479        const validationErrors = validateRequiredFields(currentStepEl);
480        if (validationErrors.length > 0) {
481            alert(validationErrors.join('\n'));
482            return state;
483        }
484
485        // Find checked radio or checkbox input (if any)
486        const checkedRadio = currentStepEl.querySelector('.wp-assessment-radio-input:checked');
487        const checkedCheckbox = currentStepEl.querySelector('.wp-assessment-checkbox-input:checked');
488
489        // Get the answer value (prefer radio, then checkbox)
490        let answer = null;
491        if (checkedRadio) {
492            answer = checkedRadio.value;
493        } else if (checkedCheckbox) {
494            // For checkboxes with routing, use the first checked value
495            answer = checkedCheckbox.value;
496        }
497
498        // If no answer was selected but this step has routing, we need a selection
499        const currentStepData = state.steps[state.currentStep];
500        if (!answer && currentStepData.routing && Object.keys(currentStepData.routing).length > 0) {
501            // Check if there's a radio or checkbox field that requires selection
502            const radioField = currentStepEl.querySelector('.wp-assessment-radio-field');
503            const checkboxField = currentStepEl.querySelector('.wp-assessment-checkbox-field');
504            if (radioField || checkboxField) {
505                alert('Please select an option before proceeding.');
506                return state;
507            }
508        }
509
510        // For steps without routing, use 'next' as answer
511        if (!answer) {
512            answer = 'next';
513        }
514
515        return handleAnswer(container, state, answer);
516    };
517
518    /**
519     * Validate required fields in current step
520     */
521    const validateRequiredFields = (stepEl) => {
522        const errors = [];
523        const requiredFields = stepEl.querySelectorAll('.wp-assessment-field[data-required="1"]');
524
525        requiredFields.forEach(field => {
526            const fieldName = field.dataset.fieldName;
527
528            // Check radio field
529            if (field.classList.contains('wp-assessment-radio-field')) {
530                const checkedRadio = field.querySelector('.wp-assessment-radio-input:checked');
531                if (!checkedRadio) {
532                    const label = field.querySelector('.wp-assessment-field-label');
533                    const labelText = label ? label.textContent.replace('*', '').trim() : fieldName;
534                    errors.push(`Please select an option for: ${labelText}`);
535                }
536            }
537            // Check checkbox field
538            else if (field.classList.contains('wp-assessment-checkbox-field')) {
539                const checkedCheckboxes = field.querySelectorAll('.wp-assessment-checkbox-input:checked');
540                if (checkedCheckboxes.length === 0) {
541                    const label = field.querySelector('.wp-assessment-field-label');
542                    const labelText = label ? label.textContent.replace('*', '').trim() : fieldName;
543                    errors.push(`Please select at least one option for: ${labelText}`);
544                }
545            }
546            // Check text input
547            else if (field.classList.contains('wp-assessment-text-field')) {
548                const input = field.querySelector('.wp-assessment-text-input');
549                if (input && !input.value.trim()) {
550                    const label = field.querySelector('.wp-assessment-field-label');
551                    const labelText = label ? label.textContent.replace('*', '').trim() : fieldName;
552                    errors.push(`Please fill in: ${labelText}`);
553                }
554            }
555            // Check number input
556            else if (field.classList.contains('wp-assessment-number-field')) {
557                const input = field.querySelector('.wp-assessment-number-input');
558                if (input && !input.value.trim()) {
559                    const label = field.querySelector('.wp-assessment-field-label');
560                    const labelText = label ? label.textContent.replace('*', '').trim() : fieldName;
561                    errors.push(`Please fill in: ${labelText}`);
562                }
563            }
564            // Check textarea
565            else if (field.classList.contains('wp-assessment-textarea-field')) {
566                const input = field.querySelector('.wp-assessment-textarea-input');
567                if (input && !input.value.trim()) {
568                    const label = field.querySelector('.wp-assessment-field-label');
569                    const labelText = label ? label.textContent.replace('*', '').trim() : fieldName;
570                    errors.push(`Please fill in: ${labelText}`);
571                }
572            }
573            // Check height_weight field
574            else if (field.classList.contains('wp-assessment-height-weight-field')) {
575                const feetInput = field.querySelector('.wp-assessment-height-feet-input');
576                const inchesInput = field.querySelector('.wp-assessment-height-inches-input');
577                const weightInput = field.querySelector('.wp-assessment-weight-input');
578
579                if (!feetInput?.value.trim()) {
580                    errors.push('Please enter your height in feet');
581                }
582                if (!inchesInput?.value.trim()) {
583                    errors.push('Please enter your height in inches');
584                }
585                if (!weightInput?.value.trim()) {
586                    errors.push('Please enter your weight');
587                }
588            }
589        });
590
591        return errors;
592    };
593
594    /**
595     * Collect all field values from current step
596     */
597    const collectFieldValues = (container, state) => {
598        const currentStepEl = container.querySelector(`[data-step="${state.currentStep}"]`);
599        if (!currentStepEl) return {};
600
601        const fields = currentStepEl.querySelectorAll('.wp-assessment-field');
602        const fieldValues = {};
603
604        fields.forEach(field => {
605            const fieldName = field.dataset.fieldName;
606
607            // Radio input
608            const radioInput = field.querySelector('.wp-assessment-radio-input:checked');
609            if (radioInput) {
610                fieldValues[fieldName] = radioInput.value;
611            }
612
613            // Checkbox inputs
614            const checkedCheckboxes = field.querySelectorAll('.wp-assessment-checkbox-input:checked');
615            if (checkedCheckboxes.length > 0) {
616                fieldValues[fieldName] = Array.from(checkedCheckboxes).map(cb => cb.value);
617            }
618
619            // Text input
620            const textInput = field.querySelector('.wp-assessment-text-input');
621            if (textInput) {
622                fieldValues[fieldName] = textInput.value;
623            }
624
625            // Number input
626            const numberInput = field.querySelector('.wp-assessment-number-input');
627            if (numberInput) {
628                fieldValues[fieldName] = numberInput.value;
629            }
630
631            // Textarea
632            const textareaInput = field.querySelector('.wp-assessment-textarea-input');
633            if (textareaInput) {
634                fieldValues[fieldName] = textareaInput.value;
635            }
636
637            // Height & Weight composite field
638            if (field.classList.contains('wp-assessment-height-weight-field')) {
639                const feetInput = field.querySelector('.wp-assessment-height-feet-input');
640                const inchesInput = field.querySelector('.wp-assessment-height-inches-input');
641                const weightInput = field.querySelector('.wp-assessment-weight-input');
642
643                if (feetInput) {
644                    fieldValues[`${fieldName}_feet`] = feetInput.value;
645                }
646                if (inchesInput) {
647                    fieldValues[`${fieldName}_inches`] = inchesInput.value;
648                }
649                if (weightInput) {
650                    fieldValues[`${fieldName}_weight`] = weightInput.value;
651                }
652
653                // Also store computed values for convenience
654                const feet = parseInt(feetInput?.value) || 0;
655                const inches = parseInt(inchesInput?.value) || 0;
656                const weight = parseFloat(weightInput?.value) || 0;
657
658                // Total height in inches
659                const totalHeightInches = (feet * 12) + inches;
660                fieldValues[`${fieldName}_height_inches`] = totalHeightInches;
661
662                // Calculate BMI if we have both height and weight
663                if (totalHeightInches > 0 && weight > 0) {
664                    // BMI = (weight in lbs / (height in inches)²) × 703
665                    const bmi = (weight / (totalHeightInches * totalHeightInches)) * 703;
666                    fieldValues[`${fieldName}_bmi`] = Math.round(bmi * 10) / 10; // Round to 1 decimal
667                }
668            }
669        });
670
671        return fieldValues;
672    };
673
674    /**
675     * Handle answer selection (button click)
676     */
677    const handleAnswer = (container, state, answer) => {
678        const currentStepData = state.steps[state.currentStep];
679
680        // Collect all field values from the step
681        const fieldValues = collectFieldValues(container, state);
682
683        // Record answer with all field data
684        const stepName = currentStepData.name || state.currentStep.toString();
685        const answerRecord = {
686            question: currentStepData.label || currentStepData.name || `Step ${state.currentStep + 1}`,
687            answer: answer,
688            stepName: stepName,
689            stepLabel: currentStepData.label || currentStepData.name || `Step ${state.currentStep + 1}`
690        };
691
692        // Add any additional field values
693        if (Object.keys(fieldValues).length > 0) {
694            answerRecord.fields = fieldValues;
695        }
696
697        // Update answer cache (stores ALL answers for restoration)
698        const newAnswerCache = {
699            ...state.answerCache,
700            [stepName]: answerRecord
701        };
702
703        // Update path taken
704        // If step is already in path, this means we're going back and changing answer
705        const existingPathIndex = state.pathTaken.indexOf(stepName);
706        let newPathTaken;
707        if (existingPathIndex >= 0) {
708            // Step exists in path - trim everything after it (path changed)
709            newPathTaken = state.pathTaken.slice(0, existingPathIndex + 1);
710        } else {
711            // New step - add to path
712            newPathTaken = [...state.pathTaken, stepName];
713        }
714
715        // Build answers array from pathTaken for results display
716        const newAnswers = newPathTaken
717            .map(sName => newAnswerCache[sName])
718            .filter(a => a); // Filter out any undefined
719
720        const newState = {
721            ...state,
722            answers: newAnswers,
723            answerCache: newAnswerCache,
724            pathTaken: newPathTaken
725        };
726
727        // Get routing from field that controls routing
728        let routing = 'next';
729
730        // Find the field that controls routing for this step
731        let routingField = null;
732        const fields = currentStepData.fields || [];
733
734        // Find the first field with options (radio/checkbox) - that controls routing
735        routingField = fields.find(f => f.options && f.options.length > 0);
736
737        if (routingField && routingField.options) {
738            // Check if this is a multi-select (checkbox) field
739            const isMultiSelect = routingField.type === 'checkbox';
740            const selectedValues = fieldValues[routingField.name];
741
742            console.log('🔀 ROUTING DECISION:', {
743                routingFieldName: routingField.name,
744                isMultiSelect: isMultiSelect,
745                selectedValues: selectedValues,
746                allOptions: routingField.options,
747            });
748
749            if (isMultiSelect && Array.isArray(selectedValues)) {
750                // MULTI-SELECT ROUTING LOGIC
751                // Default to 'next' for multi-select steps
752                // Look for first selected option with an explicit (non-empty, non-'next') goTo
753                let foundExplicitRoute = null;
754
755                for (const value of selectedValues) {
756                    const option = routingField.options.find(opt => opt.value === value);
757                    if (option && option.goTo && option.goTo !== '' && option.goTo !== 'next') {
758                        foundExplicitRoute = option.goTo;
759                        console.log('🎯 Multi-select: Found explicit route', {
760                            value: value,
761                            goTo: option.goTo
762                        });
763                        break; // First explicit match wins
764                    }
765                }
766
767                if (foundExplicitRoute) {
768                    routing = foundExplicitRoute;
769                }
770                // Otherwise, routing stays as 'next' (default for multi-select)
771
772                console.log('➡️ MULTI-SELECT FINAL ROUTING:', routing);
773
774                logDebugEvent('DEBUG', 'Multi-select routing decision', {
775                    routingFieldName: routingField.name,
776                    selectedValues: selectedValues,
777                    foundExplicitRoute: foundExplicitRoute,
778                    finalRouting: routing
779                });
780            } else {
781                // SINGLE-SELECT (RADIO) ROUTING LOGIC - existing behavior
782                let answerValue = answer;
783                if (typeof answer === 'object' && answer !== null && !Array.isArray(answer)) {
784                    answerValue = answer[routingField.name];
785                }
786
787                // Find the selected option
788                const selectedOption = routingField.options.find(opt => opt.value === answerValue);
789
790                console.log('🎯 SELECTED OPTION:', selectedOption);
791
792                if (selectedOption && selectedOption.goTo) {
793                    routing = selectedOption.goTo;
794                }
795
796                console.log('➡️ FINAL ROUTING:', routing);
797
798                logDebugEvent('DEBUG', 'Single-select routing decision', {
799                    routingFieldName: routingField.name,
800                    answer: answer,
801                    answerValue: answerValue,
802                    selectedOption: selectedOption,
803                    routing: routing
804                });
805            }
806        } else if (currentStepData.defaultRouting) {
807            // Fallback for info steps or steps without routing fields
808            routing = currentStepData.defaultRouting;
809            console.log('📋 USING DEFAULT ROUTING:', routing);
810        }
811
812        logDebugEvent('INFO', 'Answer recorded with routing', {
813            answer: answer,
814            routing: routing,
815            stepName: currentStepData.name,
816            fields: fieldValues,
817            routingField: routingField ? routingField.name : 'none'
818        });
819
820        // Dispatch step complete event
821        const currentStepEl = container.querySelector(`[data-step="${state.currentStep}"]`);
822        if (currentStepEl) {
823            dispatchStepEvent('complete', currentStepEl, newState, container);
824        }
825
826        // Handle routing
827        if (routing === 'results') {
828            // Route to results step by slug
829            return goToSpecificStep(container, newState, 'results');
830        } else if (routing.startsWith('step:')) {
831            // Route to specific step
832            const targetStepSlug = routing.substring(5); // Remove 'step:' prefix
833            return goToSpecificStep(container, newState, targetStepSlug);
834        } else {
835            // Default: next step (or stay on current if no more steps)
836            return goToNextStep(container, newState);
837        }
838    };
839
840    /**
841     * Go to next step
842     */
843    const goToNextStep = (container, state) => {
844        const nextStep = state.currentStep + 1;
845
846        // Check if we've reached the end of the assessment
847        if (nextStep >= state.totalSteps) {
848            logDebugEvent('INFO', 'Reached end of assessment, staying on current step', { currentStep: state.currentStep, totalSteps: state.totalSteps });
849            // Stay on current step (should be a results step created by user)
850            return state;
851        }
852
853        logDebugEvent('INFO', 'Navigating to next step (sequential)', { from: state.currentStep, to: nextStep });
854
855        // Add target step to pathTaken if not already there
856        const targetStepData = state.steps[nextStep];
857        const targetStepName = targetStepData.name || nextStep.toString();
858        let newPathTaken = [...state.pathTaken];
859
860        if (!newPathTaken.includes(targetStepName)) {
861            newPathTaken.push(targetStepName);
862        }
863
864        const newState = {
865            ...state,
866            currentStep: nextStep,
867            pathTaken: newPathTaken
868        };
869
870        logStateTransition(state, newState, 'NEXT_STEP');
871
872        // Start navigation animation FIRST, update UI AFTER animation completes
873        navigateToStep(container, newState, nextStep, () => {
874            // Callback: runs after animation finishes
875            updateProgress(container, newState);
876            updateURL(nextStep);
877            saveStateToLocalStorage(newState);
878            broadcastStateToDebug(newState);
879        });
880
881        return newState;
882    };
883
884    /**
885     * Go to specific step by slug
886     */
887    const goToSpecificStep = (container, state, targetStepSlug) => {
888        // Find step index by slug
889        const targetStepIndex = state.steps.findIndex(step => step.name === targetStepSlug);
890
891        if (targetStepIndex === -1) {
892            logDebugEvent('ERROR', 'Could not find target step', { targetStepSlug: targetStepSlug });
893            // Fallback to next step
894            return goToNextStep(container, state);
895        }
896
897        logDebugEvent('INFO', 'Navigating to specific step (routed)', {
898            from: state.currentStep,
899            to: targetStepIndex,
900            targetSlug: targetStepSlug
901        });
902
903        // Add target step to pathTaken if not already there
904        let newPathTaken = [...state.pathTaken];
905        if (!newPathTaken.includes(targetStepSlug)) {
906            newPathTaken.push(targetStepSlug);
907        }
908
909        const newState = {
910            ...state,
911            currentStep: targetStepIndex,
912            pathTaken: newPathTaken
913        };
914
915        logStateTransition(state, newState, 'ROUTED_STEP');
916
917        // Start navigation animation FIRST, update UI AFTER animation completes
918        navigateToStep(container, newState, targetStepIndex, () => {
919            // Callback: runs after animation finishes
920            updateProgress(container, newState);
921            updateURL(targetStepIndex);
922            saveStateToLocalStorage(newState);
923            broadcastStateToDebug(newState);
924        });
925
926        return newState;
927    };
928
929    /**
930     * Show results
931     */
932    const showResults = (container, state) => {
933        // Hide current step
934        const currentStepEl = container.querySelector(`[data-step="${state.currentStep}"]`);
935        if (currentStepEl) {
936            currentStepEl.classList.remove('active');
937        }
938
939        // Show results
940        const resultsEl = container.querySelector('[data-step="results"]');
941        if (resultsEl) {
942            resultsEl.classList.add('active');
943        }
944
945        // Format and display answers (pass container to find recommendations across all steps)
946        const resultsText = formatResults(state.answers, state.steps, container);
947        const textarea = container.querySelector('.wp-assessment-results-textarea');
948        if (textarea) {
949            textarea.value = resultsText;
950        }
951
952        // Update progress to 100%
953        const newState = {
954            ...state,
955            currentStep: state.totalSteps
956        };
957
958        updateProgress(container, newState);
959        updateURL('results');
960        clearLocalStorage(state.assessmentId); // Clear saved state when completed
961        broadcastStateToDebug(newState);
962
963        return newState;
964    };
965
966    /**
967     * Extract visible recommendations from the DOM
968     * @param {HTMLElement} container - The step container to scan for recommendations
969     * @returns {Object} Object with coaching and medical arrays
970     */
971    const extractVisibleRecommendations = (container) => {
972        const recommendations = {
973            coaching: [],
974            medical: []
975        };
976
977        if (!container) return recommendations;
978
979        // Find all recommendation cards with data attributes
980        const cards = container.querySelectorAll('.result-card[data-rec-title]');
981
982        cards.forEach(card => {
983            // Check if the card is visible (not hidden by show_if/hide_if logic)
984            const computedStyle = window.getComputedStyle(card);
985            if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
986                return;
987            }
988
989            // Also check parent elements for visibility (hide_if wraps the card)
990            let parent = card.parentElement;
991            while (parent && parent !== container) {
992                const parentStyle = window.getComputedStyle(parent);
993                if (parentStyle.display === 'none' || parentStyle.visibility === 'hidden') {
994                    return;
995                }
996                parent = parent.parentElement;
997            }
998
999            const title = card.dataset.recTitle;
1000            const desc = card.dataset.recDesc;
1001            const section = card.dataset.recSection || 'other';
1002
1003            if (title) {
1004                const rec = { title, desc: desc || '' };
1005                if (section === 'coaching') {
1006                    recommendations.coaching.push(rec);
1007                } else if (section === 'medical') {
1008                    recommendations.medical.push(rec);
1009                }
1010            }
1011        });
1012
1013        return recommendations;
1014    };
1015
1016    /**
1017     * Format results as text (legacy format with recommendations)
1018     * @param {Array} answers - Array of answer records
1019     * @param {Array} steps - Optional array of step definitions for looking up option labels
1020     * @param {HTMLElement} container - Optional container to scan for visible recommendations
1021     */
1022    const formatResults = (answers, steps = [], container = null) => {
1023        // Filter out info steps (where answer is 'continue' or 'next' with no meaningful fields)
1024        const questionAnswers = answers.filter(item => {
1025            if (item.answer === 'continue') return false;
1026            // Keep 'next' only if there are meaningful fields (like height/weight)
1027            if (item.answer === 'next' && (!item.fields || Object.keys(item.fields).length === 0)) return false;
1028            return true;
1029        });
1030
1031        // Helper to convert value to sentence case
1032        const toSentenceCase = (str) => {
1033            if (!str) return '';
1034            // Replace hyphens/underscores with spaces, then capitalize first letter
1035            return str.replace(/[-_]/g, ' ')
1036                      .replace(/\b\w/g, (c, i) => i === 0 ? c.toUpperCase() : c.toLowerCase())
1037                      .replace(/^./, c => c.toUpperCase());
1038        };
1039
1040        // Helper to get option text from step definition
1041        const getOptionText = (stepName, fieldName, value) => {
1042            const stepDef = steps.find(s => s.name === stepName);
1043            if (!stepDef || !stepDef.fields) return null;
1044
1045            const fieldDef = stepDef.fields.find(f => f.name === fieldName);
1046            if (!fieldDef || !fieldDef.options) return null;
1047
1048            const option = fieldDef.options.find(o => o.value === value);
1049            return option ? option.text : null;
1050        };
1051
1052        // Helper to format height/weight fields inline
1053        const formatHeightWeightInline = (fields, baseFieldName) => {
1054            const feet = fields[`${baseFieldName}_feet`];
1055            const inches = fields[`${baseFieldName}_inches`];
1056            const weight = fields[`${baseFieldName}_weight`];
1057            const bmi = fields[`${baseFieldName}_bmi`];
1058
1059            const parts = [];
1060            if (feet || inches) {
1061                parts.push(`Height: ${feet || 0}'${inches || 0}"`);
1062            }
1063            if (weight) {
1064                parts.push(`Weight: ${weight} lbs`);
1065            }
1066            if (bmi) {
1067                parts.push(`BMI: ${bmi}`);
1068            }
1069            return parts.join(' | ');
1070        };
1071
1072        // Helper to check if a field is part of height/weight composite
1073        const isHeightWeightField = (fieldName) => {
1074            return fieldName.endsWith('_feet') ||
1075                   fieldName.endsWith('_inches') ||
1076                   fieldName.endsWith('_weight') ||
1077                   fieldName.endsWith('_height_inches') ||
1078                   fieldName.endsWith('_bmi');
1079        };
1080
1081        // Helper to get base name of height/weight field
1082        const getHeightWeightBaseName = (fieldName) => {
1083            return fieldName.replace(/_(feet|inches|weight|height_inches|bmi)$/, '');
1084        };
1085
1086        // Build the legacy format summary
1087        const summaryLines = [];
1088
1089        questionAnswers.forEach((item) => {
1090            // Track which height/weight groups we've already processed
1091            const processedHeightWeightGroups = new Set();
1092            const displayedValues = [];
1093
1094            if (item.fields && Object.keys(item.fields).length > 0) {
1095                Object.entries(item.fields).forEach(([fieldName, fieldValue]) => {
1096                    // Skip if empty
1097                    if (fieldValue === '' || fieldValue === null || fieldValue === undefined) return;
1098
1099                    // Handle height/weight composite fields
1100                    if (isHeightWeightField(fieldName)) {
1101                        const baseName = getHeightWeightBaseName(fieldName);
1102                        if (!processedHeightWeightGroups.has(baseName)) {
1103                            processedHeightWeightGroups.add(baseName);
1104                            const formatted = formatHeightWeightInline(item.fields, baseName);
1105                            if (formatted) {
1106                                // Height/weight gets its own line at the top
1107                                summaryLines.push(formatted);
1108                            }
1109                        }
1110                        return;
1111                    }
1112
1113                    // Handle arrays (multi-select checkboxes)
1114                    if (Array.isArray(fieldValue)) {
1115                        const textValues = fieldValue.map(v => {
1116                            const optionText = getOptionText(item.stepName, fieldName, v);
1117                            return optionText || toSentenceCase(v);
1118                        });
1119                        if (textValues.length > 0) {
1120                            displayedValues.push(...textValues);
1121                        }
1122                        return;
1123                    }
1124
1125                    // Handle single values (radio, text, number, etc.)
1126                    const optionText = getOptionText(item.stepName, fieldName, fieldValue);
1127                    if (optionText) {
1128                        displayedValues.push(optionText);
1129                    } else {
1130                        displayedValues.push(toSentenceCase(String(fieldValue)));
1131                    }
1132                });
1133            } else if (item.answer && item.answer !== 'next') {
1134                displayedValues.push(toSentenceCase(item.answer));
1135            }
1136
1137            // Format as "Question label: answer(s)"
1138            if (displayedValues.length > 0) {
1139                // Extract a short label from the question
1140                let label = item.question || 'Response';
1141                // Remove trailing question mark and common prefixes
1142                label = label.replace(/\?$/, '').replace(/^(What is your |Do you |Are you |Have you )/i, '');
1143                // Capitalize first letter
1144                label = label.charAt(0).toUpperCase() + label.slice(1);
1145
1146                const valueStr = displayedValues.join(', ');
1147                summaryLines.push(`${label}: ${valueStr}`);
1148            }
1149        });
1150
1151        // Build the output
1152        let output = 'Assessment Summary:\n\n';
1153        output += summaryLines.join('\n');
1154
1155        // Add recommendations if container is provided
1156        if (container) {
1157            const recommendations = extractVisibleRecommendations(container);
1158            const hasCoaching = recommendations.coaching.length > 0;
1159            const hasMedical = recommendations.medical.length > 0;
1160
1161            if (hasCoaching || hasMedical) {
1162                output += '\n\n----------------------------------------\n';
1163                output += 'Recommended Treatment Options:\n';
1164
1165                if (hasCoaching) {
1166                    output += '\nCOACHING SERVICES:\n';
1167                    recommendations.coaching.forEach(rec => {
1168                        output += `- ${rec.title}: ${rec.desc}\n`;
1169                    });
1170                }
1171
1172                if (hasMedical) {
1173                    output += '\nMEDICAL OPTIONS:\n';
1174                    recommendations.medical.forEach(rec => {
1175                        output += `- ${rec.title}: ${rec.desc}\n`;
1176                    });
1177                }
1178
1179                output += '\n----------------------------------------';
1180            }
1181        }
1182
1183        // Add footer
1184        const today = new Date();
1185        const dateStr = today.toLocaleDateString('en-US', {
1186            year: 'numeric',
1187            month: 'long',
1188            day: 'numeric'
1189        });
1190        output += '\n\nPlease bring this summary to your consultation appointment.';
1191        output += `\nDate generated: ${dateStr}`;
1192
1193        return output;
1194    };
1195
1196    /**
1197     * Reset assessment - clear state and navigate to step 0 (no page reload)
1198     */
1199    const resetAssessment = (container, state) => {
1200        // Clear saved state from localStorage
1201        clearLocalStorage(state.assessmentId);
1202
1203        // Reset state to initial values
1204        state.currentStep = 0;
1205        state.answers = [];
1206        state.answerCache = {};
1207        state.pathTaken = [];
1208
1209        // Clear all form inputs in the assessment
1210        container.querySelectorAll('input[type="radio"], input[type="checkbox"]').forEach(input => {
1211            input.checked = false;
1212        });
1213        container.querySelectorAll('input[type="text"], input[type="email"], input[type="tel"], textarea').forEach(input => {
1214            input.value = '';
1215        });
1216        container.querySelectorAll('select').forEach(select => {
1217            select.selectedIndex = 0;
1218        });
1219
1220        // Clear data attributes on container (data-answer-*, data-field-*, data-current-step, etc.)
1221        Array.from(container.attributes).forEach(attr => {
1222            if (attr.name.startsWith('data-answer-') ||
1223                attr.name.startsWith('data-field-') ||
1224                attr.name === 'data-current-step' ||
1225                (attr.name.startsWith('data-') && (attr.value === 'selected' || attr.value === 'unselected'))) {
1226                container.removeAttribute(attr.name);
1227            }
1228        });
1229
1230        // Clear data-selected attributes on any elements
1231        container.querySelectorAll('[data-selected]').forEach(el => {
1232            el.removeAttribute('data-selected');
1233        });
1234
1235        // Remove any selection-related classes from labels/options
1236        container.querySelectorAll('.selected, .is-selected, .is-checked').forEach(el => {
1237            el.classList.remove('selected', 'is-selected', 'is-checked');
1238        });
1239
1240        // Navigate smoothly to step 0
1241        navigateToStep(container, state, 0);
1242        updateProgress(container, state);
1243        updateURL(0, true);
1244
1245        logDebugEvent('INFO', 'Assessment reset', { assessmentId: state.assessmentId });
1246
1247        return state;
1248    };
1249
1250    /**
1251     * Update progress bar and counter
1252     */
1253    const updateProgress = (container, state) => {
1254        const progressFill = container.querySelector('.wp-assessment-progress-fill');
1255        const currentProgressText = container.querySelector('.current-progress');
1256
1257        if (progressFill && currentProgressText) {
1258            // Check if current step is an endpoint
1259            const currentStepData = state.steps[state.currentStep];
1260            const isEndpoint = currentStepData && currentStepData.isEndpoint === true;
1261
1262            // Calculate progress based on actual steps only
1263            const currentStepNumber = state.currentStep + 1;
1264            const totalScreens = state.totalSteps;
1265
1266            // If on endpoint, force 100%, otherwise calculate normally
1267            const percentage = isEndpoint ? 100 : Math.round((currentStepNumber / totalScreens) * 100);
1268
1269            logDebugEvent('INFO', 'Updating progress', {
1270                currentStep: state.currentStep,
1271                currentStepNumber: currentStepNumber,
1272                totalScreens: totalScreens,
1273                isEndpoint: isEndpoint,
1274                percentage: percentage
1275            });
1276
1277            progressFill.style.width = `${percentage}%`;
1278            currentProgressText.textContent = percentage;
1279        }
1280
1281        // Update back button visibility based on path history
1282        // Hide back button only on the very first step (no path history)
1283        const currentStepData = state.steps[state.currentStep];
1284        const currentStepName = currentStepData.name || state.currentStep.toString();
1285        const currentPathIndex = state.pathTaken.indexOf(currentStepName);
1286        const shouldHideBack = currentPathIndex <= 0;
1287
1288        // Update ALL back buttons in the container
1289        container.querySelectorAll('.wp-assessment-back-btn').forEach(backBtn => {
1290            backBtn.style.display = shouldHideBack ? 'none' : '';
1291        });
1292
1293        // Re-update container height after back button visibility change
1294        const activeStep = container.querySelector('.wp-assessment-step.active');
1295        if (activeStep) {
1296            updateContainerHeight(container, activeStep);
1297        }
1298
1299        // Sync answer data attributes to container
1300        syncDataAttributes(container, state);
1301    };
1302
1303    /**
1304     * Sync answer data to container data attributes
1305     * This allows CSS-based conditional display based on user answers
1306     * e.g., [data-answer-step-name="yes"] .conditional-content { display: block; }
1307     */
1308    const syncDataAttributes = (container, state) => {
1309        // First, remove all existing answer and field data attributes
1310        const existingAttrs = Array.from(container.attributes)
1311            .filter(attr => attr.name.startsWith('data-answer-') || attr.name.startsWith('data-field-'));
1312        existingAttrs.forEach(attr => container.removeAttribute(attr.name));
1313
1314        // Add current step as data attribute
1315        const currentStepData = state.steps[state.currentStep];
1316        const currentStepName = currentStepData?.name || state.currentStep.toString();
1317        container.setAttribute('data-current-step', currentStepName);
1318
1319        // Add data attributes for each answer in the cache
1320        Object.entries(state.answerCache || {}).forEach(([stepName, answerRecord]) => {
1321            if (!answerRecord) return;
1322
1323            // Convert step name to valid data attribute name
1324            // e.g., "do-you-have-seizures" -> "data-answer-do-you-have-seizures"
1325            const attrName = `data-answer-${stepName}`;
1326
1327            // Get the answer value
1328            let answerValue = answerRecord.answer;
1329
1330            // If answer is an object (multiple fields), stringify it or use the first value
1331            if (typeof answerValue === 'object' && answerValue !== null) {
1332                if (Array.isArray(answerValue)) {
1333                    // For arrays (checkboxes), join values
1334                    answerValue = answerValue.join(',');
1335                } else {
1336                    // For objects, use the first field value or stringify
1337                    const firstKey = Object.keys(answerValue)[0];
1338                    answerValue = firstKey ? answerValue[firstKey] : JSON.stringify(answerValue);
1339                }
1340            }
1341
1342            // Set the data attribute (lowercase value for easier CSS matching)
1343            if (answerValue !== undefined && answerValue !== null && answerValue !== '') {
1344                container.setAttribute(attrName, String(answerValue).toLowerCase());
1345            }
1346
1347            // Also add individual field values if present
1348            if (answerRecord.fields) {
1349                Object.entries(answerRecord.fields).forEach(([fieldName, fieldValue]) => {
1350                    const fieldAttrName = `data-field-${fieldName.replace(/_/g, '-')}`;
1351                    let attrValue = fieldValue;
1352
1353                    if (Array.isArray(attrValue)) {
1354                        attrValue = attrValue.join(',');
1355                    } else if (typeof attrValue === 'object' && attrValue !== null) {
1356                        attrValue = JSON.stringify(attrValue);
1357                    }
1358
1359                    if (attrValue !== undefined && attrValue !== null && attrValue !== '') {
1360                        container.setAttribute(fieldAttrName, String(attrValue).toLowerCase());
1361                    }
1362                });
1363            }
1364        });
1365
1366        // Add per-selection data attributes for checkbox fields
1367        // This enables CSS targeting like [data-symptoms-headache="selected"] .warning { display: block; }
1368        Object.entries(state.answerCache || {}).forEach(([stepName, answerRecord]) => {
1369            if (!answerRecord || !answerRecord.fields) return;
1370
1371            // Find the corresponding step definition to get all options
1372            const stepData = state.steps.find(s => s.name === stepName);
1373            if (!stepData || !stepData.fields) return;
1374
1375            // Process each field in the step
1376            stepData.fields.forEach(fieldDef => {
1377                // Only process checkbox fields with options
1378                if (fieldDef.type !== 'checkbox' || !fieldDef.options || fieldDef.options.length === 0) return;
1379
1380                const fieldName = fieldDef.name;
1381                const selectedValues = answerRecord.fields[fieldName];
1382
1383                // Skip if no values recorded for this field
1384                if (!selectedValues) return;
1385
1386                // Ensure selectedValues is an array
1387                const selectedArray = Array.isArray(selectedValues) ? selectedValues : [selectedValues];
1388
1389                // Add data attribute for each option (selected or unselected)
1390                fieldDef.options.forEach(option => {
1391                    const optionValue = option.value;
1392                    const isSelected = selectedArray.includes(optionValue);
1393
1394                    // Format: data-{stepName}-{optionValue}="selected|unselected"
1395                    // Sanitize to valid data attribute name (lowercase, replace non-alphanumeric with dashes
1395)
1396                    const attrName = `data-${stepName}-${optionValue}`.toLowerCase().replace(/[^a-z0-9-]/g, '-');
1397                    container.setAttribute(attrName, isSelected ? 'selected' : 'unselected');
1398                });
1399            });
1400        });
1401
1402        logDebugEvent('DEBUG', 'Synced data attributes', {
1403            currentStep: currentStepName,
1404            attributeCount: container.attributes.length
1405        });
1406
1407        // Evaluate conditional content after syncing data attributes
1408        evaluateConditionalContent(container);
1409    };
1410
1411    /**
1412     * Clean empty paragraphs from step content
1413     * WordPress wpautop() adds empty <p></p> tags around shortcodes
1414     */
1415    const cleanEmptyParagraphs = (container) => {
1416        const stepContents = container.querySelectorAll('.wp-assessment-step-description');
1417        stepContents.forEach(content => {
1418            // Remove empty paragraphs (with optional whitespace/nbsp)
1419            content.querySelectorAll('p').forEach(p => {
1420                const text = p.textContent.trim();
1421                const html = p.innerHTML.trim();
1422                if (!text && (!html || html === '&nbsp;' || html.match(/^(<br\s*\/?>|\s|&nbsp;)*$/))) {
1423                    p.remove();
1424                }
1425            });
1426            // Remove stray <br> tags between block elements by finding and removing them directly
1427            // (Don't use innerHTML assignment as it wipes textarea values)
1428            content.querySelectorAll('br').forEach(br => {
1429                const prev = br.previousElementSibling;
1430                if (prev && prev.tagName === 'DIV') {
1431                    br.remove();
1432                }
1433            });
1434        });
1435    };
1436
1437    /**
1438     * Evaluate conditional content shortcodes [show_if] and [hide_if]
1439     * Shows/hides content based on answer data attributes on the container
1440     *
1441     * Supports:
1442     *   Simple (single condition):
1443     *   [show_if step="has-headaches" is="yes"] - radio/boolean
1444     *   [show_if step="symptoms" is="binge-eating"] - checkbox option
1445     *   [hide_if step="pregnant" is-not="no"] - negation
1446     *
1447     *   Multi-condition:
1448     *   [hide_if any="thyroid:yes,gallbladder:yes"] - OR logic
1449     *   [show_if all="age:50-plus,savings:200k"] - AND logic
1450     *   [show_if all="eating:binge,age:!12-17"] - negation with ! prefix
1451     */
1452    const evaluateConditionalContent = (container) => {
1453        // Clean empty paragraphs first
1454        cleanEmptyParagraphs(container);
1455        const conditionals = container.querySelectorAll('.wp-assessment-conditional');
1456
1457        // Log to server for debugging
1458        logDebugEvent('CONDITIONAL', 'evaluateConditionalContent called', {
1459            conditionalsFound: conditionals.length,
1460            answerAttrs: Array.from(container.attributes)
1461                .filter(a => a.name.startsWith('data-answer-') || a.name.startsWith('data-'))
1462                .map(a => ({ name: a.name, value: a.value }))
1463        });
1464
1465        conditionals.forEach(el => {
1466            const isShowType = el.classList.contains('wp-assessment-show');
1467            let conditionMet = false;
1468
1469            // Check for multi-condition syntax (any/all)
1470            const anyConditions = el.dataset.conditionAny;
1471            const allConditions = el.dataset.conditionAll;
1472
1473            if (anyConditions || allConditions) {
1474                // Multi-condition mode
1475                const isAnyMode = !!anyConditions;
1476                const conditionsStr = anyConditions || allConditions;
1477                const conditions = conditionsStr.split(',').map(c => c.trim());
1478
1479                // Parse each condition (format: "step:value" or "step:!value" for negation)
1480                const results = conditions.map(condition => {
1481                    const [step, rawValue] = condition.split(':').map(s => s.trim());
1482                    if (!step || !rawValue) return false;
1483
1484                    // Check for negation prefix (!)
1485                    const isNegated = rawValue.startsWith('!');
1486                    const value = isNegated ? rawValue.substring(1) : rawValue;
1487
1488                    let matches = false;
1489
1490                    // Try radio/boolean first: data-answer-{step}
1491                    const answerValue = container.getAttribute(`data-answer-${step}`);
1492                    if (answerValue !== null && answerValue === value) {
1493                        matches = true;
1494                    } else {
1495                        // Try checkbox: data-{step}-{value}="selected"
1496                        const checkboxAttr = container.getAttribute(`data-${step}-${value}`);
1497                        matches = checkboxAttr === 'selected';
1498                    }
1499
1500                    // Apply negation if needed
1501                    return isNegated ? !matches : matches;
1502                });
1503
1504                // Apply logic: any = OR (at least one true), all = AND (all must be true)
1505                if (isAnyMode) {
1506                    conditionMet = results.some(r => r === true);
1507                } else {
1508                    conditionMet = results.every(r => r === true);
1509                }
1510
1511                logDebugEvent('CONDITIONAL', 'Multi-condition check', {
1512                    mode: isAnyMode ? 'any (OR)' : 'all (AND)',
1513                    conditions: conditions,
1514                    results: results,
1515                    conditionMet: conditionMet,
1516                    isShowType: isShowType
1517                });
1518            } else {
1519                // Simple single-condition mode (backward compatible)
1520                const step = el.dataset.conditionStep;
1521                const value = el.dataset.conditionValue;
1522                const compare = el.dataset.conditionCompare || 'equals';
1523
1524                // Try radio/boolean first: data-answer-{step}
1525                const answerValue = container.getAttribute(`data-answer-${step}`);
1526
1527                if (answerValue !== null && answerValue === value) {
1528                    // Exact match on answer attribute
1529                    conditionMet = true;
1530                } else {
1531                    // Try checkbox: data-{step}-{value}="selected"
1532                    const checkboxAttr = container.getAttribute(`data-${step}-${value}`);
1533                    conditionMet = checkboxAttr === 'selected';
1534                }
1535
1536                // Handle is-not (negation)
1537                if (compare === 'not') {
1538                    conditionMet = !conditionMet;
1539                }
1540
1541                // Log to server for debugging
1542                logDebugEvent('CONDITIONAL', 'Single condition check', {
1543                    step: step,
1544                    expectedValue: value,
1545                    answerValue: answerValue,
1546                    checkboxAttr: container.getAttribute(`data-${step}-${value}`),
1547                    compare: compare,
1548                    conditionMet: conditionMet,
1549                    isShowType: isShowType,
1550                    action: isShowType ? (conditionMet ? 'SHOW' : 'HIDE') : (conditionMet ? 'HIDE' : 'SHOW')
1551                });
1552            }
1553
1554            // Apply visibility (use explicit 'block' to override CSS)
1555            if (isShowType) {
1556                // Show type: hidden by default, shown when condition met
1557                el.style.display = conditionMet ? 'block' : 'none';
1558            } else {
1559                // Hide type: visible by default, hidden when condition met
1560                el.style.display = conditionMet ? 'none' : 'block';
1561            }
1562        });
1563
1564        logDebugEvent('DEBUG', 'Evaluated conditional content', {
1565            conditionalCount: conditionals.length
1566        });
1567    };
1568
1569    /**
1570     * Navigate to a specific step
1571     * @param {Function} onComplete - Optional callback to run after animation completes
1572     */
1573    const navigateToStep = (container, state, stepNumber, onComplete) => {
1574        const animationType = settings.animation_type;
1575        const duration = parseInt(settings.animation_duration, 10);
1576
1577        const currentStepEl = container.querySelector('.wp-assessment-step.active');
1578        const targetStepEl = container.querySelector(`[data-step="${stepNumber}"]`);
1579
1580        if (!targetStepEl) return;
1581
1582        // If target is already active, just restore content and skip animation
1583        if (currentStepEl === targetStepEl) {
1584            logDebugEvent('NAVIGATION', 'Target already active - ski
1584pping animation', {
1585                step: stepNumber,
1586                stepName: targetStepEl.dataset.stepName
1587            });
1588
1589            restoreAnswers(targetStepEl, state);
1590            populateResultsIfPresent(targetStepEl, state, container);
1591            dispatchStepEvent('load', targetStepEl, state, container);
1592            autoPopulateFormField(targetStepEl, state, container);
1593
1594            // Update height in case content changed (e.g., results populated)
1595            updateContainerHeight(container, targetStepEl);
1596
1597            // Schedule additional height recalculations for dynamic content
1598            [100, 300, 600].forEach(delay => {
1599                setTimeout(() => updateContainerHeight(container, targetStepEl), delay);
1600            });
1601
1602            // Execute callback if provided
1603            if (onComplete && typeof onComplete === 'function') {
1604                onComplete();
1605            }
1606            return;
1607        }
1608
1609        const startTime = performance.now();
1610
1611        logDebugEvent('NAVIGATION', 'Navigation started', {
1612            from: state.currentStep,
1613            fromName: currentStepEl ? currentStepEl.dataset.stepName : 'none',
1614            to: stepNumber,
1615            toName: targetStepEl.dataset.stepName,
1616            pathTaken: state.pathTaken,
1617            animationType: animationType,
1618            duration: duration,
1619            timestamp: startTime
1620        });
1621
1622        // Smooth scroll to top of viewport if enabled
1623        if (settings.scroll_to_top) {
1624            window.scrollTo({
1625                top: 0,
1626                behavior: 'smooth'
1627            });
1628        }
1629
1630        // Prepare target step content BEFORE showing it (prevents flash)
1631        restoreAnswers(targetStepEl, state);
1632        populateResultsIfPresent(targetStepEl, state, container);
1633        autoPopulateFormField(targetStepEl, state, container);
1634
1635        logDebugEvent('ANIMATION', 'Content restored', {
1636            step: targetStepEl.dataset.stepName,
1637            elapsed: performance.now() - startTime
1638        });
1639
1640        if (animationType === 'none' || !currentStepEl) {
1641            // No animation - instant transition
1642            if (currentStepEl) {
1643                currentStepEl.classList.remove('active');
1644            }
1645            targetStepEl.classList.add('active');
1646
1647            // Update container height to match new step
1648            updateContainerHeight(container, targetStepEl);
1649
1650            dispatchStepEvent('load', targetStepEl, state, container);
1651
1652            // Schedule additional height recalculations for dynamic content
1653            [100, 300, 600].forEach(delay => {
1654                setTimeout(() => updateContainerHeight(container, targetStepEl), delay);
1655            });
1656
1657            logDebugEvent('NAVIGATION', 'Navigation completed (instant)', {
1658                duration: performance.now() - startTime
1659            });
1660
1661            // Execute callback if provided
1662            if (onComplete && typeof onComplete === 'function') {
1663                onComplete();
1664            }
1665        } else if (animationType === 'fade') {
1666            // CSS handles the transition automatically via opacity
1667            // Just toggle the active class and let CSS do the work
1668
1669            logDebugEvent('ANIMATION', 'Starting CSS transition', {
1670                elapsed: performance.now() - startTime
1671            });
1672
1673            // Update container height FIRST (so it animates with the fade)
1674            updateContainerHeight(container, targetStepEl);
1675
1676            // Remove active from current (triggers fade-out via CSS)
1677            if (currentStepEl) {
1678                currentStepEl.classList.remove('active');
1679            }
1680
1681            // Add active to target (triggers fade-in via CSS)
1682            targetStepEl.classList.add('active');
1683
1684            // Dispatch load event immediately
1685            dispatchStepEvent('load', targetStepEl, state, container);
1686
1687            logDebugEvent('ANIMATION', 'CSS transition started', {
1688                step: targetStepEl.dataset.stepName,
1689                elapsed: performance.now() - startTime
1690            });
1691
1692            // Wait for CSS transition to complete before running callback
1693            setTimeout(() => {
1694                logDebugEvent('NAVIGATION', 'Navigation completed (fade)', {
1695                    step: targetStepEl.dataset.stepName,
1696                    totalDuration: performance.now() - startTime,
1697                    expectedDuration: duration
1698                });
1699
1700                // Schedule additional height recalculations for dynamic content
1701                [100, 300, 600].forEach(delay => {
1702                    setTimeout(() => updateContainerHeight(container, targetStepEl), delay);
1703                });
1704
1705                // Execute callback AFTER animation fully completes
1706                if (onComplete && typeof onComplete === 'function') {
1707                    logDebugEvent('ANIMATION', 'Executing post-animation callback', {
1708                        step: targetStepEl.dataset.stepName,
1709                        elapsed: performance.now() - startTime
1710                    });
1711                    onComplete();
1712                }
1713            }, duration);
1714        }
1715    };
1716
1717    /**
1718     * Restore previously given answers when revisiting a step
1719     */
1720    const restoreAnswers = (stepElement, state) => {
1721        if (!stepElement) return;
1722
1723        const stepName = stepElement.dataset.stepName;
1724        const cachedAnswer = state.answerCache[stepName];
1725
1726        if (!cachedAnswer) return;
1727
1728        logDebugEvent('INFO', 'Restoring previous answers', {
1729            stepName: stepName,
1730            answer: cachedAnswer.answer,
1731            fields: cachedAnswer.fields
1732        });
1733
1734        // Restore field values
1735        if (cachedAnswer.fields && Object.keys(cachedAnswer.fields).length > 0) {
1736            Object.entries(cachedAnswer.fields).forEach(([fieldName, fieldValue]) => {
1737                const field = stepElement.querySelector(`[data-field-name="${fieldName}"]`);
1738                if (!field) return;
1739
1740                // Radio buttons
1741                if (field.classList.contains('wp-assessment-radio-field')) {
1742                    const radio = field.querySelector(`input[value="${fieldValue}"]`);
1743                    if (radio) radio.checked = true;
1744                }
1745
1746                // Checkboxes
1747                else if (field.classList.contains('wp-assessment-checkbox-field')) {
1748                    const values = Array.isArray(fieldValue) ? fieldValue : [fieldValue];
1749                    values.forEach(val => {
1750                        const checkbox = field.querySelector(`input[value="${val}"]`);
1751                        if (checkbox) checkbox.checked = true;
1752                    });
1753                }
1754
1755                // Text input
1756                else if (field.classList.contains('wp-assessment-text-field')) {
1757                    const input = field.querySelector('.wp-assessment-text-input');
1758                    if (input) input.value = fieldValue;
1759                }
1760
1761                // Number input
1762                else if (field.classList.contains('wp-assessment-number-field')) {
1763                    const input = field.querySelector('.wp-assessment-number-input');
1764                    if (input) input.value = fieldValue;
1765                }
1766
1767                // Textarea
1768                else if (field.classList.contains('wp-assessment-textarea-field')) {
1769                    const textarea = field.querySelector('.wp-assessment-textarea-input');
1770                    if (textarea) textarea.value = fieldValue;
1771                }
1772            });
1773        }
1774    };
1775
1776    /**
1777     * Populate results textarea if present in step
1778     */
1779    const populateResultsIfPresent = (stepElement, state, container = null) => {
1780        const resultsTextarea = stepElement.querySelector('.wp-assessment-results-textarea');
1781
1782        logDebugEvent('RESULTS', 'populateResultsIfPresent called', {
1783            stepName: stepElement?.dataset?.stepName,
1784            hasTextarea: !!resultsTextarea,
1785            answersCount: state.answers?.length || 0,
1786            hasSteps: !!state.steps
1787        });
1788
1789        if (resultsTextarea && state.answers && state.answers.length > 0) {
1790            // Use the main container to find recommendations across all steps
1791            const searchContainer = container || stepElement.closest('.wp-assessment-container') || stepElement;
1792            const formattedResults = formatResults(state.answers, state.steps, searchContainer);
1793            resultsTextarea.value = formattedResults;
1794
1795            logDebugEvent('RESULTS', 'Textarea populated', {
1796                resultLength: formattedResults.length,
1797                preview: formattedResults.substring(0, 100)
1798            });
1799        }
1800    };
1801
1802    /**
1803     * Dispatch step-specific custom event
1804     */
1805    const dispatchStepEvent = (eventType, stepElement, state, container) => {
1806        if (!stepElement) return;
1807
1808        const stepSlug = stepElement.dataset.stepName || stepElement.dataset.step;
1809        const stepIndex = parseInt(stepElement.dataset.step, 10);
1810        const assessmentId = container.dataset.assessmentId;
1811
1812        // Event detail object (use container to find recommendations across all steps)
1813        const detail = {
1814            stepSlug: stepSlug,
1815            stepIndex: stepIndex,
1816            assessmentId: assessmentId,
1817            answers: state.answers,
1818            formattedResults: formatResults(state.answers, state.steps, container),
1819            state: state
1820        };
1821
1822        // Dispatch step-specific event (e.g., "wp-assessment:results:load")
1823        const stepSpecificEvent = new CustomEvent(`wp-assessment:${stepSlug}:${eventType}`, {
1824            detail: detail,
1825            bubbles: true
1826        });
1827        document.dispatchEvent(stepSpecificEvent);
1828
1829        // Also dispatch generic event (e.g., "wp-assessment:load")
1830        const genericEvent = new CustomEvent(`wp-assessment:${eventType}`, {
1831            detail: detail,
1832            bubbles: true
1833        });
1834        document.dispatchEvent(genericEvent);
1835
1836        logDebugEvent('EVENT', `Dispatched wp-assessment:${stepSlug}:${eventType}`, detail);
1837    };
1838
1839    /**
1840     * Auto-populate form field if configured for this step
1841     */
1842    const autoPopulateFormField = (stepElement, state, container = null) => {
1843        if (!stepElement) return;
1844
1845        const formEnabled = stepElement.dataset.formEnabled;
1846        const formTarget = stepElement.dataset.formTarget;
1847        const formMethod = stepElement.dataset.formMethod || 'prepend';
1848
1849        if (!formEnabled || !formTarget) return;
1850
1851        try {
1852            const targetField = document.querySelector(formTarget);
1853            if (!targetField) {
1854                logDebugEvent('WARNING', `Form target field not found: ${formTarget}`);
1855                return;
1856            }
1857
1858            // Use the main container to find recommendations across all steps
1859            const searchContainer = container || stepElement.closest('.wp-assessment-container') || stepElement;
1860            const results = formatResults(state.answers, state.steps, searchContainer);
1861            if (!results) return;
1862
1863            if (formMethod === 'replace') {
1864                targetField.value = results;
1865            } else if (formMethod === 'prepend') {
1866                const existingValue = targetField.value || '';
1867                targetField.value = results + (existingValue ? '\n\n' + existingValue : '');
1868            } else if (formMethod === 'append') {
1869                const existingValue = targetField.value || '';
1870                targetField.value = (existingValue ? existingValue + '\n\n' : '') + results;
1871            }
1872
1873            logDebugEvent('INFO', 'Auto-populated form field', {
1874                target: formTarget,
1875                method: formMethod,
1876                resultsLength: results.length
1877            });
1878        } catch (error) {
1879            logDebugEvent('ERROR', 'Error auto-populating form field', {
1880                target: formTarget,
1881                error: error.message
1882            });
1883        }
1884    };
1885
1886    /**
1887     * Update URL with current step
1888     */
1889    const updateURL = (step, replace = false) => {
1890        // Get step name if it's a number
1891        let stepSlug = step;
1892
1893        if (typeof step === 'number') {
1894            const stepEl = document.querySelector(`[data-step="${step}"]`);
1895            if (stepEl && stepEl.dataset.stepName) {
1896                stepSlug = stepEl.dataset.stepName;
1897            }
1898        }
1899
1900        // Use just the slug as query param (no key)
1901        const url = new URL(window.location);
1902        // Clear all params first
1903        url.search = '';
1904        // Add just the step slug
1905        url.search = `?${stepSlug}`;
1906
1907        logDebugEvent('INFO', 'Updating URL', { step: step, slug: stepSlug, url: url.toString(), replace: replace });
1908
1909        if (replace) {
1910            history.replaceState({ step: step }, '', url.toString());
1911        } else {
1912            history.pushState({ step: step }, '', url.toString());
1913        }
1914    };
1915
1916    /**
1917     * Get step number from URL query param
1918     */
1919    const getStepFromURL = () => {
1920        // Get the query string without the '?'
1921        const query = window.location.search.substring(1);
1922
1923        if (!query) {
1924            return null;
1925        }
1926
1927        // The entire query string is the step identifier
1928        const stepIdentifier = query;
1929
1930        // Check if it's a number
1931        if (/^\d+$/.test(stepIdentifier)) {
1932            return parseInt(stepIdentifier, 10);
1933        }
1934
1935        // It's a step name - find the step index
1936        const stepEl = document.querySelector(`[data-step-name="${stepIdentifier}"]`);
1937        if (stepEl && stepEl.dataset.step) {
1938            const stepIndex = parseInt(stepEl.dataset.step, 10);
1939            logDebugEvent('INFO', 'Resolved step name from URL', { name: stepIdentifier, index: stepIndex });
1940            return stepIndex;
1941        }
1942
1943        logDebugEvent('WARNING', 'Could not resolve step from URL', { identifier: stepIdentifier });
1944        return null;
1945    };
1946
1947    /**
1948     * Save state to localStorage
1949     */
1950    const saveStateToLocalStorage = (state) => {
1951        if (!state.assessmentId) return;
1952
1953        const storageKey = `wp_assessment_${state.assessmentId}`;
1954        const dataToSave = {
1955            currentStep: state.currentStep,
1956            answers: state.answers,
1957            answerCache: state.answerCache,
1958            pathTaken: state.pathTaken,
1959            timestamp: Date.now()
1960        };
1961
1962        try {
1963            localStorage.setItem(storageKey, JSON.stringify(dataToSave));
1964            window.wpAssessmentDebug.localStorage = dataToSave;
1965            logDebugEvent('INFO', 'Saved state to localStorage', { key: storageKey, data: dataToSave });
1966        } catch (e) {
1967            logDebugEvent('ERROR', 'Failed to save state to localStorage', e.message);
1968            console.error('Failed to save assessment state:', e);
1969        }
1970    };
1971
1972    /**
1973     * Load state from localStorage
1974     */
1975    const loadStateFromLocalStorage = (assessmentId) => {
1976        if (!assessmentId) return null;
1977
1978        const storageKey = `wp_assessment_${assessmentId}`;
1979
1980        try {
1981            const saved = localStorage.getItem(storageKey);
1982            if (!saved) {
1983                logDebugEvent('INFO', 'No saved state found in localStorage', { key: storageKey });
1984                return null;
1985            }
1986
1987            const data = JSON.parse(saved);
1988
1989            // Check if data is too old (24 hours)
1990            const maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
1991            if (Date.now() - data.timestamp > maxAge) {
1992                localStorage.removeItem(storageKey);
1993                logDebugEvent('INFO', 'Saved state expired, removed from localStorage', { key: storageKey, age: Date.now() - data.timestamp });
1994                return null;
1995            }
1996
1997            window.wpAssessmentDebug.localStorage = data;
1998            logDebugEvent('INFO', 'Loaded state from localStorage', { key: storageKey, data: data });
1999
2000            return {
2001                currentStep: data.currentStep,
2002                answers: data.answers || [],
2003                answerCache: data.answerCache || {},
2004                pathTaken: data.pathTaken || []
2005            };
2006        } catch (e) {
2007            logDebugEvent('ERROR', 'Failed to load state from localStorage', e.message);
2008            console.error('Failed to load assessment state:', e);
2009            return null;
2010        }
2011    };
2012
2013    /**
2014     * Clear state from localStorage
2015     */
2016    const clearLocalStorage = (assessmentId) => {
2017        if (!assessmentId) return;
2018
2019        const storageKey = `wp_assessment_${assessmentId}`;
2020
2021        try {
2022            localStorage.removeItem(storageKey);
2023        } catch (e) {
2024            console.error('Failed to clear assessment state:', e);
2025        }
2026    };
2027
2028    /**
2029     * Initialize debug panel
2030     */
2031    const initDebugPanel = () => {
2032        const debugPanel = document.querySelector('[data-debug-panel]');
2033        if (!debugPanel) return;
2034
2035        // Setup toggle button
2036        const toggleBtn = debugPanel.querySelector('[data-debug-toggle]');
2037        if (toggleBtn) {
2038            toggleBtn.addEventListener('click', () => {
2039                debugPanel.classList.toggle('collapsed');
2040            });
2041        }
2042    };
2043
2044    /**
2045     * Broadcast state to debug panel
2046     */
2047    const broadcastStateToDebug = (state) => {
2048        const debugPanel = document.querySelector('[data-debug-panel]');
2049        if (!debugPanel) return;
2050
2051        // Update state values
2052        updateDebugElement('[data-debug-current-step]', state.currentStep + 1);
2053        updateDebugElement('[data-debug-total-steps]', state.totalSteps);
2054        updateDebugElement('[data-debug-assessment-id]', state.assessmentId || '—');
2055
2056        // Check if current step is an endpoint for progress calculation
2057        const currentStepData = state.steps[state.currentStep];
2058        const isEndpoint = currentStepData && currentStepData.isEndpoint === true;
2059        const percentage = isEndpoint ? 100 : Math.round(((state.currentStep + 1) / state.totalSteps) * 100);
2060        updateDebugElement('[data-debug-progress]', `${percentage}%`);
2061
2062        // Update journey
2063        updateDebugAnswers(state.answers);
2064    };
2065
2066    /**
2067     * Update a debug element's text content
2068     */
2069    const updateDebugElement = (selector, value) => {
2070        const element = document.querySelector(selector);
2071        if (element) {
2072            element.textContent = value;
2073        }
2074    };
2075
2076    /**
2077     * Update debug journey display
2078     */
2079    const updateDebugAnswers = (answers) => {
2080        const journeyContainer = document.querySelector('[data-debug-journey]');
2081        const answersCount = document.querySelector('[data-debug-answers-count]');
2082
2083        if (!journeyContainer) return;
2084
2085        if (answersCount) {
2086            answersCount.textContent = `(${answers.length})`;
2087        }
2088
2089        if (answers.length === 0) {
2090            journeyContainer.innerHTML = '<em>No steps taken yet</em>';
2091            return;
2092        }
2093
2094        journeyContainer.innerHTML = answers.map((item, index) => {
2095            let html = `
2096                <div class="wp-assessment-debug-journey-step">
2097                    <div class="wp-assessment-debug-journey-header">
2098                        <span class="wp-assessment-debug-step-number">${index + 1}</span>
2099                        <span class="wp-assessment-debug-step-name">${escapeHtml(item.stepLabel || item.stepName)}</span>
2100                    </div>
2101                    <div class="wp-assessment-debug-journey-question">${escapeHtml(item.question)}</div>
2102                    <div class="wp-assessment-debug-journey-answer">
2103                        <span class="label">Answer:</span>
2104                        <span class="value">${escapeHtml(item.answer.toUpperCase())}</span>
2105                    </div>
2106            `;
2107
2108            // Add field values if present
2109            if (item.fields && Object.keys(item.fields).length > 0) {
2110                html += '<div class="wp-assessment-debug-journey-fields">';
2111                Object.entries(item.fields).forEach(([fieldName, fieldValue]) => {
2112                    if (fieldValue) {
2113                        html += `
2114                            <div class="wp-assessment-debug-field-value">
2115                                <span class="label">${escapeHtml(fieldName)}:</span>
2116                                <span class="value">${escapeHtml(fieldValue)}</span>
2117                            </div>
2118                        `;
2119                    }
2120                });
2121                html += '</div>';
2122            }
2123
2124            html += '</div>';
2125            return html;
2126        }).join('');
2127    };
2128
2129
2130    /**
2131     * Escape HTML to prevent XSS
2132     */
2133    const escapeHtml = (text) => {
2134        const div = document.createElement('div');
2135        div.textContent = text;
2136        return div.innerHTML;
2137    };
2138
2139    // Initialize when DOM is ready
2140    if (document.readyState === 'loading') {
2141        document.addEventListener('DOMContentLoaded', init);
2142    } else {
2143        init();
2144    }
2145})();

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.