미디어위키:ManifestoIntro.js: 두 판 사이의 차이

(Install package: clbiwiki-mainpage-divider-follow-text-fix-20260713 / js/ManifestoIntro.js)
(Install package: clbiwiki-mainpage-divider-equal-gap-fix-20260713 / js/ManifestoIntro.js)
 
296번째 줄: 296번째 줄:
         drop.style.width = Math.round(innerRect.width) + 'px';
         drop.style.width = Math.round(innerRect.width) + 'px';
         return true;
         return true;
    }
    function readDividerGap(divider) {
        var portal = divider && divider.closest ? divider.closest('.main-portal') : null;
        var value = 8;
        if (portal && window.getComputedStyle) {
            value = parseFloat(
                window.getComputedStyle(portal).getPropertyValue('--manifesto-divider-gap')
            );
        }
        return Number.isFinite(value) ? Math.max(0, Math.round(value)) : 8;
     }
     }


307번째 줄: 320번째 줄:
         var lineRects;
         var lineRects;
         var gapsBefore;
         var gapsBefore;
        var endGap = readDividerGap(divider);
         var distance;
         var distance;


325번째 줄: 339번째 줄:
         });
         });
         distance = lineRects.length
         distance = lineRects.length
             ? Math.max(0, Math.round(lineRects[lineRects.length - 1].bottom - dividerRect.top))
             ? Math.max(0, Math.round(
             : Math.max(0, Math.round(copyRect.bottom - dividerRect.top));
                lineRects[lineRects.length - 1].bottom - dividerRect.top + endGap
            ))
             : Math.max(0, Math.round(copyRect.bottom - dividerRect.top + endGap));


         lines.forEach(function (line, index) {
         lines.forEach(function (line, index) {
336번째 줄: 352번째 줄:
             originTop: dividerRect.top,
             originTop: dividerRect.top,
             gapsBefore: gapsBefore,
             gapsBefore: gapsBefore,
            endGap: endGap,
             finalDistance: distance
             finalDistance: distance
         };
         };
492번째 줄: 509번째 줄:
             function track() {
             function track() {
                 var distance = 0;
                 var distance = 0;
                var visibleProgress = 0;


                 if (!drop.isConnected || !copy.isConnected) {
                 if (!drop.isConnected || !copy.isConnected) {
504번째 줄: 522번째 줄:
                     /* 각 행 앞의 실제 여백도 그 행이 열리는 비율만큼만 함께 연다. */
                     /* 각 행 앞의 실제 여백도 그 행이 열리는 비율만큼만 함께 연다. */
                     distance += ((geometry.gapsBefore[index] || 0) * progress) + height;
                     distance += ((geometry.gapsBefore[index] || 0) * progress) + height;
                    visibleProgress = Math.max(visibleProgress, progress);
                 });
                 });
                /* 마지막으로 보이는 행과 하강선 사이에도 같은 공통 간격을 유지한다. */
                distance += (geometry.endGap || 0) * visibleProgress;


                 distance = Math.max(lastDistance, Math.min(finalDistance, Math.round(distance)));
                 distance = Math.max(lastDistance, Math.min(finalDistance, Math.round(distance)));
844번째 줄: 866번째 줄:


     window.ManifestoIntro = {
     window.ManifestoIntro = {
         version: '20260713-divider-follow-text-004',
         version: '20260713-divider-equal-gap-005',
         init: init,
         init: init,
         status: function () {
         status: function () {

2026년 7월 13일 (월) 14:44 기준 최신판

/* =========================================
ManifestoIntro
대문 선언문 절차적 진입 애니메이션
========================================= */

/*
새 기능과 공개 이름에는 프로젝트 접두사를 사용하지 않는다.
실제 HTML 텍스트를 유지한 채 제목, 구분선, 본문 행을 순서대로 연다.
이미지, canvas, 글자 래스터화는 사용하지 않는다.
*/

(function (window, document, mw) {
    'use strict';

    var PORTAL_SELECTOR = '.main-portal';
    var TITLE_SELECTOR = '.main-manifesto-title';
    var COPY_SELECTOR = '.main-manifesto-copy';
    var FONT_QUERY = '18px "NanumMyeongjoBold Pixel 18"';
    var TITLE_DELAY = 50;
    var TITLE_DURATION = 420;
    var DIVIDER_DELAY = 190;
    var DIVIDER_DURATION = 500;
    var BODY_DELAY = DIVIDER_DELAY + DIVIDER_DURATION + 30;
    var LINE_STAGGER = 64;
    var LINE_DURATION = 230;
    var LINE_HEIGHT = 26;
    var ANIMATION_WATCHDOG_MS = 6000;
    var portalStates = typeof WeakMap === 'function' ? new WeakMap() : null;
    var portalWatchdogs = typeof WeakMap === 'function' ? new WeakMap() : null;
    var observer = null;

    function getWatchdog(portal) {
        if (!portal) return 0;
        if (portalWatchdogs) return portalWatchdogs.get(portal) || 0;
        return portal.__manifestoIntroWatchdog || 0;
    }

    function clearWatchdog(portal) {
        var timer = getWatchdog(portal);
        if (timer) window.clearTimeout(timer);
        if (portalWatchdogs && portal) portalWatchdogs.delete(portal);
        if (portal) portal.__manifestoIntroWatchdog = 0;
    }

    function armWatchdog(portal) {
        var timer;
        if (!portal) return;
        clearWatchdog(portal);
        timer = window.setTimeout(function () {
            var state = getState(portal);
            if (state === 'preparing' || state === 'running') {
                showImmediately(portal);
            }
        }, ANIMATION_WATCHDOG_MS);
        if (portalWatchdogs) portalWatchdogs.set(portal, timer);
        else portal.__manifestoIntroWatchdog = timer;
    }

    function findPortal(root) {
        var scope = root && root.querySelector ? root : document;

        if (scope.matches && scope.matches(PORTAL_SELECTOR)) {
            return scope;
        }

        return scope.querySelector(PORTAL_SELECTOR) || document.querySelector(PORTAL_SELECTOR);
    }

    function prefersReducedMotion() {
        return !!(
            window.matchMedia &&
            window.matchMedia('(prefers-reduced-motion: reduce)').matches
        );
    }

    function getState(portal) {
        if (portalStates) return portalStates.get(portal) || '';
        return portal.getAttribute('data-manifesto-intro-state') || '';
    }

    function setState(portal, state) {
        if (portalStates) portalStates.set(portal, state);
        portal.setAttribute('data-manifesto-intro-state', state);
    }

    function disarmPending() {
        document.documentElement.classList.remove('manifesto-intro-pending');
        document.documentElement.classList.remove('manifesto-intro-active');
    }

    function waitForFont() {
        if (!document.fonts || typeof document.fonts.load !== 'function') {
            return Promise.resolve();
        }

        var timeout = new Promise(function (resolve) {
            window.setTimeout(resolve, 1400);
        });

        var load = document.fonts.load(FONT_QUERY).then(function () {
            return document.fonts.ready;
        }).catch(function () {
            return null;
        });

        return Promise.race([load, timeout]);
    }

    function tokenizeParagraph(paragraph) {
        var text = String(paragraph.textContent || '').replace(/\s+/g, ' ').trim();
        var words = text ? text.split(' ') : [];

        paragraph.textContent = '';
        paragraph.setAttribute('data-manifesto-original-text', text);

        words.forEach(function (word, index) {
            var token = document.createElement('span');
            token.className = 'manifesto-measure-token';
            token.textContent = word;
            paragraph.appendChild(token);

            if (index < words.length - 1) {
                paragraph.appendChild(document.createTextNode(' '));
            }
        });

        return Array.prototype.slice.call(
            paragraph.querySelectorAll('.manifesto-measure-token')
        );
    }

    function groupTokensIntoRows(tokens) {
        var rows = [];
        var current = null;

        tokens.forEach(function (token) {
            var top = Math.round(token.getBoundingClientRect().top);

            if (!current || Math.abs(current.top - top) > 1) {
                current = { top: top, words: [] };
                rows.push(current);
            }

            current.words.push(token.textContent);
        });

        return rows.map(function (row) {
            return row.words.join(' ');
        });
    }

    function createLine(text, index) {
        var line = document.createElement('span');
        var inner = document.createElement('span');

        line.className = 'manifesto-line';
        line.setAttribute('data-manifesto-line-index', String(index));

        inner.className = 'manifesto-line-inner';
        inner.textContent = text;

        line.appendChild(inner);
        return line;
    }

    function buildRenderedLines(copy) {
        var paragraphs = Array.prototype.slice.call(copy.querySelectorAll('p'));
        var lines = [];
        var lineIndex = 0;

        if (!paragraphs.length) return lines;

        copy.classList.add('manifesto-line-measuring');

        paragraphs.forEach(function (paragraph) {
            var tokens = tokenizeParagraph(paragraph);
            var rows = groupTokensIntoRows(tokens);

            paragraph.textContent = '';

            rows.forEach(function (text) {
                var line = createLine(text, lineIndex);
                paragraph.appendChild(line);
                lines.push(line);
                lineIndex += 1;
            });
        });

        copy.classList.remove('manifesto-line-measuring');
        return lines;
    }

    function restoreOriginalText(copy) {
        Array.prototype.forEach.call(copy.querySelectorAll('p'), function (paragraph) {
            var original = paragraph.getAttribute('data-manifesto-original-text');
            if (original !== null) {
                paragraph.textContent = original;
                paragraph.removeAttribute('data-manifesto-original-text');
            }
        });
    }

    function sanitizeDividerSources(title) {
        var inner = title && title.closest ? title.closest('.main-manifesto-inner') : null;
        var portal = title && title.closest ? title.closest('.main-portal') : null;

        if (!title || !inner) return;

        /* 제목 자체가 그리던 완성형 선을 런타임에서도 확실하게 끊는다. */
        title.style.setProperty('border-bottom', '0', 'important');
        title.style.setProperty('box-shadow', 'none', 'important');
        title.style.setProperty('background-image', 'none', 'important');
        title.style.setProperty('text-decoration', 'none', 'important');

        /* 이전 버전이 남긴 모든 선과 bitmap을 지운 뒤 두 실선만 다시 만든다. */
        Array.prototype.forEach.call(
            inner.querySelectorAll(
                '.manifesto-divider, .manifesto-divider-drop, ' +
                '.main-manifesto-divider, [data-manifesto-divider-track], ' +
                'canvas.main-manifesto-bitmap'
            ),
            function (node) {
                if (node.parentNode) node.parentNode.removeChild(node);
            }
        );

        Array.prototype.forEach.call(inner.children || [], function (child) {
            if (child && child.tagName === 'HR' && child.parentNode) {
                child.parentNode.removeChild(child);
            }
        });

        if (portal) portal.classList.add('manifesto-divider-controlled');
    }

    function ensureDivider(title) {
        var divider = title.querySelector('.manifesto-divider');
        var left;
        var right;

        if (!divider) {
            divider = document.createElement('span');
            divider.className = 'manifesto-divider';
            divider.setAttribute('aria-hidden', 'true');
            title.appendChild(divider);
        }

        left = divider.querySelector('.manifesto-divider-left');
        right = divider.querySelector('.manifesto-divider-right');

        if (!left) {
            left = document.createElement('span');
            left.className = 'manifesto-divider-half manifesto-divider-left';
            divider.appendChild(left);
        }

        if (!right) {
            right = document.createElement('span');
            right.className = 'manifesto-divider-half manifesto-divider-right';
            divider.appendChild(right);
        }

        return divider;
    }

    function ensureDropDivider(title) {
        var inner = title && title.closest ? title.closest('.main-manifesto-inner') : null;
        var drop;

        if (!inner) return null;

        drop = inner.querySelector(':scope > .manifesto-divider-drop');
        if (!drop) {
            drop = document.createElement('span');
            drop.className = 'manifesto-divider-drop';
            drop.setAttribute('aria-hidden', 'true');
            inner.appendChild(drop);
        }

        return drop;
    }

    function positionDropAtDivider(drop, divider) {
        var inner;
        var innerRect;
        var dividerRect;

        if (!drop || !divider) return false;
        inner = drop.parentNode;
        if (!inner || typeof inner.getBoundingClientRect !== 'function') return false;

        innerRect = inner.getBoundingClientRect();
        dividerRect = divider.getBoundingClientRect();
        drop.style.top = Math.round(dividerRect.top - innerRect.top) + 'px';
        drop.style.left = '0px';
        drop.style.width = Math.round(innerRect.width) + 'px';
        return true;
    }

    function readDividerGap(divider) {
        var portal = divider && divider.closest ? divider.closest('.main-portal') : null;
        var value = 8;

        if (portal && window.getComputedStyle) {
            value = parseFloat(
                window.getComputedStyle(portal).getPropertyValue('--manifesto-divider-gap')
            );
        }

        return Number.isFinite(value) ? Math.max(0, Math.round(value)) : 8;
    }

    function measureDropGeometry(copy, divider) {
        var lines = Array.prototype.slice.call(copy.querySelectorAll('.manifesto-line'));
        var savedStyles = lines.map(function (line) {
            return line.getAttribute('style');
        });
        var dividerRect;
        var copyRect;
        var lineRects;
        var gapsBefore;
        var endGap = readDividerGap(divider);
        var distance;

        dividerRect = divider.getBoundingClientRect();

        lines.forEach(function (line) {
            line.style.height = LINE_HEIGHT + 'px';
            line.style.maxHeight = LINE_HEIGHT + 'px';
            line.style.overflow = 'visible';
            line.style.clipPath = 'inset(0 0 0 0)';
        });

        copyRect = copy.getBoundingClientRect();
        lineRects = lines.map(function (line) { return line.getBoundingClientRect(); });
        gapsBefore = lineRects.map(function (rect, index) {
            if (index === 0) return Math.max(0, rect.top - dividerRect.top);
            return Math.max(0, rect.top - lineRects[index - 1].bottom);
        });
        distance = lineRects.length
            ? Math.max(0, Math.round(
                lineRects[lineRects.length - 1].bottom - dividerRect.top + endGap
            ))
            : Math.max(0, Math.round(copyRect.bottom - dividerRect.top + endGap));

        lines.forEach(function (line, index) {
            if (savedStyles[index] === null) line.removeAttribute('style');
            else line.setAttribute('style', savedStyles[index]);
        });

        return {
            originTop: dividerRect.top,
            gapsBefore: gapsBefore,
            endGap: endGap,
            finalDistance: distance
        };
    }

    function measureDropDistance(copy, divider) {
        return measureDropGeometry(copy, divider).finalDistance;
    }

    function applyInitialFrame(portal, title, copy, divider, lines) {
        portal.classList.remove('manifesto-intro-complete');
        portal.classList.add('manifesto-intro-prepared');
        document.documentElement.classList.add('manifesto-intro-active');

        title.style.opacity = '0';
        divider.style.left = '0';
        divider.style.width = '100%';
        divider.style.background = 'transparent';

        Array.prototype.forEach.call(
            divider.querySelectorAll('.manifesto-divider-half'),
            function (half) {
                half.style.transform = 'scaleX(0)';
            }
        );

        lines.forEach(function (line) {
            var inner = line.querySelector('.manifesto-line-inner');

            line.style.height = '0px';
            line.style.maxHeight = '0px';
            line.style.opacity = '0';
            line.style.overflow = 'hidden';
            line.style.clipPath = 'inset(0 0 100% 0)';

            if (inner) {
                inner.style.opacity = '0.18';
                inner.style.transform = 'translateY(-10px)';
            }
        });

        copy.style.visibility = 'visible';

        /* 초기 프레임을 실제 렌더 트리에 확정한 뒤 pending을 해제한다. */
        portal.getBoundingClientRect();
        lines.forEach(function (line) { line.getBoundingClientRect(); });
        document.documentElement.classList.remove('manifesto-intro-pending');
    }

    function finishAnimation(animation, element, finalStyles) {
        if (!animation || !animation.finished) return Promise.resolve();

        return animation.finished.catch(function () {
            return null;
        }).then(function () {
            Object.keys(finalStyles).forEach(function (property) {
                element.style[property] = finalStyles[property];
            });
            animation.cancel();
        });
    }

    function animateTitle(title) {
        var animation = title.animate([
            { opacity: 0 },
            { opacity: 1 }
        ], {
            duration: TITLE_DURATION,
            delay: TITLE_DELAY,
            easing: 'cubic-bezier(.18,.72,.24,1)',
            fill: 'both'
        });

        return finishAnimation(animation, title, { opacity: '1' });
    }

    function animateDivider(divider) {
        var halves = divider.querySelectorAll('.manifesto-divider-half');
        var animations = [];

        /*
        최종 길이의 선이나 트랙을 미리 그리지 않는다.
        중앙에서 시작하는 좌·우 두 실선 자체만 바깥 방향으로 펼친다.
        */
        Array.prototype.forEach.call(halves, function (half) {
            var animation = half.animate([
                { transform: 'scaleX(0)' },
                { transform: 'scaleX(1)' }
            ], {
                duration: DIVIDER_DURATION,
                delay: DIVIDER_DELAY,
                easing: 'cubic-bezier(.2,.78,.22,1)',
                fill: 'both'
            });

            animations.push(finishAnimation(animation, half, {
                transform: 'scaleX(1)'
            }));
        });

        return Promise.all(animations);
    }

    function stopDropTracking(drop, finishAtEnd) {
        var resolve;

        if (!drop) return;
        if (drop.__manifestoDropTimer) {
            window.clearTimeout(drop.__manifestoDropTimer);
            drop.__manifestoDropTimer = 0;
        }
        if (drop.__manifestoDropFrame) {
            window.cancelAnimationFrame(drop.__manifestoDropFrame);
            drop.__manifestoDropFrame = 0;
        }

        if (finishAtEnd && Number.isFinite(drop.__manifestoDropFinalDistance)) {
            drop.style.opacity = '1';
            drop.style.transform = 'translateY(' + Math.round(drop.__manifestoDropFinalDistance) + 'px)';
        }

        resolve = drop.__manifestoDropResolve;
        drop.__manifestoDropResolve = null;
        if (typeof resolve === 'function') resolve();
    }

    function animateDropDivider(drop, copy, geometry, lines) {
        var splitAt = DIVIDER_DELAY + DIVIDER_DURATION;
        var finalDistance = geometry ? geometry.finalDistance : 0;

        if (!drop || !copy || !geometry) return Promise.resolve();

        /*
        위 구분선이 100%가 되는 순간 같은 위치에서 두 번째 선을 드러낸다.
        두 번째 선에는 독립 easing을 주지 않는다. 매 프레임 실제 copy 높이와
        첫 행의 열린 높이를 읽어 현재 본문 하단만 따라가게 한다.
        */
        stopDropTracking(drop, false);
        drop.__manifestoDropFinalDistance = finalDistance;

        return new Promise(function (resolve) {
            var lastDistance = 0;
            var settled = false;

            function finish(atEnd) {
                if (settled) return;
                settled = true;
                drop.__manifestoDropResolve = null;
                if (atEnd) {
                    drop.style.opacity = '1';
                    drop.style.transform = 'translateY(' + Math.round(finalDistance) + 'px)';
                }
                resolve();
            }

            function track() {
                var distance = 0;
                var visibleProgress = 0;

                if (!drop.isConnected || !copy.isConnected) {
                    finish(false);
                    return;
                }

                lines.forEach(function (line, index) {
                    var height = line.getBoundingClientRect().height;
                    var progress = Math.max(0, Math.min(1, height / LINE_HEIGHT));

                    /* 각 행 앞의 실제 여백도 그 행이 열리는 비율만큼만 함께 연다. */
                    distance += ((geometry.gapsBefore[index] || 0) * progress) + height;
                    visibleProgress = Math.max(visibleProgress, progress);
                });

                /* 마지막으로 보이는 행과 하강선 사이에도 같은 공통 간격을 유지한다. */
                distance += (geometry.endGap || 0) * visibleProgress;

                distance = Math.max(lastDistance, Math.min(finalDistance, Math.round(distance)));
                lastDistance = distance;

                drop.style.opacity = '1';
                drop.style.transform = 'translateY(' + distance + 'px)';

                if (distance >= finalDistance) {
                    finish(true);
                    return;
                }

                drop.__manifestoDropFrame = window.requestAnimationFrame(track);
            }

            drop.__manifestoDropResolve = function () { finish(false); };
            drop.__manifestoDropTimer = window.setTimeout(function () {
                drop.__manifestoDropTimer = 0;
                drop.style.opacity = '1';
                drop.__manifestoDropFrame = window.requestAnimationFrame(track);
            }, splitAt);
        });
    }

    function animateLine(line, index) {
        var delay = BODY_DELAY + (index * LINE_STAGGER);
        var inner = line.querySelector('.manifesto-line-inner');
        var outerAnimation = line.animate([
            {
                height: '0px',
                maxHeight: '0px',
                opacity: 0,
                clipPath: 'inset(0 0 100% 0)'
            },
            {
                offset: 0.42,
                height: '11px',
                maxHeight: '11px',
                opacity: 0.56,
                clipPath: 'inset(0 0 58% 0)'
            },
            {
                height: LINE_HEIGHT + 'px',
                maxHeight: LINE_HEIGHT + 'px',
                opacity: 1,
                clipPath: 'inset(0 0 0 0)'
            }
        ], {
            duration: LINE_DURATION,
            delay: delay,
            easing: 'cubic-bezier(.16,.76,.22,1)',
            fill: 'both'
        });
        var promises = [finishAnimation(outerAnimation, line, {
            height: LINE_HEIGHT + 'px',
            maxHeight: LINE_HEIGHT + 'px',
            opacity: '1',
            overflow: 'visible',
            clipPath: 'inset(0 0 0 0)'
        })];

        if (inner) {
            var innerAnimation = inner.animate([
                { transform: 'translateY(-10px)', opacity: 0.18 },
                { offset: 0.5, transform: 'translateY(-3px)', opacity: 0.72 },
                { transform: 'translateY(0)', opacity: 1 }
            ], {
                duration: LINE_DURATION,
                delay: delay,
                easing: 'cubic-bezier(.16,.76,.22,1)',
                fill: 'both'
            });

            promises.push(finishAnimation(innerAnimation, inner, {
                transform: 'translateY(0)',
                opacity: '1'
            }));
        }

        return Promise.all(promises);
    }

    function showImmediately(portal) {
        var title = portal && portal.querySelector(TITLE_SELECTOR);
        var copy = portal && portal.querySelector(COPY_SELECTOR);
        var divider = null;
        var drop = null;
        var dropDistance = 0;

        clearWatchdog(portal);

        if (portal) {
            Array.prototype.forEach.call(
                portal.querySelectorAll(
                    '.main-manifesto-title, .manifesto-divider-half, ' +
                    '.manifesto-divider-drop, .manifesto-line, .manifesto-line-inner'
                ),
                function (node) {
                    if (node.classList && node.classList.contains('manifesto-divider-drop')) {
                        stopDropTracking(node, false);
                    }
                    if (typeof node.getAnimations !== 'function') return;
                    node.getAnimations().forEach(function (animation) {
                        try { animation.cancel(); } catch (err) {}
                    });
                }
            );
        }

        if (title) {
            try {
                sanitizeDividerSources(title);
                divider = ensureDivider(title);
                drop = ensureDropDivider(title);
            } catch (err) {}
            title.style.opacity = '1';
            title.style.borderBottom = '0';
        }

        if (divider) {
            divider.style.background = 'transparent';
            Array.prototype.forEach.call(
                divider.querySelectorAll('.manifesto-divider-half'),
                function (half) {
                    half.style.transform = 'scaleX(1)';
                }
            );
        }

        if (copy) {
            copy.style.visibility = 'visible';
            Array.prototype.forEach.call(copy.querySelectorAll('.manifesto-line'), function (line) {
                line.style.height = LINE_HEIGHT + 'px';
                line.style.maxHeight = LINE_HEIGHT + 'px';
                line.style.opacity = '1';
                line.style.overflow = 'visible';
                line.style.clipPath = 'inset(0 0 0 0)';
            });
            Array.prototype.forEach.call(copy.querySelectorAll('.manifesto-line-inner'), function (inner) {
                inner.style.opacity = '1';
                inner.style.transform = 'translateY(0)';
            });
        }

        if (drop && divider && copy) {
            positionDropAtDivider(drop, divider);
            dropDistance = measureDropDistance(copy, divider);
            drop.style.opacity = '1';
            drop.style.transform = 'translateY(' + dropDistance + 'px)';
        }

        if (portal) {
            portal.classList.remove('manifesto-intro-prepared');
            portal.classList.remove('manifesto-intro-running');
            portal.classList.add('manifesto-intro-complete');
            setState(portal, 'complete');
            portal.setAttribute('data-manifesto-intro-played', '1');
        }

        disarmPending();
    }

    function prepare(portal) {
        var title = portal.querySelector(TITLE_SELECTOR);
        var copy = portal.querySelector(COPY_SELECTOR);
        var lines;
        var divider;
        var drop;
        var dropGeometry;

        if (!title || !copy) return null;

        sanitizeDividerSources(title);

        try {
            lines = buildRenderedLines(copy);
            divider = ensureDivider(title);
            drop = ensureDropDivider(title);
        } catch (error) {
            restoreOriginalText(copy);
            return null;
        }

        if (!lines.length || !divider || !drop) {
            restoreOriginalText(copy);
            return null;
        }

        applyInitialFrame(portal, title, copy, divider, lines);
        drop.style.opacity = '0';
        drop.style.transform = 'translateY(0px)';
        positionDropAtDivider(drop, divider);
        dropGeometry = measureDropGeometry(copy, divider);

        return {
            title: title,
            copy: copy,
            divider: divider,
            drop: drop,
            dropGeometry: dropGeometry,
            lines: lines
        };
    }

    function play(portal, prepared) {
        var animations = [];

        setState(portal, 'running');
        portal.setAttribute('data-manifesto-intro-played', '1');

        animations.push(animateTitle(prepared.title));
        animations.push(animateDivider(prepared.divider));
        animations.push(animateDropDivider(
            prepared.drop,
            prepared.copy,
            prepared.dropGeometry,
            prepared.lines
        ));

        prepared.lines.forEach(function (line, index) {
            animations.push(animateLine(line, index));
        });

        return Promise.all(animations).then(function () {
            portal.classList.add('manifesto-intro-complete');
            portal.classList.remove('manifesto-intro-prepared');
            portal.classList.remove('manifesto-intro-running');
            setState(portal, 'complete');
            clearWatchdog(portal);
            document.documentElement.classList.remove('manifesto-intro-active');
            return true;
        });
    }

    function begin(portal) {
        if (!portal || !portal.isConnected) return Promise.resolve(false);

        var state = getState(portal);
        if (state === 'complete') {
            return Promise.resolve(false);
        }

        if (state === 'preparing' || state === 'running') {
            armWatchdog(portal);
            return Promise.resolve(false);
        }

        if (prefersReducedMotion() || typeof Element.prototype.animate !== 'function') {
            showImmediately(portal);
            return Promise.resolve(false);
        }

        setState(portal, 'preparing');
        armWatchdog(portal);

        return waitForFont().then(function () {
            if (!portal.isConnected) {
                clearWatchdog(portal);
                disarmPending();
                return false;
            }

            var prepared = prepare(portal);
            if (!prepared) {
                showImmediately(portal);
                return false;
            }

            portal.classList.add('manifesto-intro-running');

            return new Promise(function (resolve) {
                window.requestAnimationFrame(function () {
                    window.requestAnimationFrame(function () {
                        play(portal, prepared).then(resolve).catch(function () {
                            showImmediately(portal);
                            resolve(false);
                        });
                    });
                });
            });
        }).catch(function () {
            showImmediately(portal);
            return false;
        });
    }

    function init(root) {
        var portal = findPortal(root);

        if (!portal) {
            return Promise.resolve(false);
        }

        return begin(portal);
    }

    function observeForPortal() {
        if (observer || !window.MutationObserver || !document.documentElement) return;

        observer = new MutationObserver(function () {
            var portal = document.querySelector(PORTAL_SELECTOR);
            if (!portal) return;
            init(portal);
        });

        observer.observe(document.documentElement, {
            childList: true,
            subtree: true
        });
    }

    function boot(root) {
        init(root || document);
        observeForPortal();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () {
            boot(document);
        }, { once: true });
    } else {
        boot(document);
    }

    window.addEventListener('load', function () {
        boot(document);
    }, { once: true });

    window.addEventListener('pageshow', function () {
        boot(document);
    });

    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function ($content) {
            boot($content && $content[0] ? $content[0] : document);
        });
    }

    window.ManifestoIntro = {
        version: '20260713-divider-equal-gap-005',
        init: init,
        status: function () {
            var portal = document.querySelector(PORTAL_SELECTOR);
            return {
                loaded: true,
                portal: !!portal,
                state: portal ? getState(portal) : '',
                prepared: !!(portal && portal.classList.contains('manifesto-intro-prepared')),
                running: !!(portal && portal.classList.contains('manifesto-intro-running')),
                complete: !!(portal && portal.classList.contains('manifesto-intro-complete')),
                lineCount: portal ? portal.querySelectorAll('.manifesto-line').length : 0
            };
        }
    };
}(window, document, window.mw));