Zum Inhalt springen

MediaWiki:Common.js: Unterschied zwischen den Versionen

Aus transformal GmbH
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
 
(3 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 1: Zeile 1:
/* === PREPRINT START === */
/* === PREPRINT START === */
/* ============================================
/* ============================================
   PREPRINT v7.17 - JS v260217r1
   PREPRINT v7.17 - JS v260225r1
   ============================================ */
   ============================================ */


Zeile 893: Zeile 893:
             // === PHASE 1: CLEAN + READ ===
             // === PHASE 1: CLEAN + READ ===
              
              
             // Remove all previous modifications
             // Remove all previous-cycle artifacts
             $('.preprint-oc-spacer').remove();
             $('.preprint-oc-spacer').remove();
             if (layout && layout[0]) layout[0].style.paddingBottom = '';
             if (layout && layout[0]) layout[0].style.paddingBottom = '';
              
              
            // Reset OC positions
             linkage.forEach(function(link) {
             linkage.forEach(function(link) {
                 link.element[0].style.top = '0px';
                 if (link.element[0].style.position !== 'absolute') {
                    link.element[0].style.position = 'absolute';
                    link.element[0].style.top = '0px';
                }
             });
             });
              
              
             // Force reflow
             // Force reflow after cleanup
             void document.body.offsetHeight;
             void document.body.offsetHeight;
                          
                          
Zeile 1.088: Zeile 1.090:
                 subtree: true, attributes: true, attributeFilter: ['class']
                 subtree: true, attributes: true, attributeFilter: ['class']
             });
             });
           
            // ResizeObserver: catches container width changes not visible to
            // window resize (TOC pin/unpin, tool menu toggle, DevTools panel).
            // Created once, never disconnected — only fires on actual size change.
            if (!layout._resizeObserver && typeof ResizeObserver !== 'undefined') {
                layout._resizeObserver = new ResizeObserver(function() {
                    clearTimeout(layout._resizeDebounce);
                    layout._resizeDebounce = setTimeout(function() {
                        if (PAL.isOrientationLocked()) return;
                        if (document.body.classList.contains('preprint-landscape')) {
                            reconcileLayout();
                        }
                    }, 200);
                });
                layout._resizeObserver.observe(layout[0]);
            }
         }
         }


Zeile 1.245: Zeile 1.263:
             getPositionAnchor: getPositionAnchor,
             getPositionAnchor: getPositionAnchor,
             getLayout: function() { return layout; },
             getLayout: function() { return layout; },
             set returnTarget(id) { returnTarget = id; }
             set returnTarget(id) { returnTarget = id; },
            pauseObserver: function() {
                if (layout && layout._ocObserver) layout._ocObserver.disconnect();
            },
            resumeObserver: function() {
                if (layout && layout[0] && layout._ocObserver) {
                    layout._ocObserver.observe(layout[0], {
                        subtree: true, attributes: true, attributeFilter: ['class']
                    });
                }
            }
         };
         };
     })();
     })();
Zeile 1.628: Zeile 1.656:
         setTimeout(function() {
         setTimeout(function() {
             Model.applyProjection(initialMode);
             Model.applyProjection(initialMode);
            Model.pauseObserver();
             $('.preprint-layout').addClass('preprint-ready');
             $('.preprint-layout').addClass('preprint-ready');
            Model.resumeObserver();
             window.preprintReadyAt = performance.now();
             window.preprintReadyAt = performance.now();
         }, 50);
         }, 50);
Zeile 1.726: Zeile 1.756:




/* Unified Consent System JavaScript v250911r1
 
/* === CONSENT START === */
/* Unified Consent System JavaScript v260224r1
  * Single file for both EN and DE versions
  * Single file for both EN and DE versions
  * Automatically detects language based on domain
  * Automatically detects language based on domain
Zeile 1.765: Zeile 1.797:
}
}


// Handle consent acquisition box - 90-day reminder for everyone
// Handle consent acquisition box - 90-day reminder for visitors (spec §8.9)
$(document).ready(function() {
$(document).ready(function() {
     var consentBox = document.getElementById('consent-acquisition-box');
     var consentBox = document.getElementById('consent-acquisition-box');
Zeile 1.787: Zeile 1.819:
         // ADD THIS LINE:
         // ADD THIS LINE:
         localStorage.setItem('consent-acquisition-dismissed-until', Date.now() + (90 * 24 * 60 * 60 * 1000));
         localStorage.setItem('consent-acquisition-dismissed-until', Date.now() + (90 * 24 * 60 * 60 * 1000));
         var targetPage = getCurrentLanguage() === 'de' ?  
         if (mw.config.get('wgUserName')) {
            '/wiki/Transformal_GmbH:Einstellungen' :  
            window.location.href = '/wiki/Special:Preferences#mw-prefsection-legal';
            '/wiki/Transformal_GmbH:Settings';
        } else {
        window.location.href = targetPage;
            var targetPage = getCurrentLanguage() === 'de' ?  
                '/wiki/Transformal_GmbH:Einstellungen' :  
                '/wiki/Transformal_GmbH:Settings';
            window.location.href = targetPage;
        }
     });
     });
});
});
Zeile 1.888: Zeile 1.924:
     }
     }
}
}
/* === CONSENT END === */
/* === CONDITIONAL START === */


/* Template 'ConditionalContent' - v250925r8 */
/* Template 'ConditionalContent' - v250925r8 */
Zeile 1.948: Zeile 1.988:
     }
     }
});
});
/* === CONDITIONAL END === */

Aktuelle Version vom 1. März 2026, 19:53 Uhr

