
(function () {
    'use strict';

    var SVGNS = 'http://www.w3.org/2000/svg';

    function decode(s) { try { return decodeURIComponent(s || ''); } catch (e) { return s || ''; } }

    var MOTIONS = ['scroll', 'drift', 'lanes', 'stack', 'zoom', 'rings', 'wavelanes', 'tilt', 'vortex', 'stickers', 'wavestack', 'drum', 'weightwalk', 'cross', 'banner', 'overprint', 'snake', 'funnel', 'tilt', 'bricks', 'coils'];

    function build(el) {
        var text = decode(el.getAttribute('data-tw-text') || '')
            
            .replace(/ /g, String.fromCharCode(160));
        if (!text) return;

        var motionRaw = (el.getAttribute('data-tw-motion') || 'none').toLowerCase();
        var spotlight = el.getAttribute('data-tw-spotlight') === '1';
        var parallax = el.getAttribute('data-tw-parallax') === '1';

        if (motionRaw === 'wave' || motionRaw === 'perspective') { motionRaw = 'none'; }
        var motion = (MOTIONS.indexOf(motionRaw) !== -1) ? motionRaw : 'none';
        var speed = parseFloat(el.getAttribute('data-tw-speed') || '1');
        if (!isFinite(speed) || speed <= 0) speed = 1;
        var space = parseInt(el.getAttribute('data-tw-space') || '24', 10);
        if (!isFinite(space) || space < 0) space = 24;
        var angle = parseFloat(el.getAttribute('data-tw-angle') || '-30');
        if (!isFinite(angle)) angle = -30;

        

        
        var accent = el.getAttribute('data-tw-color2') || '';
        
        var ticker = el.getAttribute('data-tw-ticker') === '1';

        var wfont = el.getAttribute('data-tw-font') || '';

        
        
        var images = [];
        try {
            var _imgRaw = decode(el.getAttribute('data-tw-images') || '');
            if (_imgRaw) images = JSON.parse(_imgRaw) || [];
        } catch (e) { images = []; }
        if (!Array.isArray(images)) images = [];
        var isImg = (el.getAttribute('data-tw-content') || 'text').toLowerCase() === 'images'
                    && images.length > 0;

        var dim = parseFloat(el.getAttribute('data-tw-dim'));
        if (!isFinite(dim) || dim < 0 || dim > 100) dim = 100;
        var gray = el.getAttribute('data-tw-gray') === '1';
        var cameraRaw = (el.getAttribute('data-tw-camera') || '').toLowerCase();
        var camera = (['none', 'breathe', 'pan', 'dolly', 'orbit'].indexOf(cameraRaw) !== -1) ? cameraRaw : '';

        var W = el.clientWidth, H = el.clientHeight;
        if (W < 2 || H < 2) return;

        
        el._twBuiltW = W;
        el._twBuiltH = H;

        if (el._twAnims) { el._twAnims.forEach(function (a) { try { a.cancel(); } catch (e) {} }); }
        el._twAnims = [];
        if (el._twRaf) { cancelAnimationFrame(el._twRaf); el._twRaf = null; }
        if (el._twTimers) { el._twTimers.forEach(clearInterval); el._twTimers = []; }
        el._twParUnits = [];
        if (el._twGyroOff) { try { el._twGyroOff(); } catch (e) {} el._twGyroOff = null; }
        if (el._twParallaxOff) { try { el._twParallaxOff(); } catch (e) {} el._twParallaxOff = null; }
        if (el._twSpotOff) { try { el._twSpotOff(); } catch (e) {} el._twSpotOff = null; }
        el.textContent = '';
        el.style.webkitMaskImage = '';
        el.style.maskImage = '';
        el.style.webkitMaskComposite = '';
        el.style.maskComposite = '';

        
        el.style.isolation = '';
        el.style.contain = '';
        el.style.opacity = '';

        if (!el._twBaseColor) el._twBaseColor = el.style.color || getComputedStyle(el).color || 'rgba(255,255,255,0.14)';
        el.style.color = el._twBaseColor;

        var WFONTS = {
            grotesque: '"Helvetica Neue", Helvetica, Arial, sans-serif',
            mono: 'ui-monospace, "SF Mono", Menlo, Consolas, monospace',
            serif: 'Georgia, "Times New Roman", serif',
            condensed: '"Arial Narrow", "Helvetica Neue Condensed", Impact, sans-serif'
        };
        el.style.fontFamily = WFONTS[wfont] || '';

        var probe = document.createElement('span');
        probe.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;left:-9999px;top:-9999px';
        probe.textContent = text;
        el.appendChild(probe);
        var unitW = (probe.offsetWidth || 120) + space;
        var rowH = probe.offsetHeight || (parseInt(getComputedStyle(el).fontSize, 10) || 48);
        
        probe.style.whiteSpace = 'pre';
        probe.textContent = new Array(11).join(' ');
        var spaceW = (probe.offsetWidth || 0) / 10;
        el.removeChild(probe);
        
        var sep = ' ';
        var wordSpace = Math.round(space - spaceW);

        var gap = parseInt(getComputedStyle(el).rowGap, 10) || 0;
        var stepY = rowH + gap;

        
        
        var pAmp = parallax ? 40 : 0;

        var pLayer = document.createElement('div');
        pLayer.className = 'impulse-textwall-parallax';
        pLayer.style.cssText = 'position:absolute;top:0;bottom:0;left:' + (-pAmp) + 'px;right:' + (-pAmp) + 'px;display:flex;flex-direction:column;justify-content:center;will-change:transform;'
            
            + 'word-spacing:' + wordSpace + 'px;';
        el.appendChild(pLayer);

        

        if (isImg) {
            var tileH = Math.max(24, parseInt(getComputedStyle(el).fontSize, 10) || 72);
            var tileW = Math.round(tileH * 1.34);
            var geoI = { W: W, H: H, unitW: tileW + space, rowH: tileH, gap: gap,
                         stepY: tileH + gap, pAmp: pAmp, text: text, space: space, sep: sep,
                         speed: speed, angle: angle, camera: camera, accent: accent,
                         color: el._twBaseColor,
                         images: images, tileW: tileW, tileH: tileH, gray: gray };
            
            pLayer.style.opacity = (dim < 100) ? String(dim / 100) : '';
            var camLayerI = pLayer;
            if (camera && camera !== 'none') {
                camLayerI = document.createElement('div');
                camLayerI.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;'
                    + 'justify-content:center;will-change:transform;';
                pLayer.appendChild(camLayerI);
                applyFlatCamera(el, camLayerI, camera, geoI);
            }
            if (motion === 'lanes')         { buildLanes(el, camLayerI, geoI); }
            else if (motion === 'cross')    { buildCross(el, camLayerI, geoI); }
            else if (motion === 'zoom')     { buildZoom(el, camLayerI, geoI); }
            else if (motion === 'vortex')   { buildVortex(el, camLayerI, geoI); }
            else if (motion === 'stickers') { buildStickers(el, camLayerI, geoI); }
            else if (motion === 'bricks')   { buildBricks(el, camLayerI, geoI); }
            else if (motion === 'tilt')     { buildTilt(el, pLayer, geoI); }
            else if (motion === 'drum')     { buildDrum(el, pLayer, geoI); }
            else                            { buildFlat(el, camLayerI, geoI, motion); }
        }

        var geo = { W: W, H: H, unitW: unitW, rowH: rowH, gap: gap, stepY: stepY, pAmp: pAmp,
                    text: text, space: space, sep: sep, speed: speed, angle: angle, camera: camera,
                    accent: accent, color: el._twBaseColor };

        
        
        var camLayer = pLayer;
        if (!isImg && camera && camera !== 'none' && motion !== 'drum') {
            camLayer = document.createElement('div');
            camLayer.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;'
                + 'justify-content:center;will-change:transform;';
            pLayer.appendChild(camLayer);
            applyFlatCamera(el, camLayer, camera, geo);
        }

        
        if (isImg)                       {  }
        else if (motion === 'lanes')     { buildLanes(el, camLayer, geo); }
        else if (motion === 'zoom')      { buildZoom(el, camLayer, geo); }
        else if (motion === 'rings')     { buildRings(el, camLayer, geo); }
        else if (motion === 'wavelanes') { buildWaveLanes(el, camLayer, geo); }
        else if (motion === 'vortex')    { buildVortex(el, camLayer, geo); }
        else if (motion === 'stickers')  { buildStickers(el, camLayer, geo); }
        else if (motion === 'wavestack') { buildWaveStack(el, camLayer, geo); }
        else if (motion === 'weightwalk') { buildWeightWalk(el, camLayer, geo); }
        else if (motion === 'cross')     { buildCross(el, camLayer, geo); }
        else if (motion === 'banner')    { buildBanner(el, camLayer, geo); }
        else if (motion === 'overprint') { buildOverprint(el, camLayer, geo); }
        else if (motion === 'snake')     { buildSnake(el, camLayer, geo); }
        else if (motion === 'funnel')    { buildFunnel(el, camLayer, geo); }
        else if (motion === 'tilt')      { buildTilt(el, pLayer, geo); }
        else if (motion === 'bricks')    { buildBricks(el, camLayer, geo); }
        else if (motion === 'coils')     { buildCoils(el, camLayer, geo); }
        else if (motion === 'drum')      { buildDrum(el, pLayer, geo); }
        else                             { buildFlat(el, camLayer, geo, motion); }

        
        
        if (ticker) buildTicker(el, el, geo);

        

        if (spotlight) {
            applyColor(el);

            

            

            

            

            

            var SPOT_SMOOTH = 0.18;
            var _sraf = null, _stx = null, _sty = null, _scx = null, _scy = null,
                _svx = 0, _svy = 0, _slast = 0;
            function _sTick(now) {
                var r = el.getBoundingClientRect();
                if (!r.width || !r.height) { _sraf = null; return; }
                var dt = _slast ? Math.min(0.064, (now - _slast) / 1000) : 0.016;
                _slast = now;
                var rx = damp(_scx, _stx, _svx, dt, SPOT_SMOOTH); _scx = rx[0]; _svx = rx[1];
                var ry = damp(_scy, _sty, _svy, dt, SPOT_SMOOTH); _scy = ry[0]; _svy = ry[1];
                el.style.setProperty('--tw-sx', _scx.toFixed(1) + 'px');
                el.style.setProperty('--tw-sy', _scy.toFixed(1) + 'px');
                if (Math.abs(_stx - _scx) > 0.4 || Math.abs(_sty - _scy) > 0.4 ||
                    Math.abs(_svx) > 0.4 || Math.abs(_svy) > 0.4) {
                    _sraf = requestAnimationFrame(_sTick);
                } else {
                    _scx = _stx; _scy = _sty; _svx = _svy = 0; _sraf = null; _slast = 0;

                    
                    if (Math.abs(_scx - r.width / 2) < 0.6 && Math.abs(_scy - r.height / 2) < 0.6) {
                        el.style.removeProperty('--tw-sx');
                        el.style.removeProperty('--tw-sy');
                        _scx = _scy = null;
                    } else {
                        el.style.setProperty('--tw-sx', _scx.toFixed(1) + 'px');
                        el.style.setProperty('--tw-sy', _scy.toFixed(1) + 'px');
                    }
                }
            }
            function _sMove(e) {
                var r = el.getBoundingClientRect();
                if (!r.width || !r.height) return;
                var x = e.clientX - r.left, y = e.clientY - r.top;
                var inside = x >= 0 && y >= 0 && x <= r.width && y <= r.height;
                var ntx = inside ? x : r.width / 2;
                var nty = inside ? y : r.height / 2;
                
                if (!inside && _scx === null) return;
                _stx = ntx; _sty = nty;
                if (_scx === null) { _scx = r.width / 2; _scy = r.height / 2; }  
                if (!_sraf) { _slast = 0; _sraf = requestAnimationFrame(_sTick); }
            }
            window.addEventListener('mousemove', _sMove, { passive: true });
            el._twSpotOff = function () { window.removeEventListener('mousemove', _sMove); if (_sraf) cancelAnimationFrame(_sraf); };
        }

        

        

        

        

        

        

        

        

        if (parallax && el._twParUnits && el._twParUnits.length) {
            el.style.overflow = 'hidden';   
            var tx = 0, ty = 0, cx = 0, cy = 0, vx = 0, vy = 0, raf = null, last = 0;
            var amp = pAmp * 0.6;   
            var SMOOTH = 0.5;   
            var units = el._twParUnits;
            
            var vDepth = isImg ? function () { return 1; } : function (u) { return u.depth; };
            function applyUnits() {
                for (var i = 0; i < units.length; i++) {
                    var u = units[i];
                    u.el.style.transform = 'translate(' + (cx * u.depth).toFixed(2) + 'px,'
                        + (cy * vDepth(u)).toFixed(2) + 'px)' + (u.base ? ' ' + u.base : '');
                }
            }
            function tick(now) {
                var dt = last ? Math.min(0.064, (now - last) / 1000) : 0.016;
                last = now;
                var rx = damp(cx, tx, vx, dt, SMOOTH); cx = rx[0]; vx = rx[1];
                var ry = damp(cy, ty, vy, dt, SMOOTH); cy = ry[0]; vy = ry[1];
                applyUnits();
                if (Math.abs(tx - cx) > 0.05 || Math.abs(ty - cy) > 0.05 ||
                    Math.abs(vx) > 0.05 || Math.abs(vy) > 0.05) {
                    raf = requestAnimationFrame(tick);
                } else {
                    cx = tx; cy = ty; vx = vy = 0; raf = null; last = 0;
                    applyUnits();
                }
            }
            function setTarget(nx, ny) {
                tx = -Math.max(-1, Math.min(1, nx)) * amp;
                ty = -Math.max(-1, Math.min(1, ny)) * amp * 0.55;
                if (!raf) { last = 0; raf = requestAnimationFrame(tick); }
            }
            function onMove(e) {
                var r = el.getBoundingClientRect();
                if (!r.width || !r.height) return;
                setTarget((e.clientX - (r.left + r.width / 2)) / (r.width / 2),
                          (e.clientY - (r.top + r.height / 2)) / (r.height / 2));
            }
            window.addEventListener('mousemove', onMove, { passive: true });
            el._twParallaxOff = function () { window.removeEventListener('mousemove', onMove); if (raf) cancelAnimationFrame(raf); };

            if (window.matchMedia && window.matchMedia('(hover: none)').matches && window.DeviceOrientationEvent) {
                var g0 = null;
                var onGyro = function (e) {
                    if (e.beta == null || e.gamma == null) return;
                    if (!g0) g0 = { b: e.beta, g: e.gamma };
                    setTarget(Math.max(-1, Math.min(1, (e.gamma - g0.g) / 25)),
                              Math.max(-1, Math.min(1, (e.beta - g0.b) / 25)));
                };
                window.addEventListener('deviceorientation', onGyro, { passive: true });
                el._twGyroOff = function () { window.removeEventListener('deviceorientation', onGyro); };
            }
        }
    }

    
    function unitNode(geo, i) {
        if (geo.images && geo.images.length) {
            var t = document.createElement('div');
            t.style.cssText = 'display:inline-block;vertical-align:top;flex:0 0 auto;'
                + 'width:' + geo.tileW + 'px;height:' + geo.tileH + 'px;'
                + 'margin-right:' + geo.space + 'px;overflow:hidden;border-radius:2px;'
                + 'background-image:url("' + String(geo.images[i % geo.images.length]).replace(/"/g, '%22') + '");'
                + 'background-size:cover;background-position:50% 50%;'
                + (geo.gray ? 'filter:grayscale(1);' : '');
            return t;
        }
        var u = document.createElement('span');
        u.textContent = geo.text;
        u.style.marginRight = geo.space + 'px';
        return u;
    }

    function makeRows(host, geo, w, nRows, doubleStrip, styleRow) {
        var perRow = Math.max(1, Math.ceil(w / geo.unitW) + 2);
        var rowEls = [];
        for (var r = 0; r < nRows; r++) {
            var row = document.createElement('div');
            row.className = 'impulse-textwall-row';
            row.style.cssText = 'flex:0 0 auto;overflow:hidden;white-space:nowrap;will-change:opacity,transform;'
                + (geo.images && geo.images.length ? 'height:' + geo.tileH + 'px;' : '');
            var strip = document.createElement('div');
            strip.className = 'impulse-textwall-strip';
            strip.style.cssText = 'display:inline-block;white-space:nowrap;will-change:transform;';
            var copies = doubleStrip ? perRow * 2 : perRow;
            for (var k = 0; k < copies; k++) strip.appendChild(unitNode(geo, k + r * 3));
            if (styleRow) styleRow(row, strip, r);
            row.appendChild(strip);
            host.appendChild(row);
            rowEls.push({ row: row, strip: strip });
        }
        return rowEls;
    }

    var DEPTHS = [1.5, 0.7, 1.15, 0.55, 1.3, 0.85, 1.0, 0.6];
    function regPar(el, node, i, base, depth) {
        if (!el._twParUnits) el._twParUnits = [];
        el._twParUnits.push({ el: node, depth: depth || DEPTHS[i % DEPTHS.length], base: base || '' });
    }

    function farRow(row) {
        row.style.fontSize = '0.82em';
        row.style.opacity = '0.55';
    }
    function rowDepth(i) { return (i % 2 === 0) ? 1.35 : 0.6; }

    function marqueeRows(el, rowEls, speed, speedMul) {
        rowEls.forEach(function (o, i) {
            var setW = o.strip.scrollWidth / 2;
            if (setW <= 0) return;
            var mul = speedMul ? speedMul(i) : 1;
            var dur = Math.max(6000, (setW / (55 * speed * mul)) * 1000);
            var kf = (i % 2 === 0)
                ? [{ transform: 'translateX(0)' }, { transform: 'translateX(' + (-setW) + 'px)' }]
                : [{ transform: 'translateX(' + (-setW) + 'px)' }, { transform: 'translateX(0)' }];
            try { el._twAnims.push(o.strip.animate(kf, { duration: dur, iterations: Infinity, easing: 'linear' })); } catch (e) {}
        });
    }

    function outlineRow(row) {
        row.style.webkitTextFillColor = 'transparent';
        row.style.webkitTextStrokeWidth = '1.5px';
    }

    
    function applyFlatCamera(el, layer, camera, geo) {
      
      var _cs = parseFloat(el.getAttribute('data-tw-cam-strength'));
      if (!isFinite(_cs) || _cs < 0 || _cs > 3) _cs = 1;
        
        var dur = Math.max(7000, 15000 / geo.speed);
        var kf = null;
        if (camera === 'breathe') {
            kf = [{ transform: 'scale(1)' }, { transform: 'scale(' + (1 + 0.20 * _cs).toFixed(3) + ')' }, { transform: 'scale(1)' }];
        } else if (camera === 'dolly') {
            kf = [{ transform: 'scale(1)' }, { transform: 'scale(' + (1 + 0.55 * _cs).toFixed(3) + ')' }, { transform: 'scale(1)' }];
        } else if (camera === 'pan') {
            var _ps = (1 + 0.28 * _cs).toFixed(3), _px = (9 * _cs).toFixed(2);
            kf = [{ transform: 'scale(' + _ps + ') translateX(-' + _px + '%)' },
                  { transform: 'scale(' + _ps + ') translateX(' + _px + '%)' },
                  { transform: 'scale(' + _ps + ') translateX(-' + _px + '%)' }];
        } else if (camera === 'orbit') {
            var _os = (1 + 0.34 * _cs).toFixed(3), _od = (5.5 * _cs).toFixed(2);
            kf = [{ transform: 'scale(' + _os + ') rotate(-' + _od + 'deg)' },
                  { transform: 'scale(' + _os + ') rotate(' + _od + 'deg)' },
                  { transform: 'scale(' + _os + ') rotate(-' + _od + 'deg)' }];
        }
        if (!kf) return;
        try {
            
            for (var _ke = 0; _ke < kf.length - 1; _ke++) kf[_ke].easing = 'ease-in-out';
            el._twAnims.push(layer.animate(kf,
                { duration: dur, iterations: Infinity, easing: 'linear' }));
        } catch (e) {}
    }

    function buildFlat(el, pLayer, geo, motion) {
        var wrap = document.createElement('div');
        wrap.className = 'impulse-textwall-rows';
        wrap.style.cssText = 'display:flex;flex-direction:column;align-items:stretch;width:100%;'
                           + 'row-gap:' + geo.gap + 'px;will-change:transform;';
        var nRows = Math.ceil(geo.H / geo.stepY) + (motion === 'drift' ? 4 : 2);
        var isStack = (motion === 'stack');
        var rowEls = makeRows(wrap, geo, geo.W + 2 * geo.pAmp, nRows,
            (motion === 'scroll' || isStack),
            isStack ? function (row, strip, r) { if (r % 2 === 1) outlineRow(row); }
                    : function (row, strip, r) { if (r % 2 === 1) farRow(row); });
        rowEls.forEach(function (o, i) { regPar(el, o.row, i, '', rowDepth(i)); });
        pLayer.appendChild(wrap);

        if (motion === 'scroll') {
            marqueeRows(el, rowEls, geo.speed, null);
        } else if (isStack) {

            marqueeRows(el, rowEls, geo.speed, function (i) { return [1, 0.55, 1.5][i % 3]; });
        } else if (motion === 'drift') {
            
            var _r0 = rowEls[0] && rowEls[0].row;
            var _r2 = rowEls[2] && rowEls[2].row;
            var periodY = (_r0 && _r2) ? (_r2.offsetTop - _r0.offsetTop) : (2 * geo.stepY);
            if (!(periodY > 1)) periodY = 2 * geo.stepY;
            var durY = Math.max(4000, (periodY / (14 * geo.speed)) * 1000);
            try {
                el._twAnims.push(wrap.animate(
                    [{ transform: 'translateY(0)' }, { transform: 'translateY(' + (-periodY) + 'px)' }],
                    { duration: durY, iterations: Infinity, easing: 'linear' }
                ));
            } catch (e) {}
        }
        
    }

    function buildLanes(el, pLayer, geo, ) {
        var D = Math.ceil(Math.sqrt(geo.W * geo.W + geo.H * geo.H)) + 2 * geo.pAmp;
        var rot = document.createElement('div');
        rot.style.cssText = 'position:absolute;left:50%;top:50%;width:' + D + 'px;height:' + D + 'px;'
            + 'transform:translate(-50%,-50%) rotate(' + geo.angle + 'deg);'
            + 'display:flex;flex-direction:column;justify-content:center;'
            + 'row-gap:' + geo.gap + 'px;will-change:transform;';
        var nRows = Math.ceil(D / geo.stepY) + 2;
        var rowEls = makeRows(rot, geo, D, nRows, true,
            function (row, strip, r) {
                if (r % 2 === 1) farRow(row);
                
                if (geo.accent) {
                    var kids = strip.children;
                    for (var q = 0; q < kids.length; q++) {
                        if ((q + r) % 3 === 0) kids[q].style.color = geo.accent;
                    }
                }
            });
        rowEls.forEach(function (o, i) { regPar(el, o.row, i, '', rowDepth(i)); });
        pLayer.appendChild(rot);
        marqueeRows(el, rowEls, geo.speed, null);
    }

    function buildZoom(el, pLayer, geo) {
        var ZF = 2.4;                                  
        var dur = Math.max(5000, 16000 / geo.speed);   
        for (var c = 0; c < 2; c++) {
            var layer = document.createElement('div');
            layer.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;'
                + 'justify-content:center;row-gap:' + geo.gap + 'px;'
                + 'will-change:transform,opacity;transform-origin:50% 50%;';
            var nRows = Math.ceil(geo.H / geo.stepY) + 2;
            makeRows(layer, geo, geo.W + 2 * geo.pAmp, nRows, false, null);
            pLayer.appendChild(layer);
            if (c === 1 && geo.accent) layer.style.color = geo.accent;
            try {
                el._twAnims.push(layer.animate(
                    [{ transform: 'scale(1)',            opacity: 0 },
                     { opacity: 1, offset: 0.25 },
                     { opacity: 1, offset: 0.72 },
                     { transform: 'scale(' + ZF + ')',   opacity: 0 }],
                    { duration: dur, iterations: Infinity, easing: 'linear',
                      delay: -c * (dur / 2) }
                ));
            } catch (e) {}
        }
    }

    function buildRings(el, pLayer, geo) {
        var cx = geo.W / 2 + geo.pAmp, cy = geo.H / 2;
        var maxR = Math.ceil(Math.sqrt(cx * cx + cy * cy));   
        var svg = document.createElementNS(SVGNS, 'svg');
        svg.setAttribute('width', '100%');
        svg.setAttribute('height', '100%');
        svg.style.cssText = 'position:absolute;inset:0;overflow:visible;';
        var fs = parseInt(getComputedStyle(el).fontSize, 10) || 48;
        var fw = getComputedStyle(el).fontWeight || '700';
        var step = geo.stepY;
        var unit = geo.text + geo.sep;   
        var just = [];                
        var idx = 0;

        
        var rMin = Math.max(step * 0.8, fs, (geo.unitW / (2 * Math.PI)) * 1.05);
        for (var r = rMin; r <= maxR; r += step, idx++) {
            var gp = document.createElementNS(SVGNS, 'g');   
            regPar(el, gp, idx);
            var g = document.createElementNS(SVGNS, 'g');
            g.style.transformOrigin = cx + 'px ' + cy + 'px';
            var pathId = 'twr' + Math.round(cx) + '_' + Math.round(r) + '_' + idx;
            var path = document.createElementNS(SVGNS, 'path');
            
            path.setAttribute('d',
                'M ' + (cx - r) + ' ' + cy +
                ' a ' + r + ' ' + r + ' 0 1 1 ' + (2 * r) + ' 0' +
                ' a ' + r + ' ' + r + ' 0 1 1 ' + (-2 * r) + ' 0');
            path.setAttribute('id', pathId);
            path.setAttribute('fill', 'none');
            var t = document.createElementNS(SVGNS, 'text');
            t.setAttribute('fill', 'currentColor');
            t.style.cssText = 'font-size:' + fs + 'px;font-weight:' + fw + ';';
            var tp = document.createElementNS(SVGNS, 'textPath');
            tp.setAttribute('href', '#' + pathId);
            tp.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + pathId);

            

            
            var circ = 2 * Math.PI * r;
            var perUnit = Math.max(1, geo.unitW);
            var reps = Math.max(1, Math.floor(circ / perUnit));
            var s = '';
            for (var k = 0; k < reps; k++) s += unit;
            tp.textContent = s;
            just.push({ t: t, chars: s.length, circ: circ });
            t.appendChild(tp);
            g.appendChild(path);
            g.appendChild(t);
            gp.appendChild(g);
            svg.appendChild(gp);
            var dir = (idx % 2 === 0) ? 1 : -1;
            var dur = Math.max(8000, (circ / (40 * geo.speed)) * 1000);
            try {
                el._twAnims.push(g.animate(
                    [{ transform: 'rotate(0deg)' }, { transform: 'rotate(' + (dir * 360) + 'deg)' }],
                    { duration: dur, iterations: Infinity, easing: 'linear' }
                ));
            } catch (e) {}
        }
        pLayer.appendChild(svg);

        
        
        just.forEach(function (o) {
            try {
                var natural = o.t.getComputedTextLength();
                if (natural > 0 && o.chars > 1) {
                    o.t.style.letterSpacing = ((o.circ - natural) / o.chars).toFixed(2) + 'px';
                }
            } catch (e) {}
        });
    }

    function buildWaveLanes(el, pLayer, geo) {
        var W = geo.W + 2 * geo.pAmp, H = geo.H;
        var svg = document.createElementNS(SVGNS, 'svg');
        svg.setAttribute('width', '100%');
        svg.setAttribute('height', '100%');
        svg.style.cssText = 'position:absolute;inset:0;overflow:visible;';
        var fs = parseInt(getComputedStyle(el).fontSize, 10) || 48;
        var fw = getComputedStyle(el).fontWeight || '700';
        var amp = Math.min(geo.stepY * 0.45, 42);      
        var lam = Math.max(W / 1.6, 420);              
        var margin = amp + fs;                          
        var lanes = [];
        var nRows = Math.ceil(H / geo.stepY) + 2;
        var y0 = (H - (nRows - 1) * geo.stepY) / 2;
        for (var r = 0; r < nRows; r++) {
            var yc = y0 + r * geo.stepY;
            var phase = (r % 2) * Math.PI + r * 0.7;
            
            var d = '', stepX = Math.max(24, lam / 24);
            for (var x = -margin; x <= W + margin + stepX; x += stepX) {
                var y = yc + amp * Math.sin((2 * Math.PI * x) / lam + phase);
                d += (d ? ' L ' : 'M ') + Math.round(x) + ' ' + Math.round(y * 10) / 10;
            }
            var pathId = 'twwl' + r + '_' + Math.round(W);
            var path = document.createElementNS(SVGNS, 'path');
            path.setAttribute('d', d);
            path.setAttribute('id', pathId);
            path.setAttribute('fill', 'none');
            var t = document.createElementNS(SVGNS, 'text');
            t.setAttribute('fill', 'currentColor');
            t.style.cssText = 'font-size:' + fs + 'px;font-weight:' + fw + ';';
            var tp = document.createElementNS(SVGNS, 'textPath');
            tp.setAttribute('href', '#' + pathId);
            tp.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + pathId);
            var unit = geo.text + geo.sep;
            var pathLen = (W + 2 * margin) * 1.1;      
            var reps = Math.max(2, Math.ceil(pathLen / Math.max(1, geo.unitW)) + 2);
            var s = '';
            for (var k = 0; k < reps; k++) s += unit;
            tp.textContent = s;
            t.appendChild(tp);
            svg.appendChild(path);
            svg.appendChild(t);
            regPar(el, t, r);
            lanes.push({ tp: tp, dir: (r % 2 === 0) ? 1 : -1 });
        }
        pLayer.appendChild(svg);

        var pxPerSec = 30 * geo.speed;
        var unitLen = Math.max(24, geo.unitW);
        var start = null;
        function flow(now) {
            if (!start) start = now;
            var off = ((now - start) / 1000 * pxPerSec) % unitLen;
            for (var i = 0; i < lanes.length; i++) {
                var o = lanes[i].dir === 1 ? -off : off - unitLen;
                lanes[i].tp.setAttribute('startOffset', o.toFixed(1));
            }
            el._twRaf = requestAnimationFrame(flow);
        }
        el._twRaf = requestAnimationFrame(flow);
    }

    function buildStickers(el, pLayer, geo) {
        var cellW = Math.max(geo.unitW * 0.9, 140);
        
        var cellH = geo.rowH + geo.gap;
        
        var PR = 4, PC = 5;
        var cols = Math.ceil((geo.W + 2 * geo.pAmp) / cellW) + PC + 1;
        var rows = Math.ceil(geo.H / cellH) + PR + 1;
        var ANG = [-16, 9, -7, 19, 4, -21, 12, -3];    
        var field = document.createElement('div');
        field.style.cssText = 'position:absolute;left:0;top:0;'
            + 'width:' + ((cols + 1) * cellW) + 'px;height:' + ((rows + 1) * cellH) + 'px;'
            + 'will-change:transform;';
        for (var r = 0; r < rows + 1; r++) {
            for (var c = 0; c < cols + 1; c++) {
                var seed = (r % PR) * PC + (c % PC);
                var chip = (geo.images && geo.images.length)
                    ? unitNode(geo, seed)
                    : document.createElement('span');
                if (!(geo.images && geo.images.length)) chip.textContent = geo.text;
                var a = ANG[seed % ANG.length];
                chip.style.cssText = (chip.style.cssText || '')
                    + 'position:absolute;white-space:nowrap;'
                    + 'left:' + Math.round(c * cellW + (r % 2) * cellW / 2) + 'px;'
                    + 'top:' + Math.round(r * cellH) + 'px;'
                    + 'transform:rotate(' + a + 'deg);transform-origin:50% 50%;';
                regPar(el, chip, seed, 'rotate(' + a + 'deg)');
                if (geo.images && geo.images.length) {
                    
                } else if (seed % 3 === 1) {
                    chip.style.webkitTextFillColor = 'transparent';
                    chip.style.webkitTextStrokeWidth = '1.5px';
                } else if (seed % 3 === 2) {
                    
                    chip.style.background = geo.accent || 'currentColor';
                    chip.style.color = '#fff';
                    chip.style.padding = '0.12em 0.5em';
                }
                field.appendChild(chip);
            }
        }
        pLayer.appendChild(field);
        
        var dur = Math.max(6000, (cellW * PC / (18 * geo.speed)) * 1000);
        try {
            el._twAnims.push(field.animate(
                [{ transform: 'translate(0,0)' },
                 { transform: 'translate(' + (-PC * cellW) + 'px,' + (-PR * cellH) + 'px)' }],
                { duration: dur, iterations: Infinity, easing: 'linear' }
            ));
        } catch (e) {}
    }

    function buildWaveStack(el, host, geo) {
        var wrap = document.createElement('div');
        wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;'
            + 'justify-content:center;overflow:hidden;row-gap:' + geo.gap + 'px;';
        var nRows = Math.ceil(geo.H / geo.stepY) + 2;
        var perRow = Math.max(1, Math.ceil((geo.W + 2 * geo.pAmp) / geo.unitW) + 1);
        var letters = [];
        for (var r = 0; r < nRows; r++) {
            var row = document.createElement('div');
            row.style.cssText = 'flex:0 0 auto;white-space:pre;overflow:visible;line-height:1;';
            var full = '';
            for (var k = 0; k < perRow; k++) full += geo.text + geo.sep;
            for (var i = 0; i < full.length; i++) {
                var s = document.createElement('span');
                s.textContent = full.charAt(i);
                s.style.cssText = 'display:inline-block;will-change:transform;';
                row.appendChild(s);
                letters.push({ el: s, i: i, row: r });
            }
            if (r % 2 === 1) farRow(row);
            wrap.appendChild(row);
            regPar(el, row, r, '', rowDepth(r));
        }
        host.appendChild(wrap);
        var period = Math.max(5000, 10000 / geo.speed);
        var amp = geo.rowH * 0.17;
        var lam = Math.max(24, Math.round(geo.W / 14));   
        var start = null;
        function tick(now) {
            if (!el._twInit || !el.isConnected) return;
            if (!start) start = now;
            var ph = ((now - start) / period) * Math.PI * 2;
            for (var i = 0; i < letters.length; i++) {
                var L = letters[i];
                var a = (L.i / lam) * Math.PI * 2 - ph + L.row * 0.9;
                var y = Math.sin(a) * amp;
                var rot = Math.cos(a) * 5;
                L.el.style.transform = 'translateY(' + y.toFixed(1) + 'px) rotate(' + rot.toFixed(2) + 'deg)';
            }
            el._twRaf = requestAnimationFrame(tick);
        }
        el._twRaf = requestAnimationFrame(tick);
    }

    function buildSnake(el, host, geo) {
        var W = geo.W + 2 * geo.pAmp, H = geo.H;
        var svg = document.createElementNS(SVGNS, 'svg');
        svg.setAttribute('width', '100%');
        svg.setAttribute('height', '100%');
        svg.style.cssText = 'position:absolute;inset:0;overflow:visible;';
        var fs = parseInt(getComputedStyle(el).fontSize, 10) || 48;
        var fw = getComputedStyle(el).fontWeight || '700';
        var bandH = Math.max(geo.stepY * 2.4, fs * 2.6);
        var nBands = Math.max(1, Math.ceil(H / bandH) + 1);
        var y0 = (H - (nBands - 1) * bandH) / 2;
        var lanes = [];
        for (var b = 0; b < nBands; b++) {
            var yc = y0 + b * bandH;
            var amp = bandH * 0.42;
            var lam = Math.max(W / 1.15, 360);
            var d = '', stepX = Math.max(18, lam / 40);
            var dir = (b % 2 === 0) ? 1 : -1;
            for (var x = -fs * 2; x <= W + fs * 2 + stepX; x += stepX) {
                var y = yc + dir * amp * Math.sin((2 * Math.PI * x) / lam);
                d += (d ? ' L ' : 'M ') + Math.round(x) + ' ' + (Math.round(y * 10) / 10);
            }
            var pid = 'twsn' + b + '_' + Math.round(W);
            var path = document.createElementNS(SVGNS, 'path');
            path.setAttribute('d', d);
            path.setAttribute('id', pid);
            path.setAttribute('fill', 'none');
            var t = document.createElementNS(SVGNS, 'text');
            t.setAttribute('fill', 'currentColor');
            t.style.cssText = 'font-size:' + fs + 'px;font-weight:' + fw + ';';
            if (geo.accent && b % 2 === 1) t.setAttribute('fill', geo.accent);
            var tp = document.createElementNS(SVGNS, 'textPath');
            tp.setAttribute('href', '#' + pid);
            tp.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', '#' + pid);
            var unit = geo.text + geo.sep;
            var reps = Math.max(2, Math.ceil((W * 1.4) / Math.max(1, geo.unitW)) + 2);
            var s = '';
            for (var k = 0; k < reps; k++) s += unit;
            tp.textContent = s;
            t.appendChild(tp);
            svg.appendChild(path);
            svg.appendChild(t);
            regPar(el, t, b, '', rowDepth(b));
            lanes.push({ tp: tp, dir: dir });
        }
        host.appendChild(svg);
        var pxPerSec = 34 * geo.speed, unitLen = Math.max(24, geo.unitW), start = null;
        function flow(now) {
            if (!el._twInit || !el.isConnected) return;
            if (!start) start = now;
            var off = ((now - start) / 1000 * pxPerSec) % unitLen;
            for (var i = 0; i < lanes.length; i++) {
                lanes[i].tp.setAttribute('startOffset', (lanes[i].dir === 1 ? -off : off - unitLen).toFixed(1));
            }
            el._twRaf = requestAnimationFrame(flow);
        }
        el._twRaf = requestAnimationFrame(flow);
    }

    function buildFunnel(el, host, geo) {
        var id = 'twfn' + Math.round(geo.W) + '_' + Math.round(geo.H);
        var W = Math.round(geo.W + 2 * geo.pAmp), H = Math.round(geo.H);

        
        
        var ramp = '<svg xmlns="http://www.w3.org/2000/svg" width="' + W + '" height="' + H + '">'
            + '<defs><radialGradient id="g" cx="50%" cy="50%" r="70%">'
            + '<stop offset="0%" stop-color="rgb(128,128,128)"/>'
            + '<stop offset="100%" stop-color="rgb(255,255,255)"/>'
            + '</radialGradient></defs>'
            + '<rect width="100%" height="100%" fill="url(#g)"/></svg>';
        var svg = document.createElementNS(SVGNS, 'svg');
        svg.setAttribute('width', '0'); svg.setAttribute('height', '0');
        svg.style.cssText = 'position:absolute;width:0;height:0;';
        var filter = document.createElementNS(SVGNS, 'filter');
        filter.setAttribute('id', id);
        filter.setAttribute('x', '-20%'); filter.setAttribute('y', '-20%');
        filter.setAttribute('width', '140%'); filter.setAttribute('height', '140%');
        var img = document.createElementNS(SVGNS, 'feImage');
        img.setAttribute('href', 'data:image/svg+xml;utf8,' + encodeURIComponent(ramp));
        img.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href',
            'data:image/svg+xml;utf8,' + encodeURIComponent(ramp));
        img.setAttribute('result', 'ramp');
        var disp = document.createElementNS(SVGNS, 'feDisplacementMap');
        disp.setAttribute('in', 'SourceGraphic');
        disp.setAttribute('in2', 'ramp');
        
        disp.setAttribute('scale', String(Math.round(Math.min(W, H) * 0.30)));
        disp.setAttribute('xChannelSelector', 'R');
        disp.setAttribute('yChannelSelector', 'G');
        filter.appendChild(img); filter.appendChild(disp);
        svg.appendChild(filter);
        el.appendChild(svg);

        var wrap = document.createElement('div');
        wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;'
            + 'justify-content:center;overflow:hidden;row-gap:' + geo.gap + 'px;'
            + 'filter:url(#' + id + ');';
        var nRows = Math.ceil(H / geo.stepY) + 3;
        var rows = makeRows(wrap, geo, W, nRows, true, null);
        host.appendChild(wrap);
        marqueeRows(el, rows, geo.speed, function (i) { return 1 + (i % 3) * 0.25; });
        regPar(el, wrap, 0, '', 1.0);
    }

    function buildTilt(el, pLayer, geo) {
        el.style.isolation = 'isolate';
        el.style.contain = 'paint';
        el.style.overflow = 'hidden';
        var stage = document.createElement('div');
        stage.style.cssText = 'position:absolute;inset:0;perspective:' + Math.round(geo.W * 0.9) + 'px;'
            + 'perspective-origin:50% 50%;';
        var deck = document.createElement('div');
        deck.style.cssText = 'position:absolute;inset:0;transform-style:preserve-3d;'
            + 'display:flex;flex-direction:column;justify-content:center;'
            + 'row-gap:' + Math.round(geo.gap + geo.rowH * 0.25) + 'px;';
        var nRows = Math.ceil(geo.H / (geo.stepY * 1.25)) + 2;
        
        var lean = 10;
        
        var _tRad = lean * Math.PI / 180;
        var _tPersp = geo.W * 0.9;
        var _tFar = _tPersp / (_tPersp + (geo.W / 2) * Math.sin(_tRad));
        var _tCover = Math.max(1.8, (1 / Math.cos(_tRad)) / _tFar * 1.45);
        var rowEls = makeRows(deck, geo, geo.W * _tCover, nRows, true, function (row, strip, r) {
            var d = (r % 2 === 0) ? 1 : -1;
            
            row.style.width = Math.round(geo.W * _tCover) + 'px';
            
            row.style.marginLeft = Math.round(-geo.W * (_tCover - 1) / 2) + 'px';
            row.style.transform = 'rotateY(' + (d * lean) + 'deg)';
            row.style.transformOrigin = '50% 50%';
            if (r % 2 === 1 && geo.accent) row.style.color = geo.accent;
        });
        rowEls.forEach(function (o, i) {
            regPar(el, o.row, i, o.row.style.transform, rowDepth(i));
        });
        stage.appendChild(deck);
        pLayer.appendChild(stage);
        marqueeRows(el, rowEls, geo.speed, null);
    }

    function buildBricks(el, host, geo) {
        var fs = Math.max(12, Math.round(geo.rowH * 0.42));
        var chipH = Math.round(fs * 1.9);
        var pad = Math.round(fs * 0.5);
        
        var _brImg = !!(geo.images && geo.images.length);
        
        var rowStep = _brImg
            ? geo.tileH + geo.gap
            : chipH + Math.max(3, Math.round(fs * 0.28));
        
        var _brStagger = _brImg
            ? Math.round((geo.tileW + geo.gap) / 2)
            : Math.round(fs * 2.2);
        var rows = Math.ceil(geo.H / rowStep) + 3;
        var field = document.createElement('div');
        field.style.cssText = 'position:absolute;left:0;top:0;width:' + (geo.W * 2) + 'px;'
            + 'height:' + ((rows + 1) * rowStep) + 'px;will-change:transform;';
        var words = geo.text.split(/\s+/).filter(Boolean);
        if (!words.length) words = [geo.text];
        var seq = 0;
        for (var r = 0; r < rows + 1; r++) {
            var x = (r % 2) * _brStagger;
            var y = r * rowStep;
            while (x < geo.W * 2) {
                var w = words[seq % words.length];
                if (geo.images && geo.images.length) {
                    var itile = unitNode(geo, seq);
                    itile.style.position = 'absolute';
                    itile.style.left = x + 'px';
                    itile.style.top = y + 'px';
                    itile.style.marginRight = '0';
                    field.appendChild(itile);
                    x += geo.tileW + geo.gap;
                    seq++;
                    continue;
                }
                var chip = document.createElement('span');
                chip.textContent = w;
                var inv = (seq % 3 === 1);
                chip.style.cssText = 'position:absolute;white-space:nowrap;line-height:1;'
                    + 'left:' + x + 'px;top:' + y + 'px;height:' + chipH + 'px;'
                    + 'display:inline-flex;align-items:center;padding:0 ' + pad + 'px;'
                    + 'font-size:' + fs + 'px;border-radius:' + Math.round(chipH / 2) + 'px;'
                    + (inv ? 'background:' + (geo.accent || 'currentColor') + ';color:#fff;'
                           : 'border:1px solid currentColor;');
                field.appendChild(chip);
                x += Math.round(w.length * fs * 0.62) + pad * 2 + Math.round(fs * 0.4);
                seq++;
            }
        }
        host.appendChild(field);
        
        Array.prototype.forEach.call(field.children, function (b, bi) {
            regPar(el, b, bi, b.style.transform || '');
        });
        var dur = Math.max(9000, (rowStep * 2 / (6 * geo.speed)) * 1000);
        try {
            el._twAnims.push(field.animate(
                [{ transform: 'translate(0,0)' },
                 { transform: 'translate(' + (-Math.round(geo.W * 0.5)) + 'px,' + (-rowStep * 2) + 'px)' }],
                { duration: dur, iterations: Infinity, easing: 'linear' }));
        } catch (e) {}
    }

    function buildCoils(el, host, geo) {
        var fs = Math.max(12, Math.round(geo.rowH * 0.5));
        var R = Math.max(fs * 1.1, 26);
        var colW = Math.round(R * 2.4);
        var cols = Math.max(1, Math.ceil((geo.W + 2 * geo.pAmp) / colW) + 1);
        var stage = document.createElement('div');
        stage.style.cssText = 'position:absolute;inset:0;perspective:' + Math.round(geo.W * 1.1) + 'px;'
            + 'display:flex;justify-content:space-around;align-items:center;overflow:hidden;';
        
        var perTurn = Math.max(8, Math.round((2 * Math.PI * R) / (fs * 0.62)));
        var pitch = fs * 0.16;
        var turns = Math.max(2, Math.ceil(geo.H / (pitch * perTurn / perTurn) / pitch) + 1);
        var total = Math.min(1200, Math.max(perTurn * 3, Math.ceil(geo.H / pitch) + perTurn));
        for (var c = 0; c < cols; c++) {
            var spin = document.createElement('div');
            spin.style.cssText = 'position:relative;width:' + colW + 'px;height:100%;'
                + 'transform-style:preserve-3d;will-change:transform;';
            var inner = document.createElement('div');
            inner.style.cssText = 'position:absolute;left:50%;top:50%;width:0;height:0;'
                + 'transform-style:preserve-3d;will-change:transform;';
            var txt = geo.text + geo.sep;
            var y0 = -(total * pitch) / 2;
            for (var i = 0; i < total; i++) {
                var ch = txt.charAt((i + c * 3) % txt.length);
                if (ch === ' ') continue;
                var ang = (i / perTurn) * 360;
                var s = document.createElement('span');
                s.textContent = ch;
                s.style.cssText = 'position:absolute;left:0;top:0;line-height:1;font-size:' + fs + 'px;'
                    + 'transform:translate(-50%,-50%) rotateY(' + ang.toFixed(1) + 'deg) translateZ(' + Math.round(R) + 'px)'
                    + ' translateY(' + Math.round(y0 + i * pitch) + 'px);backface-visibility:hidden;'
                    + (geo.accent && c % 2 === 1 ? 'color:' + geo.accent + ';' : '');
                inner.appendChild(s);
            }
            spin.appendChild(inner);
            stage.appendChild(spin);
            regPar(el, spin, c, '', rowDepth(c));
            var dur = Math.max(9000, (16000 + c * 1700) / geo.speed);
            try {
                el._twAnims.push(inner.animate(
                    [{ transform: 'rotateY(0deg)' }, { transform: 'rotateY(' + ((c % 2 === 0) ? 360 : -360) + 'deg)' }],
                    { duration: dur, iterations: Infinity, easing: 'linear' }));
            } catch (e) {}
        }
        host.appendChild(stage);
    }

    function buildCross(el, host, geo) {
        [1, -1].forEach(function (sign, si) {
            var D = Math.ceil(Math.sqrt(geo.W * geo.W + geo.H * geo.H)) + 2 * geo.pAmp;
            var rot = document.createElement('div');
            rot.style.cssText = 'position:absolute;left:50%;top:50%;width:' + D + 'px;height:' + D + 'px;'
                + 'transform:translate(-50%,-50%) rotate(' + (geo.angle * sign) + 'deg);'
                + 'display:flex;flex-direction:column;justify-content:center;'
                
                + 'row-gap:' + Math.round(geo.gap) + 'px;will-change:transform;'
                + (si === 1 && geo.accent ? 'color:' + geo.accent + ';' : '');
            
            var nRows = Math.ceil(D / geo.stepY) + 2;
            var rowEls = makeRows(rot, geo, D, nRows, true,
                si === 1 ? function (row) { outlineRow(row); } : null);
            rowEls.forEach(function (o, i) { regPar(el, o.row, i, '', si === 0 ? 1.35 : 0.6); });
            host.appendChild(rot);
            marqueeRows(el, rowEls, geo.speed * (si === 0 ? 1 : 0.7), null);
        });
    }

    function buildBanner(el, host, geo) {
        
        var wrap = document.createElement('div');
        wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;'
            + 'justify-content:center;overflow:hidden;row-gap:' + geo.gap + 'px;opacity:0.45;';
        var nRows = Math.ceil(geo.H / geo.stepY) + 2;
        var back = makeRows(wrap, geo, geo.W + 2 * geo.pAmp, nRows, true,
            function (row, strip, r) { if (r % 2 === 1) farRow(row); });
        back.forEach(function (o, i) { regPar(el, o.row, i, '', rowDepth(i) * 0.5); });
        host.appendChild(wrap);
        marqueeRows(el, back, geo.speed * 0.5, null);

        var D = Math.ceil(Math.sqrt(geo.W * geo.W + geo.H * geo.H)) + 2 * geo.pAmp;
        var bandH = Math.round(geo.rowH * 2.1);
        var band = document.createElement('div');
        band.style.cssText = 'position:absolute;left:50%;top:50%;width:' + D + 'px;height:' + bandH + 'px;'
            + 'transform:translate(-50%,-50%) rotate(' + geo.angle + 'deg);'
            + 'display:flex;align-items:center;overflow:hidden;white-space:nowrap;'
            + 'background:' + (geo.accent || geo.color || '#1d2327') + ';'
            + 'font-size:' + Math.round(geo.rowH * 1.25) + 'px;line-height:1;'
            + 'color:#fff;mix-blend-mode:normal;will-change:transform;';
        var strip = document.createElement('div');
        strip.style.cssText = 'display:inline-block;white-space:nowrap;will-change:transform;';
        var per = Math.max(2, Math.ceil(D / geo.unitW) * 2 + 2);
        for (var k = 0; k < per; k++) {
            var u = document.createElement('span');
            u.textContent = geo.text;
            u.style.marginRight = geo.space + 'px';
            strip.appendChild(u);
        }
        band.appendChild(strip);
        host.appendChild(band);
        regPar(el, band, 0, 'rotate(' + geo.angle + 'deg)', 1.5);
        var setW = strip.scrollWidth / 2;
        if (setW > 0) {
            try {
                el._twAnims.push(strip.animate(
                    [{ transform: 'translateX(0)' }, { transform: 'translateX(' + (-setW) + 'px)' }],
                    { duration: Math.max(5000, (setW / (70 * geo.speed)) * 1000),
                      iterations: Infinity, easing: 'linear' }));
            } catch (e) {}
        }
    }

    function buildOverprint(el, host, geo) {
        var nRows = Math.ceil(geo.H / geo.stepY) + 2;
        [0, 1].forEach(function (ply) {
            var wrap = document.createElement('div');
            wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;'
                + 'justify-content:center;overflow:hidden;row-gap:' + geo.gap + 'px;'
                + (ply === 1 ? 'color:' + (geo.accent || geo.color) + ';opacity:0.75;'
                             : '') + 'will-change:transform;';
            var rows = makeRows(wrap, geo, geo.W + 2 * geo.pAmp, nRows, true, null);
            rows.forEach(function (o, i) { regPar(el, o.row, i, '', ply === 0 ? 1.3 : 0.65); });
            host.appendChild(wrap);
            marqueeRows(el, rows, geo.speed * (ply === 0 ? 1 : 0.92), null);
            if (ply === 1) {
                var off = Math.max(2, Math.round(geo.rowH * 0.06));
                try {
                    el._twAnims.push(wrap.animate(
                        [{ transform: 'translate(' + off + 'px,' + off + 'px)' },
                         { transform: 'translate(' + (-off) + 'px,' + (off * 0.4) + 'px)' },
                         { transform: 'translate(' + off + 'px,' + off + 'px)' }],
                        { duration: Math.max(6000, 14000 / geo.speed),
                          iterations: Infinity, easing: 'ease-in-out' }));
                } catch (e) {}
            }
        });
    }

    
    function _tw_ms(base, sp) { var s = parseFloat(sp); return Math.round(base / ((isFinite(s) && s > 0) ? s : 1)); }

    function buildVortex(el, host, geo) {
        var W = geo.W, H = geo.H;
        var DEPTH = Math.round(Math.max(W, H) * 1.6);   
        var RAD   = Math.round(Math.min(W, H) * 0.42);  
        var stage = document.createElement('div');
        stage.style.cssText = 'position:absolute;inset:0;overflow:hidden;'
            + 'perspective:' + Math.round(Math.min(W, H) * 0.9) + 'px;perspective-origin:50% 50%;';
        var tube = document.createElement('div');
        tube.style.cssText = 'position:absolute;inset:0;transform-style:preserve-3d;';
        stage.appendChild(tube);
        host.appendChild(stage);
        
        regPar(el, tube, 0, '', 0.5);

        var imgs = (geo.images && geo.images.length) ? geo.images : null;

        
        var _vN = parseInt(el.getAttribute('data-tw-vcount'), 10);
        var N = (isFinite(_vN) && _vN >= 6 && _vN <= 40)
            ? _vN
            : (imgs ? Math.min(26, Math.max(14, imgs.length * 4)) : 20);
        var tw = Math.round(Math.min(W, H) * 0.26);
        var th = Math.round(tw * 0.72);
        var dur = _tw_ms(14000, geo.speed);

        for (var i = 0; i < N; i++) {
            
            var a = i * 2.39996;
            var x = Math.round(Math.cos(a) * RAD);
            var y = Math.round(Math.sin(a) * RAD);
            var t = document.createElement('div');
            t.style.cssText = 'position:absolute;left:50%;top:50%;'
                + 'width:' + tw + 'px;height:' + th + 'px;'
                + 'margin-left:' + (-tw / 2) + 'px;margin-top:' + (-th / 2) + 'px;'
                + 'will-change:transform,opacity;backface-visibility:hidden;'
                + (imgs
                    ? 'background-image:url("' + String(imgs[i % imgs.length]).replace(/"/g, '%22') + '");'
                      + 'background-size:cover;background-position:50% 50%;border-radius:2px;'
                      + (geo.gray ? 'filter:grayscale(1);' : '')
                    : 'display:flex;align-items:center;justify-content:center;white-space:nowrap;'
                      + 'font-weight:900;font-size:' + Math.round(th * 0.5) + 'px;'
                      + 'color:' + (geo.color || 'currentColor') + ';');
            if (!imgs) t.textContent = geo.text;
            tube.appendChild(t);
            
            var zFar = -DEPTH, zNear = 0;
            var at = function (z) {
                return 'translate3d(' + x + 'px,' + y + 'px,' + z + 'px)';
            };
            var from = at(zFar), to = at(zNear);
            try {
                el._twAnims.push(t.animate(
                    
                    [{ transform: from, opacity: 0 },
                     { transform: at(zFar + (zNear - zFar) * 0.10), opacity: 0.22, offset: 0.10 },
                     { transform: at(zFar + (zNear - zFar) * 0.45), opacity: 0.72, offset: 0.45 },
                     { transform: at(zFar + (zNear - zFar) * 0.82), opacity: 1, offset: 0.82 },
                     { transform: to, opacity: 0 }],
                    { duration: dur, iterations: Infinity, easing: 'linear',
                      
                      delay: -Math.round(dur * (i / N)) }));
            } catch (e) {}
        }
    }

    function buildTicker(el, host, geo) {
        
        try { if (getComputedStyle(el).position === 'static') el.style.position = 'relative'; } catch (e) {}
        var fs = Math.max(9, Math.round(geo.rowH * 0.16));
        var band = Math.round(fs * 1.9);
        var col = geo.accent || '';
        
        [['top', false], ['bottom', false]].forEach(function (side, si) {
            var vertical = side[1];

            
            var INSET = 12;
            var len = vertical
                ? Math.max(1, geo.H - INSET * 2 - band * 2)
                : Math.max(1, geo.W - INSET * 2);   
            var holder = document.createElement('div');
            var pos = vertical
                ? ((side[0] === 'left' ? 'left:' : 'right:') + INSET + 'px;top:' + (INSET + band) + 'px;')
                : ('left:' + INSET + 'px;' + (side[0] === 'top' ? 'top:' : 'bottom:') + INSET + 'px;');
            holder.style.cssText = 'position:absolute;' + pos
                + (vertical ? 'width:' + band + 'px;height:' + len + 'px;'
                            : 'height:' + band + 'px;width:' + len + 'px;')
                + 'overflow:hidden;display:flex;align-items:center;pointer-events:none;'
                + 'font-size:' + fs + 'px;line-height:1;letter-spacing:0.18em;'
                + (col ? 'color:' + col + ';' : '') + 'opacity:0.75;';
            var inner = document.createElement('div');
            inner.style.cssText = 'display:flex;align-items:center;white-space:nowrap;will-change:transform;'
                + (vertical ? 'transform-origin:0 0;position:absolute;top:0;left:' + band + 'px;'
                            + 'transform:rotate(90deg);width:' + len + 'px;height:' + band + 'px;' : '');
            var per = Math.max(4, Math.ceil(len / Math.max(40, geo.unitW * (fs / Math.max(1, geo.rowH)))) * 2 + 4);
            var s = '';
            for (var k = 0; k < per; k++) s += geo.text + '  \u00b7  ';
            var a = document.createElement('span'); a.textContent = s;
            var b = document.createElement('span'); b.textContent = s;
            inner.appendChild(a); inner.appendChild(b);
            holder.appendChild(inner);
            host.appendChild(holder);
            var setW = a.offsetWidth || len;
            var dir = (si % 2 === 0) ? -1 : 1;
            try {
                el._twAnims.push(inner.animate(
                    [{ transform: (vertical ? 'rotate(90deg) ' : '') + 'translateX(' + (dir < 0 ? 0 : -setW) + 'px)' },
                     { transform: (vertical ? 'rotate(90deg) ' : '') + 'translateX(' + (dir < 0 ? -setW : 0) + 'px)' }],
                    { duration: Math.max(9000, (setW / (26 * geo.speed)) * 1000),
                      iterations: Infinity, easing: 'linear' }));
            } catch (e) {}
        });
    }

    function buildWeightWalk(el, host, geo) {
        var wrap = document.createElement('div');
        wrap.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;'
            + 'justify-content:center;overflow:hidden;row-gap:' + geo.gap + 'px;';
        var nRows = Math.ceil(geo.H / geo.stepY) + 2;
        var rows = [];
        var perRow = Math.max(1, Math.ceil((geo.W + 2 * geo.pAmp) / geo.unitW) + 1);
        for (var r = 0; r < nRows; r++) {
            var row = document.createElement('div');
            row.style.cssText = 'flex:0 0 auto;white-space:nowrap;overflow:hidden;line-height:1;'
                + 'transition:opacity 700ms ease-in-out;';
            var s = '';
            for (var k = 0; k < perRow; k++) s += geo.text + geo.sep;
            row.textContent = s;
            wrap.appendChild(row);
            rows.push(row);
            regPar(el, row, r);
        }
        host.appendChild(wrap);
        function emphasize(idx) {
            rows.forEach(function (row, i) {
                var on = (i === idx);
                row.style.fontWeight = on ? '900' : '300';
                row.style.opacity = on ? '1' : '0.45';
                row.style.webkitTextFillColor = on ? '' : 'transparent';
                row.style.webkitTextStrokeWidth = on ? '0' : '1px';
            });
        }
        var idx = Math.floor(nRows / 2);
        emphasize(idx);
        var beat = Math.max(900, 2240 / geo.speed);
        var iv = setInterval(function () {
            if (!el._twInit || !el.isConnected) { clearInterval(iv); return; }
            idx = (idx + 1) % rows.length;
            emphasize(idx);
        }, beat);
        el._twTimers = el._twTimers || [];
        el._twTimers.push(iv);
    }

    function buildDrum(el, pLayer, geo) {
        el.style.isolation = 'isolate';
        el.style.contain = 'paint';
        el.style.overflow = 'hidden';
        var W = geo.W, H = geo.H;
        var N = 10;
        var R = Math.max(W, H) * 0.55;
        var h = Math.max(20, Math.round(2 * R * Math.sin(Math.PI / N)));
        var apothem = Math.round(R * Math.cos(Math.PI / N));
        var persp = Math.round(Math.min(W, H) * 1.4);
        var stage = document.createElement('div');
        stage.style.cssText = 'position:absolute;inset:0;perspective:' + persp + 'px;'
            + 'perspective-origin:50% 50%;';
        var ring = document.createElement('div');
        
        ring.style.cssText = 'position:absolute;inset:0;transform-style:preserve-3d;will-change:transform;';
        
        var _drRot = Math.max(-30, Math.min(30, geo.angle * 0.35));
        var _drRad = _drRot * Math.PI / 180;
        var _drCover = Math.abs(Math.cos(_drRad)) + Math.abs(Math.sin(_drRad));
        stage.style.transform = 'rotate(' + _drRot + 'deg) scale(' + _drCover.toFixed(4) + ')';
        
        var minScale = persp / (persp + apothem);
        var faceW = Math.ceil(W / minScale) + 120;
        
        var fs = Math.max(14, Math.min(Math.round(h * 0.6), geo.rowH));
        var per = Math.max(2, Math.ceil(faceW / Math.max(40, geo.unitW * (fs / Math.max(1, geo.rowH)))) + 3);

        var _drScale = fs / Math.max(1, geo.rowH);
        var rowStep = Math.max(fs * 1.05, fs * 1.05 + geo.gap * _drScale);
        var rowsPerFace = Math.max(1, Math.floor(h / rowStep));
        
        rowStep = h / rowsPerFace;
        for (var i = 0; i < N; i++) {
            var angle = -(360 / N) * i;
            var face = document.createElement('div');
            var phase = -Math.round((i % 4) * geo.unitW * (fs / Math.max(1, geo.rowH)) * 0.45);
            face.style.cssText =
                'position:absolute;left:' + (phase - Math.round((faceW - W) / 2)) + 'px;width:' + (faceW - phase) + 'px;top:50%;height:' + h + 'px;' +
                'display:flex;flex-direction:column;justify-content:center;' +
                'transform:translateY(-50%) rotateX(' + angle + 'deg) translateZ(' + (-apothem) + 'px);' +
                'backface-visibility:hidden;overflow:hidden;' +
                'font-size:' + fs + 'px;line-height:1;';
            for (var rr = 0; rr < rowsPerFace; rr++) {
                var rowEl = document.createElement('div');
                if (geo.images && geo.images.length) {
                    rowEl.style.display = 'flex';
                    for (var ik = 0; ik < per; ik++) {
                        var itl = unitNode(geo, ik + i * 2 + rr);
                        itl.style.height = Math.round(rowStep * 0.94) + 'px';
                        itl.style.width = Math.round(rowStep * 1.25) + 'px';
                        rowEl.appendChild(itl);
                    }
                    rowEl.style.cssText += 'flex:0 0 ' + Math.round(rowStep) + 'px;overflow:hidden;';
                    face.appendChild(rowEl);
                    continue;
                }
                var s = '';
                for (var k = 0; k < per; k++) s += geo.text + geo.sep;
                rowEl.textContent = s;
                rowEl.style.cssText = 'flex:0 0 ' + Math.round(rowStep) + 'px;display:flex;align-items:center;'
                    + 'white-space:nowrap;overflow:hidden;'
                    + (rr % 2 === 1 ? 'margin-left:' + Math.round(-geo.unitW * 0.3) + 'px;' : '');
                face.appendChild(rowEl);
            }
            ring.appendChild(face);
        }
        stage.appendChild(ring);
        pLayer.appendChild(stage);
        var dur = Math.max(12000, 30000 / geo.speed);
        try {
            el._twAnims.push(ring.animate(
                [{ transform: 'rotateX(0deg)' }, { transform: 'rotateX(360deg)' }],
                { duration: dur, iterations: Infinity, easing: 'linear' }
            ));
        } catch (e) {}
    }

    

    
    function damp(cur, target, vel, dt, smooth) {
        var omega = 2 / smooth;
        var x = omega * dt;
        var exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x);
        var change = cur - target;
        var temp = (vel + omega * change) * dt;
        return [target + (change + temp) * exp, (vel - omega * temp) * exp];
    }

    function applyColor(el) {
        var spotlight = el.getAttribute('data-tw-spotlight') === '1';
        var _c = el._twBaseColor || '';
        if (!spotlight) {

            el.style.color = _c;
            el.style.webkitMaskImage = ''; el.style.maskImage = '';
            return;
        }
        var _far = 0.12;
        var _am = _c.match(/rgba?\([^)]*?,\s*([\d.]+)\s*\)/);

        

        if (_am) _far = Math.max(0, Math.min(0.6, parseFloat(_am[1])));
        
        if ((el.getAttribute('data-tw-content') || '') === 'images') _far = 0;

        var _rgbm = _c.match(/(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
        el.style.color = _rgbm ? ('rgb(' + _rgbm[1] + ',' + _rgbm[2] + ',' + _rgbm[3] + ')') : _c;
        var _diam = parseInt(el.getAttribute('data-tw-spot-size'), 10);
        if (!isFinite(_diam) || _diam < 50) _diam = 600;
        
        var _near = parseFloat(el.getAttribute('data-tw-spot-opacity'));
        _near = isFinite(_near) ? Math.max(0, Math.min(100, _near)) / 100 : 1;
        
        if (_near < _far) _near = _far;
        var _mask = 'radial-gradient(circle ' + Math.round(_diam / 2) + 'px at var(--tw-sx,50%) var(--tw-sy,50%), rgba(0,0,0,' + _near + ') 0%, rgba(0,0,0,' + _far + ') 72%)';
        el.style.webkitMaskImage = _mask; el.style.maskImage = _mask;
    }

    

    
    
    function recolor(el, color) {
        if (!el) return;
        if (typeof color === 'string' && color !== '') el._twBaseColor = color;
        applyColor(el);
    }

    

    

    

    
    
    var TW_MASK_ONLY = { tw_spot_size: 'data-tw-spot-size', tw_spot_opacity: 'data-tw-spot-opacity' };
    var TW_ATTR      = { tw_speed: 'data-tw-speed', tw_space: 'data-tw-space',

                         
                         tw_motion: 'data-tw-motion', tw_spotlight: 'data-tw-spotlight',
                         tw_parallax: 'data-tw-parallax', tw_angle: 'data-tw-angle',
                         tw_camera: 'data-tw-camera', tw_color2: 'data-tw-color2',
                         tw_ticker: 'data-tw-ticker', tw_font: 'data-tw-font',
                         
                         tw_content: 'data-tw-content', tw_dim: 'data-tw-dim',
                         tw_cam_strength: 'data-tw-cam-strength',
                         tw_vcount: 'data-tw-vcount',
                         tw_gray: 'data-tw-gray' };
    var TW_STYLE     = { tw_size: ['fontSize', 'px'], tw_gap: ['rowGap', 'px'], tw_weight: ['fontWeight', ''] };

    function applySetting(el, key, value) {
        if (!el || !key) return false;
        if (key === 'tw_color') { recolor(el, value); return true; }

        
        if (key === 'tw_text') {
            el.setAttribute('data-tw-text', encodeURIComponent(String(value)));
            build(el);
            return true;
        }
        
        if (key === 'tw_images') {
            el.setAttribute('data-tw-images', encodeURIComponent(String(value)));
            build(el);
            return true;
        }

        if (TW_MASK_ONLY[key]) {
            el.setAttribute(TW_MASK_ONLY[key], String(value));
            applyColor(el);
            return true;
        }

        
        if (TW_ATTR[key])  { el.setAttribute(TW_ATTR[key], String(value)); build(el); return true; }
        if (TW_STYLE[key]) { el.style[TW_STYLE[key][0]] = String(value) + TW_STYLE[key][1]; build(el); return true; }
        return false;   
    }

    function initAll(root) {
        var scope = root && root.querySelectorAll ? root : document;
        scope.querySelectorAll('.impulse-textwall').forEach(function (el) {
            if (el._twInit) return;
            el._twInit = true;
            build(el);
            if (window.ResizeObserver) {
                
                var ro = new ResizeObserver(function () {
                    var nw = el.clientWidth, nh = el.clientHeight;
                    if (!nw || !nh) return;
                    var wChanged = Math.abs(nw - (el._twBuiltW || 0)) > 2;
                    var hOutgrown = nh > (el._twBuiltH || 0) + 8;
                    if (!wChanged && !hOutgrown) return;
                    clearTimeout(el._twT);
                    el._twT = setTimeout(function () { build(el); }, 160);
                });
                try { ro.observe(el); } catch (e) {}
                el._twRO = ro;
            }
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () { initAll(document); });
    } else {
        initAll(document);
    }

    window.ImpulseTextWall = { init: initAll, build: build, recolor: recolor, applySetting: applySetting };

    

    
    
    document.addEventListener('impulse-screen-changed', function () {
        var active = document.querySelector('.impulse-screen-active');
        if (!active) return;
        active.querySelectorAll('.impulse-textwall').forEach(function (el) {
            var w = el.clientWidth;
            if (w > 0 && el._twBuiltW !== w) { build(el); }
        });
    });

    

    
    
    if (window._impulseEditorPreview && window.MutationObserver) {
        var _twMo = new MutationObserver(function (muts) {
            for (var i = 0; i < muts.length; i++) {
                if (muts[i].addedNodes && muts[i].addedNodes.length) {
                    clearTimeout(window._twReinitT);
                    window._twReinitT = setTimeout(function () { initAll(document); }, 80);
                    return;
                }
            }
        });
        try { _twMo.observe(document.body, { childList: true, subtree: true }); } catch (e) {}
    }
})();
