PageSourceSearch

https://flowbot.atflow.fi/embed.js

js atflow.fi collected 2026-10-04 16:51:18 UTC 90,631 bytes, 1,946 lines download raw bytes

1/**
2 * FlowBot Widget Embed Script
3 * 
4 * This script creates iframe widgets for FlowBot chat and SuperSearch search.
5 * It should be included with a script tag:
6 * 
7 * <script src="https://your-domain.com/embed.js" defer></script>
8 * 
9 * Chat widget: <div data-flowbot-id="YOUR_BOT_ID"></div>
10 * SuperSearch search: <div data-flowbot-supersearch="YOUR_BOT_ID" data-flowbot-supersearch-id="INSTANCE_ID"></div>
11 * AI showcase / uutiset: height follows content via supersearch-resize (optional data-flowbot-height sets initial/min for AI showcase).
12 */
13
14(function() {
15    'use strict';
16
17    const scriptTag = document.currentScript || document.querySelector('script[src*="embed.js"]');
18    const baseUrl = scriptTag ? scriptTag.src.replace(/\/embed\.js.*$/, '') : window.location.origin;
19
20    const CONFIG = {
21        baseUrl: baseUrl
22    };
23
24    function flowbotPostTargetOrigin() {
25        try {
26            return new URL(CONFIG.baseUrl).origin || '*';
27        } catch (e) {
28            return '*';
29        }
30    }
31
32    function applyFlowbotIframeIsolation(iframe) {
33        iframe.setAttribute(
34            'sandbox',
35            'allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation allow-storage-access-by-user-activation allow-modals'
36        );
37    }
38
39    window.__flowbotIframes = window.__flowbotIframes || {};
40    // SuperSearch / direct_embed iframes (separate from chat __flowbotIframes).
41    window.__flowbotSuperSearchIframes = window.__flowbotSuperSearchIframes || {};
42
43    // Mobile breakpoint matches the widget's `useIsMobile` hook so both sides agree.
44    const MOBILE_BREAKPOINT = 640;
45    // Reserved space at top of mobile viewport so the host page's top bar stays tappable.
46    const MOBILE_TOP_RESERVE = 96;
47    // Side gutter on mobile so the iframe never bleeds to the screen edge.
48    const MOBILE_SIDE_GUTTER = 24;
49    // Desktop safety: keep at least this much breathing room from the top of the viewport.
50    const DESKTOP_TOP_RESERVE = 40;
51
52    function parsePx(value) {
53        if (typeof value !== 'string') return NaN;
54        const n = parseFloat(value);
55        return Number.isFinite(n) ? n : NaN;
56    }
57
58    function getViewport() {
59        const vv = window.visualViewport;
60        return {
61            width: (vv && vv.width) || window.innerWidth || document.documentElement.clientWidth || 0,
62            height: (vv && vv.height) || window.innerHeight || document.documentElement.clientHeight || 0,
63            top: (vv && Number.isFinite(vv.offsetTop)) ? vv.offsetTop : 0,
64            left: (vv && Number.isFinite(vv.offsetLeft)) ? vv.offsetLeft : 0,
65        };
66    }
67
68    /** Soft keyboard (or browser chrome) has shrunk the visual viewport vs layout. */
69    function isSoftKeyboardOpen() {
70        const { height } = getViewport();
71        const layoutH = window.innerHeight || document.documentElement.clientHeight || height;
72        if (!layoutH || !height) return false;
73        return (layoutH - height) > 120;
74    }
75
76    function isLauncherDimensions(width, height) {
77        return (
78            (width === '64px' && height === '64px') ||
79            (width === '44px' && height === '72px') ||
80            (width === '36px' && height === '70px')
81        );
82    }
83
84    // ── Prevent host scroll chaining through the chat iframe only ─────────────
85    // Host page must still scroll beside the panel; never freeze document.body.
86
87    function setChatIframeScrollContain(iframe, enabled) {
88        if (!iframe) return;
89        if (enabled) {
90            iframe.style.overscrollBehavior = 'contain';
91            iframe.style.overscrollBehaviorY = 'contain';
92            iframe.style.overscrollBehaviorX = 'contain';
93            iframe.dataset.flowbotScrollContain = 'true';
94        } else {
95            iframe.style.overscrollBehavior = '';
96            iframe.style.overscrollBehaviorY = '';
97            iframe.style.overscrollBehaviorX = '';
98            delete iframe.dataset.flowbotScrollContain;
99        }
100    }
101
102    function syncHostScrollLockForChat(iframe, fullViewport, width, height) {
103        if (!iframe) return;
104        var open = !!fullViewport || (width && height && !isLauncherDimensions(width, height));
105        setChatIframeScrollContain(iframe, open);
106    }
107
108    /** Host page context forwarded to iframe widgets (domain, path, `<html lang>`). */
109    function getParentPageContext() {
110        var parentHost = '';
111        var parentPath = '';
112        var parentLang = '';
113        try {
114            parentHost = window.location.hostname || '';
115            parentPath = window.location.pathname || '';
116            if (document.documentElement) {
117                parentLang = document.documentElement.getAttribute('lang') || document.documentElement.lang || '';
118            }
119        } catch (e) {}
120        return { parentHost: parentHost, parentPath: parentPath, parentLang: parentLang };
121    }
122
123    function appendParentContextQuery(url) {
124        var ctx = getParentPageContext();
125        if (ctx.parentHost) url += '&parentHost=' + encodeURIComponent(ctx.parentHost);
126        if (ctx.parentPath) url += '&parentPath=' + encodeURIComponent(ctx.parentPath);
127        if (ctx.parentLang) url += '&parentLang=' + encodeURIComponent(ctx.parentLang);
128        return url;
129    }
130
131    var FULLVIEWPORT_MS = 400;
132    var FULLVIEWPORT_EASING = 'cubic-bezier(0.34, 1.2, 0.64, 1)';
133
134    function prefersReducedMotion() {
135        try {
136            return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
137        } catch (e) {
138            return false;
139        }
140    }
141
142    function setIframeBoxTransition(iframe, enabled) {
143        if (!iframe) return;
144        if (enabled && !prefersReducedMotion()) {
145            iframe.style.transition =
146                'width ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' +
147                'height ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' +
148                'bottom ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' +
149                'right ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' +
150                'border-radius ' + FULLVIEWPORT_MS + 'ms ' + FULLVIEWPORT_EASING + ', ' +
151                'box-shadow ' + FULLVIEWPORT_MS + 'ms ease';
152        } else {
153            iframe.style.transition = '';
154        }
155    }
156
157    function clearIframeBoxTransitionLater(iframe) {
158        if (!iframe) return;
159        var token = String(Date.now());
160        iframe.dataset.flowbotTransitionToken = token;
161        window.setTimeout(function () {
162            if (iframe.dataset.flowbotTransitionToken === token) {
163                setIframeBoxTransition(iframe, false);
164                delete iframe.dataset.flowbotTransitionToken;
165            }
166        }, FULLVIEWPORT_MS + 60);
167    }
168
169    /**
170     * Expand iframe to the visual viewport so the soft keyboard resizes the
171     * shell instead of covering the composer. Without keyboard: bottom-right
172     * anchor (open morph from launcher). With keyboard: pin to VV top/left so
173     * the visible box stays centered above the keyboard.
174     * @param {HTMLIFrameElement} iframe
175     * @param {{ animate?: boolean }} [options]
176     */
177    function applyFullViewportIframe(iframe, options) {
178        if (!iframe) return;
179        const animate = !!(options && options.animate);
180        const { width, height, top, left } = getViewport();
181        const layoutW = window.innerWidth || document.documentElement.clientWidth || width;
182        const layoutH = window.innerHeight || document.documentElement.clientHeight || height;
183        const keyboardOpen = isSoftKeyboardOpen();
184        const alreadyFull = iframe.dataset.flowbotFullViewport === 'true';
185
186        iframe.style.position = 'fixed';
187
188        if (animate && !alreadyFull) {
189            setIframeBoxTransition(iframe, true);
190            // Force reflow so the transition starts from the current launcher box.
191            void iframe.offsetWidth;
192        } else {
193            setIframeBoxTransition(iframe, false);
194        }
195
196        if (keyboardOpen) {
197            // Pin exactly to the visual viewport rect (above the soft keyboard).
198            iframe.style.top = top + 'px';
199            iframe.style.left = left + 'px';
200            iframe.style.right = 'auto';
201            iframe.style.bottom = 'auto';
202            iframe.dataset.flowbotKeyboardOpen = 'true';
203        } else {
204            // Bottom-right anchoring so width/height growth expands from the FAB.
205            const bottom = Math.max(0, layoutH - top - height);
206            const right = Math.max(0, layoutW - left - width);
207            iframe.style.top = 'auto';
208            iframe.style.left = 'auto';
209            iframe.style.right = right + 'px';
210            iframe.style.bottom = bottom + 'px';
211            delete iframe.dataset.flowbotKeyboardOpen;
212        }
213
214        iframe.style.width = width + 'px';
215        iframe.style.height = height + 'px';
216        iframe.style.maxWidth = 'none';
217        iframe.style.maxHeight = 'none';
218        iframe.style.borderRadius = '0';
219        iframe.style.boxShadow = 'none';
220        iframe.style.transform = '';
221        iframe.dataset.flowbotFullViewport = 'true';
222
223        if (animate && !alreadyFull) {
224            clearIframeBoxTransitionLater(iframe);
225        }
226
227        // Notify widget so it can switch to a centered card above the keyboard.
228        if (iframe.contentWindow) {
229            try {
230                iframe.contentWindow.postMessage({
231                    type: 'flowbot-host-viewport',
232                    width: layoutW > 0 ? layoutW : width,
233                    height: height,
234                    keyboardOpen: keyboardOpen,
235                }, flowbotPostTargetOrigin());
236            } catch (e) {}
237        }
238    }
239
240    function clearFullViewportIframe(iframe) {
241        if (!iframe || iframe.dataset.flowbotFullViewport !== 'true') return;
242        delete iframe.dataset.flowbotFullViewport;
243        delete iframe.dataset.flowbotKeyboardOpen;
244        // Leave box edges in place; the next applyIframeDimensions call sets
245        // launcher/panel geometry (optionally animated from this full box).
246        iframe.style.maxWidth = '';
247        iframe.style.maxHeight = '';
248    }
249
250    /**
251     * Apply launcher/panel size. Box transition is only for leaving mobile
252     * full-viewport (shrink toward launcher). Desktop launcher↔panel always
253     * snaps size and border-radius — no 50%→16px / circular→card interpolate.
254     */
255    function applySizedIframeFromFullViewport(iframe, requestedWidth, requestedHeight, animate, fabShape) {
256        if (!iframe) return;
257        const wasFull = iframe.dataset.flowbotFullViewport === 'true';
258        // Only morph when closing mobile full-bleed; never for desktop open/close.
259        const shouldAnimate = !!(animate && wasFull && !prefersReducedMotion());
260        if (shouldAnimate) {
261            setIframeBoxTransition(iframe, true);
262            void iframe.offsetWidth;
263        } else {
264            setIframeBoxTransition(iframe, false);
265        }
266        clearFullViewportIframe(iframe);
267        if (requestedWidth && requestedHeight) {
268            iframe.dataset.flowbotRequestedWidth = String(requestedWidth);
269            iframe.dataset.flowbotRequestedHeight = String(requestedHeight);
270            if (fabShape === 'edge-tab' || fabShape === 'round') {
271                iframe.dataset.flowbotFabShape = fabShape;
272            }
273            applyIframeDimensions(iframe, requestedWidth, requestedHeight, fabShape);
274        }
275        if (shouldAnimate) {
276            clearIframeBoxTransitionLater(iframe);
277        }
278    }
279
280    /**
281     * Prefer explicit fabShape, then dimensions, then stored dataset.
282     * Dimensions win over a stale dataset value (e.g. 64×64 after leaving edge-tab).
283     */
284    function resolveLauncherShape(iframe, requestedWidth, requestedHeight, fabShape) {
285        if (fabShape === 'edge-tab' || fabShape === 'round') return fabShape;
286        if (requestedWidth === '44px' && requestedHeight === '72px') return 'edge-tab';
287        if (
288            (requestedWidth === '64px' && requestedHeight === '64px') ||
289            (requestedWidth === '36px' && requestedHeight === '70px')
290        ) {
291            return 'round';
292        }
293        const stored = iframe && iframe.dataset ? iframe.dataset.flowbotFabShape : '';
294        if (stored === 'edge-tab' || stored === 'round') return stored;
295        return 'round';
296    }
297
298    /**
299     * Apply the requested dimensions to the iframe, clamping to the current
300     * viewport so the chat never overflows on small screens. Launcher sizes
301     * bypass clamping so the FAB stays at the configured shape.
302     */
303    function applyIframeDimensions(iframe, requestedWidth, requestedHeight, fabShape) {
304        if (!iframe) return;
305
306        const isLauncherSize =
307            (requestedWidth === '64px' && requestedHeight === '64px') ||
308            (requestedWidth === '44px' && requestedHeight === '72px') ||
309            (requestedWidth === '36px' && requestedHeight === '70px');
310
311        if (isLauncherSize) {
312            const launcherShape = resolveLauncherShape(
313                iframe,
314                requestedWidth,
315                requestedHeight,
316                fabShape
317            );
318            iframe.dataset.flowbotFabShape = launcherShape;
319
320            if (launcherShape === 'edge-tab') {
321                iframe.style.width = '44px';
322                iframe.style.height = '72px';
323                iframe.style.bottom = 'calc(20px + env(safe-area-inset-bottom, 0px))';
324                iframe.style.right = '0';
325                iframe.style.left = 'auto';
326                iframe.style.top = 'auto';
327                iframe.style.transform = '';
328                iframe.style.borderRadius = '12px 0 0 12px';
329                iframe.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
330                return;
331            }
332
333            // Round launcher (default; legacy 36×70 maps here).
334            iframe.style.width = '64px';
335            iframe.style.height = '64px';
336            iframe.style.bottom = '20px';
337            iframe.style.right = '20px';
338            iframe.style.left = 'auto';
339            iframe.style.top = 'auto';
340            iframe.style.transform = '';
341            iframe.style.borderRadius = '50%';
342            iframe.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
343            return;
344        }
345
346        const reqW = parsePx(requestedWidth);
347        const reqH = parsePx(requestedHeight);
348        if (!Number.isFinite(reqW) || !Number.isFinite(reqH)) return;
349
350        const { width: vw, height: vh } = getViewport();
351        const isMobile = vw > 0 && vw <= MOBILE_BREAKPOINT;
352
353        const finalW = isMobile
354            ? Math.max(240, Math.min(reqW, vw - MOBILE_SIDE_GUTTER))
355            : Math.min(reqW, Math.max(240, vw - 16));
356        const finalH = isMobile
357            ? Math.max(320, Math.min(reqH, vh - MOBILE_TOP_RESERVE))
358            : Math.min(reqH, Math.max(320, vh - DESKTOP_TOP_RESERVE));
359
360        iframe.style.width = finalW + 'px';
361        iframe.style.height = finalH + 'px';
362        iframe.style.bottom = isMobile ? '12px' : '20px';
363        iframe.style.right = isMobile ? '12px' : '20px';
364        iframe.style.left = '';
365        iframe.style.top = '';
366        iframe.style.transform = '';
367        iframe.style.borderRadius = '16px';
368        iframe.style.boxShadow = '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)';
369    }
370
371    function postHostViewportToIframe(iframe, vw, vh, keyboardOpen) {
372        if (!iframe || !iframe.isConnected || !iframe.contentWindow) return;
373        try {
374            iframe.contentWindow.postMessage({
375                type: 'flowbot-host-viewport',
376                width: vw,
377                height: vh,
378                keyboardOpen: keyboardOpen,
379            }, flowbotPostTargetOrigin());
380        } catch (e) {}
381    }
382
383    function superSearchIframeKey(botId, instanceId) {
384        return String(botId || '') + '::' + String(instanceId || '');
385    }
386
387    function registerSuperSearchIframe(botId, instanceId, iframe) {
388        if (!iframe) return;
389        window.__flowbotSuperSearchIframes = window.__flowbotSuperSearchIframes || {};
390        window.__flowbotSuperSearchIframes[superSearchIframeKey(botId, instanceId)] = iframe;
391    }
392
393    /**
394     * Re-apply cached dimensions to every tracked iframe. Called on resize /
395     * orientationchange / visualViewport resize so rotating the device or the
396     * soft keyboard popping up keeps the widget within bounds.
397     */
398    function reapplyAllIframeDimensions() {
399        const map = window.__flowbotIframes || {};
400        const { width: vw, height: vh } = getViewport();
401        const keyboardOpen = isSoftKeyboardOpen();
402        Object.keys(map).forEach(function (id) {
403            const iframe = map[id];
404            if (!iframe || !iframe.isConnected) return;
405            // Keep the embedded widget in sync with the HOST viewport so it can
406            // re-evaluate mobile vs desktop sizing and soft-keyboard layout.
407            postHostViewportToIframe(iframe, vw, vh, keyboardOpen);
408            if (iframe.dataset.flowbotFullViewport === 'true') {
409                // Keyboard / orientation: snap, do not replay open animation.
410                applyFullViewportIframe(iframe, { animate: false });
411                return;
412            }
413            const w = iframe.dataset.flowbotRequestedWidth;
414            const h = iframe.dataset.flowbotRequestedHeight;
415            if (!w || !h) return;
416            applyIframeDimensions(iframe, w, h);
417        });
418
419        // SuperSearch iframes are not in __flowbotIframes — still need live
420        // host viewport so useHostIsMobile / DirectEmbed bar width update without reload.
421        const ssMap = window.__flowbotSuperSearchIframes || {};
422        Object.keys(ssMap).forEach(function (id) {
423            var ssIframe = ssMap[id];
424            postHostViewportToIframe(ssIframe, vw, vh, keyboardOpen);
425            if (
426                ssIframe &&
427                (ssIframe.hasAttribute('data-flowbot-direct-embed') ||
428                    ssIframe.getAttribute('data-flowbot-direct-embed-pending') === 'true')
429            ) {
430                applyDirectEmbedIframePosition(ssIframe);
431            }
432        });
433    }
434
435    function attachViewportListenersOnce() {
436        if (window.__flowbotViewportListenersAttached) return;
437        window.__flowbotViewportListenersAttached = true;
438
439        let rafId = 0;
440        const schedule = function () {
441            if (rafId) return;
442            rafId = window.requestAnimationFrame(function () {
443                rafId = 0;
444                reapplyAllIframeDimensions();
445            });
446        };
447
448        window.addEventListener('resize', schedule);
449        window.addEventListener('orientationchange', schedule);
450        if (window.visualViewport) {
451            window.visualViewport.addEventListener('resize', schedule);
452        }
453    }
454
455    function injectCriticalStyles() {
456        if (document.getElementById('flowbot-critical-styles')) return;
457        const style = document.createElement('style');
458        style.id = 'flowbot-critical-styles';
459        style.textContent = `
460            [data-flowbot-id] {
461                position: relative;
462                display: block;
463            }
464            [data-flowbot-supersearch] {
465                position: relative;
466                display: block;
467                width: 100%;
468                height: 100%;
469                min-width: 120px;
470                min-height: 40px;
471            }
472            [data-flowbot-supersearch][data-flowbot-uutiset="true"] {
473                height: auto;
474                overflow: visible;
475            }
476            iframe[data-flowbot-uutiset="true"] {
477                display: block;
478                width: 100% !important;
479                border: none !important;
480                background: transparent;
481                overflow: hidden;
482            }
483            [data-flowbot-supersearch][data-flowbot-embed-layout="direct_embed"],
484            [data-flowbot-supersearch][data-flowbot-direct-embed="true"] {
485                position: fixed !important;
486                width: 0 !important;
487                height: 0 !important;
488                min-width: 0 !important;
489                min-height: 0 !important;
490                margin: 0 !important;
491                padding: 0 !important;
492                overflow: visible !important;
493                background: transparent !important;
494                border: none !important;
495                pointer-events: none !important;
496            }
497            [data-flowbot-popup] {
498                display: none;
499            }
500            iframe[data-flowbot-direct-embed="true"] {
501                pointer-events: auto !important;
502                border: none !important;
503                background: transparent !important;
504                background-color: transparent !important;
505                color-scheme: normal;
506                overflow: hidden !important;
507            }
508            iframe[data-flowbot-direct-embed-pending="true"] {
509                visibility: hidden !important;
510                opacity: 0 !important;
511                pointer-events: none !important;
512            }
513            /* Open chat: stop scroll chaining into the host; host still scrolls beside the iframe. */
514            iframe[data-flowbot-chat="true"][data-flowbot-scroll-contain="true"] {
515                overscroll-behavior: contain;
516            }
517        `;
518        document.head.appendChild(style);
519    }
520
521    function applySuperSearch360Dimensions(foundIframe, width, height) {
522        if (!foundIframe) return;
523        var w = typeof width === 'number' ? width + 'px' : width;
524        var h = typeof height === 'number' ? height + 'px' : height;
525        if (!w || !h) return;
526
527        foundIframe.style.width = w;
528        foundIframe.style.height = h;
529        foundIframe.style.display = 'block';
530        foundIframe.style.minWidth = '0';
531        foundIframe.style.minHeight = '0';
532        applySuperSearchIframeTrans
532parency(foundIframe);
533
534        var container = foundIframe.parentElement;
535        if (container && container.hasAttribute('data-flowbot-supersearch')) {
536            container.style.position = 'relative';
537            container.style.width = w;
538            container.style.height = h;
539            container.style.margin = '0 auto';
540            container.style.top = '';
541            container.style.left = '';
542            container.style.right = '';
543            container.style.bottom = '';
544            container.style.transform = 'none';
545        }
546    }
547
548    var AI_SHOWCASE_INITIAL_HEIGHT_PX = 420;
549    var AI_SHOWCASE_HEIGHT_MIN_PX = 200;
550    var AI_SHOWCASE_HEIGHT_MAX_PX = 6000;
551
552    function parseHostHeightPx(value) {
553        if (value == null || value === '') return 0;
554        if (typeof value === 'number' && isFinite(value)) {
555            return value >= 48 ? Math.round(value) : 0;
556        }
557        var match = String(value).trim().match(/^(\d+(?:\.\d+)?)\s*(px)?$/i);
558        if (!match) return 0;
559        var n = Math.round(parseFloat(match[1]));
560        return n >= 48 ? n : 0;
561    }
562
563    /** Optional explicit initial height; otherwise a compact placeholder until content resize. */
564    function resolveAiShowcaseInitialHeightPx(container) {
565        if (!container) return AI_SHOWCASE_INITIAL_HEIGHT_PX;
566
567        var explicit =
568            parseHostHeightPx(container.getAttribute('data-flowbot-height')) ||
569            parseHostHeightPx(container.getAttribute('data-flowbot-supersearch-height'));
570
571        var parent = container.parentElement;
572        if (!explicit && parent) {
573            explicit = parseHostHeightPx(parent.getAttribute('data-flowbot-height'));
574        }
575
576        if (explicit >= AI_SHOWCASE_HEIGHT_MIN_PX) {
577            return Math.min(AI_SHOWCASE_HEIGHT_MAX_PX, explicit);
578        }
579        return AI_SHOWCASE_INITIAL_HEIGHT_PX;
580    }
581
582    function clampAiShowcaseHeightPx(value) {
583        if (typeof value !== 'number' || !isFinite(value)) return null;
584        return Math.min(
585            AI_SHOWCASE_HEIGHT_MAX_PX,
586            Math.max(AI_SHOWCASE_HEIGHT_MIN_PX, Math.round(value))
587        );
588    }
589
590    function applyAiShowcaseHostSize(iframe, container) {
591        if (!iframe) return;
592        var heightPx = resolveAiShowcaseInitialHeightPx(container);
593        var heightCss = heightPx + 'px';
594        var hostParent = container && container.parentElement;
595
596        iframe.setAttribute('data-flowbot-ai-showcase', 'true');
597        iframe.style.border = 'none';
598        iframe.style.borderRadius = '28px';
599        iframe.style.boxShadow = 'none';
600        iframe.style.background = 'transparent';
601        iframe.style.display = 'block';
602        iframe.style.overflow = 'hidden';
603        setImportantStyle(iframe, 'width', '100%');
604        setImportantStyle(iframe, 'height', heightCss);
605        setImportantStyle(iframe, 'min-height', '0');
606
607        if (!container || !container.hasAttribute('data-flowbot-supersearch')) return;
608
609        container.dataset.flowbotHostHeight = String(heightPx);
610        container.setAttribute('data-flowbot-ai-showcase', 'true');
611        container.style.position = 'relative';
612        container.style.top = '';
613        container.style.left = '';
614        container.style.right = '';
615        container.style.bottom = '';
616        container.style.display = 'block';
617        container.style.maxWidth = '';
618        container.style.marginLeft = '';
619        container.style.marginRight = '';
620        container.style.overflow = 'visible';
621        container.style.background = 'transparent';
622        container.style.border = 'none';
623        container.style.borderRadius = '28px';
624        container.style.boxShadow = 'none';
625        setImportantStyle(container, 'width', '100%');
626        setImportantStyle(container, 'height', heightCss);
627        setImportantStyle(container, 'min-height', '0');
628
629        if (hostParent) {
630            hostParent.style.overflow = 'visible';
631            hostParent.style.maxWidth = '';
632        }
633    }
634
635    function applyAiShowcaseResize(iframe, height) {
636        if (!iframe) return;
637        var heightPx = clampAiShowcaseHeightPx(height);
638        if (heightPx == null) return;
639        var heightCss = heightPx + 'px';
640        var container = iframe.parentElement;
641
642        setImportantStyle(iframe, 'width', '100%');
643        setImportantStyle(iframe, 'height', heightCss);
644        setImportantStyle(iframe, 'min-height', '0');
645
646        if (!container || !container.hasAttribute('data-flowbot-supersearch')) return;
647        container.dataset.flowbotHostHeight = String(heightPx);
648        container.style.maxWidth = '';
649        setImportantStyle(container, 'width', '100%');
650        setImportantStyle(container, 'height', heightCss);
651        setImportantStyle(container, 'min-height', '0');
652    }
653
654    var UUTISET_INITIAL_HEIGHT_PX = 132;
655    var UUTISET_HEIGHT_MIN_PX = 88;
656    var UUTISET_HEIGHT_MAX_PX = 8000;
657
658    function clampUutisetHeightPx(value) {
659        if (typeof value !== 'number' || !isFinite(value)) return null;
660        return Math.min(
661            UUTISET_HEIGHT_MAX_PX,
662            Math.max(UUTISET_HEIGHT_MIN_PX, Math.round(value))
663        );
664    }
665
666    /**
667     * The uutiset iframe has an explicit px height. Release clipping on the
668     * immediate parent so sibling widgets do not collapse that height.
669     */
670    function releaseUutisetParentOverflow(container) {
671        if (!container) return;
672        var parent = container.parentElement;
673        if (!parent || parent === document.body || parent === document.documentElement) return;
674        parent.style.overflow = 'visible';
675        parent.style.maxHeight = 'none';
676    }
677
678    function applyUutisetFrameChrome(iframe) {
679        if (!iframe) return;
680        iframe.setAttribute('data-flowbot-uutiset', 'true');
681        iframe.setAttribute('scrolling', 'no');
682        iframe.style.border = 'none';
683        iframe.style.borderRadius = '0';
684        iframe.style.boxShadow = 'none';
685        iframe.style.background = 'transparent';
686        iframe.style.display = 'block';
687        iframe.style.overflow = 'hidden';
688        setImportantStyle(iframe, 'width', '100%');
689        setImportantStyle(iframe, 'min-height', '0');
690        applySuperSearchIframeTransparency(iframe);
691    }
692
693    function applyUutisetHostSize(iframe, container) {
694        if (!iframe) return;
695        applyUutisetFrameChrome(iframe);
696        setImportantStyle(iframe, 'height', UUTISET_INITIAL_HEIGHT_PX + 'px');
697        setImportantStyle(iframe, 'min-height', UUTISET_INITIAL_HEIGHT_PX + 'px');
698
699        if (!container || !container.hasAttribute('data-flowbot-supersearch')) return;
700        container.setAttribute('data-flowbot-uutiset', 'true');
701        container.dataset.flowbotHostHeight = String(UUTISET_INITIAL_HEIGHT_PX);
702        container.style.position = 'relative';
703        container.style.top = '';
704        container.style.left = '';
705        container.style.right = '';
706        container.style.bottom = '';
707        container.style.display = 'block';
708        container.style.maxWidth = '';
709        container.style.marginLeft = '';
710        container.style.marginRight = '';
711        container.style.overflow = 'visible';
712        container.style.background = 'transparent';
713        container.style.border = 'none';
714        container.style.borderRadius = '0';
715        container.style.boxShadow = 'none';
716        setImportantStyle(container, 'width', '100%');
717        setImportantStyle(container, 'height', UUTISET_INITIAL_HEIGHT_PX + 'px');
718        setImportantStyle(container, 'min-height', UUTISET_INITIAL_HEIGHT_PX + 'px');
719        releaseUutisetParentOverflow(container);
720    }
721
722    function applyUutisetResize(iframe, height) {
723        if (!iframe) return;
724        var heightPx = clampUutisetHeightPx(height);
725        if (heightPx == null) return;
726        var heightCss = heightPx + 'px';
727        var container = iframe.parentElement;
728
729        applyUutisetFrameChrome(iframe);
730        setImportantStyle(iframe, 'height', heightCss);
731        setImportantStyle(iframe, 'min-height', heightCss);
732
733        if (!container || !container.hasAttribute('data-flowbot-supersearch')) return;
734        container.setAttribute('data-flowbot-uutiset', 'true');
735        container.dataset.flowbotHostHeight = String(heightPx);
736        container.style.maxWidth = '';
737        setImportantStyle(container, 'width', '100%');
738        setImportantStyle(container, 'height', heightCss);
739        setImportantStyle(container, 'min-height', heightCss);
740        releaseUutisetParentOverflow(container);
741    }
742
743    function isUutisetSupersearchFrame(iframe) {
744        if (!iframe) return false;
745        if (iframe.hasAttribute('data-flowbot-uutiset')) return true;
746        var parent = iframe.parentElement;
747        return !!(parent && parent.hasAttribute('data-flowbot-uutiset'));
748    }
749
750    var DIRECT_EMBED_ANCHORS = [
751        'top-left', 'top-center', 'top-right',
752        'bottom-left', 'bottom-center', 'bottom-right'
753    ];
754    var DIRECT_EMBED_OFFSET_MIN = -500;
755    var DIRECT_EMBED_OFFSET_MAX = 500;
756    // Floor rejects border-only first measures (~2–4px) from a starved iframe.
757    var DIRECT_EMBED_WIDTH_MIN = 48;
758    var DIRECT_EMBED_HEIGHT_MIN = 40;
759    var DIRECT_EMBED_DIM_MAX = 2000;
760    var DIRECT_EMBED_ZINDEX_MIN = 1;
761    var DIRECT_EMBED_ZINDEX_MAX = 2147483646;
762
763    var trustedFlowbotOrigin = '';
764    try {
765        trustedFlowbotOrigin = new URL(CONFIG.baseUrl).origin;
766    } catch (e) {}
767
768    function isTrustedFlowbotMessageOrigin(event) {
769        if (!event || !event.origin || !trustedFlowbotOrigin) return false;
770        if (event.origin === trustedFlowbotOrigin) return true;
771        // Allow firebase hosting alias pair for same site name
772        try {
773            var a = new URL(trustedFlowbotOrigin);
774            var b = new URL(event.origin);
775            if (a.protocol !== b.protocol) return false;
776            var aHost = a.hostname;
777            var bHost = b.hostname;
778            function projectId(host) {
779                if (host.endsWith('.firebaseapp.com')) return host.slice(0, -'.firebaseapp.com'.length);
780                if (host.endsWith('.web.app')) return host.slice(0, -'.web.app'.length);
781                return null;
782            }
783            var pa = projectId(aHost);
784            var pb = projectId(bHost);
785            return !!(pa && pb && pa === pb);
786        } catch (e) { return false; }
787    }
788
789    function clampDirectEmbedDimension(value, min) {
790        if (typeof value !== 'number' || !isFinite(value)) return null;
791        var floor = typeof min === 'number' && isFinite(min) ? min : DIRECT_EMBED_WIDTH_MIN;
792        return Math.min(DIRECT_EMBED_DIM_MAX, Math.max(floor, Math.round(value)));
793    }
794
795    function clampDirectEmbedOffset(value, fallback) {
796        var n = typeof value === 'number' && isFinite(value) ? value : fallback;
797        return Math.min(DIRECT_EMBED_OFFSET_MAX, Math.max(DIRECT_EMBED_OFFSET_MIN, n));
798    }
799
800    function normalizeDirectEmbedAnchor(anchor) {
801        return DIRECT_EMBED_ANCHORS.indexOf(anchor) > -1 ? anchor : 'top-center';
802    }
803
804    function getNormalizedEmbedLayout(container) {
805        if (!container) return '';
806        var raw = container.getAttribute('data-flowbot-embed-layout');
807        if (!raw) return '';
808        return raw.trim().toLowerCase();
809    }
810
811    function setImportantStyle(el, prop, value) {
812        if (!el) return;
813        el.style.setProperty(prop, value, 'important');
814    }
815
816    // Keep in sync with computeEffectiveDirectEmbedBarWidth (superSearchService.ts):
817    // never wider than viewport − gutter; may go below 240 when viewport requires it.
818    function getDirectEmbedInitialWidth(layout) {
819        var vw = getViewport().width || 0;
820        var barWidth = 400;
821        var maxWidthVw = 100;
822        var gutter = 32;
823        if (layout && typeof layout === 'object') {
824            if (typeof layout.barWidth === 'number' && isFinite(layout.barWidth)) {
825                barWidth = Math.min(640, Math.max(240, Math.round(layout.barWidth)));
826            }
827            if (typeof layout.maxWidthVw === 'number' && isFinite(layout.maxWidthVw)) {
828                maxWidthVw = Math.min(100, Math.max(40, Math.round(layout.maxWidthVw)));
829            }
830        }
831        if (vw <= 0) return barWidth;
832        var vwCap = Math.floor(vw * maxWidthVw / 100);
833        var available = Math.max(1, vw - gutter);
834        return Math.min(barWidth, vwCap, available);
835    }
836
837    function hideDirectEmbedIframePending(iframe) {
838        if (!iframe) return;
839        iframe.style.visibility = 'hidden';
840        iframe.style.opacity = '0';
841        iframe.style.pointerEvents = 'none';
842        iframe.setAttribute('data-flowbot-direct-embed-pending', 'true');
843    }
844
845    function revealDirectEmbedIframe(iframe) {
846        if (!iframe || iframe.dataset.flowbotDirectEmbedHidden === '1') return;
847        iframe.style.display = 'block';
848        iframe.style.visibility = 'visible';
849        iframe.style.opacity = '1';
850        iframe.style.pointerEvents = 'auto';
851        iframe.removeAttribute('data-flowbot-direct-embed-pending');
852    }
853
854    function markDirectEmbedIframeHidden(iframe) {
855        if (!iframe) return;
856        iframe.dataset.flowbotDirectEmbedHidden = '1';
857        iframe.removeAttribute('data-flowbot-direct-embed-pending');
858        iframe.style.display = 'none';
859        iframe.style.visibility = 'hidden';
860        iframe.style.opacity = '0';
861        iframe.style.pointerEvents = 'none';
862    }
863
864    function findDirectEmbedContainer(iframe) {
865        if (!iframe) return null;
866        var mountId = iframe.dataset.flowbotMountId;
867        if (mountId) {
868            var linked = document.querySelector(
869                '[data-flowbot-supersearch][data-flowbot-mount-id="' + mountId + '"]'
870            );
871            if (linked) return linked;
872        }
873        var parent = iframe.parentElement;
874        if (parent && parent.hasAttribute('data-flowbot-supersearch')) {
875            return parent;
876        }
877        return null;
878    }
879
880    function ensureDirectEmbedIframeOnBody(iframe, container) {
881        if (!iframe || !container) return;
882        if (!container.dataset.flowbotMountId) {
883            var botId = container.getAttribute('data-flowbot-supersearch') || 'unknown';
884            var instanceId = container.getAttribute('data-flowbot-supersearch-id') || '';
885            container.dataset.flowbotMountId = 'flowbot-de-' + botId + (instanceId ? '-' + instanceId : '');
886        }
887        iframe.dataset.flowbotMountId = container.dataset.flowbotMountId;
888        if (iframe.parentElement !== document.body) {
889            document.body.appendChild(iframe);
890        }
891    }
892
893    function applySuperSearchIframeTransparency(iframe) {
894        if (!iframe) return;
895        iframe.style.background = 'transparent';
896        iframe.style.backgroundColor = 'transparent';
897        iframe.style.colorScheme = 'normal';
898        iframe.setAttribute('allowTransparency', 'true');
899    }
900
901    function applyDirectEmbedIframePosition(iframe) {
902        if (!iframe) return;
903        var resolved = normalizeDirectEmbedAnchor(
904            iframe.dataset.flowbotDirectEmbedAnchor || 'top-center'
905        );
906        var offsetX = clampDirectEmbedOffset(
907            parseFloat(iframe.dataset.flowbotDirectEmbedOffsetX),
908            0
909        );
910        var offsetY = clampDirectEmbedOffset(
911            parseFloat(iframe.dataset.flowbotDirectEmbedOffsetY),
912            16
913        );
914        var zIndex = iframe.dataset.flowbotDirectEmbedZIndex || '9998';
915        var vv = getViewport();
916        var keyboardOpen = isSoftKeyboardOpen();
917        var iframeWidth =
918            parseFloat(iframe.dataset.flowbotDirectEmbedWidth) ||
919            parseFloat(iframe.style.width) ||
920            0;
921
922        iframe.style.position = 'fixed';
923        setImportantStyle(iframe, 'z-index', String(zIndex));
924        iframe.style.top = '';
925        iframe.style.bottom = '';
926        iframe.style.left = '';
927        iframe.style.right = '';
928        iframe.style.transform = 'none';
929
930        if (keyboardOpen && resolved.indexOf('bottom') !== 0) {
931            // Pin into the visual viewport so Chrome does not paint a layout-viewport ghost.
932            iframe.style.top = (vv.top + offsetY) + 'px';
933            iframe.style.bottom = 'auto';
934        } else if (keyboardOpen && resolved.indexOf('bottom') === 0) {
935            var layoutH = window.innerHeight || document.documentElement.clientHeight || vv.height;
936            var gapBelow = Math.max(0, layoutH - vv.top - vv.height);
937            iframe.style.bottom = (gapBelow + offsetY) + 'px';
938            iframe.style.top = 'auto';
939        } else if (resolved.indexOf('bottom') === 0) {
940            iframe.style.bottom = offsetY + 'px';
941            iframe.style.top = 'auto';
942        } else {
943            iframe.style.top = offsetY + 'px';
944            iframe.style.bottom = 'auto';
945        }
946
947        if (resolved.indexOf('-left') > -1) {
948            iframe.style.left = (keyboardOpen ? vv.left + offsetX : offsetX) + 'px';
949            iframe.style.right = 'auto';
950        } else if (resolved.indexOf('-right') > -1) {
951            iframe.style.right = offsetX + 'px';
952            iframe.style.left = 'auto';
953        } else if (iframeWidth > 0) {
954            // Avoid transform: translateX(-50%) — Chrome can leave a compositor ghost layer.
955            if (keyboardOpen) {
956                iframe.style.left = (vv.left + vv.width / 2 - iframeWidth / 2 + offsetX) + 'px';
957            } else {
958                iframe.style.left = 'calc(50% + ' + offsetX + 'px - ' + (iframeWidth / 2) + 'px)';
959            }
960            iframe.style.right = 'auto';
961        } else {
962            iframe.style.left = 'calc(50% + ' + offsetX + 'px)';
963            iframe.style.right = 'auto';
964            iframe.style.transform = 'translateX(-50%)';
965        }
966    }
967
968    function parseDirectEmbedLayout(layout) {
969        var resolved = layout && typeof layout === 'object' ? layout : {};
970        return {
971            anchor: normalizeDirectEmbedAnchor(
972                typeof resolved.anchor === 'string' ? resolved.anchor : 'top-center'
973            ),
974            offsetX: clampDirectEmbedOffset(resolved.offsetX, 0),
975            offsetY: clampDirectEmbedOffset(resolved.offsetY, 16),
976            zIndex: typeof resolved.zIndex === 'number' && isFinite(resolved.zIndex)
977                ? Math.min(DIRECT_EMBED_ZINDEX_MAX, Math.max(DIRECT_EMBED_ZINDEX_MIN, Math.round(resolved.zIndex)))
978                : 9998
979        };
980    }
981
982    function applyDirectEmbedLayoutToIframe(iframe, layout) {
983        if (!iframe) return;
984        var parsed = parseDirectEmbedLayout(layout);
985        iframe.setAttribute('data-flowbot-direct-embed', 'true');
986        iframe.dataset.flowbotDirectEmbedAnchor = parsed.anchor;
987        iframe.dataset.flowbotDirectEmbedOffsetX = String(parsed.offsetX);
988        iframe.dataset.flowbotDirectEmbedOffsetY = String(parsed.offsetY);
989        iframe.dataset.flowbotDirectEmbedZIndex = String(parsed.zIndex);
990        applyDirectEmbedIframePosition(iframe);
991    }
992
993    function applyDirectEmbedContainerMountStyles(container, iframe) {
994        if (!container || !iframe) return;
995        container.setAttribute('data-flowbot-direct-embed', 'true');
996        ensureDirectEmbedIframeOnBody(iframe, container);
997
998        // Zero-size mount marker — anchor/position live on the iframe (fixed to viewport)
999        setImportantStyle(container, 'position', 'fixed');
1000        setImportantStyle(container, 'width', '0');
1001        setImportantStyle(container, 'height', '0');
1002        setImportantStyle(container, 'display', 'block');
1003        setImportantStyle(container, 'padding', '0');
1004        container.style.overflow = 'visible';
1005        container.style.background = 'transparent';
1006        container.style.border = 'none';
1007        container.style.borderRadius = '0';
1008        container.style.boxShadow = 'none';
1009        setImportantStyle(container, 'margin', '0');
1010        container.style.pointerEvents = 'none';
1011        setImportantStyle(container, 'min-width', '0');
1012        setImportantStyle(container, 'min-height', '0');
1013        container.style.maxWidth = '';
1014        container.style.maxHeight = '';
1015        setImportantStyle(container, 'top', 'auto');
1016        setImportantStyle(container, 'bottom', 'auto');
1017        setImportantStyle(container, 'left', 'auto');
1018        setImportantStyle(container, 'right', 'auto');
1019        container.style.transform = '';
1020        container.style.zIndex = '';
1021
1022        iframe.style.border = 'none';
1023        iframe.style.borderRadius = '0';
1024        iframe.style.boxShadow = 'none';
1025        iframe.style.overflow = 'hidden';
1026        iframe.setAttribute('scrolling', 'no');
1027        iframe.style.display = 'block';
1028        iframe.style.minWidth = '0';
1029        iframe.style.minHeight = '0';
1030        applySuperSearchIframeTransparency(iframe);
1031    }
1032
1033    // Taller chrome bar (~title + input row) needs >48px to avoid first-paint clip.
1034    var DIRECT_EMBED_INITIAL_HEIGHT_PX = 64;
1035    // Collapsed pill (expandOnActivate) — keep init reveal close to real pill size.
1036    var DIRECT_EMBED_COLLAPSED_INITIAL_HEIGHT_PX = 48;
1037
1038    function estimateCollapsedPillWidth(layout) {
1039        var label = 'Haku';
1040        if (layout && typeof layout.collapsedLabel === 'string' && layout.collapsedLabel.trim()) {
1041            label = layout.collapsedLabel.trim();
1042        }
1043        // Rough text + icon + horizontal padding; clamp so init isn't a full bar.
1044        var approx = Math.ceil(label.length * 8 + 56);
1045        return Math.min(200, Math.max(72, approx));
1046    }
1047
1048    function getDirectEmbedBootDimensions(layout) {
1049        var collapsed = !!(layout && layout.expandOnActivate);
1050        if (collapsed) {
1051            return {
1052                width: estimateCollapsedPillWidth(layout),
1053                height: DIRECT_EMBED_COLLAPSED_INITIAL_HEIGHT_PX
1054            };
1055        }
1056        return {
1057            width: getDirectEmbedInitialWidth(layout),
1058            height: DIRECT_EMBED_INITIAL_HEIGHT_PX
1059        };
1060    }
1061
1062    function ensureDirectEmbedInitialDimensions(iframe, layout) {
1063        if (!iframe || iframe.dataset.flowbotDirectEmbedHeight) return;
1064        var boot = getDirectEmbedBootDimensions(layout);
1065        iframe.style.width = boot.width + 'px';
1066        iframe.style.height = boot.height + 'px';
1067        iframe.dataset.flowbotDirectEmbedWidth = String(boot.width);
1068        iframe.dataset.flowbotDirectEmbedHeight = String(boot.height);
1069    }
1070
1071    function handleDirectEmbedSupersearchResize(iframe, width, height) {
1072        if (!iframe) return false;
1073        var isDirectEmbed =
1074            iframe.hasAttribute('data-flowbot-direct-embed') ||
1075            iframe.getAttribute('data-flowbot-direct-embed-pending') === 'true';
1076        if (!isDirectEmbed) return false;
1077        // Ignore content-measure resizes until init reveals with boot dimensions.
1078        // Early posts can be ~border-width (2–4px) while the iframe is still pending.
1079        if (iframe.getAttribute('data-flowbot-direct-embed-pending') === 'true') {
1080            return true;
1081        }
1082        if (!iframe.hasAttribute('data-flowbot-direct-embed')) {
1083            iframe.setAttribute('data-flowbot-direct-embed', 'true');
1084        }
1085        applyDirectEmbedIframeDimensions(iframe, width, height);
1086        return true;
1087    }
1088
1089    function handleDirectEmbedSupersearchInit(iframe, layout) {
1090        if (!iframe) return;
1091        var directContainer = findDirectEmbedContainer(iframe);
1092        if (directContainer) {
1093            applyDirectEmbedContainerMountStyles(directContainer, iframe);
1094        }
1095        if (layout) {
1096            applyDirectEmbedLayoutToIframe(iframe, layout);
1097        }
1098        // Always re-seed boot size on init while revealing so a stale full-bar
1099        // width cannot flash for expandOnActivate / collapsed pills.
1100        var boot = getDirectEmbedBootDimensions(layout);
1101        iframe.dataset.flowbotDirectEmbedWidth = String(boot.width);
1102        iframe.dataset.flowbotDirectEmbedHeight = String(boot.height);
1103        applyDirectEmbedIframeDimensions(iframe, boot.width, boot.height);
1104    }
1105
1106    function bootstrapDirectEmbedIframe(container, iframe) {
1107        if (!iframe) return;
1108        if (container) {
1109            applyDirectEmbedContainerMountStyles(container, iframe);
1110        }
1111        hideDirectEmbedIframePending(iframe);
1112        iframe.style.position = 'fixed';
1113        iframe.style.top = '0';
1114        iframe.style.left = '0';
1115        applySuperSearchIframeTransparency(iframe);
1116        // Seed a pill-sized box (still visibility:hidden) so first in-iframe layout
1117        // is not max-w-full-starved to ~1–4px before supersearch-init.
1118        var boot = getDirectEmbedBootDimensions({ expandOnActivate: true });
1119        iframe.style.width = boot.width + 'px';
1120        iframe.style.height = boot.height + 'px';
1121        iframe.dataset.flowbotDirectEmbedWidth = String(boot.width);
1122        iframe.dataset.flowbotDirectEmbedHeight = String(boot.height);
1123        setImportantStyle(iframe, 'z-index', '9998');
1124    }
1125
1126    function applyDirectEmbedContainerStyles(container, iframe, layout) {
1127        if (!iframe) return;
1128        if (container) {
1129            applyDirectEmbedContainerMountStyles(container, iframe);
1130        }
1131        if (layout) {
1132            applyDirectEmbedLayoutToIframe(iframe, layout);
1133        }
1134        ensureDirectEmbedInitialDimensions(iframe, layout);
1135    }
1136
1137    // Host snaps W/H immediately — expand/collapse morph runs inside the iframe.
1138    function applyDirectEmbedIframeDimensions(foundIframe, width, height) {
1139        if (!foundIframe || foundIframe.dataset.flowbotDirectEmbedHidden === '1') return;
1140        var w = clampDirectEmbedDimension(width, DIRECT_EMBED_WIDTH_MIN);
1141        var h = clampDirectEmbedDimension(height, DIRECT_EMBED_HEIGHT_MIN);
1142        if (w == null && h == null) return;
1143
1144        // Always snap (no size transition). Pending-hide first paint stays instant.
1145        foundIframe.style.transition = '';
1146
1147        if (w != null) {
1148            foundIframe.style.width = w + 'px';
1149            foundIframe.dataset.flowbotDirectEmbedWidth = String(w);
1150        }
1151        if (h != null) {
1152            foundIframe.style.height = h + 'px';
1153            foundIframe.dataset.flowbotDirectEmbedHeight = String(h);
1154        }
1155
1156        foundIframe.style.display = 'block';
1157        foundIframe.style.minWidth = '0';
1158        foundIframe.style.minHeight = '0';
1159        foundIframe.style.overflow = 'hidden';
1160        applySuperSearchIframeTransparency(foundIframe);
1161
1162        applyDirectEmbedIframePosition(foundIframe);
1163        revealDirectEmbedIframe(foundIframe);
1164    }
1165
1166    function findIframeByEventSource(eventSource) {
1167        if (!eventSource) return null;
1168        try {
1169            var iframes = document.querySelectorAll('iframe');
1170            for (var i = 0; i < iframes.length; i++) {
1171                if (iframes[i].contentWindow === eventSource) {
1172                    return iframes[i];
1173                }
1174            }
1175        } catch (e) {}
1176        return null;
1177    }
1178
1179    function isTrustedSupersearchMessage(event) {
1180        return isTrustedFlowbotMessageOrigin(event) || !!findIframeByEventSource(event.source);
1181    }
1182
1183    function isSafeSupersearchNavigateUrl(url) {
1184        if (typeof url !== 'string') return false;
1185        var trimmed = url.trim();
1186        if (!trimmed || trimmed === '#' || trimmed.length > 4096) return false;
1187        try {
1188            var parsed = new URL(trimmed, window.location.href);
1189            return parsed.protocol === 'http:' || parsed.protocol === 'https:';
1190        } catch (e) {
1191            return false;
1192        }
1193    }
1194
1195    /** Open a SuperSearch hit on the host page (iframe target=_top is sandboxed). */
1196    function handleSupersearchNavigate(event) {
1197        if (!event || !event.data || event.data.type !== 'supersearch-navigate') return false;
1198        if (!isTrustedSupersearchMessage(event)) return false;
1199        if (!isSafeSupersearchNavigateUrl(event.data.url)) return false;
1200        try {
1201            var parsed = new URL(String(event.data.url).trim(), window.location.href);
1202            window.location.assign(parsed.href);
1203        } catch (e) {
1204            return false;
1205        }
1206        return true;
1207    }
1208
1209    function truncInt(n) {
1210        return n < 0 ? Math.ceil(n) : Math.floor(n);
1211    }
1212
1213    function replyFlowbotAddToCartResult(event, tid, success) {
1214        if (!event || !event.source || typeof event.source.postMessage !== 'function') return;
1215        try {
1216            event.source.postMessage({
1217                type: 'flowbot-add-to-cart-result',
1218                tid: tid,
1219                success: !!success
1220            }, event.origin && event.origin !== 'null' ? event.origin : flowbotPostTargetOrigin());
1221        } catch (e) {}
1222    }
1223
1224    /** Explicit false / { success: false } is failure; a resolved/non-throwing add is success. */
1225    function isCartAddSuccess(res) {
1226        if (res === false) return false;
1227        if (res && typeof res === 'object' && res.success === false) return false;
1228        return true;
1229    }
1230
1231    function finishHostCartAdd(site, res, event, tid) {
1232        var ok = isCartAddSuccess(res);
1233        if (ok && site && site.cart && typeof site.cart.update === 'function') {
1234            try { site.cart.update(); } catch (e) {}
1235        }
1236        replyFlowbotAddToCartResult(event, tid, ok);
1237    }
1238
1239    function handleFlowbotAddToCart(event) {
1240        var tidRaw = event.data.tid;
1241        var tid = typeof tidRaw === 'number' ? tidRaw : Number(tidRaw);
1242        if (!isFinite(tid)) {
1243            replyFlowbotAddToCartResult(event, tidRaw, false);
1244            return;
1245        }
1246        tid = truncInt(tid);
1247        if (tid < 1) {
1248            replyFlowbotAddToCartResult(event, tid, false);
1249            return;
1250        }
1251
1252        var amount = 1;
1253
1254        var site = window.site;
1255        if (!site || !site.cart || typeof site.cart.add !== 'function') {
1256            replyFlowbotAddToCartResult(event, tid, false);
1257            return;
1258        }
1259
1260        try {
1261            // Dataflow cart.add(tid, amount) — scalars, not {tid}/{amount} objects.
1262            var pending = site.cart.add(tid, amount);
1263            if (!pending || typeof pending.then !== 'function') {
1264                finishHostCartAdd(site, pending, event, tid);
1265                return;
1266            }
1267            pending.then(function (res) {
1268                finishHostCartAdd(site, res, event, tid);
1269            }, function () {
1270                replyFlowbotAddToCartResult(event, tid, false);
1271            });
1272        } catch (e) {
1273            replyFlowbotAddToCartResult(event, tid, false);
1274        }
1275    }
1276
1277    function attachFlowbotMessageListenerOnce() {
1278        if (window.__flowbotResizeListenerAttached) return;
1279        window.__flowbotResizeListenerAttached = true;
1280
1281        window.addEventListener('message', function(event) {
1282            if (!event.data || typeof event.data.type !== 'string') return;
1283            if (handleSupersearchNavigate(event)) return;
1284
1285            if (event.data.type === 'flowbot-add-to-cart') {
1286                if (!isTrustedFlowbotMessageOrigin(event)) return;
1287                handleFlowbotAddToCart(event);
1288                return;
1289            }
1290
1291            if (event.data.type === 'flowbot-hide') {
1292                if (!isTrustedFlowbotMessageOrigin(event)) return;
1293                var foundIframe = findIframeByEventSource(event.source);
1294                if (foundIframe) {
1295                    if (
1296                        foundIframe.hasAttribute('data-flowbot-direct-embed') ||
1297                        foundIframe.getAttribute('data-flowbot-direct-embed-
1297pending') === 'true'
1298                    ) {
1299                        markDirectEmbedIframeHidden(foundIframe);
1300                    } else {
1301                        foundIframe.style.display = 'none';
1302                    }
1303                    var parentContainer = foundIframe.parentElement;
1304                    if (parentContainer && (
1305                        parentContainer.hasAttribute('data-flowbot-supersearch') ||
1306                        parentContainer.hasAttribute('data-flowbot-id')
1307                    )) {
1308                        parentContainer.style.display = 'none';
1309                    }
1310                } else {
1311                    var targetBotId = event.data.botId;
1312                    var targetIframe = targetBotId
1313                        ? window.__flowbotIframes[targetBotId]
1314                        : Object.values(window.__flowbotIframes)[0];
1315                    if (targetIframe) {
1316                        if (
1317                            targetIframe.hasAttribute('data-flowbot-direct-embed') ||
1318                            targetIframe.getAttribute('data-flowbot-direct-embed-pending') === 'true'
1319                        ) {
1320                            markDirectEmbedIframeHidden(targetIframe);
1321                        } else {
1322                            targetIframe.style.display = 'none';
1323                        }
1324                    }
1325                }
1326                return;
1327            }
1328            if (event.data.type === 'flowbot-resize' && event.data.dimensions) {
1329                if (!isTrustedFlowbotMessageOrigin(event)) return;
1330                var dimensions = event.data.dimensions;
1331                var fullViewport = event.data.fullViewport;
1332                var resizeBotId = event.data.botId;
1333                var resizeIframe = resizeBotId
1334                    ? window.__flowbotIframes[resizeBotId]
1335                    : Object.values(window.__flowbotIframes)[0];
1336                if (!resizeIframe) return;
1337                syncHostScrollLockForChat(
1338                    resizeIframe,
1339                    fullViewport,
1340                    dimensions && dimensions.width,
1341                    dimensions && dimensions.height
1342                );
1343                if (fullViewport) {
1344                    applyFullViewportIframe(resizeIframe, { animate: true });
1345                    return;
1346                }
1347                if (dimensions.width && dimensions.height) {
1348                    applySizedIframeFromFullViewport(
1349                        resizeIframe,
1350                        dimensions.width,
1351                        dimensions.height,
1352                        true,
1353                        event.data.fabShape
1354                    );
1355                } else {
1356                    clearFullViewportIframe(resizeIframe);
1357                }
1358                return;
1359            }
1360            if (event.data.type === 'supersearch-init') {
1361                if (!isTrustedSupersearchMessage(event)) return;
1362                var displayType = event.data.displayType;
1363                var initIframe = findIframeByEventSource(event.source);
1364                if (initIframe && displayType === '360haku') {
1365                    initIframe.setAttribute('data-flowbot-360haku', 'true');
1366                    initIframe.style.border = 'none';
1367                    initIframe.style.borderRadius = '0';
1368                    initIframe.style.boxShadow = 'none';
1369                    initIframe.style.minHeight = '0';
1370                    initIframe.style.minWidth = '0';
1371                    initIframe.style.overflow = 'visible';
1372                    applySuperSearchIframeTransparency(initIframe);
1373
1374                    var initContainer = initIframe.parentElement;
1375                    if (initContainer && initContainer.hasAttribute('data-flowbot-supersearch')) {
1376                        initContainer.setAttribute('data-flowbot-360haku', 'true');
1377                        initContainer.style.border = 'none';
1378                        initContainer.style.borderRadius = '0';
1379                        initContainer.style.boxShadow = 'none';
1380                        initContainer.style.background = 'transparent';
1381                        initContainer.style.minHeight = '0';
1382                        initContainer.style.minWidth = '0';
1383                        initContainer.style.overflow = 'visible';
1384                        initContainer.style.position = 'relative';
1385                        initContainer.style.margin = '0 auto';
1386                        initContainer.style.width = 'auto';
1387                        initContainer.style.height = 'auto';
1388                        initContainer.style.top = '';
1389                        initContainer.style.left = '';
1390                        initContainer.style.right = '';
1391                        initContainer.style.bottom = '';
1392                        initContainer.style.transform = 'none';
1393
1394                        var hostParent = initContainer.parentElement;
1395                        if (hostParent) {
1396                            hostParent.style.overflow = 'visible';
1397                            if (!hostParent.style.position || hostParent.style.position === 'static') {
1398                                hostParent.style.position = 'relative';
1399                            }
1400                        }
1401                    }
1402                } else if (initIframe && displayType === 'direct_embed') {
1403                    handleDirectEmbedSupersearchInit(initIframe, event.data.layout);
1404                } else if (initIframe && displayType === 'ai_showcase') {
1405                    // Must live here: this listener is attached first and blocks the chat-path duplicate.
1406                    applyAiShowcaseHostSize(initIframe, initIframe.parentElement);
1407                } else if (initIframe && displayType === 'uutiset') {
1408                    applyUutisetHostSize(initIframe, initIframe.parentElement);
1409                }
1410                return;
1411            }
1412            if (event.data.type === 'supersearch-resize') {
1413                if (!isTrustedSupersearchMessage(event)) return;
1414                var resizeWidth = event.data.width;
1415                var resizeHeight = event.data.height;
1416                var supersearchIframe = findIframeByEventSource(event.source);
1417                if (!supersearchIframe) return;
1418
1419                var supersearchContainer = findDirectEmbedContainer(supersearchIframe);
1420                if (supersearchContainer && supersearchContainer.hasAttribute('data-flowbot-360haku')) {
1421                    applySuperSearch360Dimensions(supersearchIframe, resizeWidth, resizeHeight);
1422                    return;
1423                }
1424                if (handleDirectEmbedSupersearchResize(supersearchIframe, resizeWidth, resizeHeight)) {
1425                    return;
1426                }
1427                if (
1428                    supersearchIframe.hasAttribute('data-flowbot-ai-showcase') ||
1429                    (supersearchIframe.parentElement &&
1430                        supersearchIframe.parentElement.hasAttribute('data-flowbot-ai-showcase'))
1431                ) {
1432                    applyAiShowcaseResize(supersearchIframe, resizeHeight);
1433                    return;
1434                }
1435                if (
1436                    event.data.displayType === 'uutiset' ||
1437                    isUutisetSupersearchFrame(supersearchIframe)
1438                ) {
1439                    applyUutisetResize(supersearchIframe, resizeHeight);
1440                    return;
1441                }
1442
1443                supersearchContainer = supersearchIframe.parentElement;
1444
1445                var genericW = clampDirectEmbedDimension(resizeWidth);
1446                var genericH = clampDirectEmbedDimension(resizeHeight);
1447                if (genericW != null) supersearchIframe.style.width = genericW + 'px';
1448                if (genericH != null) supersearchIframe.style.height = genericH + 'px';
1449
1450                if (supersearchContainer && supersearchContainer.hasAttribute('data-flowbot-supersearch')) {
1451                    if (genericW != null) supersearchContainer.style.width = genericW + 'px';
1452                    if (genericH != null) supersearchContainer.style.height = genericH + 'px';
1453                }
1454                return;
1455            }
1456        });
1457    }
1458
1459    function initialize() {
1460        injectCriticalStyles();
1461        attachFlowbotMessageListenerOnce();
1462        
1463        function createIframes() {
1464            const widgetContainers = document.querySelectorAll('[data-flowbot-id]:not([data-flowbot-initialized])');
1465
1466            widgetContainers.forEach((container) => {
1467                const botId = container.getAttribute('data-flowbot-id');
1468                
1469                if (!botId) {
1470                    console.error('FlowBot: Element has data-flowbot-id attribute but no value');
1471                    return;
1472                }
1473
1474                if (container.hasAttribute('data-flowbot-initialized')) {
1475                    return;
1476                }
1477
1478                const existingIframe = window.__flowbotIframes[botId];
1479                if (existingIframe && existingIframe.isConnected) {
1480                    container.setAttribute('data-flowbot-initialized', 'true');
1481                    return;
1482                }
1483
1484                try {
1485                    const iframe = document.createElement('iframe');
1486                    const { width: vw } = getViewport();
1487                    // Pass the HOST viewport width to the widget. Inside the iframe the
1488                    // widget can only measure its own (always narrow) width, so without
1489                    // this it would always think it is "mobile" and ignore size presets.
1490                    var iframeSrc = CONFIG.baseUrl + '?mode=public&botId=' + encodeURIComponent(botId) +
1491                        '&parentWidth=' + (vw || window.innerWidth || 0);
1492                    iframe.src = appendParentContextQuery(iframeSrc);
1493
1494                    iframe.style.position = 'fixed';
1495                    iframe.style.bottom = '20px';
1496                    iframe.style.right = '20px';
1497                    iframe.style.left = 'auto';
1498                    iframe.style.top = 'auto';
1499                    iframe.style.width = '64px';
1500                    iframe.style.height = '64px';
1501                    iframe.style.borderRadius = '50%';
1502                    iframe.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
1503                    iframe.style.border = 'none';
1504                    iframe.style.zIndex = '9999';
1505                    iframe.style.backgroundColor = 'transparent';
1506                    // No default box transition: desktop launcher↔panel snaps.
1507                    // Mobile full-viewport morph opts in via setIframeBoxTransition.
1508                    iframe.style.transition = '';
1509                    iframe.setAttribute('allow', 'microphone; storage-access');
1510                    applyFlowbotIframeIsolation(iframe);
1511                    iframe.setAttribute('title', 'FlowBot Chat');
1512                    iframe.setAttribute('data-flowbot-bot-id', botId);
1513                    iframe.setAttribute('data-flowbot-chat', 'true');
1514                    
1515                    if (!window.__flowbotResizeListenerAttached) {
1516                        window.__flowbotResizeListenerAttached = true;
1517                        window.addEventListener('message', function(event) {
1518                            if (!event.data || typeof event.data.type !== 'string') return;
1519                            if (handleSupersearchNavigate(event)) return;
1520
1521                            if (event.data.type === 'flowbot-hide') {
1522                                if (!isTrustedFlowbotMessageOrigin(event)) return;
1523                                let foundIframe = null;
1524                                if (event.source) {
1525                                    try {
1526                                        const iframes = document.querySelectorAll('iframe');
1527                                        for (let i = 0; i < iframes.length; i++) {
1528                                            if (iframes[i].contentWindow === event.source) {
1529                                                foundIframe = iframes[i];
1530                                                break;
1531                                            }
1532                                        }
1533                                    } catch (e) {}
1534                                }
1535                                if (foundIframe) {
1536                                    if (
1537                                        foundIframe.hasAttribute('data-flowbot-direct-embed') ||
1538                                        foundIframe.getAttribute('data-flowbot-direct-embed-
1538pending') === 'true'
1539                                    ) {
1540                                        markDirectEmbedIframeHidden(foundIframe);
1541                                    } else {
1542                                        foundIframe.style.display = 'none';
1543                                    }
1544                                    const parentContainer = foundIframe.parentElement;
1545                                    if (parentContainer && (
1546                                        parentContainer.hasAttribute('data-flowbot-supersearch') ||
1547                                        parentContainer.hasAttribute('data-flowbot-id')
1548                                    )) {
1549                                        parentContainer.style.display = 'none';
1550                                    }
1551                                } else {
1552                                    const targetBotId = event.data.botId;
1553                                    const targetIframe = targetBotId
1554                                        ? window.__flowbotIframes[targetBotId]
1555                                        : Object.values(window.__flowbotIframes)[0];
1556                                    if (targetIframe) {
1557                                        if (
1558                                            targetIframe.hasAttribute('data-flowbot-direct-embed') ||
1559                                            targetIframe.getAttribute('data-flowbot-direct-embed-pending') === 'true'
1560                                        ) {
1561                                            markDirectEmbedIframeHidden(targetIframe);
1562                                        } else {
1563                                            targetIframe.style.display = 'none';
1564                                        }
1565                                    }
1566                                }
1567                                return;
1568                            }
1569                            if (event.data.type === 'flowbot-resize' && event.data.dimensions) {
1570                                if (!isTrustedFlowbotMessageOrigin(event)) return;
1571                                const { dimensions, fullViewport } = event.data;
1572                                const targetBotId = event.data.botId;
1573                                const targetIframe = targetBotId
1574                                    ? window.__flowbotIframes[targetBotId]
1575                                    : Object.values(window.__flowbotIframes)[0];
1576                                if (!targetIframe) return;
1577
1578                                syncHostScrollLockForChat(
1579                                    targetIframe,
1580                                    fullViewport,
1581                                    dimensions && dimensions.width,
1582                                    dimensions && dimensions.height
1583                                );
1584
1585                                if (fullViewport) {
1586                                    applyFullViewportIframe(targetIframe, { animate: true });
1587                                    return;
1588                                }
1589
1590                                if (dimensions.width && dimensions.height) {
1591                                    applySizedIframeFromFullViewport(
1592                                        targetIframe,
1593                                        dimensions.width,
1594                                        dimensions.height,
1595                                        true,
1596                                        event.data.fabShape
1597                                    );
1598                                } else {
1599                                    clearFullViewportIframe(targetIframe);
1600                                }
1601                            }
1602                            if (event.data.type === 'supersearch-init') {
1603                                if (!isTrustedSupersearchMessage(event)) return;
1604                                const displayType = event.data.displayType;
1605                                const foundIframe = findIframeByEventSource(event.source);
1606                                if (foundIframe) {
1607                                    if (displayType === '360haku') {
1608                                        foundIframe.setAttribute('data-flowbot-360haku', 'true');
1609                                        foundIframe.style.border = 'none';
1610                                        foundIframe.style.borderRadius = '0';
1611                                        foundIframe.style.boxShadow = 'none';
1612                                        foundIframe.style.minHeight = '0';
1613                                        foundIframe.style.minWidth = '0';
1614                                        foundIframe.style.width = '100%';
1615                                        foundIframe.style.height = '100%';
1616                                        foundIframe.style.overflow = 'visible';
1617                                        applySuperSearchIframeTransparency(foundIframe);
1618                                        
1619                                        const container = foundIframe.parentElement;
1620                                        if (container && container.hasAttribute('data-flowbot-supersearch')) {
1621                                            container.setAttribute('data-flowbot-360haku', 'true');
1622                                            container.style.border = 'none';
1623                                            container.style.borderRadius = '0';
1624                                            container.style.boxShadow = 'none';
1625                                            container.style.background = 'transparent';
1626                                            container.style.minHeight = '0';
1627                                            container.style.minWidth = '0';
1628                                            container.style.overflow = 'visible';
1629                                            
1630                                            // Fill host div — size comes from the host page's container
1631                                            container.style.position = 'absolute';
1632                                            container.style.top = '0';
1633                                            container.style.left = '0';
1634                                            container.style.right = '0';
1635                                            container.style.bottom = '0';
1636                                            container.style.width = '100%';
1637                                            container.style.height = '100%';
1638                                            container.style.transform = 'none';
1639                                            
1640                                            const hostParent = container.parentElement;
1641                                            if (hostParent) {
1642                                                hostParent.style.overflow = 'visible';
1643                                                if (!hostParent.style.position || hostParent.style.position === 'static') {
1644                                                    hostParent.style.position = 'relative';
1645                                                }
1646                                            }
1647                                        }
1648                                    } else if (displayType === 'direct_embed') {
1649                                        handleDirectEmbedSupersearchInit(foundIframe, event.data.layout);
1650                                    } else if (displayType === 'ai_showcase') {
1651                                        applyAiShowcaseHostSize(
1652                                            foundIframe,
1653                                            foundIframe.parentElement
1654                                        );
1655                                    } else if (displayType === 'uutiset') {
1656                                        applyUutisetHostSize(
1657                                            foundIframe,
1658                                            foundIframe.parentElement
1659                                        );
1660                                    }
1661                                }
1662                                return;
1663                            }
1664                            if (event.data.type === 'supersearch-resize') {
1665                                if (!isTrustedSupersearchMessage(event)) return;
1666                                const { width, height } = event.data;
1667                                const foundIframe = findIframeByEventSource(event.source);
1668                                if (foundIframe) {
1669                                    if (handleDirectEmbedSupersearchResize(foundIframe, width, height)) {
1670                                        return;
1671                                    }
1672                                    const container = foundIframe.parentElement;
1673                                    // Floating search (360haku display type) fills the host div — never apply fixed pixel sizes
1674                                    if (container && container.hasAttribute('data-flowbot-360haku')) {
1675                                        return;
1676                                    }
1677                                    if (
1678                                        foundIframe.hasAttribute('data-flowbot-ai-showcase') ||
1679                                        (container && container.hasAttribute('data-flowbot-ai-showcase'))
1680                                    ) {
1681                                        applyAiShowcaseResize(foundIframe, height);
1682                                        return;
1683                                    }
1684                                    if (
1685                                        event.data.displayType === 'uutiset' ||
1686                                        isUutisetSupersearchFrame(foundIframe)
1687                                    ) {
1688                                        applyUutisetResize(foundIframe, height);
1689                                        return;
1690                                    }
1691                                    var genericW = clampDirectEmbedDimension(width);
1692                                    var genericH = clampDirectEmbedDimension(height);
1693                                    if (genericW != null) foundIframe.
1693style.width = genericW + 'px';
1694                                    if (genericH != null) foundIframe.style.height = genericH + 'px';
1695                                    
1696                                    if (container && container.hasAttribute('data-flowbot-supersearch')) {
1697                                        if (genericW != null) container.style.width = genericW + 'px';
1698                                        if (genericH != null) container.style.height = genericH + 'px';
1699                                    }
1700                                }
1701                                return;
1702                            }
1703                        });
1704                    }
1705
1706                    attachViewportListenersOnce();
1707
1708                    document.body.appendChild(iframe);
1709                    window.__flowbotIframes[botId] = iframe;
1710                    container.setAttribute('data-flowbot-initialized', 'true');
1711                } catch (error) {
1712                    console.error(`FlowBot: Error creating iframe for botId ${botId}:`, error);
1713                }
1714            });
1715
1716            // SuperSearch search widgets
1717            const superSearchContainers = document.querySelectorAll('[data-flowbot-supersearch]:not([data-flowbot-initialized])');
1718            superSearchContainers.forEach((container) => {
1719                const botId = container.getAttribute('data-flowbot-supersearch');
1720                const instanceId = container.getAttribute('data-flowbot-supersearch-id') || '';
1721                if (!botId) return;
1722                if (container.hasAttribute('data-flowbot-initialized')) return;
1723
1724                try {
1725                    var isDirectEmbedLayout = getNormalizedEmbedLayout(container) === 'direct_embed';
1726                    const parent = container.parentElement;
1727                    var parentHostWidth = 0;
1728                    var parentHostHeight = 0;
1729                    var hasMeaningfulParentHeight = false;
1730                    container.style.minHeight = '0';
1731                    // Only trust explicit height attrs/inline — never collapsed offsetHeight.
1732                    var explicitHostHeight =
1733                        parseHostHeightPx(container.getAttribute('data-flowbot-height')) ||
1734                        parseHostHeightPx(container.style && container.style.height) ||
1735                        (parent
1736                            ? parseHostHeightPx(parent.getAttribute('data-flowbot-height')) ||
1737                              parseHostHeightPx(parent.style && parent.style.height)
1738                            : 0);
1739                    if (parent && !isDirectEmbedLayout) {
1740                        parent.style.position = 'relative';
1741                        parent.style.overflow = 'visible';
1742                        parentHostWidth = parent.offsetWidth || 0;
1743                        parentHostHeight = explicitHostHeight || 0;
1744                        hasMeaningfulParentHeight = parentHostHeight >= 48;
1745                        container.style.position = 'relative';
1746                        container.style.display = 'block';
1747                        container.style.margin = '0 auto';
1748                        container.style.width = '100%';
1749                        if (hasMeaningfulParentHeight) {
1750                            container.style.height = parentHostHeight + 'px';
1751                            container.dataset.flowbotHostHeight = String(parentHostHeight);
1752                        }
1753                    } else if (explicitHostHeight && !isDirectEmbedLayout) {
1754                        parentHostHeight = explicitHostHeight;
1755                        hasMeaningfulParentHeight = true;
1756                        container.style.height = explicitHostHeight + 'px';
1757                        container.dataset.flowbotHostHeight = String(explicitHostHeight);
1758                    }
1759
1760                    const iframe = document.createElement('iframe');
1761                    // parentHost: asiakassivun hostname Callable domain -tarkistusta ja konfiguraatiota varten
1762                    // (document.referrer iframessa voi olla tyhjä tiukan Referrer-Policy:n takia).
1763                    var parentHost = '';
1764                    var parentPath = '';
1765                    try {
1766                        parentHost = window.location.hostname || '';
1767                        parentPath = window.location.pathname || '';
1768                    } catch (e) {}
1769                    const { width: vw } = getViewport();
1770                    var embedLayout = '';
1771                    try {
1772                        // Explicit direct_embed wins. 360 only when this container
1773                        // is the panorama widget itself, not a nested search.
1774                        if (isDirectEmbedLayout) {
1775                            embedLayout = 'direct_embed';
1776                        } else if (
1777                            container.classList.contains('panorama-superSearch') ||
1778                            (container.parentElement &&
1779                                container.parentElement.classList.contains('panorama-superSearch'))
1780                        ) {
1781                            embedLayout = '360haku';
1782                        }
1783                    } catch (e) {}
1784                    var iframeSrc = CONFIG.baseUrl + '?mode=superSearch&botId=' + encodeURIComponent(botId) +
1785                        (instanceId ? '&instanceId=' + encodeURIComponent(instanceId) : '') +
1786                        '&parentWidth=' + (parentHostWidth || vw) +
1787                        (hasMeaningfulParentHeight ? '&parentHeight=' + parentHostHeight : '') +
1788                        (embedLayout ? '&embedLayout=' + encodeURIComponent(embedLayout) : '');
1789                    iframeSrc = appendParentContextQuery(iframeSrc);
1790                    applyFlowbotIframeIsolation(iframe);
1791                    iframe.style.display = 'block';
1792                    iframe.style.minHeight = '0';
1793                    iframe.setAttribute('allow', 'microphone; storage-access');
1794                    iframe.setAttribute('title', 'FlowBot SuperSearch');
1795                    applySuperSearchIframeTransparency(iframe);
1796                    iframe.src = iframeSrc;
1797                    container.innerHTML = '';
1798
1799                    if (isDirectEmbedLayout) {
1800                        iframe.setAttribute('scrolling', 'no');
1801                        iframe.style.overflow = 'hidden';
1802                        bootstrapDirectEmbedIframe(container, iframe);
1803                    } else {
1804                        container.appendChild(iframe);
1805                        iframe.style.width = '100%';
1806                        if (hasMeaningfulParentHeight) {
1807                            iframe.style.height = '100%';
1808                        }
1809                        iframe.style.border = '1px solid rgba(0,0,0,0.12)';
1810                        iframe.style.borderRadius = '8px';
1811                    }
1812
1813                    // Initial host viewport (resize listener only fires later on window resize).
1814                    iframe.addEventListener('load', function onSuperSearchLoad() {
1815                        iframe.removeEventListener('load', onSuperSearchLoad);
1816                        var vp = getViewport();
1817                        postHostViewportToIframe(
1818                            iframe,
1819                            vp.width,
1820                            vp.height,
1821                            isSoftKeyboardOpen()
1822                        );
1823                    });
1824
1825                    attachViewportListenersOnce();
1826                    registerSuperSearchIframe(botId, instanceId, iframe);
1827                    container.setAttribute('data-flowbot-initialized', 'true');
1828                } catch (error) {
1829                    console.error('FlowBot: Error creating SuperSearch iframe:', error);
1830                }
1831            });
1832
1833            // Popup Ad widgets
1834            var popupContainers = document.querySelectorAll('[data-flowbot-popup]:not([data-flowbot-initialized]
1834)');
1835            console.log('[FlowBot Popup] Found containers:', popupContainers.length);
1836            popupContainers.forEach(function(container) {
1837                var botId = container.getAttribute('data-flowbot-popup');
1838                if (!botId) { console.warn('[FlowBot Popup] No botId on container'); return; }
1839                if (container.hasAttribute('data-flowbot-initialized')) { console.log('[FlowBot Popup] Already initialized'); return; }
1840
1841                container.setAttribute('data-flowbot-initialized', 'true');
1842
1843                try {
1844                    var iframeSrc = CONFIG.baseUrl + '?mode=popupAd&botId=' + encodeURIComponent(botId);
1845                    iframeSrc = appendParentContextQuery(iframeSrc);
1846                    console.log('[FlowBot Popup] Creating hidden overlay, iframe src:', iframeSrc);
1847
1848                    // Overlay starts invisible so page is usable during load + delay
1849                    var overlay = document.createElement('div');
1850                    overlay.id = 'flowbot-popup-overlay-' + botId;
1851                    overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;display:flex;align-items:flex-end;justify-content:center;padding:0;box-sizing:border-box;pointer-events:none;background:rgba(15,23,42,0);transition:background 0.35s ease;';
1852
1853                    var iframe = document.createElement('iframe');
1854                    iframe.src = iframeSrc;
1855                    iframe.style.cssText = 'border:none;width:100%;max-width:100%;height:100%;max-height:100%;background:transparent;opacity:0;transition:opacity 0.25s ease;';
1856                    iframe.setAttribute('title', 'FlowBot Popup');
1857                    iframe.setAttribute('allow', 'autoplay; storage-access');
1858                    applyFlowbotIframeIsolation(iframe);
1859
1860                    overlay.appendChild(iframe);
1861                    document.body.appendChild(overlay);
1862                    console.log('[FlowBot Popup] Hidden overlay added to DOM (waiting for POPUP_AD_READY)');
1863
1864                    var currentVariationId = null;
1865                    var didShowPopup = false;
1866                    var closed = false;
1867
1868                    function seenStorageKey(variationId) {
1869                        return 'flowbot_popup_seen_' + botId + '_' + variationId;
1870                    }
1871
1872                    function readSeen(variationId) {
1873                        if (!variationId) return false;
1874                        try {
1875                            return sessionStorage.getItem(seenStorageKey(variationId)) === '1';
1876                        } catch (err) {
1877                            return false;
1878                        }
1879                    }
1880
1881                    function writeSeen(variationId) {
1882                        if (!variationId) return;
1883                        try {
1884                            sessionStorage.setItem(seenStorageKey(variationId), '1');
1885                        } catch (err) {}
1886                    }
1887
1888                    function closePopup(markSeen) {
1889                        if (closed) return;
1890                        closed = true;
1891                        console.log('[FlowBot Popup] Closing popup');
1892                        if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
1893                        if (markSeen && currentVariationId) writeSeen(currentVariationId);
1894                        window.removeEventListener('message', onPopupMessage);
1895                        overlay.removeEventListener('click', onOverlayClick);
1896                    }
1897
1898                    function showOverlay() {
1899                        console.log('[FlowBot Popup] POPUP_AD_READY received — showing overlay and iframe');
1900                        didShowPopup = true;
1901                        // Plain dim instead of a full-viewport backdrop-filter blur: blurring the
1902                        // entire host page forces the browser to re-composite everything behind the
1903                        // overlay every frame, which made the popup feel laggy on the host site.
1904                        overlay.style.background = 'rgba(15,23,42,0.58)';
1905                        overlay.style.pointerEvents = 'auto';
1906                        iframe.style.opacity = '1';
1907                    }
1908
1909                    function onOverlayClick(e) {
1910                        if (e.target === overlay) { console.log('[FlowBot Popup] Overlay clicked, closing'); closePopup(true); }
1911                    }
1912
1913                    function onPopupMessage(e) {
1914                        if (e.source !== iframe.contentWindow) return;
1915                        if (e.data && e.data.type === 'POPUP_AD_READY') {
1916                            currentVariationId = e.data.variationId || currentVariationId;
1917                            if (readSeen(currentVariationId)) {
1918                                console.log('[FlowBot Popup] Already seen this campaign, skipping');
1919                                closePopup(false);
1920                                return;
1921                            }
1922                            showOverlay();
1923                        } else if (e.data && e.data.type === 'POPUP_AD_CLOSE') {
1924                            console.log('[FlowBot Popup] Received POPUP_AD_CLOSE message');
1925                            if (!currentVariationId && e.data.variationId) currentVariationId = e.data.variationId;
1926                            closePopup(didShowPopup);
1927                        }
1928                    }
1929
1930                    overlay.addEventListener('click', onOverlayClick);
1931                    window.addEventListener('message', onPopupMessage);
1932                } catch (error) {
1933                    console.error('FlowBot: Error creating Popup Ad iframe:', error);
1934                }
1935            });
1936        }
1937
1938        if (document.readyState === 'loading') {
1939            document.addEventListener('DOMContentLoaded', createIframes);
1940        } else {
1941            setTimeout(createIframes, 0);
1942        }
1943    }
1944
1945    initialize();
1946})();

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.