/* === PREPRINT START === */
/* ============================================
   PREPRINT v7.17 - JS v260225r1
   ============================================ */

   (function() {
    'use strict';

    // Prevent repeated initialization
    if (window.preprintInitialized) {
        return;
    }
    window.preprintInitialized = true;

    /* ============================================
       PLATFORM ADAPTATION LAYER (PAL)
       
       Contracts:
       - PAL.ready(): Promise that resolves when platform is stable
       - PAL.getDeviceWidth(): returns actual device width in CSS pixels
       - PAL.isNarrow(): returns true if width < 768
       - PAL.onProjectionChange(callback): fires when orientation stable
       ============================================ */

    var PAL = (function() {
        var isMinerva = mw.config.get('skin') === 'minerva';
        var viewportSource = 'unknown';
        var domStabilized = false;
        var imagesStabilized = false;
        
        // Debug logging with source tracking (silent in production)
        function log(adapter, message) {
        }

        /* --- G.2.1 DOM Readiness Adapter --- */
        
        function waitForDOMStability() {
            return new Promise(function(resolve) {
                if (!isMinerva) {
                    // Desktop: DOM is ready immediately after document.ready
                    domStabilized = true;
                    log('DOM', 'Desktop mode, DOM ready');
                    resolve();
                    return;
                }
                
                // Minerva: observe mutations, unwrap sections as they appear
                var debounceTimer = null;
                var timeoutTimer = null;
                var observer = null;
                
                function cleanup() {
                    if (observer) observer.disconnect();
                    clearTimeout(debounceTimer);
                    clearTimeout(timeoutTimer);
                }
                
                function done(reason) {
                    cleanup();
                    disableMinervaCollapse();
                    domStabilized = true;
                    log('DOM', reason);
                    resolve();
                }
                
                function unwrapSections() {
                    var sections = $('.mw-parser-output > section');
                    if (sections.length > 0) {
                        sections.each(function() {
                            $(this).replaceWith($(this).contents());
                        });
                        log('DOM', 'Unwrapped ' + sections.length + ' sections');
                        return true;
                    }
                    return false;
                }
                
                function resetDebounce() {
                    clearTimeout(debounceTimer);
                    debounceTimer = setTimeout(function() {
                        done('Stable (100ms silence)');
                    }, 100);
                }
                
                var parserOutput = document.querySelector('.mw-parser-output');
                if (!parserOutput) {
                    done('No .mw-parser-output found');
                    return;
                }
                
                observer = new MutationObserver(function() {
                    unwrapSections();
                    resetDebounce();
                });
                observer.observe(parserOutput, { childList: true, subtree: true });
                
                // Unwrap any sections already present
                unwrapSections();
                
                // Start silence timer
                resetDebounce();
                
                // Timeout fallback: 1000ms
                timeoutTimer = setTimeout(function() {
                    done('Timeout (1000ms fallback)');
                }, 1000);
            });
        }
        
        function disableMinervaCollapse() {
            function sweep() {
                $('.mw-parser-output section').removeClass('collapsible-block');
                $('[hidden]').removeAttr('hidden');
                $('.collapsible-heading').removeClass('collapsible-heading');
                $('.section-heading').removeClass('section-heading');
                $('.mf-section-hidden').removeClass('mf-section-hidden');
                $('.toggle-indicator, .mf-icon').remove();
            }
            sweep();
            // Minerva re-creates collapse after PAL cleanup; CSS hides
            // instantly (no flash), delayed sweep cleans DOM structure
            setTimeout(sweep, 2000);
            log('DOM', 'Minerva collapse disabled (CSS+delayed sweep)');
        }

        /* --- G.2.2 Viewport Adapter --- */
        
        function initViewport() {
            if (!isMinerva) {
                viewportSource = 'standard';
                log('Viewport', 'Using window.innerWidth (standard)');
                return;
            }
            
            // Check if Minerva has set problematic viewport
            var viewportMeta = $('meta[name="viewport"]');
            var content = viewportMeta.attr('content') || '';
            
            if (content.indexOf('width=1120') !== -1 || 
                content.indexOf('width=980') !== -1) {
                // Override to device-width
                viewportMeta.attr('content', 'width=device-width, initial-scale=1');
                viewportSource = 'minerva-override';
                log('Viewport', 'Overrode Minerva viewport meta');
            } else {
                viewportSource = 'minerva-native';
                log('Viewport', 'Minerva viewport already sane');
            }
            
            // Add data attribute for debugging
            document.body.setAttribute('data-viewport-source', viewportSource);
        }
        
        function getDeviceWidth() {
            if (isMinerva) {
                // Minerva: always use screen.width (viewport meta unreliable)
                return screen.width;
            }
            
            // Desktop: use innerWidth if sane, fallback to screen.width
            if (window.innerWidth > 0 && window.innerWidth < screen.width * 1.5) {
                return window.innerWidth;
            }
            return screen.width;
        }
        
        function isNarrow() {
            return getDeviceWidth() < 768;
        }

        /* --- G.2.3 Image Stability Adapter --- */
        
        function ensureImageStability() {
            return new Promise(function(resolve) {
                if (!isMinerva) {
                    // Desktop: images load normally
                    imagesStabilized = true;
                    log('Image', 'Desktop mode, no lazy placeholders');
                    resolve();
                    return;
                }
                
                var placeholders = $('.mw-parser-output .lazy-image-placeholder');
                if (placeholders.length === 0) {
                    imagesStabilized = true;
                    log('Image', 'No lazy placeholders found');
                    resolve();
                    return;
                }
                
                var total = placeholders.length;
                var loaded = 0;
                var failed = 0;
                var timeout = null;
                
                function checkComplete() {
                    if (loaded + failed >= total) {
                        clearTimeout(timeout);
                        imagesStabilized = true;
                        log('Image', 'Complete: ' + loaded + ' loaded, ' + failed + ' failed');
                        resolve();
                    }
                }
                
                placeholders.each(function() {
                    var $placeholder = $(this);
                    var src = $placeholder.attr('data-mw-src');
                    var width = $placeholder.attr('data-width');
                    var height = $placeholder.attr('data-height');
                    var imgClass = $placeholder.attr('data-class') || '';
                    
                    if (!src) {
                        failed++;
                        checkComplete();
                        return;
                    }
                    
                    var $img = $('<img>')
                        .attr('width', width)
                        .attr('height', height)
                        .addClass(imgClass)
                        .on('load', function() {
                            loaded++;
                            checkComplete();
                        })
                        .on('error', function() {
                            failed++;
                            $(this).addClass('preprint-image-failed');
                            checkComplete();
                        })
                        .attr('src', src);  // Set src last to trigger load
                    
                    $placeholder.replaceWith($img);
                });
                
                // Timeout fallback: 2000ms
                timeout = setTimeout(function() {
                    log('Image', 'Timeout: ' + loaded + ' loaded, ' + (total - loaded) + ' pending');
                    imagesStabilized = true;
                    resolve();
                }, 2000);
            });
        }

        /* --- G.2.5 TOC Stability Adapter (Minerva) --- */
        
        function waitForTOCStability() {
            return new Promise(function(resolve) {
                if (!isMinerva) {
                    log('TOC', 'Desktop mode, no wait needed');
                    resolve();
                    return;
                }
                
                var attempts = 0;
                var maxAttempts = 20;  // 20 * 50ms = 1000ms max
                
                function poll() {
                    attempts++;
                    if ($('.toc').length > 0) {
                        log('TOC', 'Found after ' + attempts + ' polls');
                        resolve();
                    } else if (attempts >= maxAttempts) {
                        log('TOC', 'Timeout, no TOC found (OK for pages without headings)');
                        resolve();
                    } else {
                        setTimeout(poll, 50);
                    }
                }
                
                poll();
            });
        }

        /* --- G.2.4 Orientation Adapter --- */
        
        var orientationCallbacks = [];
        var preChangeCallbacks = [];
        var currentOrientation = null;
        var orientationLocked = false;
        
        function initOrientationHandler() {
            currentOrientation = isNarrow() ? 'portrait' : 'landscape';
            setOrientationClass(currentOrientation);
            
            var resizeTimer = null;
            var transitionGeneration = 0;
            
            var resizeTimer = null;
            var transitionGeneration = 0;
            
            $(window).on('resize orientationchange', function() {
                var targetOrientation = isNarrow() ? 'portrait' : 'landscape';
                
                if (targetOrientation === currentOrientation) {
                    // Cancel any in-flight transition (timer or promise)
                    if (orientationLocked) {
                        clearTimeout(resizeTimer);
                        transitionGeneration++;
                        setOrientationClass(currentOrientation);
                        orientationLocked = false;
                        // Restore layout visibility (pre-change hid it)
                        var el = document.querySelector('.preprint-layout');
                        if (el) { el.style.transition = 'opacity 0.15s'; el.style.opacity = '1'; }
                        log('Orientation', 'Cancelled pending transition, staying ' + currentOrientation);
                    }
                    return;
                }
                
                // Fire pre-change callbacks IMMEDIATELY (anchor capture)
                preChangeCallbacks.forEach(function(cb) {
                    try {
                        cb({ from: currentOrientation, to: targetOrientation });
                    } catch (e) {
                        console.error('Preprint: Pre-change callback error', e);
                    }
                });
                
                // Lock during transition, invalidate any in-flight promise
                orientationLocked = true;
                clearTimeout(resizeTimer);
                var myGeneration = ++transitionGeneration;
                
                resizeTimer = setTimeout(function() {
                    setOrientationClass(targetOrientation);
                    
                    // Force-load images if going to landscape and not yet loaded
                    var imagePromise = (targetOrientation === 'landscape' && !imagesStabilized)
                        ? ensureImageStability()
                        : Promise.resolve();
                    
                    Promise.all([waitForLayoutStability(targetOrientation), imagePromise]).then(function() {
                        // Stale transition: a newer one superseded us
                        if (myGeneration !== transitionGeneration) {
                            log('Orientation', 'Dropped stale transition (gen ' + myGeneration + ', current ' + transitionGeneration + ')');
                            return;
                        }
                        
                        var previousOrientation = currentOrientation;
                        currentOrientation = targetOrientation;
                        
                        // Fire callbacks
                        var event = {
                            from: previousOrientation,
                            to: targetOrientation
                        };
                        orientationCallbacks.forEach(function(cb) {
                            try {
                                cb(event);
                            } catch (e) {
                                console.error('Preprint: Orientation callback error', e);
                            }
                        });
                        
                        orientationLocked = false;
                    });
                }, 150);
            });

            // Wake-detect: device sleep doesn't fire resize/orientationchange
            document.addEventListener('visibilitychange', function() {
                if (document.visibilityState !== 'visible') return;
                var targetOrientation = isNarrow() ? 'portrait' : 'landscape';
                if (targetOrientation !== currentOrientation) {
                    log('Orientation', 'Wake: ' + currentOrientation + ' -> ' + targetOrientation);
                    $(window).trigger('resize');
                }
            });
        }
        
        function waitForLayoutStability(targetOrientation) {
            return new Promise(function(resolve) {
                // Set class first so CSS applies
                setOrientationClass(targetOrientation);
                
                var layout = document.querySelector('.preprint-layout');
                if (!layout) {
                    resolve();
                    return;
                }
                
                var lastWidth = -1;
                var lastHeight = -1;
                var stableCount = 0;
                var attempts = 0;
                var maxAttempts = 20;  // 20 * 50ms = 1000ms max
                
                function poll() {
                    attempts++;
                    var rect = layout.getBoundingClientRect();
                    
                    if (rect.width === lastWidth && rect.height === lastHeight) {
                        stableCount++;
                        if (stableCount >= 2) {
                            // Stable for 100ms
                            log('Orientation', 'Layout stable after ' + attempts + ' polls');
                            resolve();
                            return;
                        }
                    } else {
                        stableCount = 0;
                        lastWidth = rect.width;
                        lastHeight = rect.height;
                    }
                    
                    if (attempts >= maxAttempts) {
                        log('Orientation', 'Timeout after ' + attempts + ' polls (fallback)');
                        resolve();
                        return;
                    }
                    
                    setTimeout(poll, 50);
                }
                
                // Start after a reflow
                requestAnimationFrame(function() {
                    poll();
                });
            });
        }
        
        function setOrientationClass(orientation) {
            if (orientation === 'portrait') {
                $('body').addClass('preprint-portrait').removeClass('preprint-landscape');
            } else {
                $('body').addClass('preprint-landscape').removeClass('preprint-portrait');
            }
        }
        
        function onProjectionChange(callback) {
            orientationCallbacks.push(callback);
        }
        
        function onPreProjectionChange(callback) {
            preChangeCallbacks.push(callback);
        }
        
        function isOrientationLocked() {
            return orientationLocked;
        }

        /* --- PAL.ready() --- */
        
        function ready() {
            return new Promise(function(resolve) {
                $(document).ready(function() {
                    // Check activation first
                    if (!$('#preprint-magic').length) {
                        log('ready', 'No #preprint-magic, skipping');
                        resolve({ activated: false });
                        return;
                    }
                    
                    if ($('#mw-content-text').hasClass('preprint-processed')) {
                        log('ready', 'Already processed, skipping');
                        resolve({ activated: false });
                        return;
                    }
                    
                    log('ready', 'Starting PAL initialization');
                    
                    // Sequential initialization
                    initViewport();
                    
                    waitForDOMStability()
                        .then(function() {
                            if (isNarrow()) {
                                // Portrait: TOC only, images load progressively
                                return waitForTOCStability();
                            }
                            // Landscape: images + TOC in parallel
                            return Promise.all([ensureImageStability(), waitForTOCStability()]);
                        })
                        .then(function() {

                            initOrientationHandler();
                            log('ready', 'PAL ready (Minerva=' + isMinerva + ')');
                            resolve({ 
                                activated: true,
                                isMinerva: isMinerva,
                                viewportSource: viewportSource
                            });
                        });
                });
            });
        }

        // Public API
        return {
            ready: ready,
            getDeviceWidth: getDeviceWidth,
            isNarrow: isNarrow,
            onProjectionChange: onProjectionChange,
            onPreProjectionChange: onPreProjectionChange,
            isOrientationLocked: isOrientationLocked,
            isMinerva: function() { return isMinerva; },
            disableMinervaCollapse: disableMinervaCollapse
        };
    })();

    /* ============================================
       MODEL LAYER
       
       Implements: Projection-Invariant Content Linkage
       
       - Dimensional Intent: IC, OC, LH, 2C (from markup)
       - Linkage Fabric: markers <-> elements (bidirectional)
       - Projection: landscape grid | portrait flow
       - User State: element-relative position tracking
       ============================================ */

    var Model = (function() {
        var linkage = [];  // Array of {marker: $marker, element: $element, type: string}
        var layout = null;
        var positionAnchor = null;  // {element: DOM, type: string, headingId: string}
        var returnTarget = null;

        /* --- Element Classification (Dimensional Intent) --- */
        
        function classifyElement($el) {
            // 2C types
            if ($el.hasClass('preprint-2c-wrapper')) return 'wrapper-2c';
            if ($el.hasClass('place-2c')) return 'place-2c';
            
            // OC types (will create linkage)
            if ($el.hasClass('place-oc')) return 'place-oc';
            if ($el.hasClass('place-lh')) return 'place-lh';
            if ($el.hasClass('preprint-table-oc-wrapper')) return 'table-oc';
            
            // IC types
            if ($el.hasClass('place-ic')) return 'place-ic';
            if ($el.hasClass('preprint-table-ic-wrapper')) return 'table-ic';
            
            // Structure
            if ($el.hasClass('book-nav') || $el.hasClass('book-progress')) return 'book-nav';
            if ($el.is('h1, h2, h3, h4, h5, h6')) return 'heading';
            
            // Skip
            if ($el.attr('id') === 'preprint-magic') return 'skip';
            
            // Default: content for IC
            return 'content';
        }

        /* --- Linkage Fabric --- */
        
        function createMarker($element, type) {
            var $marker = $('<span class="oc-marker"></span>');
            $marker.data('preprintTarget', $element);
            $marker.data('preprintType', type);
            
            // Reverse link: element knows its marker
            $element.data('preprintMarker', $marker);
            
            linkage.push({
                marker: $marker,
                element: $element,
                type: type
            });
            
            return $marker;
        }
        
        function getLinkage() {
            return linkage;
        }

        /* --- List Repair --- */
        
        // MW parser ejects template-generated spans (Place) from list items:
        //   Pattern A (start): <li class="mw-empty-elt"/> + <p><span>...</span>text</p>
        //   Pattern B (end):   <li>text</li> + <p><span>...</span></p>
        // Both followed optionally by a continuation list.
        // This repairs the DOM before structure analysis.
        function repairListPlacement() {
            var $output = $('.mw-parser-output');
            if (!$output.length) return;
            var repaired = 0;
            
            function findEmptyItem($list) {
                var $li = $list.find('li.mw-empty-elt').last();
                if ($li.length) return $li;
                var $dd = $list.find('dd').filter(function() {
                    return this.childNodes.length === 0;
                }).last();
                return $dd.length ? $dd : null;
            }
            
            function isNestingWrapper(el) {
                if (el.tagName !== 'LI') return false;
                if (el.children.length !== 1) return false;
                if (!/^(UL|OL|DL)$/.test(el.firstElementChild.tagName)) return false;
                for (var i = 0; i < el.childNodes.length; i++) {
                    if (el.childNodes[i].nodeType === 3 && el.childNodes[i].textContent.trim()) return false;
                }
                return true;
            }
            
            $output.find('p').each(function() {
                var p = this;
                if (!p.parentNode) return;
                var first = p.firstChild;
                if (!first || first.nodeType !== 1) return;
                if (!first.classList.contains('place-lh') && !first.classList.contains('place-oc')) return;
                
                var $p = $(p);
                var $prevList = $p.prev('ul, ol, dl');
                if (!$prevList.length) return;
                
                // Pattern A: empty item (Place at start of list item)
                var $target = findEmptyItem($prevList);
                var isPatternA = $target !== null;
                
                // Pattern B: no empty item (Place at end of list item)
                if (!$target) {
                    $target = $prevList.find('li').last();
                    if (!$target.length) $target = $prevList.find('dd').last();
                    if (!$target.length) return;
                }
                
                if (isPatternA) $target.removeClass('mw-empty-elt');
                
                // Move <p> contents into target item
                while (p.firstChild) {
                    $target[0].appendChild(p.firstChild);
                }
                
                // Merge continuation list if same type follows
                var $next = $p.next();
                if ($next.length && $next[0].tagName === $prevList[0].tagName) {
                    var targetAtRoot = $target[0].parentNode === $prevList[0];
                    
                    var children = $next.children().get();
                    for (var i = 0; i < children.length; i++) {
                        if (isNestingWrapper(children[i])) {
                            var subList = children[i].firstElementChild;
                            if (targetAtRoot) {
                                // Pattern B: sub-list becomes child of target
                                $target[0].appendChild(subList);
                            } else {
                                // Pattern A: sub-list items become siblings
                                var parentList = $target[0].parentNode;
                                while (subList.firstChild) {
                                    parentList.appendChild(subList.firstChild);
                                }
                            }
                        } else {
                            $prevList[0].appendChild(children[i]);
                        }
                    }
                    $next.remove();
                }
                
                $p.remove();
                repaired++;
            });
            
        }

        /* --- Structure Building --- */
        
        function buildStructure() {
            repairListPlacement();
            var parserOutput = $('.mw-parser-output');
            var elements = parserOutput.children().toArray();
            
            layout = $('<div class="preprint-layout"></div>');
            var currentInner = null;
            var currentOuter = null;
            var pendingOC = [];
            var bookNavElements = [];
            
            // Process each element
            elements.forEach(function(el) {
                var $el = $(el);
                var type = classifyElement($el);
                
                switch (type) {
                    case 'wrapper-2c':
                    case 'place-2c':
                        // 2C: flush columns, append at grid root
                        flushColumns();
                        layout.append($el);
                        break;
                    
                    case 'place-oc':
                    case 'place-lh':
                    case 'table-oc':
                        // OC types: create marker in IC, collect element
                        ensureColumns();
                        var marker = createMarker($el, type);
                        currentInner.append(marker);
                        pendingOC.push($el);
                        break;
                    
                    case 'book-nav':
                        // Collect for later insertion before layout
                        bookNavElements.push($el);
                        break;
                    
                    case 'heading':
                        // Headings create section boundaries
                        flushColumns();
                        ensureColumns();
                        currentInner.append($el);
                        break;
                    
                    case 'skip':
                        // Remove magic div
                        $el.remove();
                        break;
                    
                    default:
                        // Content: append to IC, check for nested OC
                        ensureColumns();
                        currentInner.append($el);
                        
                        // Find nested OC/LH elements
                        $el.find('.place-oc, .place-lh').each(function() {
                            var $nested = $(this);
                            var nestedType = $nested.hasClass('place-lh') ? 'place-lh' : 'place-oc';
                            var nestedMarker = createMarker($nested, nestedType);
                            $nested.before(nestedMarker);
                            pendingOC.push($nested.detach());
                        });
                }
            });
            
            // Final flush
            flushColumns();
            
            // Replace content
            parserOutput.empty().append(layout);
            
            // Prepend BookNav before layout
            bookNavElements.reverse().forEach(function($el) {
                layout.before($el);
            });
            
            // Mark processed
            $('#mw-content-text').addClass('preprint-processed');
            
            // Mark LH elements with media
            $('.place-lh').each(function() {
                var $lh = $(this);
                if ($lh.find('img, .lazy-image-placeholder, .mw-file-element').length > 0) {
                    $lh.addClass('lh-has-media');
                }
            });
                        
            // Helper functions (closure)
            function ensureColumns() {
                if (!currentInner) {
                    currentInner = $('<div class="inner-column"></div>');
                    currentOuter = $('<div class="outer-column"></div>');
                }
            }
            
            function flushColumns() {
                if (currentInner && currentInner.children().length > 0) {
                    // Move pending OC to outer column
                    pendingOC.forEach(function($el) {
                        currentOuter.append($el);
                    });
                    pendingOC = [];
                    
                    // Append columns
                    layout.append(currentInner);
                    layout.append(currentOuter);
                    
                    currentInner = null;
                    currentOuter = null;
                }
            }
            
            return layout;
        }

        /* --- Projection --- */
        
        function applyProjection(mode) {
            if (mode === 'portrait') {
                applyPortraitProjection();
            } else {
                applyLandscapeProjection();
            }
        }
        
        function applyPortraitProjection() {
            if (layout && layout[0]) layout[0].style.paddingBottom = '';
            $('.preprint-oc-spacer').remove();
            // Clear inline positioning set by landscape projection
            linkage.forEach(function(link) {
                link.element[0].style.top = '';
                link.element[0].style.position = '';
            });
            
            // Separate elements by portrait placement
            var topElements = [];
            var bottomElements = [];
            var defaultElements = [];
            
            linkage.forEach(function(link) {
                var placement = link.element.attr('data-portrait');
                if (placement === 'top') {
                    topElements.push(link);
                } else if (placement === 'bottom') {
                    bottomElements.push(link);
                } else {
                    defaultElements.push(link);
                }
            });
            
            // Default: move to marker position
            defaultElements.forEach(function(link) {
                link.element.detach();
                link.marker.after(link.element);
            });
            
            // Top: insert before first content in first IC
            if (topElements.length > 0) {
                var $firstIC = layout.find('.inner-column').first();
                if ($firstIC.length) {
                    var $firstChild = $firstIC.children().first();
                    topElements.forEach(function(link) {
                        link.element.detach();
                        if ($firstChild.length) {
                            $firstChild.before(link.element);
                        } else {
                            $firstIC.prepend(link.element);
                        }
                    });
                }
            }
            
            // Bottom: append after last content in last IC
            if (bottomElements.length > 0) {
                var $lastIC = layout.find('.inner-column').last();
                if ($lastIC.length) {
                    bottomElements.forEach(function(link) {
                        link.element.detach();
                        $lastIC.append(link.element);
                    });
                }
            }
            
            scrollToHash();
        }
        
        function applyLandscapeProjection() {
            // Move elements back to their correct outer columns via linkage
            linkage.forEach(function(link) {
                var $oc = link.marker.closest('.inner-column').next('.outer-column');
                if ($oc.length) {
                    link.element.detach();
                    $oc.append(link.element);
                }
            });
            
            // Reset scroll for accurate offset calculations
            var savedBodyScroll = document.body.scrollTop;
            var savedHtmlScroll = document.documentElement.scrollTop;
            document.body.scrollTop = 0;
            document.documentElement.scrollTop = 0;
            
            // Force reflow then position
            if (layout && layout[0]) {
                void layout[0].offsetHeight;
            }
            
            reconcileLayout();
            
            // Restore scroll
            document.body.scrollTop = savedBodyScroll;
            document.documentElement.scrollTop = savedHtmlScroll;
            
            scrollToHash();
        }
        
        function reconcileLayout() {
            if (!document.body.classList.contains('preprint-landscape')) return;
            if (reconcileLayout._running) return;
            reconcileLayout._running = true;
            
            // Disconnect observer to prevent re-entry from our writes
            if (layout._ocObserver) layout._ocObserver.disconnect();
            // Save and reset scroll for accurate offset calculations (Minerva scrolls body)
            var savedBodyScroll = document.body.scrollTop;
            var savedHtmlScroll = document.documentElement.scrollTop;
            document.body.scrollTop = 0;
            document.documentElement.scrollTop = 0;

            // === PHASE 1: CLEAN + READ ===
            
            // Remove all previous-cycle artifacts
            $('.preprint-oc-spacer').remove();
            if (layout && layout[0]) layout[0].style.paddingBottom = '';
            
            linkage.forEach(function(link) {
                if (link.element[0].style.position !== 'absolute') {
                    link.element[0].style.position = 'absolute';
                    link.element[0].style.top = '0px';
                }
            });
            
            // Force reflow after cleanup
            void document.body.offsetHeight;
                        
            // Read marker positions, OC container offsets, element geometry
            var items = [];
            linkage.forEach(function(link, i) {
                var $oc = link.element.closest('.outer-column');
                if (!$oc.length) { items.push(null); return; }
                
                var markerTop = link.marker.offset().top;
                var ocTop = $oc.offset().top;
                
                items.push({
                    index: i,
                    link: link,
                    markerTop: markerTop,
                    ocTop: ocTop,
                    ocEl: $oc[0],
                    height: link.element.outerHeight(true),
                    margin: parseFloat(link.element.css('margin-top')) || 0,
                    landscapeDir: link.element.attr('data-landscape') || null,
                    top: markerTop - ocTop,
                    docTop: markerTop
                });
            });
            
            // Read first visible IC child offset per OC container (for elevator)
            var ocContainers = [];
            var ocFirstICTop = [];
            $('.outer-column').each(function() {
                ocContainers.push(this);
                var $ic = $(this).prev('.inner-column');
                if (!$ic.length) { ocFirstICTop.push(0); return; }
                var ocTop = $(this).offset().top;
                var found = false;
                $ic.children().each(function() {
                    if (this.getBoundingClientRect().height > 0) {
                        ocFirstICTop.push($(this).offset().top - ocTop);
                        found = true;
                        return false;
                    }
                });
                if (!found) ocFirstICTop.push(0);
            });
            
            // Read protruding IC elements
            var protrusions = [];
            $('.inner-column').each(function() {
                var icWidth = this.getBoundingClientRect().width;
                $(this).children().each(function() {
                    if (this.classList.contains('oc-marker')) return;
                    var elWidth = this.scrollWidth || this.getBoundingClientRect().width;
                    if (elWidth > icWidth + 1) {
                        var top = $(this).offset().top;
                        protrusions.push({
                            element: this,
                            top: top,
                            bottom: top + this.getBoundingClientRect().height
                        });
                    }
                });
            });
            
            // === PHASE 2: COMPUTE (no DOM access) ===
            
            // Elevator adjustments
            items.forEach(function(item) {
                if (!item || !item.landscapeDir) return;
                
                var siblings = items.filter(function(other) {
                    return other && other.ocEl === item.ocEl && other.index !== item.index;
                }).sort(function(a, b) { return a.top - b.top; });
                
                if (item.landscapeDir === 'top') {
                    var above = null;
                    for (var i = siblings.length - 1; i >= 0; i--) {
                        if (siblings[i].top < item.top) { above = siblings[i]; break; }
                    }
                    if (above) {
                        item.top = above.top + above.height - item.margin;
                    } else {
                        var ocIdx = ocContainers.indexOf(item.ocEl);
                        if (ocIdx >= 0) {
                            item.top = ocFirstICTop[ocIdx] - item.margin;
                        }
                    }
                } else if (item.landscapeDir === 'bottom') {
                    var below = null;
                    for (var i = 0; i < siblings.length; i++) {
                        if (siblings[i].top > item.top) { below = siblings[i]; break; }
                    }
                    if (below) {
                        item.top = below.top - item.height;
                    }
                }
                item.docTop = item.ocTop + item.top;
            });
            
            // Fix overlaps (type-pair minimum gaps)
            var valid = items.filter(function(item) { return item !== null; });
            valid.sort(function(a, b) { return a.docTop - b.docTop; });
            
            function elementGap(item) {
                if (item.link.type === 'place-lh') return 2;
                if (item.height >= 50) return 16;
                return 4;
            }
            
            var lastItem = null;
            valid.forEach(function(item) {
                if (lastItem) {
                    var minGap = Math.max(elementGap(item), elementGap(lastItem));
                    var requiredTop = lastItem.docTop + lastItem.height + minGap;
                    var overlap = requiredTop - item.docTop;
                    if (overlap > 0) {
                        item.top += overlap;
                        item.docTop += overlap;
                    }
                }
                lastItem = item;
            });
            
            // Conflict detection: protruding IC vs computed OC positions
            var spacers = [];
            protrusions.forEach(function(prot) {
                var maxConflictBottom = 0;
                valid.forEach(function(item) {
                    var ocBottom = item.docTop + item.height;
                    if (prot.top < ocBottom && prot.bottom > item.docTop) {
                        if (ocBottom > maxConflictBottom) maxConflictBottom = ocBottom;
                    }
                });
                if (maxConflictBottom > 0) {
                    var pushdown = maxConflictBottom - prot.top;
                    if (pushdown > 0) {
                        spacers.push({ element: prot.element, height: pushdown });
                    }
                }
            });
            
            // === PHASE 3: WRITE (single pass) ===
            
            // Set OC positions
            valid.forEach(function(item) {
                item.link.element.css({
                    'position': 'absolute',
                    'top': item.top + 'px'
                });
            });
            
            // Insert spacers
            spacers.forEach(function(s) {
                var spacer = document.createElement('div');
                spacer.className = 'preprint-oc-spacer';
                spacer.style.height = s.height + 'px';
                s.element.parentNode.insertBefore(spacer, s.element);
            });
            
            // Final measure: layout padding
            void document.body.offsetHeight;
            var scrollY = window.pageYOffset || 0;
            var layoutBottom = layout[0].getBoundingClientRect().bottom + scrollY;
            var maxOCBottom = 0;
            valid.forEach(function(item) {
                var rect = item.link.element[0].getBoundingClientRect();
                if (rect.bottom + scrollY > maxOCBottom) maxOCBottom = rect.bottom + scrollY;
            });
            if (maxOCBottom > layoutBottom) {
                layout[0].style.paddingBottom = (maxOCBottom - layoutBottom) + 'px';
            }
            
            // Restore scroll
            document.body.scrollTop = savedBodyScroll;
            document.documentElement.scrollTop = savedHtmlScroll;
            reconcileLayout._running = false;
            
            // Reconnect or create observer
            if (!layout._ocObserver) {
                layout._ocObserver = new MutationObserver(function() {
                    clearTimeout(layout._ocDebounce);
                    layout._ocDebounce = setTimeout(reconcileLayout, 300);
                });
            }
            layout._ocObserver.observe(layout[0], {
                subtree: true, attributes: true, attributeFilter: ['class']
            });
            
            // ResizeObserver: catches container width changes not visible to
            // window resize (TOC pin/unpin, tool menu toggle, DevTools panel).
            // Created once, never disconnected — only fires on actual size change.
            if (!layout._resizeObserver && typeof ResizeObserver !== 'undefined') {
                layout._resizeObserver = new ResizeObserver(function() {
                    clearTimeout(layout._resizeDebounce);
                    layout._resizeDebounce = setTimeout(function() {
                        if (PAL.isOrientationLocked()) return;
                        if (document.body.classList.contains('preprint-landscape')) {
                            reconcileLayout();
                        }
                    }, 200);
                });
                layout._resizeObserver.observe(layout[0]);
            }
        }

        // Reposition after images load (initial positioning may precede image render)
        $('.preprint-layout img').on('load', function() {
            if (document.body.classList.contains('preprint-landscape')) {
                reconcileLayout();
            }
        });

        $(window).on('resize', (function() {
            var timer;
            return function() {
                clearTimeout(timer);
                timer = setTimeout(function() {
                    if (PAL.isOrientationLocked()) return;
                    if (document.body.classList.contains('preprint-landscape')) {
                        reconcileLayout();
                    }
                }, 200);
            };
        })());

        function scrollToHash() {
            // Check return target first (set by Features.initBookNavFooter)
            if (returnTarget) {
                var el = document.getElementById(returnTarget);
                if (el) {
                    el.scrollIntoView({ block: 'start' });
                    window.scrollBy(0, -60);
                }
                returnTarget = null;
                return;
            }
            
            // Existing hash handling
            if (!window.location.hash) return;
            var target = document.getElementById(window.location.hash.slice(1));
            if (target) {
                target.scrollIntoView({ block: 'start' });
                window.scrollBy(0, -60);
            }
        }

        /* --- User State (Position Tracking) --- */
        
        function captureAnchor() {
            if (PAL.isOrientationLocked()) return;
            
            var viewportTop = 70;  // Account for fixed headers
            var isLandscape = !PAL.isNarrow();
            positionAnchor = null;
            var headingFallback = null;
            
            // Find fallback heading
            var fallbackHeadingId = null;
            $('.preprint-layout').find('h2, h3, h4, h5').each(function() {
                var rect = this.getBoundingClientRect();
                if (rect.top <= viewportTop + 50) {
                    var id = $(this).find('.mw-headline').attr('id') || this.id;
                    if (id && id !== 'mw-toc-heading') {
                        fallbackHeadingId = id;
                    }
                }
            });
            
            // Check 2C elements first
            var found = false;
            $('.preprint-layout > .preprint-2c-wrapper').each(function() {
                var rect = this.getBoundingClientRect();
                if (rect.top < viewportTop + 50 && rect.bottom > 0 && rect.height > 10) {
                    positionAnchor = { element: this, type: '2C', headingId: fallbackHeadingId };
                    found = true;
                    return false;
                }
            });
            if (found) return;
            
            // Check IC children (all inner-columns)
            $('.preprint-layout .inner-column').each(function() {
                if (found) return false;
                $(this).children().each(function() {
                var $el = $(this);
                
                // Skip OC elements in portrait
                if (!isLandscape && ($el.hasClass('place-oc') || $el.hasClass('place-lh') || 
                    $el.hasClass('preprint-table-oc-wrapper'))) {
                    return true;
                }
                
                // Skip headings (prefer content), but remember as fallback
                if ($el.is('h1,h2,h3,h4,h5,h6')) {
                    if (!headingFallback) {
                        var hRect = $el[0].getBoundingClientRect();
                        if (hRect.top < viewportTop + 50 && hRect.bottom > 0 && hRect.height > 10) {
                            headingFallback = { element: $el[0], type: 'heading', headingId: fallbackHeadingId };
                        }
                    }
                    return true;
                }
                
                var rect = this.getBoundingClientRect();
                if (rect.top < viewportTop + 50 && rect.bottom > 0 && rect.height > 10) {
                    positionAnchor = { element: this, type: this.tagName.toLowerCase(), headingId: fallbackHeadingId };
                    found = true;
                    return false;
                }
                });
            });
            if (found) return;
            
            // Check OC elements in landscape
            if (isLandscape) {
                $('.preprint-layout .outer-column').children().each(function() {
                    var rect = this.getBoundingClientRect();
                    if (rect.top < viewportTop + 50 && rect.bottom > 0 && rect.height > 10) {
                        positionAnchor = { element: this, type: 'oc', headingId: fallbackHeadingId };
                        return false;
                    }
                });
            }
            
            // Use heading if no content element found
            if (!positionAnchor && headingFallback) {
                positionAnchor = headingFallback;
            }
        }
        
        function restoreAnchor() {
            if (!positionAnchor) return;
            
            if (positionAnchor.element && document.contains(positionAnchor.element)) {
                positionAnchor.element.scrollIntoView({ block: 'start' });
                window.scrollBy(0, -60);
            } else if (positionAnchor.headingId) {
                var el = document.getElementById(positionAnchor.headingId);
                if (el) {
                    el.scrollIntoView({ block: 'start' });
                    window.scrollBy(0, -60);
                }
            }
        }
        
        function getPositionAnchor() {
            return positionAnchor;
        }

        // Public API
        return {
            classifyElement: classifyElement,
            buildStructure: buildStructure,
            getLinkage: getLinkage,
            applyProjection: applyProjection,
            positionOCElements: reconcileLayout,
            captureAnchor: captureAnchor,
            restoreAnchor: restoreAnchor,
            getPositionAnchor: getPositionAnchor,
            getLayout: function() { return layout; },
            set returnTarget(id) { returnTarget = id; },
            pauseObserver: function() {
                if (layout && layout._ocObserver) layout._ocObserver.disconnect();
            },
            resumeObserver: function() {
                if (layout && layout[0] && layout._ocObserver) {
                    layout._ocObserver.observe(layout[0], {
                        subtree: true, attributes: true, attributeFilter: ['class']
                    });
                }
            }
        };
    })();

    /* ============================================
       FEATURE LAYER
       
       Built on Model, provides user-facing features
       ============================================ */

    var Features = (function() {
        
        /* --- Icon Positioning --- */
        
        var iconMetrics = null;  // Calculated once from landscape
        
        function calculateIconMetrics() {
            var layout = document.querySelector('.preprint-layout');
            if (!layout) return;
            var icon = document.querySelector('.book-nav-icon');
            if (!icon) return;
            
            var layoutRect = layout.getBoundingClientRect();
            var viewportWidth = document.documentElement.clientWidth;
            var deadZone = viewportWidth - layoutRect.right;
            var iconWidth = icon.getBoundingClientRect().width;
            
            if (deadZone < iconWidth + 4) return;  // Doesn't fit
            
            var margin = Math.round((deadZone - iconWidth) / 2);
            iconMetrics = { margin: margin };
            applyIconMetrics();
        }
        
        function applyIconMetrics() {
            var icon = document.querySelector('.book-nav-icon');
            if (!icon) return;
            
            if (PAL.isNarrow() || !iconMetrics) {
                icon.style.right = '';
                icon.style.bottom = '';
                return;
            }
            
            icon.style.right = iconMetrics.margin + 'px';
            icon.style.bottom = iconMetrics.margin + 'px';
        }

        /* --- BookNav Icon --- */
        
        function initBookNavFooter() {
            var bookNavData = $('.book-nav-data');
            if (!bookNavData.length) return;
            
            var book = bookNavData.data('book');
            var rootAnchor = bookNavData.data('root-anchor');
            
            var currentPage = mw.config.get('wgPageName').replace(/_/g, ' ');
            var bookRoot = (book || '').replace(/_/g, ' ');
            var isBookRoot = (currentPage === bookRoot);
            
            // Navigation icon: Minerva only
            if (PAL.isMinerva()) {
                var $icon = $('<a class="book-nav-icon"></a>');
                if (isBookRoot) {
                    var saved = sessionStorage.getItem('preprint-position');
                    if (saved) {
                        var pos = JSON.parse(saved);
                        $icon.attr('href', '/wiki/' + pos.article);
                    } else {
                        $icon.attr('href', '#');
                        $icon.addClass('book-nav-icon-disabled');
                    }
                } else {
                    $icon.attr('href', '/wiki/' + book + (rootAnchor ? '#' + rootAnchor : ''));
                }
                var logos = mw.config.get('wgLogos') || {};
                var iconSrc = logos.svg || logos.icon || '/images/logo/logo.svg';
                $icon.append($('<img>').attr('src', iconSrc).attr('alt', ''));
                
                if (isBookRoot) {
                    $icon.on('click', function(e) {
                        e.preventDefault();
                        var saved = sessionStorage.getItem('preprint-position');
                        if (saved) {
                            var pos = JSON.parse(saved);
                            window.location.href = '/wiki/' + pos.article;
                        }
                        // No action when disabled — no homepage fallback
                    });
                } else {
                    $icon.on('click', function(e) {
                        e.preventDefault();
                        Model.captureAnchor();
                        var anchor = Model.getPositionAnchor();
                        var pos = {
                            article: mw.config.get('wgPageName'),
                            headingId: anchor ? anchor.headingId : null
                        };
                        sessionStorage.setItem('preprint-position', JSON.stringify(pos));
                        window.location.href = '/wiki/' + book + (rootAnchor ? '#' + rootAnchor : '');
                    });
                }
                
                $('body').append($icon);
            }

            if (!isBookRoot && !window.location.hash) {
                var saved = sessionStorage.getItem('preprint-position');
                if (saved) {
                    var pos = JSON.parse(saved);
                    if (pos.article === mw.config.get('wgPageName') && pos.headingId) {
                        Model.returnTarget = pos.headingId;
                        sessionStorage.removeItem('preprint-position');
                    }
                }
            }
        }

        /* --- Sidebar Navigation Link (Vector only) --- */
        
        function initSidebarNav() {
            if (PAL.isMinerva()) return;
            
            var bookNavData = $('.book-nav-data');
            if (!bookNavData.length) return;
            
            var book = bookNavData.data('book');
            if (!book) return;
            
            var rootAnchor = bookNavData.data('root-anchor') || '';
            
            var $landmark = $('.vector-page-tools-landmark').first();
            if (!$landmark.length) return;
            
            var currentPage = mw.config.get('wgPageName').replace(/_/g, ' ');
            var bookRoot = (book || '').replace(/_/g, ' ');
            var isBookRoot = (currentPage === bookRoot);
            
            var lang = mw.config.get('wgContentLanguage') || '';
            
            var $link = $('<a class="book-nav-sidebar"></a>')
                .css({ display: 'inline-block', padding: '12px 12px 7px', fontSize: '14px', color: '#36c' });
            
            if (isBookRoot) {
                var label = lang.indexOf('de') === 0
                    ? 'Letzte Seite' : 'Last Page';
                $link.attr('href', '#').text(label);
                
                var saved = sessionStorage.getItem('preprint-position');
                if (!saved) {
                    $link.css({ color: '#ccc', pointerEvents: 'none' });
                }
                
                $link.on('click', function(e) {
                    e.preventDefault();
                    var pos = JSON.parse(sessionStorage.getItem('preprint-position') || 'null');
                    if (pos) {
                        window.location.href = '/wiki/' + pos.article;
                    } else {
                        window.location.href = '/';
                    }
                });
            } else {
                var label = lang.indexOf('de') === 0
                    ? 'Inhaltsverzeichnis' : 'Table of Contents';
                $link.attr('href', '/wiki/' + book).text(label);
                
                $link.on('click', function(e) {
                    e.preventDefault();
                    Model.captureAnchor();
                    var anchor = Model.getPositionAnchor();
                    var pos = {
                        article: mw.config.get('wgPageName'),
                        headingId: anchor ? anchor.headingId : null
                    };
                    sessionStorage.setItem('preprint-position', JSON.stringify(pos));
                    window.location.href = '/wiki/' + book + (rootAnchor ? '#' + rootAnchor : '');
                });
            }
            
            $landmark.before($link);
            if (mw.config.get('wgUserName')) {
                var style = document.createElement('style');
                style.textContent = '@media(max-width:790px){.book-nav-sidebar{display:none!important}}';
                document.head.appendChild(style);
            }
        }

        /* --- Snippet Support --- */
        
        function initSnippetSupport() {
            if (!$('.preprint-layout').length) return;
            
            var $button = null;
            
            function createButton() {
                if (!$button) {
                    $button = $('<button class="snippet-button"></button>');
                    $button.text($('#ca-talk a').text() || 'Diskussion');
                    $button.hide();
                    $('body').append($button);
                    
                    $button.on('click', function() {
                        var selection = window.getSelection();
                        var text = selection.toString().trim();
                        
                        if (text.length < 2) {
                            $button.hide();
                            return;
                        }
                        
                        // Find nearest heading
                        var range = selection.getRangeAt(0);
                        var $container = $(range.startContainer).closest('p, div, li').first();
                        var $heading = $container.prevAll('h1,h2,h3,h4,h5,h6').first();
                        
                        if (!$heading.length) {
                            $heading = $container.parent().prevAll('h1,h2,h3,h4,h5,h6').first();
                        }
                        if (!$heading.length) {
                            $heading = $container.closest('.inner-column').find('h1,h2,h3,h4,h5,h6').last();
                        }
                        
                        var anchorId = $heading.find('.mw-headline').attr('id') || $heading.attr('id') || '';
                        var pageName = mw.config.get('wgPageName').replace(/_/g, ' ');
                        
                        localStorage.setItem('preprint-snippet', JSON.stringify({
                            page: pageName,
                            anchor: anchorId,
                            text: text,
                            timestamp: Date.now()
                        }));
                        
                        var talkPage = $('#ca-talk a').attr('href');
                        if (talkPage) {
                            window.location.href = talkPage + '?action=edit&section=new';
                        }
                        
                        $button.hide();
                    });
                }
                return $button;
            }
            
            $(document).on('mouseup', function() {
                setTimeout(function() {
                    var selection = window.getSelection();
                    var text = selection.toString().trim();
                    
                    if (text.length >= 2) {
                        var btn = createButton();
                        var range = selection.getRangeAt(0);
                        var rect = range.getBoundingClientRect();
                        
                        btn.css({
                            position: 'fixed',
                            top: (rect.bottom + 5) + 'px',
                            left: (rect.left + rect.width / 2 - 40) + 'px',
                            zIndex: 10000
                        });
                        btn.show();
                    } else if ($button) {
                        $button.hide();
                    }
                }, 10);
            });
            
            $(document).on('mousedown', function(e) {
                if ($button && !$(e.target).is('.snippet-button')) {
                    $button.hide();
                }
            });
        }

        /* --- Snippet Pre-fill (edit pages) --- */
        
        function initSnippetPrefill() {
            if (mw.config.get('wgAction') !== 'edit') return;
            
            var data = localStorage.getItem('preprint-snippet');
            if (!data) return;
            
            var snippet = JSON.parse(data);
            
            // Only use if recent (5 minutes)
            if (Date.now() - snippet.timestamp > 300000) {
                localStorage.removeItem('preprint-snippet');
                return;
            }
            
            var sectionTitle = snippet.anchor ? 'Re: ' + snippet.anchor.replace(/_/g, ' ') : 'Anmerkung';
            var text = '== ' + sectionTitle + ' ==\n';
            text += '{{Snippet\n';
            text += '|page=' + snippet.page + '\n';
            if (snippet.anchor) {
                text += '|anchor=' + snippet.anchor + '\n';
            }
            text += '|text=' + snippet.text + '\n';
            text += '}}\n\n';
            text += '~~' + '~~';
            
            var $textarea = $('#wpTextbox1');
            if ($textarea.length && $textarea.val() === '') {
                $textarea.val(text);
                var cursorPos = text.lastIndexOf('\n~~' + '~~');
                $textarea[0].setSelectionRange(cursorPos, cursorPos);
                $textarea.focus();
            }
            
            localStorage.removeItem('preprint-snippet');
        }

        /* --- TOC Suppression --- */
        function suppressTOC() {
            var isPreprintLayout = $('.preprint-layout').length > 0;
            
            var bookNavData = $('.book-nav-data');
            var isBookRoot = false;
            if (bookNavData.length) {
                var currentPage = mw.config.get('wgPageName').replace(/_/g, ' ');
                var bookRoot = (bookNavData.data('book') || '').replace(/_/g, ' ');
                isBookRoot = (currentPage === bookRoot);
            }
            
            if (isBookRoot && PAL.isMinerva()) {
                // Mobile: root page IS the TOC, hide MW-generated one
                $('.toc, #toc').hide();
            } else if (isPreprintLayout && bookNavData.length && PAL.isMinerva()) {
                // Mobile book chapters: linear reading, no TOC
                $('.toc, #toc').hide();
            }
            // Desktop: TOC always visible (sticky sidebar navigation)
        }

        // Public API
        return {
            calculateIconMetrics: calculateIconMetrics,
            applyIconMetrics: applyIconMetrics,
            initBookNavFooter: initBookNavFooter,
            initSidebarNav: initSidebarNav,
            suppressTOC: suppressTOC,
            initSnippetSupport: initSnippetSupport,
            initSnippetPrefill: initSnippetPrefill
        };
    })();

    /* ============================================
       INITIALIZATION
       ============================================ */

    // Snippet pre-fill runs on any edit page
    $(document).ready(function() {
        Features.initSnippetPrefill();
    });

    // Main initialization
    PAL.ready().then(function(status) {
        if (!status.activated) {
            // Still init BookNav footer on book root pages
            if ($('.book-nav-data').length) {
                $('body').addClass('preprint-book');
                if (PAL.isMinerva()) {
                    PAL.disableMinervaCollapse();
                }
                setTimeout(function() {
                    Features.initBookNavFooter();
                    if (!PAL.isMinerva()) {
                        Features.initSidebarNav();
                    }
                    Features.suppressTOC();
                }, 100);
            }
            return;
        }
                
        // Build structure (Model)
        Model.buildStructure();
        
        // Apply initial projection after brief delay for layout to settle
        var initialMode = PAL.isNarrow() ? 'portrait' : 'landscape';
        setTimeout(function() {
            Model.applyProjection(initialMode);
            Model.pauseObserver();
            $('.preprint-layout').addClass('preprint-ready');
            Model.resumeObserver();
            window.preprintReadyAt = performance.now();
        }, 50);

        // Initialize features
        Features.initBookNavFooter();
        if (!PAL.isMinerva()) {
            Features.initSidebarNav();
        }
        Features.suppressTOC();
        Features.initSnippetSupport();
        
        // Set up anchor tracking
        $(document).on('touchend', function() {
            setTimeout(function() {
                Model.captureAnchor();
            }, 400);
        });
        
        // Initial anchor capture
        setTimeout(function() {
            Model.captureAnchor();
        }, 500);

        // Hide icon early on rotation (before browser reflows)
        window.addEventListener('orientationchange', function() {
            var icon = document.querySelector('.book-nav-icon');
            if (icon) icon.style.visibility = 'hidden';
        });
        
        // Calculate icon metrics from landscape
        if (!PAL.isNarrow()) {
            setTimeout(function() { Features.calculateIconMetrics(); }, 100);
        }

        // Hide icon early on rotation (before reflow causes visible jump)
        PAL.onPreProjectionChange(function() {
            var icon = document.querySelector('.book-nav-icon');
            if (icon) icon.style.visibility = 'hidden';
            var el = document.querySelector('.preprint-layout');
            if (el) { el.style.transition = 'none'; el.style.opacity = '0'; }
        });
        
        PAL.onProjectionChange(function(event) {
            
            // Icon already hidden by onPreProjectionChange
            var icon = document.querySelector('.book-nav-icon');
            
            // Apply new projection
            Model.applyProjection(event.to);

            // Recalculate icon from landscape geometry, then reveal
            if (event.to === 'landscape') {
                setTimeout(function() { 
                    Features.calculateIconMetrics();
                    if (icon) icon.style.visibility = '';
                    Model.positionOCElements();
                }, 100);
            } else {
                Features.applyIconMetrics();
                setTimeout(function() {
                    if (icon) icon.style.visibility = '';
                }, 150);
            }
            
            // Restore position
            Model.restoreAnchor();
            
            // Reveal layout after projection complete
            var el = document.querySelector('.preprint-layout');
            if (el) {
                setTimeout(function() {
                    el.style.transition = 'opacity 0.15s';
                    el.style.opacity = '1';
                }, 30);
            }
        });
        
        // Expose for debugging and external access
        window.Preprint = {
            PAL: PAL,
            Model: Model,
            Features: Features
        };
        
        // Also expose positionAnchor for BookNav compatibility
        Object.defineProperty(window, 'positionAnchor', {
            get: function() { return Model.getPositionAnchor(); }
        });
        
        // mw.loader.load('/wiki/MediaWiki:Common.js/debug.js?action=raw&ctype=text/javascript');        
    });

})();

