PageSourceSearch

https://hr-on.com/wp-content/plugins/HRON-New-Price/assets/js/app.js?v=1783685006&ver=1783685006

js hr-on.com collected 2026-10-02 03:08:33 UTC 93,554 bytes, 1,901 lines download raw bytes

1// Shared per-module discount display for the calculator and its price modal.
2window.HRONUpdateComboBadges = function (container, selected, sizes, serverRates, enterprise) {
3    if (!container) return;
4    var config = window.HRONNewPrice || {};
5    var limits = config.enterpriseLimits || {};
6    var count = Object.keys(selected).filter(function (key) { return selected[key]; }).length;
7    var unavailable = enterprise || sizes.employees > limits.employees || sizes.jobslots > limits.jobslots;
8    var lang = (document.documentElement.lang || 'da').slice(0, 2).toLowerCase();
9    var labels = { en: ['with Staff', 'combo'], de: ['mit Staff', 'Kombi'], da: ['med Staff', 'kombination'], nb: ['med Staff', 'kombinasjon'] };
10    var words = labels[lang] || labels.en;
11    container.querySelectorAll('.module-item[data-module], .hron-newprice-system-card[data-module]').forEach(function (card) {
12        var key = card.dataset.module;
13        var heading = card.querySelector('.module-title, h4');
14        if (!heading) return;
15        var badge = heading.querySelector('.hron-combo-badge');
16        if (!badge) {
17            badge = document.createElement('span');
18            badge.className = 'hron-combo-badge';
19            badge.hidden = true;
20            badge.setAttribute('aria-live', 'polite');
21            heading.appendChild(badge);
22        }
23        var rates = (config.comboRates || {})[key] || {};
24        var rate = serverRates && typeof serverRates[key] === 'number'
25            ? serverRates[key] * 100
26            : Number(key === 'staff' || !selected.staff ? rates.withoutStaff : rates.withStaff);
27        var visible = !unavailable && count > 1 && selected[key] && Number.isFinite(rate) && rate > 0;
28        if (!visible) {
29            badge.hidden = true;
30            badge.textContent = '';
31            return;
32        }
33        var text = '−' + Math.round(rate) + '% · ' + (selected.staff && key !== 'staff' ? words[0] : words[1]);
34        var changed = badge.hidden || badge.textContent !== text;
35        badge.textContent = text;
36        badge.hidden = false;
37        if (changed && badge.animate && !window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
38            badge.getAnimations().forEach(function (animation) { animation.cancel(); });
39            badge.animate([{ opacity: 0.35, transform: 'translateY(3px)' }, { opacity: 1, transform: 'translateY(0)' }], { duration: 180, easing: 'ease-out' });
40        }
41    });
42};
43
44// Author: Eser Sariyar <[email protected]> | Company: HR-ON (hr-on.com)
45// Define the updateIconsVisibility function globally to be used throughout the app
46window.updateIconsVisibility = function () {
47    var root = document.querySelector('.hron-new-price-root');
48    if (!root) return;
49
50    var miniModuleContainer = root.querySelector('.mini-module-icons');
51    if (!miniModuleContainer) return;
52
53    // Get all selected modules
54    var selectedModules = [];
55    var moduleItems = root.querySelectorAll('.module-item.selected');
56    moduleItems.forEach(function (module) {
57        var moduleId = module.getAttribute('data-module');
58        if (moduleId) {
59            selectedModules.push(moduleId);
60        }
61    });
62
63    // Update visibility of mini module icons
64    var miniIcons = miniModuleContainer.querySelectorAll('.mini-module-icon');
65    miniIcons.forEach(function (icon) {
66        var moduleId = icon.getAttribute('data-module');
67        if (moduleId) {
68            // Show only if the module is selected
69            var isSelected = selectedModules.includes(moduleId);
70            icon.style.display = isSelected ? '' : 'none';
71        }
72    });
73
74    // Reorder visible icons to remove gaps
75    var visibleIcons = Array.from(miniIcons).filter(function (icon) {
76        return icon.style.display !== 'none';
77    });
78
79    visibleIcons.forEach(function (icon, index) {
80        var leftOffset = index > 0 ? -3 * index : 0;
81        icon.style.transition = 'all 0.3s ease';
82        icon.style.marginLeft = leftOffset + 'px';
83        icon.style.marginRight = (index === visibleIcons.length - 2) ? '2px' : '-2px';
84        icon.style.zIndex = (index + 1).toString();
85    });
86};
87
88(function () {
89    function qs(root, sel) {
90        return root.querySelector(sel);
91    }
92
93    function qsa(root, sel) {
94        return Array.prototype.slice.call(root.querySelectorAll(sel));
95    }
96
97    var hronWebhookFired = false;
98
99    function getUtmParams() {
100        var result = {};
101        var keys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id'];
102        var savedAtKey = 'hron_utm_saved_at';
103        var ttlMs = 90 * 24 * 60 * 60 * 1000;
104        try {
105            var search = window.location.search || '';
106            var params = new URLSearchParams(search);
107            keys.forEach(function (key) {
108                var val = params.get(key);
109                if (val) result[key] = val;
110            });
111
112            // The current URL is authoritative when it carries campaign data.
113            if (Object.keys(result).length) return result;
114
115            // Price submission is AJAX-based and does not serialize the hidden
116            // fields added to regular forms, so use recent stored attribution.
117            var savedAt = Number(window.localStorage.getItem(savedAtKey));
118            if (!savedAt || Date.now() - savedAt > ttlMs) {
119                keys.forEach(function (key) {
120                    window.localStorage.removeItem(key);
121                });
122                window.localStorage.removeItem(savedAtKey);
123                return result;
124            }
125
126            keys.forEach(function (key) {
127                var val = window.localStorage.getItem(key);
128                if (val) result[key] = val;
129            });
130        } catch (_) { }
131        return result;
132    }
133
134    /**
135     * Refresh the WP nonce via a lightweight AJAX call.
136     * Returns a Promise that resolves once HRONNewPrice.nonce is updated.
137     */
138    var _nonceRefreshing = null;
139    function refreshHRONNonce() {
140        if (_nonceRefreshing) return _nonceRefreshing;
141        if (!window.HRONNewPrice) return Promise.resolve();
142        var ajaxUrl = HRONNewPrice.ajaxUrl || '/wp-admin/admin-ajax.php';
143        var action  = HRONNewPrice.refreshNonceAction || 'hron_refresh_nonce';
144        var fd = new FormData();
145        fd.append('action', action);
146        _nonceRefreshing = fetch(ajaxUrl, { method: 'POST', body: fd })
147            .then(function (r) { return r.json(); })
148            .then(function (resp) {
149                if (resp && resp.success && resp.data && resp.data.nonce) {
150                    HRONNewPrice.nonce     = resp.data.nonce;
151                    HRONNewPrice.nonceTime = resp.data.nonceTime || Math.floor(Date.now() / 1000);
152                }
153            })
154            .catch(function () { /* silent */ })
155            .then(function () { _nonceRefreshing = null; });
156        return _nonceRefreshing;
157    }
158
159    /**
160     * Proactively refresh nonce if the page has been open > 10 hours.
161     */
162    function ensureFreshNonce() {
163        if (!window.HRONNewPrice || !HRONNewPrice.nonceTime) return;
164        var ageSeconds = Math.floor(Date.now() / 1000) - parseInt(HRONNewPrice.nonceTime, 10);
165        if (ageSeconds > 10 * 3600) {
166            refreshHRONNonce();
167        }
168    }
169
170    document.addEventListener('DOMContentLoaded', function () {
171        // Proactively refresh nonce if it may be expired (cached page)
172        ensureFreshNonce();
173
174        (function initBottomPage() {
175            var pricePage = document.querySelector('.hron-price-bottom-page');
176            if (!pricePage) return;
177            var tabs = document.querySelectorAll('.hron-price-bottom-tab');
178            var tabsNav = document.querySelector('.hron-price-bottom-tabs');
179            var tabsContainer = document.querySelector('.hron-price-bottom-tabs-container');
180            var sections = document.querySelectorAll('.hron-price-bottom-card');
181            var pluginsColumn = document.querySelector('.hron-price-bottom-plugins-column');
182            if (!tabsNav || !tabsContainer || !sections.length) return;
183
184            var pageTop = 0;
185            var tabsContainerTop = 0;
186            var footerTop = 0;
187            var lastSection = sections[sections.length - 1];
188            var tabScrollLock = false;
189            var lockedTabIndex = -1;
190            var lockedTargetY = null;
191            var tabLockTimer = null;
192
193            var pluginsColumnOriginalTop = 0;
194            var pluginsColumnOriginalPosition = '';
195
196            function recalcMetrics() {
197                var pageRect = pricePage.getBoundingClientRect();
198                pageTop = pageRect.top + window.scrollY;
199                var tabsContainerRect = tabsContainer.getBoundingClientRect();
200                tabsContainerTop = tabsContainerRect.top + window.scrollY;
201                footerTop = lastSection.offsetTop + lastSection.offsetHeight + pageTop;
202
203                if (pluginsColumn) {
204                    var style = window.getComputedStyle(pluginsColumn);
205                    pluginsColumnOriginalPosition = style.position;
206                    pluginsColumnOriginalTop = pluginsColumn.getBoundingClientRect().top + window.scrollY;
207                }
208            }
209            recalcMetrics();
210
211            function setActiveTab(index) {
212                Array.prototype.forEach.call(tabs, function (t) {
213                    t.classList.remove('active');
214                });
215                if (tabs[index]) {
216                    tabs[index].classList.add('active');
217                }
218            }
219
220            Array.prototype.forEach.call(tabs, function (tab, index) {
221                tab.addEventListener('click', function () {
222                    setActiveTab(index);
223                    var viewportHeight = window.innerHeight;
224                    var sectionHeight = sections[index].offsetHeight;
225                    var headerOffset = 120;
226                    var centeredOffset;
227                    if (sectionHeight < viewportHeight - headerOffset) {
228                        centeredOffset = headerOffset + ((viewportHeight - headerOffset - sectionHeight) / 2);
229                    } else {
230                        centeredOffset = headerOffset + 50;
231                    }
232                    var targetY = sections[index].offsetTop + pageTop - centeredOffset;
233                    lockedTabIndex = index;
234                    lockedTargetY = targetY;
235                    tabScrollLock = true;
236                    if (tabLockTimer) {
237                        clearTimeout(tabLockTimer);
238                    }
239                    tabLockTimer = setTimeout(function () {
240                        tabScrollLock = false;
241                        lockedTabIndex = -1;
242                        lockedTargetY = null;
243                        updateStickyState();
244                    }, 1000);
245                    window.scrollTo({
246                        top: targetY,
247                        behavior: 'smooth'
248                    });
249                });
250            });
251
252            function updateStickyState() {
253                var currentScroll = window.scrollY;
254                var windowHeight = window.innerHeight;
255
256                var tabsNavHeight = tabsNav.offsetHeight;
257                var bottomEdge = footerTop - 30;
258                var startSticky = tabsContainerTop - 80;
259                var endSticky = bottomEdge - tabsNavHeight - 80;
260
261                var backToTopButton = document.getElementById('hron-back-to-top');
262                if (backToTopButton) {
263                    var buttonShowPosition = startSticky + (endSticky - startSticky) * 0.6; // %60
264
265                    if (currentScroll >= buttonShowPosition) {
266                        backToTopButton.classList.add('visible');
267                    } else {
268                        backToTopButton.classList.remove('visible');
269                    }
270                }
271
272                if (currentScroll > startSticky && currentScroll < endSticky) {
273                    tabsNav.classList.add('sticky');
274                    tabsNav.classList.remove('absolute');
275                } else if (currentScroll >= endSticky) {
276                    tabsNav.classList.remove('sticky');
277                    tabsNav.classList.add('absolute');
278                } else {
279                    tabsNav.classList.remove('sticky');
280                    tabsNav.classList.remove('absolute');
281                }
282                var canAutoSyncTab = true;
283                if (tabScrollLock && lockedTabIndex > -1) {
284                    setActiveTab(lockedTabIndex);
285                    if (lockedTargetY !== null && Math.abs(window.scrollY - lockedTargetY) <= 6) {
286                        tabScrollLock = false;
287                        lockedTabIndex = -1;
288                        lockedTargetY = null;
289                        if (tabLockTimer) {
290                            clearTimeout(tabLockTimer);
291                            tabLockTimer = null;
292                        }
293                    } else {
294                        canAutoSyncTab = false;
295                    }
296                }
297
298                if (canAutoSyncTab) {
299                    var activeIndex = 0;
300                    // Use a viewport-relative focus line for scrollspy.
301                    // A dynamic anchor works better for mixed section heights
302                    // and prevents snapping back to the previous tab.
303                    var anchorY = currentScroll + Math.max(220, Math.round(windowHeight * 0.45));
304                    Array.prototype.forEach.call(sections, function (section, index) {
305                        var sectionTop = pageTop + section.offsetTop;
306                        if (anchorY >= sectionTop) {
307                            activeIndex = index;
308                        }
309                    });
310                    var currentActive = document.querySelector('.hron-price-bottom-tab.active');
311                    if (currentActive && currentActive !== tabs[activeIndex]) {
312                        currentActive.classList.remove('active');
313                        tabs[activeIndex].classList.add('active');
314                    } else if (!currentActive) {
315                        tabs[activeIndex].classList.add('active');
316                    }
317                }
318
319                if (window.innerWidth <= 880) {
320                    if (!tabsNav.classList.contains('sticky')) {
321                        tabsNav.style.width = '100%';
322                        tabsNav.style.maxWidth = '300px';
323                    } else {
324                        tabsNav.style.width = '';
325                        tabsNav.style.maxWidth = '';
326                    }
327                } else {
328                    tabsNav.style.width = '';
329                    tabsNav.style.maxWidth = '';
330                }
331            }
332
333            (function () {
334                if (!pluginsColumn) return;
335
336                var pluginsInner = pluginsColumn.querySelector('.hron-price-bottom-plugins-inner') || pluginsColumn;
337
338                var colRectInitial = null;
339
340                function readTabsTop() {
341                    var cs = window.getComputedStyle(tabsNav);
342                    var top = parseInt(cs.top, 10);
343                    if (!isFinite(top) || cs.position !== 'fixed') top = 100;
344                    return top;
345                }
346
347                function measureCol() {
348                    var r = pluginsColumn.getBoundingClientRect();
349                    colRectInitial = {
350                        left: r.left + window.scrollX,
351                        width: r.width,
352                        topPage: r.top + window.scrollY
353                    };
354                }
355
356                function applySidebarState() {
357
358                    if (window.innerWidth <= 880) {
359                        pluginsColumn.style.height = 'auto';
360                        pluginsInner.style.position = '';
361                        pluginsInner.style.top = '';
362                        pluginsInner.style.left = '';
363                        pluginsInner.style.width = '';
364                        return;
365                    }
366
367                    if (!colRectInitial) measureCol();
368
369                    var currentScroll = window.scrollY;
370                    var tabsNavHeight = tabsNav.offsetHeight;
371                    var bottomEdge = footerTop - 30;
372                    var startSticky = tabsContainerTop - 80;
373                    var panelH = pluginsInner.offsetHeight;
374                    var endSticky = bottomEdge - panelH - 80;
375
376                    if (currentScroll > startSticky && currentScroll < endSticky) {
377                        pluginsColumn.style.height = panelH + 'px';
378                        pluginsColumn.style.marginTop = '20px';
379                        pluginsInner.style.position = 'fixed';
380                        pluginsInner.style.top = readTabsTop() + 'px';
381                        pluginsInner.style.marginTop = '80px';
382                        pluginsInner.style.left = colRectInitial.left + 'px';
383                        pluginsInner.style.width = 270 + 'px';
384
385                    } else if (currentScroll >= endSticky) {
386                        var desiredTopPage = bottomEdge - panelH;
387                        var topRelative = desiredTopPage - colRectInitial.topPage;
388
389                        pluginsColumn.style.height = panelH + 'px';
390                        pluginsColumn.style.marginTop = '20px';
391                        pluginsInner.style.position = 'absolute';
392                        pluginsInner.style.top = topRelative + 'px';
393                        pluginsInner.style.marginTop = '80px';
394                        pluginsInner.style.left = '0px';
395                        pluginsInner.style.width = '100%';
396
397                    } else {
398                        pluginsColumn.style.height = 'auto';
399                        pluginsInner.style.position = '';
400                        pluginsInner.style.top = '0';
401                        pluginsInner.style.marginTop = '0';
402                        pluginsInner.style.left = '';
403                        pluginsInner.style.width = '';
404                        pluginsColumn.style.top = '0';
405                        pluginsColumn.style.marginTop = '0';
406                    }
407                }
408
409                var _origUpdate = updateStickyState;
410                updateStickyState = function () {
411                    _origUpdate();
412                    applySidebarState();
413                };
414
415                window.addEventListener('resize', function () {
416                    colRectInitial = null;
417                    applySidebarState();
418                });
419                window.addEventListener('load', function () {
420                    colRectInitial = null;
421                    applySidebarState();
422                });
423                document.addEventListener('DOMContentLoaded', function () {
424                    colRectInitial = null;
425                    applySidebarState();
426                });
427                applySidebarState();
428            })();
429
430            tabsNav.classList.remove('sticky');
431            tabsNav.classList.remove('absolute');
432
433            window.addEventListener('scroll', function () {
434                updateStickyState();
435            });
436
437            updateStickyState();
438            setTimeout(function () {
439                recalcMetrics();
440                updateStickyState();
441            }, 100);
442
443            window.addEventListener('resize', function () {
444                recalcMetrics();
445                updateStickyState();
446            });
447
448            window.addEventListener('load', function () {
449                recalcMetrics();
450                updateStickyState();
451            });
452        })();
453
454        if (!document.getElementById('tooltip-container')) {
455            var tooltipContainer = document.createElement('div');
456            tooltipContainer.id = 'tooltip-container';
457            document.body.appendChild(tooltipContainer);
458        }
459        qsa(document, '.hron-new-price-root').forEach(function (root) {
460            var options = qsa(root, '.hron-price-option');
461            var contactSection = qs(root, '#contact-section');
462            var suiteInputs = qs(root, '#suite-inputs');
463            var customModules = qs(root, '#custom-modules');
464            var jobSlotsInput = qs(root, '#job-slots-input');
465            var employeesInput = qs(root, '#employees-input');
466            var jobSlotsGroup = qs(root, '#job-slots-group');
467            var employeesGroup = qs(root, '#employees-group');
468            var isSuiteSelected = false;
469
470            var currencyRate = window.HRONNewPrice ? window.HRONNewPrice.currencyRate : 0.134328;
471            var currentCurrency = 'DKK';
472            var originalPrices = {};
473            var selectedModules = ['staff', 'recruit', 'boarding', 'wellbeing', 'comply'];
474            var effectiveDiscountRate = 0;
475            // Until a price call comes back we do not know the saving, so the badge
476            // must stay hidden rather than show the placeholder from the markup.
477            var effectiveDiscountKnown = false;
478            var discountRefreshTimer = null;
479            var priceRequestSeq = 0;
480            // What the pill currently shows, and the animations in flight for it.
481            var displayedDiscountRate = null;
482            var discountAnimFrame = null;
483            var discountPulseTimer = null;
484            // Last standalone price per module, with the sizes they were computed for.
485            // Lets the badge repaint instantly when a module is ticked or unticked.
486            var standaloneCache = null;
487            var pluginUrl = window.HRONNewPrice && window.HRONNewPrice.pluginUrl ? window.HRONNewPrice.pluginUrl : '/wp-content/plugins/HRON-New-Price/';
488
489            function setupTooltips() {
490                qsa(root, '.module-help-icon').forEach(function (icon) {
491                    icon.addEventListener('mouseenter', function () {
492                        var tooltip = icon.querySelector('.tooltip');
493                        if (!tooltip) return;
494                        tooltip.hidden = false;
495                        tooltip.setAttribute('aria-hidden', 'false');
496                        var rect = icon.getBoundingClientRect();
497                        var isInputIcon = !!icon.closest('.help-input-group');
498                        if (isInputIcon) {
499                            tooltip.style.left = (rect.left - 10) + 'px';
500                            tooltip.style.top = (rect.top - 15) + 'px';
501                            tooltip.style.transform = 'translate(-50%, -100%)';
502                        } else {
503                            tooltip.style.left = (rect.left + rect.width / 2) + 'px';
504                            tooltip.style.top = (rect.top - 15) + 'px';
505                            tooltip.style.transform = 'translate(-50%, -100%)';
506                        }
507                        tooltip.classList.add('tooltip-active');
508                    });
509                    icon.addEventListener('mouseleave', function () {
510                        var tooltip = icon.querySelector('.tooltip');
511                        if (tooltip) {
512                            tooltip.classList.remove('tooltip-active');
513                            tooltip.hidden = true;
514                            tooltip.setAttribute('aria-hidden', 'true');
515                        }
516                    });
517                });
518            }
519
520            function toggleInputs() {
521                if (isSuiteSelected) {
522                    if (jobSlotsGroup) jobSlotsGroup.style.display = 'block';
523                    if (employeesGroup) employeesGroup.style.display = 'block';
524                    if (suiteInputs) suiteInputs.classList.remove('hide');
525                    // Keep custom modules visible even in Suite mode
526                    if (customModules) customModules.classList.add('show');
527                } else {
528                    if (customModules) customModules.classList.add('show');
529                    if (contactSection) contactSection.classList.remove('show');
530
531                    // Check specific module selections - always get fresh status
532                    var recruitSel = !!qs(root, '.module-item[data-module="recruit"].selected');
533                    var staffSel = !!qs(root, '.module-item[data-module="staff"].selected');
534                    var boardingSel = !!qs(root, '.module-item[data-module="boarding"].selected');
535                    var wellbeingSel = !!qs(root, '.module-item[data-module="wellbeing"].selected');
536                    var complySel = !!qs(root, '.module-item[data-module="comply"].selected');
537
538                    // Any module selected at all?
539                    var hasSelectedModules = recruitSel || staffSel || boardingSel || wellbeingSel || complySel;
540
541
542                    // Show/hide job slots input ONLY if recruit is selected
543                    if (jobSlotsGroup) {
544                        jobSlotsGroup.style.display = recruitSel ? 'block' : 'none';
545                    }
546
547                    // Show/hide employees input if any employee-based module is selected
548                    var showEmployees = staffSel || boardingSel || wellbeingSel || complySel;
549                    if (employeesGroup) {
550                        employeesGroup.style.display = showEmployees ? 'block' : 'none';
551
552                    }
553
554                    // Master control of suite-inputs div
555                    if (suiteInputs) {
556                        if (!hasSelectedModules) {
557                            // No modules selected, hide everything
558                            suiteInputs.classList.add('hide');
559                        } else {
560                            // At least one module selected, show the container
561                            suiteInputs.classList.remove('hide');
562                        }
563                    }
564                }
565            }
566
567            function checkAndShowContactSection() {
568                if (!contactSection) return;
569                var show = false;
570
571                // Always get the latest module selection state directly from the DOM
572                var recruitSel = !!qs(root, '.module-item[data-module="recruit"].selected');
573                var staffSel = !!qs(root, '.module-item[data-module="staff"].selected');
574                var boardingSel = !!qs(root, '.module-item[data-module="boarding"].selected');
575                var wellbeingSel = !!qs(root, '.module-item[data-module="wellbeing"].selected');
576                var complySel = !!qs(root, '.module-item[data-module="comply"].selected');
577                var hasSelectedModules = recruitSel || staffSel || boardingSel || wellbeingSel || complySel;
578
579                if (!hasSelectedModules) {
580                    // No modules selected, don't show contact section and hide inputs
581                    show = false;
582                    if (jobSlotsGroup) jobSlotsGroup.style.display = 'none';
583                    if (employeesGroup) employeesGroup.style.display = 'none';
584                    if (suiteInputs) suiteInputs.classList.add('hide');
585                } else if (isSuiteSelected) {
586                    var hasJobSlots = jobSlotsInput && jobSlotsInput.value.trim() !== '';
587                    var hasEmployees = employeesInput && employeesInput.value.trim() !== '';
588                    show = hasJobSlots && hasEmployees;
589                } else {
590                    // We have at least one module selected
591
592                    // Check if we need job slots input (only if recruit is selected)
593                    var needsJobSlots = recruitSel;
594                    if (jobSlotsGroup) {
595                        jobSlotsGroup.style.display = needsJobSlots ? 'block' : 'none';
596                    }
597
598                    // Comply is priced per employee too, so it belongs here. Leaving it
599                    // out hid the employees input for a Comply-only deal, which then
600                    // had no size to price against.
601                    var needsEmployees = staffSel || boardingSel || wellbeingSel || complySel;
602                    if (employeesGroup) {
603                        employeesGroup.style.display = needsEmployees ? 'block' : 'none';
604                    }
605
606                    // For input values
607                    var hasJob = jobSlotsInput && jobSlotsInput.value.trim() !== '';
608                    var hasEmp = employeesInput && employeesInput.value.trim() !== '';
609
610                    // Logic for showing contact section
611                    if (needsJobSlots && !needsEmployees) {
612                        // Only recruit selected, need job slots value
613                        show = hasJob;
614                    }
615                    else if (!needsJobSlots && needsEmployees) {
616                        // Only employee-based modules, need employees value
617                        show = hasEmp;
618                    }
619                    else if (needsJobSlots && needsEmployees) {
620                        // Both kinds selected, need both values
621                        show = hasJob && hasEmp;
622                    }
623                }
624
625                contactSection.classList.toggle('show', show);
626            }
627
628            setupTooltips();
629            toggleInputs();
630
631            // Activate all modules by default since we start with custom modules
632            selectedModules = []; // Reset the selectedModules array defined at higher scope
633            qsa(root, '.module-item').forEach(function (module) {
634                module.classList.add('selected');
635                var moduleId = module.getAttribute('data-module');
636                if (moduleId) {
637                    selectedModules.push(moduleId);
638                }
639            });
640            toggleInputs();
641
642            // Add event listener for the custom-chip section
643            var customChip = qs(root, '.custom-chip');
644            if (customChip) {
645                customChip.addEventListener('click', function (e) {
646                    // Always prevent default behavior for custom-chip click
647                    e.stopPropagation();
648                    e.preventDefault();
649
650                    // Prevent the click from showing inputs if no modules are selected
651                    var hasSelectedModules = selectedModules.length > 0;
652                    if (!hasSelectedModules) {
653                        return;
654                    }
655
656                });
657            }
658
659            options.forEach(function (option, idx) {
660                option.addEventListener('click', function () {
661                    options.forEach(function (o) {
662                        o.classList.remove('selected');
663                    });
664                    option.classList.add('selected');
665                    // Only a genuine suite option (i.e. more than one choice) may set
666                    // this. There is a single "compose your own" option today, and the
667                    // module items sit inside it, so every module click bubbles here -
668                    // latching this to true would freeze the selection at all five.
669                    isSuiteSelected = (options.length > 1 && idx === 0);
670                    toggleInputs();
671                    checkAndShowContactSection();
672                    updateDiscountNow();
673                    scheduleDiscountRefresh();
674                });
675            });
676
677            if (jobSlotsInput) {
678                jobSlotsInput.addEventListener('input', function () {
679
680                    var value = this.value.replace(/[^0-9]/g, '');
681                    if (value === '0' || value.startsWith('0')) value = value.replace(/^0+/, '');
682                    this.value = value;
683                    jobSlotsInput.classList.remove('error');
684                    jobSlotsInput.removeAttribute('aria-invalid');
685                    var fg = jobSlotsInput.closest('.form-group');
686                    if (fg) fg.classList.remove('has-error');
687                    if (fg) { if (value) fg.classList.add('has-value'); else fg.classList.remove('has-value'); }
688                    jobSlotsInput.style.borderColor = '';
689                    jobSlotsInput.style.boxShadow = '';
690                    checkAndShowContactSection();
691                });
692
693                jobSlotsInput.addEventListener('paste', function (e) {
694                    e.preventDefault();
695                    var pastedText = (e.clipboardData || window.clipboardData).getData('text');
696                    var numericOnly = pastedText.replace(/[^0-9]/g, '');
697                    if (numericOnly) {
698                        this.value = numericOnly;
699                        var fg = jobSlotsInput.closest('.form-group');
700                        if (fg) fg.classList.add('has-value');
701                        checkAndShowContactSection();
702                    }
703                });
704            }
705            if (employeesInput) {
706                employeesInput.addEventListener('input', function () {
707
708                    var value = this.value.replace(/[^0-9]/g, '');
709                    if (value === '0' || value.startsWith('0')) value = value.replace(/^0+/, '');
710                    this.value = value;
711                    employeesInput.classList.remove('error');
712                    employeesInput.removeAttribute('aria-invalid');
713                    var fg = employeesInput.closest('.form-group');
714                    if (fg) fg.classList.remove('has-error');
715                    if (fg) { if (value) fg.classList.add('has-value'); else fg.classList.remove('has-value'); }
716                    employeesInput.style.borderColor = '';
717                    employeesInput.style.boxShadow = '';
718                    checkAndShowContactSection();
719                });
720
721                employeesInput.addEventListener('paste', function (e) {
722                    e.preventDefault();
723                    var pastedText = (e.clipboardData || window.clipboardData).getData('text');
724                    var numericOnly = pastedText.replace(/[^0-9]/g, '');
725                    if (numericOnly) {
726                        this.value = numericOnly;
727                        var fg = employeesInput.closest('.form-group');
728                        if (fg) fg.classList.add('has-value');
729                        checkAndShowContactSection();
730                    }
731                });
732            }
733
734            var emailField = qs(root, 'input[type="email"]');
735            if (emailField) {
736
737                emailField.addEventListener('keydown', function (e) {
738                    if (e.key === ' ' || e.code === 'Space' || e.keyCode === 32) {
739                        e.preventDefault();
740                    }
741                });
742
743                var allowedRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~@-]+$/;
744
745                emailField.addEventListener('beforeinput', function (e) {
746                    if (e.inputType === 'insertText') {
747                        var data = e.data || '';
748                        if (!allowedRegex.test(data)) {
749                            e.preventDefault();
750                        }
751                    }
752                });
753
754                emailField.addEventListener('paste', function (e) {
755                    var pastedText = (e.clipboardData || window.clipboardData).getData('text');
756                    var cleaned = (pastedText || '').replace(/[^a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~@-]/g, '');
757                    if (cleaned !== pastedText) {
758                        e.preventDefault();
759                        if (cleaned) document.execCommand('insertText', false, cleaned);
760                    }
761                });
762
763                emailField.addEventListener('input', function () {
764                    var v = this.value || '';
765                    var fixed = v.replace(/[^a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~@-]/g, '');
766                    if (fixed !== v) this.value = fixed;
767                });
768                emailField.addEventListener('blur', function () {
769                    this.value = (this.value || '').trim();
770                });
771            }
772
773            var phoneInput = qs(root, 'input[type="tel"]');
774            if (phoneInput) {
775                phoneInput.addEventListener('input', function () {
776
777                    var value = this.value.replace(/[^0-9+\-\s\(\)]/g, '');
778                    this.value = value;
779
780                    phoneInput.classList.remove('error');
781                    phoneInput.removeAttribute('aria-invalid');
782                    var fg = phoneInput.closest('.form-group');
783                    if (fg) fg.classList.remove('has-error');
784                    phoneInput.style.borderColor = '';
785                    phoneInput.style.boxShadow = '';
786                });
787
788                phoneInput.addEventListener('paste', function (e) {
789                    e.preventDefault();
790                    var pastedText = (e.clipboardData || window.clipboardData).getData('text');
791                    var phoneOnly = pastedText.replace(/[^0-9+\-\s\(\)]/g, '');
792                    if (phoneOnly) {
793                        this.value = phoneOnly;
794                    }
795                });
796            }
797
798            qsa(root, 'input.form-input').forEach(function (inp) {
799                inp.addEventListener('input', function () {
800                    inp.classList.remove('error');
801                    inp.removeAttribute('aria-invalid');
802                    var fg = inp.closest('.form-group');
803                    if (fg) fg.classList.remove('has-error');
804                    inp.style.borderColor = '';
805                    inp.style.boxShadow = '';
806                });
807            });
808
809            qsa(root, '.module-item').forEach(function (module) {
810                module.addEventListener('click', function (e) {
811                    e.stopPropagation();
812                    e.preventDefault();
813
814                    // Store the previous state of selectedModules before updating
815                    var previousModules = [...selectedModules];
816
817                    module.classList.toggle('selected');
818                    var moduleId = module.getAttribute('data-module');
819
820                    var isSelected = module.classList.contains('selected');
821                    if (moduleId === 'recruit' && jobSlotsGroup) {
822                        jobSlotsGroup.style.display = isSelected ? 'block' : 'none';
823                        // Keep the typed value. Wiping it meant that unticking and
824                        // reticking Recruit silently dropped the size, and the saving
825                        // could not reappear until the visitor typed it again.
826                    }
827
828                    var staffSel = !!qs(root, '.module-item[data-module="staff"].selected');
829                    var boardingSel = !!qs(root, '.module-item[data-module="boarding"].selected');
830                    var wellbeingSel = !!qs(root, '.module-item[data-module="wellbeing"].selected');
831                    var complySel = !!qs(root, '.module-item[data-module="comply"].selected');
832                    var showEmployees = staffSel || boardingSel || wellbeingSel || complySel;
833                    if (employeesGroup) employeesGroup.style.display = showEmployees ? 'block' : 'none';
834                    // Same here: the headcount is remembered while the field is hidden.
835                    if (!isSuiteSelected) {
836                        var recruitSel = !!qs(root, '.module-item[data-module="recruit"].selected');
837
838                        // Always update the input visibility through toggleInputs()
839                        // This ensures consistent rules are applied everywhere
840                        toggleInputs();
841
842                        checkAndShowContactSection();
843                    }
844
845                    updateSelectedModules();
846
847                    // Make sure customModules stays visible
848                    if (customModules) customModules.classList.add('show');
849                });
850            });
851
852            var ctaButton = qs(root, '.cta-button');
853            if (ctaButton) {
854                ctaButton.addEventListener('click', function (e) {
855
856                    var requiredInputs = [];
857
858                    var selectedIndex = options.findIndex(function (o) {
859                        return o.classList.contains('selected');
860                    });
861                    var isSuiteSelected = (selectedIndex === 0);
862
863                    var nameInput = qs(root, '#contact-name');
864                    var emailInput = qs(root, 'input[type="email"]');
865                    var phoneInput = qs(root, 'input[type="tel"]');
866                    if (nameInput) requiredInputs.push(nameInput);
867                    if (emailInput) requiredInputs.push(emailInput);
868                    if (phoneInput) requiredInputs.push(phoneInput);
869
870                    var recruitSel = !!qs(root, '.module-item[data-module="recruit"].selected');
871                    var staffSel = !!qs(root, '.module-item[data-module="staff"].selected');
872                    if (isSuiteSelected || recruitSel) {
873                        if (jobSlotsInput) requiredInputs.push(jobSlotsInput);
874                    }
875                    if (isSuiteSelected || staffSel) {
876                        if (employeesInput) requiredInputs.push(employeesInput);
877                    }
878
879                    var hasError = false;
880                    requiredInputs.forEach(function (input) {
881                        // Skip validation if the input or its parent container is not visible
882                        var isVisible = true;
883                        var parent = input.parentNode;
884
885                        // Check if this is a job slots or employees input and check visibility accordingly
886                        var isJobSlotsOrEmployees = (input === jobSlotsInput || input === employeesInput);
887
888                        if (isJobSlotsOrEmployees) {
889                            // Check if we're in hidden inputs mode and the inputs aren't shown yet
890                            var rootElement = document.querySelector('.hron-new-price-root');
891                            var inHideInputsMode = rootElement && rootElement.classList.contains('hron-hide-inputs');
892                            var inputsShown = rootElement && rootElement.classList.contains('show-inputs');
893
894                            if (inHideInputsMode && !inputsShown) {
895                                // Skip validation for hidden inputs
896                                isVisible = false;
897                            }
898
899                            // Also check if the specific input container is visible
900                            while (parent && parent !== document.body) {
901                                var style = window.getComputedStyle(parent);
902                                if (style.display === 'none' || style.visibility === 'hidden') {
903                                    isVisible = false;
904                                    break;
905                                }
906                                parent = parent.parentNode;
907                            }
908                        }
909
910                        // Skip validation for invisible elements
911                        if (!isVisible) {
912                            return;
913                        }
914
915                        // Check if this input was just revealed for the first time in hide_inputs mode
916                        var justRevealed = input.dataset && input.dataset.justRevealed === 'true';
917
918                        // If the input was just revealed, clear the flag and skip validation for now
919                        if (justRevealed) {
920                            delete input.dataset.justRevealed;
921                            return;
922                        }
923
924                        var val = (input.value || '').trim();
925                        var isEmail = input.type === 'email';
926
927                        var emailRegex = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
928                        var isNumberInput = input === jobSlotsInput || input === employeesInput;
929                        var isPhone = input === phoneInput;
930                        var ok = val !== '' && (!isEmail || emailRegex.test(val));
931                        if (isPhone) {
932                            var d = val.replace(/\D+/g, '');
933                            if (d.length < 8) ok = false;
934                        }
935                        if (isNumberInput && (val === '0' || val === '' || isNaN(Number(val)) || Number(val) < 1)) {
936                            ok = false;
937
938                        }
939                        if (!ok) {
940                            input.classList.add('error');
941                            input.setAttribute('aria-invalid', 'true');
942                            var fg = input.closest('.form-group');
943                            if (fg) fg.classList.add('has-error');
944                            try {
945                                input.style.borderColor = '#FF3B30';
946                                input.style.boxShadow = '0 0 0 4px rgba(255,59,48,0.12)';
947                            } catch (_) { }
948                            hasError = true;
949                        } else {
950                            input.classList.remove('error');
951                            input.removeAttribute('aria-invalid');
952                            var fg2 = input.closest('.form-group');
953                            if (fg2) fg2.classList.remove('has-error');
954                            try {
955                                input.style.borderColor = '';
956                                input.style.boxShadow = '';
957                            } catch (_) { }
958                        }
959                    });
960
961                    if (emailInput) {
962                        emailInput.addEventListener('input', function (e) {
963                            var allowed = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~@-]*$/;
964                            if (!allowed.test(this.value)) {
965                                this.value = this.value.replace(/[^a-zA-Z0-9.!#$%&'*+\\/?^_`{|}~@-]/g, '');
966                            }
967                        });
968                    }
969
970                    if (hasError) {
971                        e.preventDefault();
972                        e.stopPropagation();
973
974                        var firstInvalid = requiredInputs.find(function (inp) {
975                            return inp.classList.contains('error');
976                        });
977                        if (firstInvalid) {
978                            try {
979                                firstInvalid.focus({
980                                    preventScroll: true
981                                });
982                            } catch (_) {
983                                try {
984                                    firstInvalid.focus();
985                                } catch (_) { }
986                            }
987                            try {
988                                firstInvalid.scrollIntoView({
989                                    behavior: 'smooth',
990                                    block: 'center'
991                                });
992                            } catch (_) { }
993                        }
994                        return false;
995                    }
996
997                    if (!hronWebhookFired) {
998                        try {
999
1000                            var selected = {
1001                                staff: !!qs(root, '.module-item[data-module="staff"].selected'),
1002                                recruit: !!qs(root, '.module-item[data-module="recruit"].selected'),
1003                                boarding: !!qs(root, '.module-item[data-module="boarding"].selected'),
1004                                wellbeing: !!qs(root, '.module-item[data-module="wellbeing"].selected'),
1005                                comply: !!qs(root, '.module-item[data-module="comply"].selected')
1006                            };
1007                            var currencyEl = qs(root, '.currency-option.selected');
1008                            var currency = currencyEl ? currencyEl.getAttribute('data-currency') : 'DKK';
1009                            var employeesVal = (selected.staff || selected.boarding || selected.wellbeing || selected.comply) && employeesInput ? (employeesInput.value || '').trim() : '';
1010                            var jobslotsVal = selected.recruit && jobSlotsInput ? (jobSlotsInput.value || '').trim() : '';
1011
1012                            var selectedSystem = (function () {
1013                                // Only individual module selection exists now (no suite option)
1014                                // Build string based on actually selected modules
1015                                var mods = [];
1016                                if (selected.recruit) mods.push('recruit');
1017                                if (selected.boarding) mods.push('boarding');
1018                                if (selected.staff) mods.push('staff');
1019                                if (selected.wellbeing) mods.push('wellbeing');
1020                                if (selected.comply) mods.push('comply');
1021                                return mods.length ? mods.join(',') : '';
1022                            })();
1023
1024                            calculatePrices(parseInt(employeesVal || '0') || 0, parseInt(jobslotsVal || '0') || 0, currency, selected)
1025                                .finally(function () {
1026                                    var nameInput = qs(root, '#contact-name');
1027                                    var emailInput = qs(root, '#contact-section input[type="email"]');
1028                                    var phoneInput = qs(root, '#contact-section input[type="tel"]');
1029
1030                                    var payload = {
1031                                        your_name: nameInput ? (nameInput.value || '').trim() : '',
1032                                        your_email: emailInput ? (emailInput.value || '').trim() : '',
1033                                        your_phone: phoneInput ? (phoneInput.value || '').trim() : '',
1034                                        theurl: window.location.href,
1035                                        selectedSystem: selectedSystem,
1036                                        employees: employeesVal,
1037                                        jobslots: jobslotsVal,
1038                                        currency: (window.HRONPriceModal && window.HRONPriceModal.currentCurrency) || currency,
1039                                        hiddenTextArea: ''
1040                                    };
1041
1042                                    var utms = getUtmParams();
1043                                    for (var k in utms) {
1044                                        if (Object.prototype.hasOwnProperty.call(utms, k)) {
1045                                            payload[k] = utms[k];
1046                                        }
1047                                    }
1048
1049                                    var buildWebhookFd = function () {
1050                                        var fd = new FormData();
1051                                        fd.append('action', HRONNewPrice.webhookAction);
1052                                        fd.append('nonce', HRONNewPrice.nonce);
1053                                        Object.keys(payload).forEach(function (key) {
1054                                            fd.append(key, payload[key]);
1055                                        });
1056                                        return fd;
1057                                    };
1058
1059                                    fetch(HRONNewPrice.ajaxUrl, {
1060                                        method: 'POST',
1061                                        body: buildWebhookFd(),
1062                                        credentials: 'same-origin'
1063                                    })
1064                                        .then(function (r) {
1065                                            // Retry once on 403 (nonce expired)
1066                                            if (r.status === 403) {
1067                                                return refreshHRONNonce().then(function () {
1068                                                    return fetch(HRONNewPrice.ajaxUrl, {
1069                                                        method: 'POST',
1070                                                        body: buildWebhookFd(),
1071                                                        credentials: 'same-origin'
1072                                                    });
1073                                                });
1074                                            }
1075                                            return r;
1076                                        })
1077                                        .finally(function () {
1078                                            hronWebhookFired = true;
1079                                        });
1080                                });
1081                        } catch (_) {
1082
1083                            hronWebhookFired = true;
1084                        }
1085                    }
1086
1087                    if (window.HRONPriceModal && typeof window.HRONPriceModal.open === 'function') {
1088                        window.HRONPriceModal.open();
1089                    }
1090                });
1091            }
1092
1093            function calculatePrices(employees, jobslots, currency) {
1094                var selections = arguments[3];
1095                var buildFormData = function () {
1096                    var formData = new FormData();
1097                    formData.append('action', 'calculate_prices');
1098                    formData.append('nonce', HRONNewPrice.nonce);
1099                    formData.append('employees', employees);
1100                    formData.append('jobslots', jobslots);
1101                    formData.append('currency', currency);
1102                    formData.append('select_staff', selections && selections.staff ? '1' : '0');
1103                    formData.append('select_recruit', selections && selections.recruit ? '1' : '0');
1104                    formData.append('select_boarding', selections && selections.boarding ? '1' : '0');
1105                    formData.append('select_wellbeing', selections && selections.wellbeing ? '1' : '0');
1106                    formData.append('select_comply', selections && selections.comply ? '1' : '0');
1107                    return formData;
1108                };
1109
1110                var mySeq = ++priceRequestSeq;
1111
1112                return new Promise(function (resolve, reject) {
1113                    fetch(HRONNewPrice.ajaxUrl, {
1114                        method: 'POST',
1115                        body: buildFormData()
1116                    })
1117                        .then(function (response) {
1118                            // Retry once on 403 (nonce expired)
1119                            if (response.status === 403) {
1120                                return refreshHRONNonce().then(function () {
1121                                    return fetch(HRONNewPrice.ajaxUrl, {
1122                                        method: 'POST',
1123                                        body: buildFormData()
1124                                    });
1125                                });
1126                            }
1127                            return response;
1128                        })
1129                        .then(function (response) {
1130                            return response.json();
1131                        })
1132                        .then(function (data) {
1133                            if (data.success) {
1134                                // Effective saving off standalone, straight from the
1135                                // server so the badge and the modal agree.
1136                                var rawTotals = data.data && data.data.totals && data.data.totals.raw
1137                                    ? data.data.totals.raw : null;
1138                                var savedPct = null;
1139                                if (rawTotals && typeof rawTotals.saved_percent === 'number') {
1140                                    savedPct = rawTotals.saved_percent;
1141                                } else if (rawTotals && typeof rawTotals.effective_rate === 'number') {
1142                                    savedPct = rawTotals.effective_rate * 100;
1143                                }
1144                                // Ignore a response that a newer request has superseded.
1145                                // A module's standalone price does not depend on the
1146                                // rest of the selection, so cache these and reuse them.
1147                                if (mySeq === priceRequestSeq && rawTotals && rawTotals.standalone_dkk) {
1148                                    standaloneCache = {
1149                                        employees: employees,
1150                                        jobslots: jobslots,
1151                                        prices: rawTotals.standalone_dkk
1152                                    };
1153                                }
1154
1155                                if (mySeq === priceRequestSeq) {
1156                                    if (data.data && data.data.is_enterprise) {
1157                                        // Enterprise deals are quoted by hand, so there
1158                                        // is no standard saving to advertise.
1159                                        effectiveDiscountKnown = false;
1160                                        updateDiscountBadge();
1161                                    } else if (typeof savedPct === 'number') {
1162                                        effectiveDiscountRate = Math.round(savedPct);
1163                                        effectiveDiscountKnown = true;
1164                                        updateDiscountBadge();
1165                                    }
1166                                }
1167                                resolve(data.data);
1168                            } else {
1169                                reject(new Error('Price calculation failed'));
1170                            }
1171                        })
1172                        .catch(function (error) {
1173                            reject(error);
1174                        });
1175                });
1176            }
1177
1178            // Current module selection, read straight from the DOM.
1179            function readModuleSelection() {
1180                return {
1181                    recruit:   !!qs(root, '.module-item[data-module="recruit"].selected'),
1182                    staff:     !!qs(root, '.module-item[data-module="staff"].selected'),
1183                    boarding:  !!qs(root, '.module-item[data-module="boarding"].selected'),
1184                    wellbeing: !!qs(root, '.module-item[data-module="wellbeing"].selected'),
1185                    comply:    !!qs(root, '.module-item[data-module="comply"].selected')
1186                };
1187            }
1188
1189            // Sizes currently in the inputs, plus which of them this selection needs.
1190            function readSizes(selected) {
1191                var employeesEl = qs(root, '#employees-input');
1192                var jobSlotsEl  = qs(root, '#job-slots-input');
1193                return {
1194                    employees: employeesEl ? parseInt((employeesEl.value || '').trim(), 10) || 0 : 0,
1195                    jobslots:  jobSlotsEl  ? parseInt((jobSlotsEl.value  || '').trim(), 10) || 0 : 0,
1196                    needsEmployees: !!(selected.staff || selected.boarding || selected.wellbeing || selected.comply),
1197                    needsJobslots:  !!selected.recruit
1198                };
1199            }
1200
1201            // Blend the saving out of the cached standalone prices. Every module's
1202            // standalone price is independent of the rest of the deal, so this gives
1203            // the same figure the server would return, with no round-trip.
1204            // Returns null when the cache does not cover this selection.
1205            function savingFromCache(selected) {
1206                var sizes = readSizes(selected);
1207                var limits = (window.HRONNewPrice && HRONNewPrice.enterpriseLimits) || {};
1208                if (sizes.employees > limits.employees || sizes.jobslots > limits.jobslots) return null;
1209                if ((sizes.needsEmployees && sizes.employees <= 0) ||
1210                    (sizes.needsJobslots && sizes.jobslots <= 0)) return null;
1211
1212                // Use the server's pricing configuration on the first render too.
1213                // This mirrors raw_module_dkk; no network round-trip is needed.
1214                var models = (window.HRONNewPrice && HRONNewPrice.modulePricing) || {};
1215                var localPrices = {};
1216                Object.keys(models).forEach(function (key) {
1217                    var m = models[key];
1218                    var units = m.unit_of === 'jobslots' ? sizes.jobslots : sizes.employees;
1219                    var curve = function (n) { return m.start + m.unit * Math.pow(n, 1 - m.d); };
1220                    var price = m.knee && units > m.knee
1221                        ? curve(m.knee) * Math.pow(units / m.knee, 1 - m.d2)
1222                        : curve(units);
1223                    localPrices[key] = units > 0 ? Math.max(price, m.min_annual) : 0;
1224                });
1225                if (Object.keys(models).length) {
1226                    standaloneCache = { employees: sizes.employees, jobslots: sizes.jobslots, prices: localPrices };
1227                }
1228                if (!standaloneCache || !standaloneCache.prices) return null;
1229                if (sizes.needsEmployees && standaloneCache.employees !== sizes.employees) return null;
1230                if (sizes.needsJobslots  && standaloneCache.jobslots  !== sizes.jobslots)  return null;
1231
1232                var rates = (window.HRONNewPrice && HRONNewPrice.comboRates) || {};
1233                var keys = [];
1234                Object.keys(selected).forEach(function (k) { if (selected[k]) keys.push(k); });
1235                if (keys.length < 2) return 0;
1236
1237                var hasStaff = !!selected.staff;
1238                var full = 0, net = 0;
1239                for (var i = 0; i < keys.length; i++) {
1240                    var k = keys[i];
1241                    var price = standaloneCache.prices[k];
1242                    if (!(price > 0)) return null;
1243                    var r = rates[k] || {};
1244                    // Staff always uses its own rate; the others depend on whether
1245                    // Staff is in the deal. Mirrors the server's combo_rate().
1246                    var pct = (k === 'staff')
1247                        ? (+r.withoutStaff || 0)
1248                        : (hasStaff ? (+r.withStaff || 0) : (+r.withoutStaff || 0));
1249                    full += price;
1250                    net  += price * (1 - pct / 100);
1251                }
1252                return full > 0 ? (1 - net / full) * 100 : 0;
1253            }
1254
1255            // Repaint the badge immediately, before any request leaves the page.
1256            function updateDiscountNow() {
1257                // Invalidate pending responses immediately, including the debounce window.
1258                priceRequestSeq++;
1259                var selected = readModuleSelection();
1260                window.HRONUpdateComboBadges(root, selected, readSizes(selected));
1261                var count = 0;
1262                Object.keys(selected).forEach(function (k) { if (selected[k]) count++; });
1263
1264                // A single module never gets a combo discount.
1265                if (count < 2) {
1266                    effectiveDiscountRate = 0;
1267                    effectiveDiscountKnown = true;
1268                    updateDiscountBadge();
1269                    return;
1270                }
1271
1272                var pct = savingFromCache(selected);
1273                if (pct === null) {
1274                    // Nothing cached for these sizes yet, so wait for the request
1275                    // rather than show a figure we cannot stand behind.
1276                    effectiveDiscountKnown = false;
1277                    updateDiscountBadge();
1278                    return;
1279                }
1280                effectiveDiscountRate = Math.round(pct);
1281                effectiveDiscountKnown = true;
1282                updateDiscountBadge();
1283            }
1284
1285            // Confirms the instant figure and refreshes the cache after a size change.
1286            function refreshDiscountRate() {
1287                var selected = readModuleSelection();
1288                var count = 0;
1289                Object.keys(selected).forEach(function (k) { if (selected[k]) count++; });
1290
1291                // A single module never gets a combo discount, so there is nothing to fetch.
1292                if (count < 2) {
1293                    effectiveDiscountRate = 0;
1294                    effectiveDiscountKnown = true;
1295                    updateDiscountBadge();
1296                    return;
1297                }
1298
1299                var sizes = readSizes(selected);
1300
1301                // Every selected module needs its own size before the saving means
1302                // anything. Until then stay quiet rather than show a partial figure.
1303                if ((sizes.needsEmployees && sizes.employees <= 0) ||
1304                    (sizes.needsJobslots  && sizes.jobslots  <= 0)) {
1305                    effectiveDiscountKnown = false;
1306                    updateDiscountBadge();
1307                    return;
1308                }
1309
1310                var currencyEl = qs(root, '.currency-option.selected');
1311                var currency = currencyEl ? currencyEl.getAttribute('data-currency') : 'DKK';
1312
1313                // Send both sizes as entered so the cached prices cover every module,
1314                // not just the ones ticked right now.
1315                var pending = calculatePrices(sizes.employees, sizes.jobslots, currency, selected);
1316                var refreshSeq = priceRequestSeq;
1317                pending.catch(function () {
1318                        if (refreshSeq !== priceRequestSeq) return;
1319                        effectiveDiscountKnown = false;
1320                        updateDiscountBadge();
1321                    });
1322            }
1323
1324            // Debounced so typing a headcount does not fire a request per keystroke.
1325            // The badge itself has already repainted via updateDiscountNow().
1326            function scheduleDiscountRefresh(delay) {
1327                if (discountRefreshTimer) window.clearTimeout(discountRefreshTimer);
1328                discountRefreshTimer = window.setTimeout(
1329                    refreshDiscountRate,
1330                    typeof delay === 'number' ? delay : 300
1331                );
1332            }
1333
1334            function updateSelectedModules() {
1335
1336                if (isSuiteSelected) {
1337                    selectedModules = ['staff', 'recruit', 'boarding', 'wellbeing', 'comply'];
1338                } else {
1339                    // Store the previous modules before updating
1340                    var previousModules = [...selectedModules];
1341
1342                    // Update the current selected modules
1343                    selectedModules = [];
1344                    qsa(root, '.module-item.selected').forEach(function (module) {
1345                        var moduleId = module.getAttribute('data-module');
1346                        if (moduleId) {
1347                            selectedModules.push(moduleId);
1348                        }
1349                    });
1350
1351                    // Find removed modules (ones that were in previous but not in current)
1352                    var removedModules = previousModules.filter(function (mod) {
1353                        return !selectedModules.includes(mod);
1354                    });
1355
1356                    // Find added modules (ones that are in current but weren't in previous)
1357                    var addedModules = selectedModules.filter(function (mod) {
1358                        return !previousModules.includes(mod);
1359                    });
1360
1361                    if (removedModules.length > 0) {
1362                        // Update mini-module icons with removal animation
1363                        updateMiniModuleIcons(removedModules, 'remove');
1364                    }
1365
1366                    if (addedModules.length > 0) {
1367                        // Update mini-module icons with add animation
1368                        updateMiniModuleIcons(addedModules, 'add');
1369                    }
1370                }
1371
1372                // Repaint the saving straight away from the cached standalone prices,
1373                // then confirm it with the server in the background.
1374                updateDiscountNow();
1375                scheduleDiscountRefresh();
1376
1377                // Update icon visibility in the top image
1378                if (typeof window.updateIconsVisibility === 'function') {
1379                    window.updateIconsVisibility();
1380                }
1381            }
1382
1383            function updateMiniModuleIcons(modules, action) {
1384                var miniModuleContainer = qs(root, '.mini-module-icons');
1385                if (!miniModuleContainer) {
1386                    return;
1387                }
1388
1389                var miniModuleIcons = qsa(miniModuleContainer, '.mini-module-icon');
1390
1391                // Store module info for potential use
1392                var moduleImages = {
1393                    'recruit': 'price-logo-recruit.svg',
1394                    'boarding': 'price-logo-boarding.svg',
1395                    'staff': 'price-logo-staff.svg',
1396                    'wellbeing': 'price-logo-wellbeing.svg',
1397                    'comply': 'price-logo-comply.svg'
1398                };
1399
1400                var moduleTitles = {
1401                    'recruit': 'Recruit',
1402                    'boarding': 'Onboard',
1403                    'staff': 'Staff',
1404                    'wellbeing': 'Thrive',
1405                    'comply': 'Comply'
1406                };
1407
1408                // Default to remove if no action specified
1409                action = action || 'remove';
1410
1411                if (modules && modules.length) {
1412                    if (action === 'remove') {
1413                        // Handle removed module animations
1414
1415                        modules.forEach(function (moduleId) {
1416                            var iconToRemove = qs(miniModuleContainer, '.mini-module-icon[data-module="' + moduleId + '"]');
1417                            if (iconToRemove) {
1418                                var parentDiv = iconToRemove;
1419
1420                                if (parentDiv) {
1421                                    // Get position before removing
1422                                    var rect = parentDiv.getBoundingClientRect();
1423
1424                                    // Clone the element for animation
1425                                    var clone = parentDiv.cloneNode(true);
1426                                    document.body.appendChild(clone);
1427
1428                                    // Apply animation style to clone
1429                                    clone.style.position = 'fixed';
1430                                    clone.style.left = rect.left + 'px';
1431                                    clone.style.top = rect.top + 'px';
1432                                    clone.style.width = rect.width + 'px';
1433                                    clone.style.height = rect.height + 'px';
1434                                    clone.style.transition = 'all 0.6s ease-out';
1435                                    clone.style.zIndex = '10000';
1436
1437                                    // Start animation
1438                                    setTimeout(function () {
1439                                        clone.style.transform = 'translateY(-30px)';
1440                                        clone.style.opacity = '0';
1441                                    }, 10);
1442
1443                                    // Remove after animation completes
1444                                    setTimeout(function () {
1445                                        clone.remove();
1446                                    }, 650);
1447
1448                                    // Remove original icon
1449                                    parentDiv.remove();
1450                                }
1451                            }
1452                        });
1453                    } else if (action === 'add') {
1454
1455                        modules.forEach(function (moduleId) {
1456                            // Check if the icon already exists
1457                            if (qs(miniModuleContainer, '.mini-module-icon[data-module="' + moduleId + '"]')) {
1458                                return;
1459                            }
1460
1461                            // Find the correct position to insert
1462                            var iconOrder = ['staff', 'recruit', 'boarding', 'wellbeing', 'comply'];
1463                            var currentModules = selectedModules.slice().sort(function (a, b) {
1464                                return iconOrder.indexOf(a) - iconOrder.indexOf(b);
1465                            });
1466
1467                            var insertIndex = currentModules.indexOf(moduleId);
1468
1469                            // Create new icon
1470                            var newIcon = document.createElement('div');
1471                            newIcon.className = 'mini-module-icon';
1472                            newIcon.setAttribute('data-module', moduleId);
1473
1474                            // Styling
1475                            newIcon.style.border = 'solid 2px #dbdbdb';
1476                            newIcon.style.width = '24px';
1477                            newIcon.style.height = '24px';
1478                            newIcon.style.borderRadius = '6px';
1479                            newIcon.style.overflow = 'hidden';
1480                            newIcon.style.backgroundColor = 'white';
1481                            newIcon.style.position = 'relative';
1482                            newIcon.style.boxShadow = '0 1px 3px rgba(0,0,0,0.1)';
1483                            newIcon.style.opacity = '0';
1484                            newIcon.style.transform = 'translateY(-30px)';
1485                            newIcon.style.transition = 'all 0.6s ease-out';
1486
1487                            // Create image element
1488                            var img = document.createElement('img');
1489                            img.src = window.HRONNewPrice && window.HRONNewPrice.pluginUrl ?
1490                                (window.HRONNewPrice.pluginUrl + 'assets/img/' + moduleImages[moduleId]) :
1491                                (pluginUrl + 'assets/img/' + moduleImages[moduleId]);
1492                            img.alt = moduleTitles[moduleId] || moduleId;
1493                            img.style.width = '100%';
1494                            img.style.height = '100%';
1495                            img.style.objectFit = 'contain';
1496
1497                            newIcon.appendChild(img);
1498
1499                            // Find where to insert the new icon
1500                            if (insertIndex === 0 || miniModuleIcons.length === 0) {
1501                                // Insert at beginning
1502                                miniModuleContainer.insertBefore(newIcon, miniModuleContainer.firstChild);
1503                            } else if (insertIndex >= miniModuleIcons.length) {
1504                                // Insert at end
1505                                miniModuleContainer.appendChild(newIcon);
1506                            } else {
1507                                // Insert at specific position
1508                                var referenceNode = miniModuleIcons[insertIndex];
1509                                if (referenceNode) {
1510                                    miniModuleContainer.insertBefore(newIcon, referenceNode);
1511                                } else {
1512                                    miniModuleContainer.appendChild(newIcon);
1513                                }
1514                            }
1515
1516                            // Animate the icon appearing
1517                            setTimeout(function () {
1518                                newIcon.style.opacity = '1';
1519                                newIcon.style.transform = 'translateY(0)';
1520                            }, 10);
1521                        });
1522                    }
1523
1524                    // Reorder all icons after the animation starts
1525                    setTimeout(function () {
1526                        var allIcons = qsa(miniModuleContainer, '.mini-module-icon');
1527
1528                        allIcons.forEach(function (icon, index) {
1529                            var leftOffset = index * (-3); // Same as original layout
1530                            icon.style.transition = 'all 0.5s ease';
1531                            icon.style.marginLeft = index > 0 ? leftOffset + 'px' : '0';
1532                            icon.style.marginRight = (index == allIcons.length - 2) ? '2px' : '-2px';
1533                            icon.style.zIndex = (index + 1).toString();
1534                        });
1535                    }, 150);
1536                }
1537            }
1538
1539            // Cancel any count-up or pulse still in flight. Without this, fast
1540            // toggling left several animations running at once and whichever
1541            // finished last won, so the pill could settle on a stale number.
1542            function stopDiscountAnimation() {
1543                if (discountAnimFrame !== null) {
1544                    if (window.cancelAnimationFrame) window.cancelAnimationFrame(discountAnimFrame);
1545                    discountAnimFrame = null;
1546                }
1547                if (discountPulseTimer !== null) {
1548                    window.clearTimeout(discountPulseTimer);
1549                    discountPulseTimer = null;
1550                }
1551            }
1552
1553            // displayedDiscountRate is the single source of truth for what the pill
1554            // shows. Reading it back out of the DOM mid-animation is what made the
1555            // old version drift.
1556            function setDiscountText(el, value) {
1557                displayedDiscountRate = value;
1558                el.textContent = String(value);
1559            }
1560
1561            function animateDiscountTo(el, target, duration) {
1562                stopDiscountAnimation();
1563
1564                // Nothing on screen yet: land on the figure rather than counting up
1565                // from the placeholder in the markup.
1566                if (displayedDiscountRate === null || displayedDiscountRate === target) {
1567                    setDiscountText(el, target);
1568                    return;
1569                }
1570
1571                var from = displayedDiscountRate;
1572                var startTime = null;
1573                function step(timestamp) {
1574                    if (startTime === null) startTime = timestamp;
1575                    var progress = Math.min((timestamp - startTime) / duration, 1);
1576                    setDiscountText(el, Math.round(from + (target - from) * progress));
1577                    if (progress < 1) {
1578                        discountAnimFrame = window.requestAnimationFrame(step);
1579                    } else {
1580                        discountAnimFrame = null;
1581                        setDiscountText(el, target);
1582                    }
1583                }
1584                discountAnimFrame = window.requestAnimationFrame(step);
1585            }
1586
1587            function updateDiscountBadge() {
1588                var discountBtn = qs(root, '.new-price-discount-btn');
1589                var discountRateText = qs(root, '#new-price-discount');
1590                if (!discountBtn || !discountRateText) return;
1591
1592                var customChip = discountBtn.closest('.custom-chip');
1593                var discountLabel = qs(root, '.custom-chip .discount-label');
1594
1595                // Count from the DOM, not from the cached selectedModules array, so
1596                // this can never disagree with what the visitor has t
1596icked.
1597                var selected = readModuleSelection();
1598                var moduleCount = 0;
1599                Object.keys(selected).forEach(function (k) { if (selected[k]) moduleCount++; });
1600
1601                var comboRates = (window.HRONNewPrice && HRONNewPrice.comboRates) || {};
1602                var allModulesSelected = moduleCount >= Object.keys(comboRates).length;
1603                var discountRate = effectiveDiscountRate;
1604
1605                // A figure is shown only when one exists: two or more modules, a
1606                // saving already computed, and that saving above zero.
1607                var hasRealDiscount = moduleCount >= 2 && effectiveDiscountKnown && discountRate > 0;
1608
1609                // The chip also carries the mini module icons and the label, so its
1610                // visibility follows the module count, not the saving.
1611                if (customChip) {
1612                    customChip.style.display = moduleCount < 1 ? 'none' : 'flex';
1613                }
1614
1615                if (!hasRealDiscount) {
1616                    // Stop mid-flight animations and forget the shown value, so the
1617                    // pill starts clean the next time a saving applies.
1618                    stopDiscountAnimation();
1619                    displayedDiscountRate = null;
1620                    discountBtn.hidden = true;
1621                    discountBtn.style.display = 'none';
1622                    if (discountLabel) discountLabel.style.display = 'none';
1623                    return;
1624                }
1625
1626                discountBtn.hidden = false;
1627                discountBtn.style.display = '';
1628                if (discountLabel) discountLabel.style.display = '';
1629
1630                if (displayedDiscountRate === discountRate) {
1631                    stopDiscountAnimation();
1632                    return;
1633                }
1634
1635                // Kept short: the figure is already correct, this is just polish.
1636                animateDiscountTo(discountRateText, discountRate, 320);
1637
1638                if (allModulesSelected) {
1639                    discountPulseTimer = window.setTimeout(function () {
1640                        discountPulseTimer = null;
1641                        animateDiscountButton(discountBtn);
1642                    }, 380);
1643                }
1644            }
1645
1646            function updatePrices(currency) {
1647                if (currency === currentCurrency) return;
1648
1649                currentCurrency = currency;
1650                var rate = currency === 'EUR' ? currencyRate : 1;
1651
1652                qsa(root, '.module-price').forEach(function (priceEl) {
1653                    priceEl.remove();
1654                });
1655
1656                qsa(root, '.hron-price').forEach(function (priceEl) {
1657                    var priceText = priceEl.textContent;
1658                    if (!originalPrices[priceEl]) {
1659                        originalPrices[priceEl] = priceText;
1660                    }
1661
1662                    var originalText = originalPrices[priceEl];
1663
1664                    var match = originalText.match(/(?:^fra\s+)?(\d+)kr\/mdr/i);
1665                    if (match) {
1666                        var dkkAmount = parseInt(match[1]);
1667                        var convertedAmount = currency === 'EUR' ?
1668                            Math.round(dkkAmount * rate) : dkkAmount;
1669                        var currencySuffix = currency === 'EUR' ? '€' : 'kr';
1670
1671                        priceEl.textContent = 'fra ' + convertedAmount + currencySuffix + '/mdr';
1672                    }
1673                });
1674
1675                var employeesInput = qs(root, '#employees-input');
1676                var jobslotsInput = qs(root, '#job-slots-input');
1677
1678                var employees = parseInt(employeesInput ? employeesInput.value : 0) || 0;
1679                var jobslots = parseInt(jobslotsInput ? jobslotsInput.value : 0) || 0;
1680
1681                var selected = {
1682                    recruit: !!qs(root, '.module-item[data-module="recruit"].selected'),
1683                    boarding: !!qs(root, '.module-item[data-module="boarding"].selected'),
1684                    staff: !!qs(root, '.module-item[data-module="staff"].selected'),
1685                    wellbeing: !!qs(root, '.module-item[data-module="wellbeing"].selected'),
1686                    comply: !!qs(root, '.module-item[data-module="comply"].selected')
1687                };
1688
1689                if (employees > 0 || jobslots > 0) {
1690                    calculatePrices(employees, jobslots, currency, selected)
1691                        .then(function (prices) {
1692                            updatePriceDisplay(prices);
1693                        })
1694                        .catch(function (error) {
1695
1696                        });
1697                }
1698            }
1699
1700            function updatePriceDisplay(prices) {
1701
1702                if (window.HRONPriceModal && typeof window.HRONPriceModal.updateModalPrices === 'function') {
1703                    window.HRONPriceModal.updateModalPrices();
1704                }
1705            }
1706
1707            var currencyIndicator = qs(root, '.currency-indicator');
1708            if (currencyIndicator) {
1709                var selectedCurrency = qs(root, '.selected-currency');
1710                var currencyOptions = qsa(root, '.currency-option');
1711                currencyIndicator.addEventListener('click', function (e) {
1712                    e.stopPropagation();
1713                    currencyIndicator.classList.toggle('open');
1714                });
1715                currencyOptions.forEach(function (option) {
1716                    option.addEventListener('click', function (e) {
1717                        e.stopPropagation();
1718                        var currency = option.getAttribute('data-currency');
1719
1720                        if (selectedCurrency) selectedCurrency.textContent = (currency === 'DKK' ? 'DKK' : currency);
1721
1722                        currencyOptions.forEach(function (o) {
1723                            o.classList.remove('selected');
1724                        });
1725                        option.classList.add('selected');
1726
1727                        updatePrices(currency);
1728
1729                        setTimeout(function () {
1730                            currencyIndicator.classList.remove('open');
1731                        }, 100);
1732                    });
1733                });
1734                document.addEventListener('click', function () {
1735                    currencyIndicator.classList.remove('open');
1736                });
1737            }
1738
1739            var employeesInput = qs(root, '#employees-input');
1740            var jobslotsInput = qs(root, '#job-slots-input');
1741
1742            if (employeesInput) {
1743                employeesInput.addEventListener('input', function () {
1744                    var currency = qs(root, '.currency-option.selected');
1745                    var currentCurrency = currency ? currency.getAttribute('data-currency') : 'DKK';
1746                    updatePrices(currentCurrency);
1747                    updateDiscountNow();
1748                    scheduleDiscountRefresh();
1749                });
1750            }
1751
1752            if (jobslotsInput) {
1753                jobslotsInput.addEventListener('input', function () {
1754                    var currency = qs(root, '.currency-option.selected');
1755                    var currentCurrency = currency ? currency.getAttribute('data-currency') : 'DKK';
1756                    updatePrices(currentCurrency);
1757                    updateDiscountNow();
1758                    scheduleDiscountRefresh();
1759                });
1760            }
1761
1762            var currencyIndicatorEl = qs(root, '.currency-indicator');
1763            if (currencyIndicatorEl) {
1764                currencyIndicatorEl.style.display = '';
1765            }
1766
1767            function clearLoading() {
1768                root.classList.remove('is-loading');
1769            }
1770            clearLoading();
1771            updateDiscountNow();
1772            window.requestAnimationFrame(clearLoading);
1773            setTimeout(clearLoading, 100);
1774
1775            // Make sure custom modules are shown on page load
1776            setTimeout(function () {
1777                if (customModules) {
1778                    customModules.classList.add('show');
1779
1780                    // Add click handler to prevent clicks from propagating through custom modules
1781                    customModules.addEventListener('click', function (e) {
1782                        e.stopPropagation();
1783                    });
1784                }
1785                if (suiteInputs) suiteInputs.classList.remove('hide');
1786
1787                // Initialize icon visibility based on selected modules
1788                if (typeof window.updateIconsVisibility === 'function') {
1789                    window.updateIconsVisibility();
1790                }
1791
1792                // Initialize discount badge with animation
1793                updateDiscountBadge();
1794
1795                // Warm the standalone-price cache so the very first module toggle
1796                // repaints instantly instead of waiting for a round-trip.
1797                scheduleDiscountRefresh(0);
1798            }, 200);
1799
1800            // Function to animate the discount button with pulse and glow effect
1801            function animateDiscountButton(discountBtn) {
1802                // Prevent multiple animations from running at the same time
1803                if (discountBtn.dataset.animating === 'true') return;
1804
1805                discountBtn.dataset.animating = 'true';
1806
1807                // Store original styles to restore later
1808                const originalTransition = discountBtn.style.transition;
1809                const originalTransform = discountBtn.style.transform;
1810
1811                // Add CSS for the scale animation
1812                discountBtn.style.transition = 'all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
1813                discountBtn.style.transform = 'scale(1.15)';
1814
1815                // Add the glow animation class
1816                discountBtn.classList.add('discount-btn-pulse');
1817
1818                // Revert back after a delay
1819                setTimeout(() => {
1820                    discountBtn.style.transform = 'scale(1)';
1821
1822                    // Reset the animating flag and restore original transition
1823                    setTimeout(() => {
1824                        discountBtn.style.transition = originalTransition;
1825                        discountBtn.classList.remove('discount-btn-pulse');
1826                        discountBtn.dataset.animating = 'false';
1827                    }, 600);
1828                }, 800);
1829            }
1830
1831            // ---------------------------------------------------------------
1832            // Kiosk reset: the fair/exhibition screen never reloads, so when the
1833            // price panel is closed we must wipe everything the previous visitor
1834            // entered and re-arm the one-shot webhook guard, exactly as a fresh
1835            // page load would. Exposed globally so the kiosk shell can call it.
1836            // ---------------------------------------------------------------
1837            function resetPriceForm() {
1838                // Re-arm the one-shot webhook so the next visitor's submission
1839                // sends again (this is the "limit" that otherwise sticks).
1840                hronWebhookFired = false;
1841                isSuiteSelected = false;
1842
1843                // Clear every text / number / contact input and its visual state.
1844                qsa(root, 'input').forEach(function (inp) {
1845                    var t = (inp.type || '').toLowerCase();
1846                    if (t === 'button' || t === 'submit' || t === 'hidden' || t === 'checkbox' || t === 'radio') return;
1847                    inp.value = '';
1848                    inp.classList.remove('error');
1849                    inp.removeAttribute('aria-invalid');
1850                    inp.style.borderColor = '';
1851                    inp.style.boxShadow = '';
1852                    var fg = inp.closest('.form-group');
1853                    if (fg) { fg.classList.remove('has-error'); fg.classList.remove('has-value'); }
1854                });
1855
1856                // Switch pricing back to DKK if the visitor changed currency.
1857                var dkkOpt = qs(root, '.currency-option[data-currency="DKK"]');
1858                if (dkkOpt && !dkkOpt.classList.contains('selected')) {
1859                    qsa(root, '.currency-option').forEach(function (o) { o.classList.remove('selected'); });
1860                    dkkOpt.classList.add('selected');
1861                    var selCur = qs(root, '.selected-currency');
1862                    if (selCur) selCur.textContent = 'DKK';
1863                    updatePrices('DKK');
1864                }
1865                var curInd = qs(root, '.currency-indicator');
1866                if (curInd) curInd.classList.remove('open');
1867
1868                // Re-select every module (default = all four selected); the helper
1869                // restores the mini icons + discount badge for the re-added ones.
1870                qsa(root, '.module-item').forEach(function (m) { m.classList.add('selected'); });
1871                updateSelectedModules();
1872
1873                // Re-evaluate input/contact visibility for the default state.
1874                toggleInputs();
1875                checkAndShowContactSection();
1876
1877                // Restore the blue "enter number of employees" gate exactly as it
1878                // appears on a fresh load. The visitor types here first, and once
1879                // they continue the plugin hides this overlay permanently — so on a
1880                // never-reloading kiosk we must clear its value and show it again.
1881                var blurInput = document.getElementById('hron-blur-employee-input');
1882                if (blurInput) {
1883                    blurInput.value = '';
1884                    blurInput.classList.remove('error');
1885                    blurInput.style.borderColor = '';
1886                    blurInput.style.boxShadow = '';
1887                }
1888                var blurBtn = document.getElementById('hron-blur-unlock-btn');
1889                if (blurBtn) blurBtn.classList.remove('visible');
1890                var blurOverlay = document.getElementById('hron-blur-overlay');
1891                if (blurOverlay) {
1892                    blurOverlay.style.display = '';
1893                    blurOverlay.classList.remove('fade-out');
1894                }
1895                var recBanner = document.getElementById('hron-recommendation-banner');
1896                if (recBanner) recBanner.classList.remove('visible', 'hiding');
1897            }
1898            window.HRONResetPriceForm = resetPriceForm;
1899        });
1900    });
1901})();

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.