/* === PREPRINT END === */



/* === CONSENT START === */
/* Unified Consent System JavaScript v260224r1 
 * Single file for both EN and DE versions
 * Automatically detects language based on domain
 */
/* Common JavaScript - Keep only consent system if needed */

// Language detection helper
function getCurrentLanguage() {
    var hostname = window.location.hostname;
    return (hostname === 'de.transformal.com' || 
            hostname === 'de.olaflangmack.info' ||
            hostname === 'de.mediawiki.transformal.com') ? 'de' : 'en';
}

// Localized strings
var strings = {
    en: {
        journalingEnabled: 'Journaling enabled',
        journalingDisabled: 'Journaling disabled',
        proofOfWorkEnabled: 'Proof-of-work enabled',
        proofOfWorkDisabled: 'Proof-of-work disabled',
        preferencesSaved: 'Your preferences are being saved',
        checkboxLabel: 'Enabled'
    },
    de: {
        journalingEnabled: 'Aufzeichnungen zugelassen',
        journalingDisabled: 'Aufzeichnungen nicht zugelassen',
        proofOfWorkEnabled: 'Proof-of-work zugelassen',
        proofOfWorkDisabled: 'Proof-of-work nicht zugelassen',
        preferencesSaved: 'Ihre Einstellungen werden gespeichert',
        checkboxLabel: 'Erlaubt'
    }
};

function getString(key) {
    var lang = getCurrentLanguage();
    return strings[lang][key] || strings.en[key];
}

// Handle consent acquisition box - 90-day reminder for visitors (spec §8.9)
$(document).ready(function() {
    var consentBox = document.getElementById('consent-acquisition-box');
    if (!consentBox) return;
    
    // Check if dismissed within last 90 days
    var dismissedUntil = localStorage.getItem('consent-acquisition-dismissed-until');
    var now = Date.now();
    
    if (dismissedUntil && parseInt(dismissedUntil) > now) {
        // Still within 90-day dismissal period
        consentBox.style.display = 'none';
    } else {
        // Show box - dismissal expired or never dismissed
        consentBox.style.display = 'block';
        consentBox.classList.add('show');
    }
    
    consentBox.addEventListener('click', function() {
        sessionStorage.setItem('consent-acquisition-dismissed', 'true');
        // ADD THIS LINE:
        localStorage.setItem('consent-acquisition-dismissed-until', Date.now() + (90 * 24 * 60 * 60 * 1000));
        if (mw.config.get('wgUserName')) {
            window.location.href = '/wiki/Special:Preferences#mw-prefsection-legal';
        } else {
            var targetPage = getCurrentLanguage() === 'de' ? 
                '/wiki/Transformal_GmbH:Einstellungen' : 
                '/wiki/Transformal_GmbH:Settings';
            window.location.href = targetPage;
        }
    });
});

// Convert template placeholders to actual checkboxes
$(function() {
    $('.consent-option').each(function() {
        var $option = $(this);
        if ($option.find('.consent-checkbox').length > 0) return;
        
        var consentId = $option.find('.consent-data').text().trim();
        var status = $option.find('.consent-status-data').text().trim();
        
        var $checkboxRow = $option.find('.checkbox-row');
        if (!$checkboxRow.length) return;
        
        var $container = $('<span class="checkbox-container"></span>');
        var $checkbox = $('<input>', {
            type: 'checkbox',
            id: consentId,
            class: 'consent-checkbox'
        });
        
        if (status === 'disabled') {
            $checkbox.prop('disabled', true);
        }
        
        var label = $option.find('.checkbox-placeholder').attr('data-label') || getString('checkboxLabel');
        var $label = $('<label>', {
          'for': consentId,
          text: ' ' + label,
          style: 'cursor: pointer;'
        });
        
        $container.append($checkbox).append($label);
        $checkboxRow.append($container);
        $checkboxRow.find('.checkbox-placeholder').remove();
        
        var saved = localStorage.getItem(consentId) === 'true';
        $checkbox.prop('checked', saved);
        
        $checkbox.on('change', function() {
            localStorage.setItem(consentId, this.checked);
            if (typeof handleConsentChange === 'function') {
              handleConsentChange(consentId, this.checked);
            }
            if (typeof showConsentFeedback === 'function') {
                showConsentFeedback(consentId, this.checked);
            }
        });
    });
});

// Handle consent changes for different features
function handleConsentChange(feature, enabled) {
    if (feature === 'matomo-consent') {
        if (enabled) {
            if (window._paq) {
                // Enable enhanced tracking
                _paq.push(['setCookieConsentGiven']);
                _paq.push(['setConsentGiven']);
                // Track this page view again with full details
                _paq.push(['trackPageView']);
                _paq.push(['enableHeartBeatTimer']);
                _paq.push(['trackVisibleContentImpressions']);
            }
        } else {
            if (window._paq) {
                // Revert to anonymous tracking only
                _paq.push(['forgetCookieConsentGiven']);
                _paq.push(['forgetConsentGiven']);
                // Note: Already tracked pages remain in this session
            }
        }
    }
}

// Show feedback when consent changes
function showConsentFeedback(feature, enabled) {
    var feedbackDiv = document.getElementById('consent-feedback');
    if (!feedbackDiv) return;
    
    var message = '';
    if (feature === 'matomo-consent') {
        message = enabled ? getString('journalingEnabled') : getString('journalingDisabled');
    } else if (feature === 'altcha-consent') {
        message = enabled ? getString('proofOfWorkEnabled') : getString('proofOfWorkDisabled');
    }
    
    if (message) {
        feedbackDiv.innerHTML = message + ' &ndash; ' + getString('preferencesSaved');
        feedbackDiv.style.display = 'block';
        
        setTimeout(function() {
            feedbackDiv.style.display = 'none';
        }, 3000);
    }
}
/* === CONSENT END === */


/* === CONDITIONAL START === */

/* Template 'ConditionalContent' - v250925r8 */
/* Two templates for alternative running text (ConditionalContent) and alternative sections (ConditionalSection) */ 

mw.loader.using('mediawiki.user').then(function() {
    if (mw.user.isAnon()) {
        // Inline content
        $('.logged-out-only').each(function() {
            if ($(this).is(':empty') || !$(this).text().replace(/\s/g, '')) {
                $(this).css('display', 'none');
            } else {
                $(this).css('display', 'inline');
            }
        });
        $('.logged-in-only').css('display', 'none');
        
        // Block content
        $('.logged-out-section').each(function() {
            if ($(this).is(':empty') || !$(this).text().replace(/\s/g, '')) {
                $(this).css('display', 'none');
            } else {
                $(this).css('display', 'block');
            }
        });
        $('.logged-in-section').css('display', 'none');
    } else {
        // Inline content
        $('.logged-in-only').each(function() {
            if ($(this).is(':empty') || !$(this).text().replace(/\s/g, '')) {
                $(this).css('display', 'none');
            } else {
                $(this).css('display', 'inline');
            }
        });
        $('.logged-out-only').css('display', 'none');
        
        // Block content
        $('.logged-in-section').each(function() {
            if ($(this).is(':empty') || !$(this).text().replace(/\s/g, '')) {
                $(this).css('display', 'none');
            } else {
                $(this).css('display', 'block');
            }
        });
        $('.logged-out-section').css('display', 'none');
    }

    // Clean TOC
    var toc = $('.vector-toc')[0] || $('#toc')[0] || $('.toc')[0];
    if (toc) {
        $('.logged-in-section:hidden, .logged-out-section:hidden').each(function() {
            $(this).find('h1, h2, h3, h4, h5, h6').each(function() {
                var id = this.id || $(this).find('.mw-headline').attr('id');
                if (id) {
                    $(toc).find('a[href="#' + id + '"]').closest('li').remove();
                }
            });
        });
    }
});


/* === CONDITIONAL END === */