跳转到内容
打开/关闭菜单
  • 451 条目
  • 806 文件
  • 22 用户
  • 9214 编辑
Blockland Wiki
打开/关闭外观设置菜单
打开/关闭个人菜单
未登录
未登录用户的IP地址会在进行任意编辑后公开展示。

MediaWiki:KuranokaPageTransition.js:修订间差异

MediaWiki界面页面
无编辑摘要
无编辑摘要
 
第1行: 第1行:
/* ============================================================
/* ============================================================
  Kuranoka / Kuronata — Page Transition V41 / V25 faithful restore
  Kuranoka / Kuronata — Page Transition V42 / V25 faithful restore
  V25 ORIGINAL fire edge, translucent fire pockets, smoke and dawn.
  V25 ORIGINAL fire edge, translucent fire pockets, smoke and dawn.
  Lightweight WebGL2 is restricted to subtle ember points, NEVER the
  Lightweight WebGL2 is restricted to subtle ember points, NEVER the
  fire curtain nor the reverse transition. Carbon sheets are 2D.
  fire curtain nor the reverse transition. Carbon sheets are 2D.
  Both cues start three seconds before the V25 curtains, fade in over
  Both cues start three seconds before the V25 curtains, fade in over
  those three seconds, then fade OUT over 18 seconds after reveal.
  those three seconds, then fade OUT over 18 seconds after reveal.
  Only this page-scoped JS/CSS; never modify Citizen or loader.js.
  Only this page-scoped JS/CSS; never modify Citizen or loader.js.
  Real page navigation after the transition; iframe is visual-only.
  Real page navigation after the transition; iframe is visual-only.
  ============================================================ */
  ============================================================ */
(function initKkpt(retry) {
(function initKkpt(retry) {
    'use strict';
  'use strict';
    retry=retry||0;
  retry=retry||0;
    if (!window.mw || !mw.config || typeof mw.config.get!=='function') {
  if (!window.mw || !mw.config || typeof mw.config.get!=='function') {
        if(retry<40)setTimeout(()=>initKkpt(retry+1),100);
    if(retry<40)setTimeout(()=>initKkpt(retry+1),100);
        return;
    return;
  }
  const pathMatch = location.pathname.match(/\/wiki\/(Kuranoka|Kuronata)\/?$/);
  const urlPage = pathMatch ? pathMatch[1]
    : new URLSearchParams(location.search).get('title');
  const basePage = ['Kuranoka','Kuronata'].includes(urlPage)
    ? urlPage : mw.config.get('wgPageName');
  const action = new URLSearchParams(location.search).get('action') || 'view';
  if (!['Kuranoka','Kuronata'].includes(basePage) || action !== 'view') return;
  const root=document.documentElement;
  const themeNames=['skin-theme-clientpref-day','skin-theme-clientpref-os',
    'skin-theme-clientpref-night','skin-theme-clientpref-black',
    'skin-citizen-light'];
  let wantsNight=basePage==='Kuronata';
  let changingTheme=false;
  function syncTheme(){
    if(changingTheme)return;
    const wanted=wantsNight?'skin-theme-clientpref-night':
      'skin-theme-clientpref-day';
    const shadow=wantsNight?'kkpt-kuronata-dark':null;
    const unexpected=themeNames.filter(c=>c!==wanted)
      .concat(wantsNight?[]:['kkpt-kuronata-dark']);
    if(root.classList.contains(wanted)&&
      (!shadow||root.classList.contains(shadow))&&
      unexpected.every(c=>!root.classList.contains(c))){
      return;
     }
     }
 
    changingTheme=true;
     const pathMatch = location.pathname.match(/\/wiki\/(Kuranoka|Kuronata)\/?$/);
     root.classList.remove(...themeNames,'kkpt-kuronata-dark');
     const urlPage = pathMatch ? pathMatch[1]
    root.classList.add(wanted);
        : new URLSearchParams(location.search).get('title');
     if(shadow)root.classList.add(shadow);
     const basePage = ['Kuranoka','Kuronata'].includes(urlPage)
     changingTheme=false;
        ? urlPage : mw.config.get('wgPageName');
  }
    const action = new URLSearchParams(location.search).get('action') || 'view';
  function showTheme(dark){wantsNight=dark;syncTheme();}
     if (!['Kuranoka','Kuronata'].includes(basePage) || action !== 'view') return;
  syncTheme();
 
  new MutationObserver(syncTheme).observe(root,{
     // V24: the two article worlds have deliberate, deterministic palettes.
    attributes:true,attributeFilter:['class']
    // "os" is NOT a light theme: it can resolve to dark on the reader's
  });
     // computer. Session storage may also be unavailable. Therefore, never
  function ensureWorldStyles(){
     // restore a stale "os" token when landing back on Kuranoka.
    if(!document.head || document.getElementById('kkpt-css-retry'))return;
     const root=document.documentElement;
     try{
     const themeNames=['skin-theme-clientpref-day','skin-theme-clientpref-os',
      if(getComputedStyle(root).getPropertyValue('--kkpt-css-v42').trim()==='ready')return;
        'skin-theme-clientpref-night','skin-theme-clientpref-black',
    }catch(_){}
        'skin-citizen-light'];
     const link=document.createElement('link');
     let wantsNight=basePage==='Kuronata';
     link.id='kkpt-css-retry';
     let changingTheme=false;
     link.rel='stylesheet';
     function syncTheme(){
     const target=new URL('/index.php',location.origin);
        if(changingTheme)return;
     target.searchParams.set('title','MediaWiki:KuranokaPageTransition.css');
        const wanted=wantsNight?'skin-theme-clientpref-night':
    target.searchParams.set('action','raw');
            'skin-theme-clientpref-day';
    target.searchParams.set('ctype','text/css');
        const shadow=wantsNight?'kkpt-kuronata-dark':null;
     target.searchParams.set('kkptv','42');
        const unexpected=themeNames.filter(c=>c!==wanted)
     link.href=target.href;
            .concat(wantsNight?[]:['kkpt-kuronata-dark']);
     document.head.appendChild(link);
        if(root.classList.contains(wanted)&&
  }
            (!shadow||root.classList.contains(shadow))&&
  setTimeout(ensureWorldStyles,1200);
             unexpected.every(c=>!root.classList.contains(c))){
  function navigateRealPage(href){
            return;
    window.location.assign(href);
  }
  function installKuronataAfterglow() {
    if(basePage!=='Kuronata'||window.__kkptDarkExtrasReady)return;
    window.__kkptDarkExtrasReady=true;
    const prefersReduced=window.matchMedia('(prefers-reduced-motion: reduce)');
    const spoilerOverlays=new WeakMap();
    const renderedSpoilerLines=(node)=>{
      const lines=[];
      const walk=document.createTreeWalker(node,NodeFilter.SHOW_TEXT);
      const range=document.createRange();
      let text,budget=0;
      while((text=walk.nextNode())&&budget<340){
        for(let i=0;i<text.length&&budget<340;i++,budget++){
          const ch=text.nodeValue[i];
          if(ch==='\n'||ch==='\r')continue;
          range.setStart(text,i);
          range.setEnd(text,i+1);
          const r=range.getBoundingClientRect();
          if(!r.width||!r.height)continue;
          let line=lines.find(row=>Math.abs(row.top-r.top)<3);
          if(!line){
            line={top:r.top,left:r.left,right:r.right,
              height:r.height,text:''};
             lines.push(line);
          }
          line.left=Math.min(line.left,r.left);
          line.right=Math.max(line.right,r.right);
          line.height=Math.max(line.height,r.height);
          line.text+=ch;
         }
         }
        changingTheme=true;
      }
        root.classList.remove(...themeNames,'kkpt-kuronata-dark');
      range.detach?.();
         root.classList.add(wanted);
      return lines.sort((a,b)=>a.top-b.top);
         if(shadow)root.classList.add(shadow);
    };
        changingTheme=false;
    const spoilerFault=(node)=>{
     }
      if(prefersReduced.matches)return;
     function showTheme(dark){wantsNight=dark;syncTheme();}
      const rects=node.getClientRects();
     syncTheme();
      if(rects.length<2)return;
    // Citizen may reapply OS-preference classes after its own scripts load.
      const lines=renderedSpoilerLines(node);
    // Keep each world's class correct without changing the user's setting.
      if(lines.length<2)return;
    new MutationObserver(syncTheme).observe(root,{
      node.setAttribute('data-kkpt-multiline','1');
         attributes:true,attributeFilter:['class']
      spoilerOverlays.get(node)?.remove();
     });
      const overlay=document.createElement('div');
     // V31's page-style retry solves occasional initial unstyled portals.
      overlay.className='kkpt-heimu-fault';
    function ensureWorldStyles(){
      overlay.setAttribute('aria-hidden','true');
         if(!document.head || document.getElementById('kkpt-css-retry'))return;
      const font=getComputedStyle(node);
      for(const row of lines){
        const fragment=document.createElement('span');
        fragment.className='kkpt-heimu-fault-line';
        fragment.dataset.copy=row.text;
        fragment.style.left=row.left+'px';
        fragment.style.top=row.top+'px';
        fragment.style.width=(row.right-row.left+6)+'px';
         fragment.style.height=(row.height+4)+'px';
        fragment.style.fontFamily=font.fontFamily;
        fragment.style.fontSize=font.fontSize;
        fragment.style.fontWeight=font.fontWeight;
        fragment.style.fontStyle=font.fontStyle;
         fragment.style.lineHeight=font.lineHeight;
        fragment.style.letterSpacing=font.letterSpacing;
        overlay.append(fragment);
      }
      document.body.append(overlay);
      spoilerOverlays.set(node,overlay);
     };
     const clearSpoilerFault=(node)=>{
      const overlay=spoilerOverlays.get(node);
      if(!overlay)return;
      spoilerOverlays.delete(node);
      overlay.classList.add('kkpt-fault-away');
      setTimeout(()=>overlay.remove(),210);
    };
     const decorateSpoilers=()=>{
      document.querySelectorAll('span.heimu').forEach(node=>{
        if(node.dataset.kkptBound)return;
        node.dataset.kkptBound='1';
        const label=(node.textContent||'').replace(/\s+/g,' ').trim();
        if(label&&label.length<=240&&!node.children.length)
          node.setAttribute('data-kkpt-glitch',label);
         node.addEventListener('mouseenter',()=>spoilerFault(node));
        node.addEventListener('mouseleave',()=>clearSpoilerFault(node));
      });
     };
     const decorateLinks=()=>{
      const article=document.querySelector('#mw-content-text,.mw-parser-output');
      if(!article)return;
      article.querySelectorAll('a[href]').forEach(link=>{
         if(link.dataset.kkptLinkReady ||
          link.classList.contains('kkpt-fate-gear') ||
          link.querySelector('img,svg,video'))return;
        link.dataset.kkptLinkReady='1';
        link.classList.add('kkpt-link-glitch');
         try{
         try{
             if(getComputedStyle(root).getPropertyValue('--kkpt-css-v41').trim()==='ready')return;
          const url=new URL(link.href,document.baseURI);
          const path=url.pathname.match(/\/wiki\/([^/?#]+)/);
          const name=url.searchParams.get('title')||
             (path?decodeURIComponent(path[1]):'');
          if(url.origin===location.origin &&
            name.replace(/_/g,' ')==='Kuranoka'){
            link.classList.add('kkpt-link-origin');
            const label=(link.textContent||'').trim();
            if(!link.children.length &&
              link.getClientRects().length===1 &&
              label.length>0&&label.length<100)
              link.dataset.kkptLinkCopy=label;
          }
         }catch(_){}
         }catch(_){}
        const link=document.createElement('link');
      });
        link.id='kkpt-css-retry';
        link.rel='stylesheet';
        const target=new URL('/index.php',location.origin);
        target.searchParams.set('title','MediaWiki:KuranokaPageTransition.css');
        target.searchParams.set('action','raw');
        target.searchParams.set('ctype','text/css');
        target.searchParams.set('kkptv','41');
        link.href=target.href;
        document.head.appendChild(link);
    }
    setTimeout(ensureWorldStyles,1200);
    function navigateRealPage(href){
        window.location.assign(href);
    }
 
    // V17: the dark-world extras run in *both* the visible document and
    // its same-origin preview iframe. They never alter other Wiki pages.
    function installKuronataAfterglow() {
        if(basePage!=='Kuronata'||window.__kkptDarkExtrasReady)return;
        window.__kkptDarkExtrasReady=true;
        const prefersReduced=window.matchMedia('(prefers-reduced-motion: reduce)');
        // Multiline .heimu cannot use a single nowrap pseudo-element:
        // it piles all copied text onto line one. Project each rendered
        // line into two SHORT clipped chromatic slices only on hover.
        // Range measurements leave the original article DOM and wrapping
        // entirely untouched, including italic/link children.
        const spoilerOverlays=new WeakMap();
        const renderedSpoilerLines=(node)=>{
            const lines=[];
            const walk=document.createTreeWalker(node,NodeFilter.SHOW_TEXT);
            const range=document.createRange();
            let text,budget=0;
            while((text=walk.nextNode())&&budget<340){
                for(let i=0;i<text.length&&budget<340;i++,budget++){
                    const ch=text.nodeValue[i];
                    if(ch==='\n'||ch==='\r')continue;
                    range.setStart(text,i);
                    range.setEnd(text,i+1);
                    const r=range.getBoundingClientRect();
                    if(!r.width||!r.height)continue;
                    let line=lines.find(row=>Math.abs(row.top-r.top)<3);
                    if(!line){
                        line={top:r.top,left:r.left,right:r.right,
                            height:r.height,text:''};
                        lines.push(line);
                    }
                    line.left=Math.min(line.left,r.left);
                    line.right=Math.max(line.right,r.right);
                    line.height=Math.max(line.height,r.height);
                    line.text+=ch;
                }
            }
            range.detach?.();
            return lines.sort((a,b)=>a.top-b.top);
        };
        const spoilerFault=(node)=>{
            if(prefersReduced.matches)return;
            const rects=node.getClientRects();
            if(rects.length<2)return;
            const lines=renderedSpoilerLines(node);
            if(lines.length<2)return;
            node.setAttribute('data-kkpt-multiline','1');
            spoilerOverlays.get(node)?.remove();
            const overlay=document.createElement('div');
            overlay.className='kkpt-heimu-fault';
            overlay.setAttribute('aria-hidden','true');
            const font=getComputedStyle(node);
            for(const row of lines){
                const fragment=document.createElement('span');
                fragment.className='kkpt-heimu-fault-line';
                fragment.dataset.copy=row.text;
                fragment.style.left=row.left+'px';
                fragment.style.top=row.top+'px';
                fragment.style.width=(row.right-row.left+6)+'px';
                fragment.style.height=(row.height+4)+'px';
                fragment.style.fontFamily=font.fontFamily;
                fragment.style.fontSize=font.fontSize;
                fragment.style.fontWeight=font.fontWeight;
                fragment.style.fontStyle=font.fontStyle;
                fragment.style.lineHeight=font.lineHeight;
                fragment.style.letterSpacing=font.letterSpacing;
                overlay.append(fragment);
            }
            document.body.append(overlay);
            spoilerOverlays.set(node,overlay);
        };
        const clearSpoilerFault=(node)=>{
            const overlay=spoilerOverlays.get(node);
            if(!overlay)return;
            spoilerOverlays.delete(node);
            overlay.classList.add('kkpt-fault-away');
            setTimeout(()=>overlay.remove(),210);
        };
        const decorateSpoilers=()=>{
            document.querySelectorAll('span.heimu').forEach(node=>{
                if(node.dataset.kkptBound)return;
                node.dataset.kkptBound='1';
                const label=(node.textContent||'').replace(/\s+/g,' ').trim();
                if(label&&label.length<=240&&!node.children.length)
                    node.setAttribute('data-kkpt-glitch',label);
                node.addEventListener('mouseenter',()=>spoilerFault(node));
                node.addEventListener('mouseleave',()=>clearSpoilerFault(node));
            });
        };
        const decorateLinks=()=>{
            const article=document.querySelector('#mw-content-text,.mw-parser-output');
            if(!article)return;
            article.querySelectorAll('a[href]').forEach(link=>{
                if(link.dataset.kkptLinkReady ||
                    link.classList.contains('kkpt-fate-gear') ||
                    link.querySelector('img,svg,video'))return;
                link.dataset.kkptLinkReady='1';
                link.classList.add('kkpt-link-glitch');
                try{
                    const url=new URL(link.href,document.baseURI);
                    const path=url.pathname.match(/\/wiki\/([^/?#]+)/);
                    const name=url.searchParams.get('title')||
                        (path?decodeURIComponent(path[1]):'');
                    if(url.origin===location.origin &&
                        name.replace(/_/g,' ')==='Kuranoka'){
                        link.classList.add('kkpt-link-origin');
                        // Copy a link only if it truly occupies one
                        // rendered line. Multiline links keep real text.
                        const label=(link.textContent||'').trim();
                        if(!link.children.length &&
                          link.getClientRects().length===1 &&
                          label.length>0&&label.length<100)
                            link.dataset.kkptLinkCopy=label;
                    }
                }catch(_){}
            });
        };
        // V29's useful identity faults, transplanted without its transition
        // shaders. Only text nodes are wrapped; Wiki links stay clickable.
        const decorateIdentityMentions=()=>{
            const article=document.querySelector('#mw-content-text,.mw-parser-output');
            if(!article)return;
            const hasIdentity=/Kuranoka|Kurano\s+Kanata|天野[ \u00a0\u3000]*彼方/i;
            const exactIdentity=/Kuranoka|Kurano\s+Kanata|天野[ \u00a0\u3000]*彼方/gi;
            const walker=document.createTreeWalker(article,NodeFilter.SHOW_TEXT,{
                acceptNode(node){
                    if(!node.nodeValue||!hasIdentity.test(node.nodeValue))
                        return NodeFilter.FILTER_REJECT;
                    const parent=node.parentElement;
                    if(!parent||parent.closest('script,style,textarea,code,pre,'+
                        'input,button,select,[contenteditable="true"],'+
                        '.kkpt-identity-glitch,.kkpt-heimu-fault,'+
                        'span.heimu,a.kkpt-link-origin,'+
                        '#kkpt-afterglow,#kkpt-toggle'))
                        return NodeFilter.FILTER_REJECT;
                    return NodeFilter.FILTER_ACCEPT;
                }
            });
            const matches=[];
            let node;
            while((node=walker.nextNode())&&matches.length<1800)matches.push(node);
            let index=0;
            for(const textNode of matches){
                const raw=textNode.nodeValue;
                const fragment=document.createDocumentFragment();
                let previous=0,match;
                exactIdentity.lastIndex=0;
                while((match=exactIdentity.exec(raw))){
                    if(match.index>previous)
                        fragment.append(document.createTextNode(raw.slice(previous,match.index)));
                    const el=document.createElement('span');
                    el.className='kkpt-identity-glitch';
                    el.dataset.kkptIdentity=match[0];
                    el.textContent=match[0];
                    el.style.setProperty('--kkpt-identity-delay',
                        (-((index++*7)%23)*.21).toFixed(2)+'s');
                    fragment.append(el);
                    previous=match.index+match[0].length;
                }
                if(previous<raw.length)
                    fragment.append(document.createTextNode(raw.slice(previous)));
                textNode.replaceWith(fragment);
            }
        };
        const createAmbience=()=>{
            if(prefersReduced.matches||document.getElementById('kkpt-afterglow')||!document.body)return;
            const overlay=document.createElement('div');
            overlay.id='kkpt-afterglow';
            overlay.setAttribute('aria-hidden','true');
            const el=(type,i)=>{
                const part=document.createElement('span');
                part.className=type==='soot'?'kkpt-soot-cell':'kkpt-ash-cell';
                const smoke=type==='soot';
                // All sprites are static layers with compositor-only
                // transform/opacity animation. No persistent canvas or RAF.
                // The soot is a side-bank haze, not a random full-screen
                // spread of isolated circular clouds.
                const fromLeft=i%2===0;
                part.style.left=(smoke
                    ? (fromLeft?-26+Math.random()*22:79+Math.random()*21)
                    : Math.random()*100)+'%';
                part.style.top=(smoke?8+Math.random()*94:15+Math.random()*94)+'%';
                part.style.setProperty('--size',(smoke?220+Math.random()*240:2+Math.random()*4).toFixed(1)+'px');
                part.style.setProperty('--delay',(-Math.random()*(smoke?29:18)).toFixed(2)+'s');
                part.style.setProperty('--duration',(smoke?18+Math.random()*14:8+Math.random()*11).toFixed(2)+'s');
                part.style.setProperty('--wind',((Math.random()-.5)*(smoke?100:230)).toFixed(1)+'px');
                return part;
            };
            for(let i=0;i<(innerWidth<700?8:16);i++)overlay.append(el('soot',i));
            for(let i=0;i<30;i++)overlay.append(el('ash',i));
            document.body.append(overlay);
        };
        const init=()=>{
            decorateSpoilers();
            decorateLinks();
            decorateIdentityMentions();
            createAmbience();
            mw.hook('wikipage.content').add(()=>{
                decorateSpoilers();
                decorateLinks();
                decorateIdentityMentions();
            });
        };
        if(document.readyState==='loading')
            document.addEventListener('DOMContentLoaded',init,{once:true});
        else init();
 
        // One brief spatial fault roughly every 14-36 seconds, never a
        // full-screen white strobe. Pause while the iframe is hidden.
        const pulse=()=>{
            const inactive=document.hidden||prefersReduced.matches||
                root.classList.contains('kkpt-frame-inactive')||
                root.classList.contains('kkpt-world-suspended')||
                (window!==window.top&&window.frameElement?.getAttribute('aria-hidden')==='true');
            if(!inactive){
                root.style.setProperty('--kkpt-rift-y',(14+Math.random()*70).toFixed(1)+'%');
                root.classList.add('kkpt-rift');
                window.setTimeout(()=>root.classList.remove('kkpt-rift'),390);
            }
            window.setTimeout(pulse,5000+Math.random()*15000);
        };
        window.setTimeout(pulse,5000+Math.random()*15000);
    }
    installKuronataAfterglow();
 
    // The nested preview only applies its own theme; no recursive iframe.
    if (window!==window.top || window.__kkptV12) return;
    window.__kkptV12=true;
    window.__kkptVersion='41.0';
 
    // Easter eggs + Infobox gear are the default. No floating pill.
    const SHOW_FLOATING_BUTTON=false;
    const otherPage=basePage==='Kuranoka'?'Kuronata':'Kuranoka';
    const baseUrl=location.href;
    const baseTitle=document.title;
    const otherUrl=new URL('/wiki/'+encodeURIComponent(otherPage),location.href);
    const clipSide=basePage==='Kuranoka'?'right':'left';
    const hideClip=clipSide==='right'?'inset(0 0 0 100%)':'inset(0 100% 0 0)';
    const sleep=ms=>new Promise(r=>setTimeout(r,ms));
    // C2-smooth cinematic ease; starts and settles with zero acceleration.
    // Its midpoint stays at t=.5, preserving 00:33 / 02:39 beat alignment.
    const smooth=t=>{t=Math.max(0,Math.min(1,t));
        return t*t*t*(t*(t*6-15)+10);};
    const imagePool=new Map();
    // V24: preload both original background photographs. The target
    // image is decoded while its real-document iframe is preparing,
    // reducing flashes of the bare background-color at navigation.
    const worldBackdrops={
        Kuranoka:'https://blockland.wiki/images/0/05/%E7%A7%AF%E9%9B%A8%E4%BA%911.png',
        Kuronata:'https://blockland.wiki/images/a/ab/%E9%BB%84%E6%98%8F%EF%BC%8C%E9%BB%91%E4%BA%91.png'
     };
     };
     function primeWorldBackdrops(){
     const decorateIdentityMentions=()=>{
        const current=worldBackdrops[basePage];
      const article=document.querySelector('#mw-content-text,.mw-parser-output');
        if(current&&document.head){
      if(!article)return;
            const link=document.createElement('link');
      const hasIdentity=/Kuranoka|Kurano\s+Kanata|天野[ \u00a0\u3000]*彼方/i;
            link.rel='preload';
      const exactIdentity=/Kuranoka|Kurano\s+Kanata|天野[ \u00a0\u3000]*彼方/gi;
             link.as='image';
      const walker=document.createTreeWalker(article,NodeFilter.SHOW_TEXT,{
            link.href=current;
        acceptNode(node){
            link.setAttribute('fetchpriority','high');
          if(!node.nodeValue||!hasIdentity.test(node.nodeValue))
             link.dataset.kkptBackgroundPreload='1';
             return NodeFilter.FILTER_REJECT;
             document.head.appendChild(link);
          const parent=node.parentElement;
            warmImage(current).catch(()=>{});
          if(!parent||parent.closest('script,style,textarea,code,pre,'+
            'input,button,select,[contenteditable="true"],'+
             '.kkpt-identity-glitch,.kkpt-heimu-fault,'+
             'span.heimu,a.kkpt-link-origin,'+
            '#kkpt-afterglow,#kkpt-toggle'))
            return NodeFilter.FILTER_REJECT;
          return NodeFilter.FILTER_ACCEPT;
         }
         }
        // Give the current photo first priority; then prepare the next
      });
        // world so the iframe and the eventual real page can reuse it.
      const matches=[];
        setTimeout(()=>{
      let node;
            const next=worldBackdrops[otherPage];
      while((node=walker.nextNode())&&matches.length<1800)matches.push(node);
            if(next)warmImage(next).catch(()=>{});
      let index=0;
        },300);
      for(const textNode of matches){
    }
        const raw=textNode.nodeValue;
    primeWorldBackdrops();
        const fragment=document.createDocumentFragment();
 
        let previous=0,match;
    const ENABLE_TRANSITION_AUDIO = true;
        exactIdentity.lastIndex=0;
    const FIRE_VOLUME = .30;
         while((match=exactIdentity.exec(raw))){
    const DAWN_VOLUME = .20;
          if(match.index>previous)
    let soundContext=null,fireBuffer=null,activeSound=null;
             fragment.append(document.createTextNode(raw.slice(previous,match.index)));
 
          const el=document.createElement('span');
    // Public-domain PADsynth recording, ~8.4s / 120KB. Change this
          el.className='kkpt-identity-glitch';
    // to a same-origin Wiki-upload URL if external media are blocked.
          el.dataset.kkptIdentity=match[0];
    const REBIRTH_PAD_URL=
          el.textContent=match[0];
         'https://upload.wikimedia.org/wikipedia/commons/e/ed/Padsynth_example_synth_piano.ogg';
          el.style.setProperty('--kkpt-identity-delay',
    let padBytesPromise=null,padDecodePromise=null,padBuffer=null;
            (-((index++*7)%23)*.21).toFixed(2)+'s');
    function preloadRebirthPad(){
          fragment.append(el);
        if(padBytesPromise)return padBytesPromise;
          previous=match.index+match[0].length;
        padBytesPromise=fetch(REBIRTH_PAD_URL,{mode:'cors',cache:'force-cache'})
             .then(resp=>{
                if(!resp.ok)throw new Error('PAD HTTP '+resp.status);
                return resp.arrayBuffer();
            })
            .catch(err=>{
                console.debug('[KKPT] PAD file unavailable:',err);
                return null;
            });
        return padBytesPromise;
    }
    function decodeRebirthPad(ctx){
        if(padBuffer)return Promise.resolve(padBuffer);
        if(!padDecodePromise){
            padDecodePromise=preloadRebirthPad()
                .then(bytes=>bytes?ctx.decodeAudioData(bytes.slice(0)):null)
                .then(result=>(padBuffer=result))
                .catch(err=>{
                    console.debug('[KKPT] Cannot decode PAD:',err);
                    return null;
                });
         }
         }
         return padDecodePromise;
         if(previous<raw.length)
          fragment.append(document.createTextNode(raw.slice(previous)));
        textNode.replaceWith(fragment);
      }
    };
    const createAmbience=()=>{
      if(prefersReduced.matches||document.getElementById('kkpt-afterglow')||!document.body)return;
      const overlay=document.createElement('div');
      overlay.id='kkpt-afterglow';
      overlay.setAttribute('aria-hidden','true');
      const el=(type,i)=>{
        const part=document.createElement('span');
        part.className=type==='soot'?'kkpt-soot-cell':'kkpt-ash-cell';
        const smoke=type==='soot';
        const fromLeft=i%2===0;
        part.style.left=(smoke
          ? (fromLeft?-26+Math.random()*22:79+Math.random()*21)
          : Math.random()*100)+'%';
        part.style.top=(smoke?8+Math.random()*94:15+Math.random()*94)+'%';
        part.style.setProperty('--size',(smoke?220+Math.random()*240:2+Math.random()*4).toFixed(1)+'px');
        part.style.setProperty('--delay',(-Math.random()*(smoke?29:18)).toFixed(2)+'s');
        part.style.setProperty('--duration',(smoke?18+Math.random()*14:8+Math.random()*11).toFixed(2)+'s');
        part.style.setProperty('--wind',((Math.random()-.5)*(smoke?100:230)).toFixed(1)+'px');
        return part;
      };
      for(let i=0;i<(innerWidth<700?8:16);i++)overlay.append(el('soot',i));
      for(let i=0;i<30;i++)overlay.append(el('ash',i));
      document.body.append(overlay);
    };
    const init=()=>{
      decorateSpoilers();
      decorateLinks();
      decorateIdentityMentions();
      createAmbience();
      mw.hook('wikipage.content').add(()=>{
        decorateSpoilers();
        decorateLinks();
        decorateIdentityMentions();
      });
    };
    if(document.readyState==='loading')
      document.addEventListener('DOMContentLoaded',init,{once:true});
    else init();
    const pulse=()=>{
      const inactive=document.hidden||prefersReduced.matches||
        root.classList.contains('kkpt-frame-inactive')||
        root.classList.contains('kkpt-world-suspended')||
        (window!==window.top&&window.frameElement?.getAttribute('aria-hidden')==='true');
      if(!inactive){
        root.style.setProperty('--kkpt-rift-y',(14+Math.random()*70).toFixed(1)+'%');
        root.classList.add('kkpt-rift');
        window.setTimeout(()=>root.classList.remove('kkpt-rift'),390);
      }
      window.setTimeout(pulse,5000+Math.random()*15000);
    };
    window.setTimeout(pulse,5000+Math.random()*15000);
  }
  installKuronataAfterglow();
  if (window!==window.top || window.__kkptV12) return;
  window.__kkptV12=true;
  window.__kkptVersion='42.0';
  const SHOW_FLOATING_BUTTON=false;
  const otherPage=basePage==='Kuranoka'?'Kuronata':'Kuranoka';
  const baseUrl=location.href;
  const baseTitle=document.title;
  const otherUrl=new URL('/wiki/'+encodeURIComponent(otherPage),location.href);
  const clipSide=basePage==='Kuranoka'?'right':'left';
  const hideClip=clipSide==='right'?'inset(0 0 0 100%)':'inset(0 100% 0 0)';
  const sleep=ms=>new Promise(r=>setTimeout(r,ms));
  const smooth=t=>{t=Math.max(0,Math.min(1,t));
    return t*t*t*(t*(t*6-15)+10);};
  const imagePool=new Map();
  const worldBackdrops={
    Kuranoka:'https://blockland.wiki/images/0/05/%E7%A7%AF%E9%9B%A8%E4%BA%911.png',
    Kuronata:'https://blockland.wiki/images/a/ab/%E9%BB%84%E6%98%8F%EF%BC%8C%E9%BB%91%E4%BA%91.png'
  };
  function primeWorldBackdrops(){
    const current=worldBackdrops[basePage];
    if(current&&document.head){
      const link=document.createElement('link');
      link.rel='preload';
      link.as='image';
      link.href=current;
      link.setAttribute('fetchpriority','high');
      link.dataset.kkptBackgroundPreload='1';
      document.head.appendChild(link);
      warmImage(current).catch(()=>{});
     }
     }
     // A plain media element can still play the recording when a site's
     setTimeout(()=>{
    // CORS policy disallows reading the file into Web Audio.
      const next=worldBackdrops[otherPage];
    function playRebirthPadFallback(durationMs){
      if(next)warmImage(next).catch(()=>{});
        const sound=new Audio(REBIRTH_PAD_URL);
    },300);
        sound.volume=0;
  }
        sound.play().then(()=>{
  primeWorldBackdrops();
            const begin=performance.now();
  const ENABLE_TRANSITION_AUDIO = true;
            const timer=setInterval(()=>{
  const FIRE_VOLUME = .30;
                const t=Math.min(1,(performance.now()-begin)/durationMs);
  const DAWN_VOLUME = .20;
                sound.volume=Math.max(0,Math.min(DAWN_VOLUME,
  let soundContext=null,fireBuffer=null,activeSound=null;
                    DAWN_VOLUME*Math.min(t/.27,(1-t)/.26)));
  const REBIRTH_PAD_URL=
                if(t>=1){clearInterval(timer);sound.pause();sound.src='';}
    'https://upload.wikimedia.org/wikipedia/commons/e/ed/Padsynth_example_synth_piano.ogg';
            },80);
  let padBytesPromise=null,padDecodePromise=null,padBuffer=null;
         }).catch(err=>console.debug('[KKPT] PAD playback unavailable:',err));
  function preloadRebirthPad(){
    if(padBytesPromise)return padBytesPromise;
    padBytesPromise=fetch(REBIRTH_PAD_URL,{mode:'cors',cache:'force-cache'})
      .then(resp=>{
        if(!resp.ok)throw new Error('PAD HTTP '+resp.status);
        return resp.arrayBuffer();
      })
      .catch(err=>{
        console.debug('[KKPT] PAD file unavailable:',err);
        return null;
      });
    return padBytesPromise;
  }
  function decodeRebirthPad(ctx){
    if(padBuffer)return Promise.resolve(padBuffer);
    if(!padDecodePromise){
      padDecodePromise=preloadRebirthPad()
        .then(bytes=>bytes?ctx.decodeAudioData(bytes.slice(0)):null)
        .then(result=>(padBuffer=result))
         .catch(err=>{
          console.debug('[KKPT] Cannot decode PAD:',err);
          return null;
        });
     }
     }
 
    return padDecodePromise;
     function primeSound(){
  }
        if(!ENABLE_TRANSITION_AUDIO)return;
  function playRebirthPadFallback(durationMs){
        try{
    const sound=new Audio(REBIRTH_PAD_URL);
            const AudioEngine=window.AudioContext||window.webkitAudioContext;
    sound.volume=0;
            if(!AudioEngine)return;
    sound.play().then(()=>{
            if(!soundContext)soundContext=new AudioEngine();
      const begin=performance.now();
            if(soundContext.state==='suspended')soundContext.resume().catch(()=>{});
      const timer=setInterval(()=>{
            decodeRebirthPad(soundContext);
        const t=Math.min(1,(performance.now()-begin)/durationMs);
        }catch(err){console.debug('[KKPT] Audio unavailable:',err);}
        sound.volume=Math.max(0,Math.min(DAWN_VOLUME,
          DAWN_VOLUME*Math.min(t/.27,(1-t)/.26)));
        if(t>=1){clearInterval(timer);sound.pause();sound.src='';}
      },80);
     }).catch(err=>console.debug('[KKPT] PAD playback unavailable:',err));
  }
  function primeSound(){
    if(!ENABLE_TRANSITION_AUDIO)return;
    try{
      const AudioEngine=window.AudioContext||window.webkitAudioContext;
      if(!AudioEngine)return;
      if(!soundContext)soundContext=new AudioEngine();
      if(soundContext.state==='suspended')soundContext.resume().catch(()=>{});
      decodeRebirthPad(soundContext);
    }catch(err){console.debug('[KKPT] Audio unavailable:',err);}
  }
  function createFireAudio(ctx){
    const length=Math.round(ctx.sampleRate*4.5);
    const buffer=ctx.createBuffer(1,length,ctx.sampleRate);
    const data=buffer.getChannelData(0);
    let low=0,middle=0;
    for(let i=0;i<length;i++){
      const random=Math.random()*2-1;
      low=low*.992+random*.008;
      middle=middle*.92+random*.08;
      data[i]=Math.max(-1,Math.min(1,
        low*3.9+middle*.33+random*.055));
     }
     }
     function createFireAudio(ctx){
     for(let k=0;k<350;k++){
        const length=Math.round(ctx.sampleRate*4.5);
      const at=Math.floor(Math.random()*(length-1500));
        const buffer=ctx.createBuffer(1,length,ctx.sampleRate);
      const decay=36+Math.random()*330;
        const data=buffer.getChannelData(0);
      const duration=Math.min(1400,Math.ceil(decay*4));
        let low=0,middle=0;
      const power=.20+Math.pow(Math.random(),3)*.68;
        for(let i=0;i<length;i++){
      for(let j=0;j<duration;j++){
            const random=Math.random()*2-1;
        const env=Math.exp(-j/decay);
            low=low*.992+random*.008;
        data[at+j]=Math.max(-1,Math.min(1,data[at+j]+
            middle=middle*.92+random*.08;
          (Math.random()*2-1)*power*env));
            data[i]=Math.max(-1,Math.min(1,
      }
                low*3.9+middle*.33+random*.055));
        }
        // Micro-crackles: impulsive bursts with different decay times.
        for(let k=0;k<350;k++){
            const at=Math.floor(Math.random()*(length-1500));
            const decay=36+Math.random()*330;
            const duration=Math.min(1400,Math.ceil(decay*4));
            const power=.20+Math.pow(Math.random(),3)*.68;
            for(let j=0;j<duration;j++){
                const env=Math.exp(-j/decay);
                data[at+j]=Math.max(-1,Math.min(1,data[at+j]+
                    (Math.random()*2-1)*power*env));
            }
        }
        return buffer;
     }
     }
     function transitionSound(dark,durationMs){
     return buffer;
        const ctx=soundContext;
  }
        if(!ctx||ctx.state!=='running')return;
  function transitionSound(dark,durationMs){
        const start=ctx.currentTime+.018;
    const ctx=soundContext;
        const span=durationMs/1000;
    if(!ctx||ctx.state!=='running')return;
        if(activeSound){
    const start=ctx.currentTime+.018;
            const old=activeSound.gain;
    const span=durationMs/1000;
            old.cancelScheduledValues(start);
    if(activeSound){
            old.setTargetAtTime(.0001,start,.05);
      const old=activeSound.gain;
        }
      old.cancelScheduledValues(start);
        const master=ctx.createGain();
      old.setTargetAtTime(.0001,start,.05);
        master.gain.setValueAtTime(.0001,start);
        master.gain.linearRampToValueAtTime(dark?FIRE_VOLUME:DAWN_VOLUME,
            start+(dark?.75:1.10));
        master.gain.setValueAtTime(dark?FIRE_VOLUME:DAWN_VOLUME,
            start+span-(dark?.88:1.0));
        master.gain.exponentialRampToValueAtTime(.0001,start+span);
        master.connect(ctx.destination);
        activeSound={gain:master};
        if(dark){
            if(!fireBuffer)fireBuffer=createFireAudio(ctx);
            const src=ctx.createBufferSource();
            src.buffer=fireBuffer;src.loop=true;
            const lowpass=ctx.createBiquadFilter();
            lowpass.type='lowpass';lowpass.frequency.value=2050;
            const bass=ctx.createBiquadFilter();
            bass.type='highpass';bass.frequency.value=55;
            src.connect(lowpass);lowpass.connect(bass);bass.connect(master);
            src.start(start);src.stop(start+span+.12);
        }else{
            // A REAL recorded PADsynth sample; no generated wind-chimes.
            decodeRebirthPad(ctx).then(buffer=>{
                if(!buffer){playRebirthPadFallback(durationMs);return;}
                const at=Math.max(start,ctx.currentTime+.020);
                if(at>start+span-.55)return;
                const player=ctx.createBufferSource();
                player.buffer=buffer;
                player.connect(master);
                player.start(at,Math.max(0,at-start)+.95);
                player.stop(start+span);
            }).catch(()=>{});
        }
        setTimeout(()=>{
            if(activeSound&&activeSound.gain===master)activeSound=null;
            try{master.disconnect();}catch(_){}
        },durationMs+350);
     }
     }
 
    const master=ctx.createGain();
     // Audio starts AT ITS TRANSITION SECTION, not at 00:00. Three seconds of
     master.gain.setValueAtTime(.0001,start);
     // crescendo accompany the fate-wheel awakening. V25's actual fire and
    master.gain.linearRampToValueAtTime(dark?FIRE_VOLUME:DAWN_VOLUME,
     // particle painting remains unchanged. Music 00:33 / 02:39 now lands
      start+(dark?.75:1.10));
     // in the MIDDLE of the corresponding original V25 reveal.
     master.gain.setValueAtTime(dark?FIRE_VOLUME:DAWN_VOLUME,
     const SCORE_KEY='kkpt:score-handoff:41';
      start+span-(dark?.88:1.0));
     const SCORE_MUTE_KEY='kkpt:score-muted';
     master.gain.exponentialRampToValueAtTime(.0001,start+span);
    const SCORES={
     master.connect(ctx.destination);
        Kuronata:{
     activeSound={gain:master};
            id:'marenoia',target:'Kuronata',
     if(dark){
            url:'https://blkl.fun/wp-content/uploads/2026/10/Marenoia.mp3?kkpt_audio=v35',
      if(!fireBuffer)fireBuffer=createFireAudio(ctx);
            startAt:28.275,fireStart:31.275,fireEnd:34.725,
      const src=ctx.createBufferSource();
            stopAt:52.725,fadeInSeconds:3,fadeSeconds:18
      src.buffer=fireBuffer;src.loop=true;
        },
      const lowpass=ctx.createBiquadFilter();
         Kuranoka:{
      lowpass.type='lowpass';lowpass.frequency.value=2050;
            id:'anotherme',target:'Kuranoka',
      const bass=ctx.createBiquadFilter();
            url:'https://blkl.fun/wp-content/uploads/2026/10/AnotherMe.mp3?kkpt_audio=20261024_043627',
      bass.type='highpass';bass.frequency.value=55;
            startAt:154.45,fireStart:157.45,fireEnd:160.55,
      src.connect(lowpass);lowpass.connect(bass);bass.connect(master);
            stopAt:178.55,fadeInSeconds:3,fadeSeconds:18
      src.start(start);src.stop(start+span+.12);
         }
    }else{
    };
      decodeRebirthPad(ctx).then(buffer=>{
    let returnScore=null;
         if(!buffer){playRebirthPadFallback(durationMs);return;}
    let scoreOmen=null;
        const at=Math.max(start,ctx.currentTime+.020);
    let fatePrelude=null;
        if(at>start+span-.55)return;
    function userWantsMute(){
         const player=ctx.createBufferSource();
         try{return sessionStorage.getItem(SCORE_MUTE_KEY)==='1';}
        player.buffer=buffer;
        catch(_){return false;}
        player.connect(master);
        player.start(at,Math.max(0,at-start)+.95);
         player.stop(start+span);
      }).catch(()=>{});
     }
     }
     const AUDIO_ICONS={
     setTimeout(()=>{
        speaker:'<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M15 9a5 5 0 0 1 0 6"/><path d="M18 6a9 9 0 0 1 0 12"/>',
      if(activeSound&&activeSound.gain===master)activeSound=null;
        mute:'<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="m16 9 5 6m0-6-5 6"/>',
      try{master.disconnect();}catch(_){}
        play:'<path d="m8 5 11 7-11 7V5Z"/>'
    },durationMs+350);
    };
  }
    function updateAudioToggle(score){
  const SCORE_KEY='kkpt:score-handoff:42';
        if(!score?.hud)return;
  const SCORE_MUTE_KEY='kkpt:score-muted';
        const paused=score.blocked||score.audio.paused;
  const SCORES={
        const icon=paused?'play':score.audio.muted?'mute':'speaker';
    Kuronata:{
        score.hud.innerHTML='<svg viewBox="0 0 24 24" fill="none" '+
      id:'marenoia',target:'Kuronata',
            'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '+
      url:'https://blkl.fun/wp-content/uploads/2026/10/Marenoia.mp3?kkpt_audio=v35',
            'stroke-linejoin="round" aria-hidden="true">'+AUDIO_ICONS[icon]+'</svg>';
      startAt:28.275,fireStart:31.275,fireEnd:34.725,
        score.hud.setAttribute('aria-label',paused?'恢复播放':
      stopAt:52.725,fadeInSeconds:3,fadeSeconds:18
            score.audio.muted?'取消静音':'静音');
    },
        score.hud.setAttribute('aria-pressed',String(score.audio.muted));
    Kuranoka:{
        score.hud.classList.toggle('kkpt-audio-needs-resume',paused);
      id:'anotherme',target:'Kuranoka',
      url:'https://blkl.fun/wp-content/uploads/2026/10/AnotherMe.mp3?kkpt_audio=20261024_043627',
      startAt:154.45,fireStart:157.45,fireEnd:160.55,
      stopAt:178.55,fadeInSeconds:3,fadeSeconds:18
     }
     }
     function mountAudioToggle(score){
  };
        const old=document.getElementById('kkpt-audio-toggle');
  let returnScore=null;
        old?.remove();
  let scoreOmen=null;
        const hud=document.createElement('button');
  let fatePrelude=null;
        hud.type='button';hud.id='kkpt-audio-toggle';
  function userWantsMute(){
        hud.setAttribute('aria-label','静音');
    try{return sessionStorage.getItem(SCORE_MUTE_KEY)==='1';}
        score.hud=hud;
    catch(_){return false;}
        document.body.append(hud);
  }
        const audio=score.audio;
  const AUDIO_ICONS={
        hud.addEventListener('click',()=>{
    speaker:'<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M15 9a5 5 0 0 1 0 6"/><path d="M18 6a9 9 0 0 1 0 12"/>',
            if(audio.paused||score.blocked){
    mute:'<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="m16 9 5 6m0-6-5 6"/>',
                score.blocked=false;
    play:'<path d="m8 5 11 7-11 7V5Z"/>'
                const resume=typeof score.resume==='function'?
  };
                    score.resume():audio.play();
  function updateAudioToggle(score){
                Promise.resolve(resume).then(()=>{
    if(!score?.hud)return;
                    updateAudioToggle(score);
    const paused=score.blocked||score.audio.paused;
                }).catch(err=>{
    const icon=paused?'play':score.audio.muted?'mute':'speaker';
                    console.info('[KKPT] Audio resume blocked:',err);
    score.hud.innerHTML='<svg viewBox="0 0 24 24" fill="none" '+
                    score.blocked=true;updateAudioToggle(score);
      'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '+
                });
      'stroke-linejoin="round" aria-hidden="true">'+AUDIO_ICONS[icon]+'</svg>';
                updateAudioToggle(score);
    score.hud.setAttribute('aria-label',paused?'恢复播放':
                return;
      score.audio.muted?'取消静音':'静音');
            }
    score.hud.setAttribute('aria-pressed',String(score.audio.muted));
            audio.muted=!audio.muted;
     score.hud.classList.toggle('kkpt-audio-needs-resume',paused);
            try{sessionStorage.setItem(SCORE_MUTE_KEY,audio.muted?'1':'0');}
  }
            catch(_){}
  function mountAudioToggle(score){
            updateAudioToggle(score);
    const old=document.getElementById('kkpt-audio-toggle');
    old?.remove();
    const hud=document.createElement('button');
    hud.type='button';hud.id='kkpt-audio-toggle';
    hud.setAttribute('aria-label','静音');
    score.hud=hud;
    document.body.append(hud);
    const audio=score.audio;
    hud.addEventListener('click',()=>{
      if(audio.paused||score.blocked){
        score.blocked=false;
        const resume=typeof score.resume==='function'?
          score.resume():audio.play();
        Promise.resolve(resume).then(()=>{
          updateAudioToggle(score);
        }).catch(err=>{
          console.info('[KKPT] Audio resume blocked:',err);
          score.blocked=true;updateAudioToggle(score);
         });
         });
        audio.addEventListener('play',()=>updateAudioToggle(score));
        audio.addEventListener('pause',()=>updateAudioToggle(score));
         updateAudioToggle(score);
         updateAudioToggle(score);
        return;
      }
      audio.muted=!audio.muted;
      try{sessionStorage.setItem(SCORE_MUTE_KEY,audio.muted?'1':'0');}
      catch(_){}
      updateAudioToggle(score);
    });
    audio.addEventListener('play',()=>updateAudioToggle(score));
    audio.addEventListener('pause',()=>updateAudioToggle(score));
    updateAudioToggle(score);
  }
  function removeScoreOmen(){
    if(scoreOmen){
      const old=scoreOmen;
      const opacity=getComputedStyle(old).opacity;
      scoreOmen=null;
      old.style.opacity=opacity;
      old.classList.add('kkpt-omen-closing');
      requestAnimationFrame(()=>requestAnimationFrame(()=>{
        old.style.opacity='0';
      }));
      setTimeout(()=>old.remove(),1540);
    }
    document.body.classList.remove('kkpt-omen-running');
  }
  function startScoreOmen(score){
    if(score?.config?.id!=='marenoia'||scoreOmen)return;
    const omen=document.createElement('div');
    omen.id='kkpt-omen';omen.setAttribute('aria-hidden','true');
    omen.innerHTML='<i class="kkpt-omen-glow"></i>'+
      '<i class="kkpt-omen-line"></i>';
    document.body.append(omen);
    scoreOmen=omen;
    document.body.classList.add('kkpt-omen-running');
  }
  function clearFatePrelude(){
    fatePrelude?.remove();fatePrelude=null;
  }
  function releaseFatePrelude(){
    if(!fatePrelude)return;
    const node=fatePrelude;
    node.classList.add('kkpt-fate-merging');
    requestAnimationFrame(()=>{if(node.isConnected)node.style.opacity='0';});
    setTimeout(()=>{
      if(fatePrelude===node)clearFatePrelude();
      else node.remove();
    },1700);
  }
  function fateSigilMarkup(isDark){
    const at=(r,d)=>{
      const a=(d-90)*Math.PI/180;
      return [(150+Math.cos(a)*r).toFixed(2),
        (150+Math.sin(a)*r).toFixed(2)];
    };
    const ring=(r,cls)=>'<circle class="'+cls+
      '" cx="150" cy="150" r="'+r+'"/>';
    const stroke=(a,b,cls)=>'<path class="'+cls+
      '" d="M'+a.join(' ')+' L'+b.join(' ')+'"/>';
    const poly=(points,cls)=>'<polygon class="'+cls+'" points="'+
      points.map(p=>p.join(',')).join(' ')+'"/>';
    const outer=ring(133,'kkpt-sigil-outer')+
      ring(128,'kkpt-sigil-orbit')+
      ring(121,'kkpt-sigil-ghost')+
      ring(105,'kkpt-sigil-seal')+
      ring(100,'kkpt-sigil-trace')+
      ring(83,'kkpt-sigil-ghost');
    const glyphs=Array.from({length:48},(_,i)=>{
      const angle=i*7.5;
      const major=i%4===0;
      return stroke(at(121,angle),at(major?132:i%2?125:128,angle),
        major?'kkpt-sigil-mark kkpt-sigil-major':'kkpt-sigil-mark');
    }).join('');
    const cardinal=Array.from({length:8},(_,i)=>{
      const a=i*45;
      return poly([at(99,a),at(111,a+4.2),at(120,a),at(111,a-4.2)],
        'kkpt-sigil-diamond')+
        stroke(at(85,a),at(96,a),'kkpt-sigil-needle');
    }).join('');
    const darkDrawing=
      ring(54,'kkpt-sigil-core-orbit')+
      ring(43,'kkpt-sigil-inner-orbit')+
      '<path class="kkpt-sigil-dark-mark" d="M150 115L170 150 150 185 130 150Z"/>'+
      '<path class="kkpt-sigil-dark-cut" d="M150 130L156 150 150 170 144 150Z"/>'+
      '<path class="kkpt-sigil-axis" d="M150 96V108M150 192V204"/>';
    const hopeDrawing=
      ring(54,'kkpt-sigil-core-orbit')+
      ring(43,'kkpt-sigil-inner-orbit')+
      '<path class="kkpt-sigil-light-mark" d="M150 119L158 142 181 150 158 158 150 181 142 158 119 150 142 142Z"/>'+
      '<circle class="kkpt-sigil-center" cx="150" cy="150" r="7"/>'+
      '<path class="kkpt-sigil-axis" d="M150 96V108M150 192V204"/>';
    return '<svg class="kkpt-fate-sigil '+(isDark?'kkpt-sigil-ominous':
      'kkpt-sigil-hope')+'" viewBox="0 0 300 300" '+
      'xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'+
      '<g class="kkpt-sigil-engraving" fill="none" stroke="'+
        (isDark?'#ff8c9b':'#f7edcf')+'" stroke-width="1.3">'+
        outer+glyphs+cardinal+
        (isDark?darkDrawing:hopeDrawing)+'</g></svg>';
  }
  function startFatePrelude(anchor,targetPage){
    clearFatePrelude();
    const box=anchor?.getBoundingClientRect?.();
    const x=box?box.left+box.width/2:innerWidth/2;
    const y=box?box.top+box.height/2:innerHeight/2;
    const prelude=document.createElement('div');
    prelude.id='kkpt-fate-prelude';
    prelude.setAttribute('aria-hidden','true');
    const isDark=targetPage==='Kuronata';
    prelude.className=isDark?'kkpt-fate-dark':'kkpt-fate-light';
    prelude.style.setProperty('--kkpt-fate-x',x+'px');
    prelude.style.setProperty('--kkpt-fate-y',y+'px');
    prelude.style.setProperty('--kkpt-fate-dx',(innerWidth/2-x)+'px');
    prelude.style.setProperty('--kkpt-fate-dy',(innerHeight/2-y)+'px');
    prelude.style.setProperty('--kkpt-fate-dx15',((innerWidth/2-x)*.15)+'px');
    prelude.style.setProperty('--kkpt-fate-dy15',((innerHeight/2-y)*.15)+'px');
    prelude.style.setProperty('--kkpt-fate-dx91',((innerWidth/2-x)*.91)+'px');
    prelude.style.setProperty('--kkpt-fate-dy91',((innerHeight/2-y)*.91)+'px');
    prelude.innerHTML='<div class="kkpt-fate-orbit">'+
      '<i class="kkpt-fate-aura"></i>'+
      '<i class="kkpt-fate-ring kkpt-fate-ring-a"></i>'+
      '<i class="kkpt-fate-ring kkpt-fate-ring-b"></i>'+
      '<i class="kkpt-fate-arc"></i>'+
      '<i class="kkpt-fate-spark"></i>'+
      '<i class="kkpt-fate-wave"></i>'+
      fateSigilMarkup(isDark)+'</div>';
    root.append(prelude);
    fatePrelude=prelude;
    setTimeout(()=>{
      if(fatePrelude===prelude)clearFatePrelude();
      else prelude.remove();
    },30000);
  }
  const MARENOIA_TRANSIENTS=[
    [28.453,.56],[28.682,.58],[28.892,.76],[29.121,.64],
    [29.341,.72],[29.570,.48],[29.780,.87],[30.009,.93],
    [30.438,.44],[30.658,.77],[30.887,.83],[31.356,.68],
    [31.556,.79],[31.775,.93],[32.364,.29],[32.883,.36],
    [33.192,.38],[33.561,1],[33.751,.97],[34.250,.86],
    [34.469,.54],[35.317,.86],[35.786,.69]
  ];
  function createMarenoiaBeatScene(score){
    if(score.config.id!=='marenoia')return null;
    const lightningLayer=document.createElement('div');
    lightningLayer.id='kkpt-lightning-layer';
    lightningLayer.setAttribute('aria-hidden','true');
    document.body.append(lightningLayer);
    let live=true,raf=0,lastTick=0,nextBeat=0,progress=0;
    let lastLightning=-100;
    function lightning(t,strength){
      if(!live||t<.11||t>.96)return;
      const w=Math.max(1,innerWidth),h=Math.max(1,innerHeight);
      const rng=(a,b)=>a+Math.random()*(b-a);
      const edge=w*(1-t);
      let x=Math.min(w*.93,Math.max(edge+35,edge+(w-edge)*.53));
      const top=rng(-70,-16),bottom=rng(h*.46,h*.80);
      const points=[[x,top]];
      const count=9;
      for(let i=1;i<=count;i++){
        const yy=top+(bottom-top)*i/count;
        const xx=x+rng(-39,39)*(i/count)+Math.sin(i*2.4)*14;
        points.push([Math.max(10,Math.min(w-10,xx)),yy]);
      }
      const path=points.map((p,i)=>(i?'L':'M')+
        p[0].toFixed(1)+','+p[1].toFixed(1)).join(' ');
      const branchAt=Math.floor(rng(3,7));
      const p=points[branchAt];
      const branch='M'+p[0].toFixed(1)+','+p[1].toFixed(1)+
        ' l'+rng(-46,46).toFixed(1)+','+rng(45,86).toFixed(1)+
        ' l'+rng(-28,28).toFixed(1)+','+rng(38,71).toFixed(1);
      const ns='http://www.w3.org/2000/svg';
      const svg=document.createElementNS(ns,'svg');
      svg.setAttribute('class','kkpt-storm-bolt');
      svg.setAttribute('viewBox','0 0 '+w+' '+h);
      svg.setAttribute('preserveAspectRatio','none');
      svg.style.opacity=String(Math.min(1,.46+strength*.42));
      svg.style.clipPath='inset(0 0 0 '+
        Math.max(0,(1-t)*100).toFixed(2)+'%)';
      const addPath=(d,klass)=>{
        const el=document.createElementNS(ns,'path');
        el.setAttribute('d',d);el.setAttribute('class',klass);
        svg.append(el);
      };
      addPath(path,'kkpt-bolt-haze');
      addPath(path,'kkpt-bolt-core');
      addPath(branch,'kkpt-bolt-branch');
      lightningLayer.append(svg);
      setTimeout(()=>svg.remove(),390);
     }
     }
     function removeScoreOmen(){
     function tick(now){
        if(scoreOmen){
      if(!live)return;
            const old=scoreOmen;
      raf=requestAnimationFrame(tick);
            const opacity=getComputedStyle(old).opacity;
      if(now-lastTick<30)return;lastTick=now;
            scoreOmen=null;
      const audio=score.audio;
            old.style.opacity=opacity;
      const audioTime=audio.currentTime;
            old.classList.add('kkpt-omen-closing');
      while(nextBeat<MARENOIA_TRANSIENTS.length&&
            // Let the V25 fire steadily replace the red veil. Abruptly
        MARENOIA_TRANSIENTS[nextBeat][0]<=audioTime){
            // removing it at t=0 was the visible "turns bright" flash.
        const [moment,strength]=MARENOIA_TRANSIENTS[nextBeat++];
            requestAnimationFrame(()=>requestAnimationFrame(()=>{
        if(audioTime-moment<.22){
                old.style.opacity='0';
          if(progress>.11&&strength>.81&&moment-lastLightning>.18){
             }));
             lightning(progress,strength);
             setTimeout(()=>old.remove(),1540);
             lastLightning=moment;
          }
         }
         }
        document.body.classList.remove('kkpt-omen-running');
      }
     }
     }
     function startScoreOmen(score){
     raf=requestAnimationFrame(tick);
        if(score?.config?.id!=='marenoia'||scoreOmen)return;
    return {
         const omen=document.createElement('div');
      update(t){progress=t;},
         omen.id='kkpt-omen';omen.setAttribute('aria-hidden','true');
      finish(){
        omen.innerHTML='<i class="kkpt-omen-glow"></i>'+
        live=false;cancelAnimationFrame(raf);
            '<i class="kkpt-omen-line"></i>';
         setTimeout(()=>lightningLayer.remove(),450);
        document.body.append(omen);
      },
        scoreOmen=omen;
      dispose(){
        document.body.classList.add('kkpt-omen-running');
        live=false;cancelAnimationFrame(raf);
         lightningLayer.remove();
      }
    };
  }
  const AFTER_DARK_KEY='kkpt:dark-afterglow:42';
  const DAWN_HALO_KEY='kkpt:dawn-cloudbreak:42';
  const MARENOIA_EIGHT_BEAT_KICKS=[
    35.311,38.843,42.375,45.907,49.429
  ];
  const MARENOIA_LOUDNESS=[
    .61,.71,.98,1,.85,.96,.92,.72,.57,.54,.45,.46,
    .53,.54,.56,.63,.92,1,.88,.95,.94,.77,.61,.61,
    .52,.41,.46,.48,.44,.50,.81,1,.92,.98,.90,.80,
    .63,.58,.54,.61,.65,.67,.65,.69,.83,1,.90,.93,
    .94,.81,.65,.65,.66,.65,.62,.64,.60,.58,.70,1,
    .91,.90,.95,.83,.62,.58,.46,.54,.47,.57,.55,.63
  ];
  function marenoiaLoudness(time){
    const pos=Math.max(0,Math.min(MARENOIA_LOUDNESS.length-1,
      (time-34.75)*4));
    const i=Math.floor(pos),j=Math.min(MARENOIA_LOUDNESS.length-1,i+1);
    return MARENOIA_LOUDNESS[i]*(1-(pos-i))+
      MARENOIA_LOUDNESS[j]*(pos-i);
  }
  function marenoiaDeepPulse(age){
    if(age<0||age>=2.98)return 0;
    if(age<.42)return smooth(age/.42);
    if(age<.72)return 1;
    return 1-smooth((age-.72)/2.26);
  }
  let kuronataAftermath=null;
  function mountKuronataAftermath(score){
    if(kuronataAftermath){
      if(score)kuronataAftermath.attach(score);
      return kuronataAftermath;
     }
     }
     function clearFatePrelude(){
    const layer=document.createElement('div');
         fatePrelude?.remove();fatePrelude=null;
    layer.id='kkpt-beat-scene';
    layer.setAttribute('aria-hidden','true');
    layer.className='kkpt-beat-after kkpt-beat-idle';
    layer.innerHTML='<div class="kkpt-beat-focus"></div>'+
      '<div class="kkpt-beat-lens"></div>'+
      '<div class="kkpt-beat-fringe"></div>'+
      '<div class="kkpt-beat-rupture"></div>'+
      '<svg class="kkpt-beat-scars" viewBox="0 0 1440 900" '+
      'preserveAspectRatio="none" aria-hidden="true">'+
      '<path d="M-20 340L126 317 203 355 294 339 383 382 477 352 605 395 728 365 842 399 982 371 1125 397 1264 351 1460 390" />'+
      '<path d="M-20 635L158 605 294 646 404 612 547 668 679 622 846 653 991 621 1149 668 1277 636 1460 660" />'+
      '<path d="M203 355l-19 33 27 39M842 399l30 34-20 47M404 612l-44 42 23 29M1149 668l39 41-18 32"/>'+
      '</svg>';
    root.append(layer);
    let live=true,raf=0,idleTimer=0,lastTick=0,source=null,lastKick=-100;
    let cameraReleaseTimer=null;
    let smoothPressure=0,index=0;
    const layerValues=Object.create(null),rootValues=Object.create(null);
    const setFx=(node,cache,name,value)=>{
      if(cache[name]===value)return;
      cache[name]=value;
      node.style.setProperty(name,value);
    };
    const setLayer=(name,value)=>setFx(layer,layerValues,name,value);
    const setRoot=(name,value)=>setFx(root,rootValues,name,value);
    const stopTicks=()=>{
      cancelAnimationFrame(raf);
      clearTimeout(idleTimer);
      idleTimer=0;
    };
    const queueTick=(delay=0)=>{
      if(!delay){raf=requestAnimationFrame(tick);return;}
      idleTimer=setTimeout(()=>{
        idleTimer=0;
        if(live)raf=requestAnimationFrame(tick);
      },delay);
    };
    const releaseCamera=()=>{
      setRoot('--kkpt-camera-zoom','1');
      clearTimeout(cameraReleaseTimer);
      cameraReleaseTimer=setTimeout(()=>{
        if(!live)root.classList.remove('kkpt-camera-running');
      },420);
    };
    const quiet=()=>{
      setLayer('--kkpt-beat-dark','.60');
      setLayer('--kkpt-beat-scale','1.025');
      setLayer('--kkpt-beat-blur','0.28px');
      setLayer('--kkpt-beat-glitch','.12');
      setLayer('--kkpt-beat-shear','0px');
      setLayer('--kkpt-beat-shear-reverse','0px');
      setLayer('--kkpt-beat-scars','.035');
      setRoot('--kkpt-camera-zoom','1');
      setRoot('--kkpt-camera-focus-y',
        (scrollY+innerHeight*.5).toFixed(1)+'px');
      setRoot('--kkpt-chroma-red','0px');
      setRoot('--kkpt-chroma-shadow','0px');
    };
     function tick(now){
      if(!live)return;
      if(now-lastTick<38){
        queueTick(Math.max(0,38-(now-lastTick)));
        return;
      }
      const dt=Math.min(.12,Math.max(.038,(now-lastTick)/1000));
      lastTick=now;
      if(!source||source.failed||source.audio.ended||
        source.audio.currentTime>=source.config.stopAt-.02){
         source=null;layer.classList.add('kkpt-beat-idle');
        quiet();live=false;stopTicks();
        releaseCamera();
        return;
      }
      const audioTime=source.audio.currentTime;
      while(index<MARENOIA_EIGHT_BEAT_KICKS.length&&
        MARENOIA_EIGHT_BEAT_KICKS[index]<=audioTime){
        lastKick=MARENOIA_EIGHT_BEAT_KICKS[index++];
      }
      const actualGain=Number(source.audio.volume);
      const playerGain=Number.isFinite(actualGain)
        ? Math.max(0,Math.min(1,actualGain/.78)):1;
      const soundStrength=(.48+.52*marenoiaLoudness(audioTime))*
        playerGain;
      const targetPressure=marenoiaDeepPulse(audioTime-lastKick)*
        soundStrength;
      const follow=1-Math.exp(-dt/.21);
      smoothPressure+=(targetPressure-smoothPressure)*follow;
      const pressure=smoothPressure;
      root.classList.add('kkpt-camera-running');
      setLayer('--kkpt-beat-dark',
        (.60+.38*pressure).toFixed(2));
      setLayer('--kkpt-beat-scale',
        (1.025-.175*pressure).toFixed(3));
      setLayer('--kkpt-beat-blur',
        (.28+2.6*pressure).toFixed(1)+'px');
      setLayer('--kkpt-beat-glitch',
        (.12+.63*pressure).toFixed(2));
      const shear=Math.sin(index*1.47)*pressure*9.0;
      setLayer('--kkpt-beat-shear',
        shear.toFixed(1)+'px');
      setLayer('--kkpt-beat-shear-reverse',
        (-shear*.86).toFixed(1)+'px');
      setLayer('--kkpt-beat-scars',
        (.035+.76*pressure).toFixed(2));
      setRoot('--kkpt-camera-zoom',
        (1.008+.054*pressure).toFixed(4));
      setRoot('--kkpt-camera-focus-y',
        (scrollY+innerHeight*.5).toFixed(1)+'px');
      setRoot('--kkpt-chroma-red',
        (-4.4*pressure).toFixed(1)+'px');
      setRoot('--kkpt-chroma-shadow',
        (5.6*pressure).toFixed(1)+'px');
      const nextKick=MARENOIA_EIGHT_BEAT_KICKS[index]??Infinity;
      if(pressure<.003&&targetPressure===0&&nextKick-audioTime>.22){
        queueTick(90);
      }else queueTick(38);
     }
     }
     function releaseFatePrelude(){
     quiet();
         if(!fatePrelude)return;
    kuronataAftermath={
         const node=fatePrelude;
      attach(nextScore){
         node.classList.add('kkpt-fate-merging');
        source=nextScore;
         requestAnimationFrame(()=>{if(node.isConnected)node.style.opacity='0';});
        nextScore.aftermath=kuronataAftermath;
         // The old fixed 6.6s removal could create a blank interlude when
         stopTicks();
         // MP3 seeking/buffering postponed its cue. Release only after fire.
         clearTimeout(cameraReleaseTimer);
        live=true;lastTick=0;lastKick=-100;smoothPressure=0;
        index=0;
         layer.classList.remove('kkpt-beat-idle');
         raf=requestAnimationFrame(tick);
      },
      settle(){
        source=null;live=false;stopTicks();
        layer.classList.add('kkpt-beat-idle');quiet();
        releaseCamera();
      },
      fadeToDawn(){
        source=null;live=false;stopTicks();
         releaseCamera();
         layer.classList.add('kkpt-beat-outro');
        const old=layer;
         setTimeout(()=>{
         setTimeout(()=>{
            if(fatePrelude===node)clearFatePrelude();
          old.remove();
            else node.remove();
          if(kuronataAftermath?.layer===old)kuronataAftermath=null;
         },1700);
        },2400);
      },
      dispose(){
        source=null;live=false;stopTicks();
        clearTimeout(cameraReleaseTimer);
        root.classList.remove('kkpt-camera-running');
         root.style.removeProperty('--kkpt-camera-zoom');
        root.style.removeProperty('--kkpt-camera-focus-y');
        root.style.removeProperty('--kkpt-chroma-red');
        root.style.removeProperty('--kkpt-chroma-shadow');
        layer.remove();kuronataAftermath=null;
      },
      layer
    };
    if(score)kuronataAftermath.attach(score);
    return kuronataAftermath;
  }
  function createCloudbreakAfterglow(continueFromNavigation=false){
    const previous=document.getElementById('kkpt-cloudbreak');
    previous?.remove();
    let timestamp=Date.now();
    if(continueFromNavigation){
      try{timestamp=Number(sessionStorage.getItem(DAWN_HALO_KEY));}
      catch(_){return;}
      if(!Number.isFinite(timestamp)||timestamp<=0)return;
    }else{
      try{sessionStorage.setItem(DAWN_HALO_KEY,String(timestamp));}
      catch(_){}
     }
     }
     function fateSigilMarkup(isDark){
     const elapsed=Math.max(0,Date.now()-timestamp);
        const at=(r,d)=>{
    if(elapsed>6900){
            const a=(d-90)*Math.PI/180;
      try{sessionStorage.removeItem(DAWN_HALO_KEY);}catch(_){}
            return [(150+Math.cos(a)*r).toFixed(2),
      return;
                (150+Math.sin(a)*r).toFixed(2)];
        };
        const ring=(r,cls)=>'<circle class="'+cls+
            '" cx="150" cy="150" r="'+r+'"/>';
        const stroke=(a,b,cls)=>'<path class="'+cls+
            '" d="M'+a.join(' ')+' L'+b.join(' ')+'"/>';
        const poly=(points,cls)=>'<polygon class="'+cls+'" points="'+
            points.map(p=>p.join(',')).join(' ')+'"/>';
        // V41: let the BORDER carry the ornamentation. The middle is a
        // single quiet emblem; no eye / thorns / wings / stacked rays.
        const outer=ring(133,'kkpt-sigil-outer')+
            ring(128,'kkpt-sigil-orbit')+
            ring(121,'kkpt-sigil-ghost')+
            ring(105,'kkpt-sigil-seal')+
            ring(100,'kkpt-sigil-trace')+
            ring(83,'kkpt-sigil-ghost');
        const glyphs=Array.from({length:48},(_,i)=>{
            const angle=i*7.5;
            const major=i%4===0;
            return stroke(at(121,angle),at(major?132:i%2?125:128,angle),
                major?'kkpt-sigil-mark kkpt-sigil-major':'kkpt-sigil-mark');
        }).join('');
        const cardinal=Array.from({length:8},(_,i)=>{
            const a=i*45;
            return poly([at(99,a),at(111,a+4.2),at(120,a),at(111,a-4.2)],
                'kkpt-sigil-diamond')+
                stroke(at(85,a),at(96,a),'kkpt-sigil-needle');
        }).join('');
        const darkDrawing=
            ring(54,'kkpt-sigil-core-orbit')+
            ring(43,'kkpt-sigil-inner-orbit')+
            '<path class="kkpt-sigil-dark-mark" d="M150 115L170 150 150 185 130 150Z"/>'+
            '<path class="kkpt-sigil-dark-cut" d="M150 130L156 150 150 170 144 150Z"/>'+
            '<path class="kkpt-sigil-axis" d="M150 96V108M150 192V204"/>';
        const hopeDrawing=
            ring(54,'kkpt-sigil-core-orbit')+
            ring(43,'kkpt-sigil-inner-orbit')+
            '<path class="kkpt-sigil-light-mark" d="M150 119L158 142 181 150 158 158 150 181 142 158 119 150 142 142Z"/>'+
            '<circle class="kkpt-sigil-center" cx="150" cy="150" r="7"/>'+
            '<path class="kkpt-sigil-axis" d="M150 96V108M150 192V204"/>';
        return '<svg class="kkpt-fate-sigil '+(isDark?'kkpt-sigil-ominous':
            'kkpt-sigil-hope')+'" viewBox="0 0 300 300" '+
            'xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'+
            '<g class="kkpt-sigil-engraving" fill="none" stroke="'+
                (isDark?'#ff8c9b':'#f7edcf')+'" stroke-width="1.3">'+
                outer+glyphs+cardinal+
                (isDark?darkDrawing:hopeDrawing)+'</g></svg>';
     }
     }
     function startFatePrelude(anchor,targetPage){
     const bloom=document.createElement('div');
        clearFatePrelude();
    bloom.id='kkpt-cloudbreak';
        const box=anchor?.getBoundingClientRect?.();
    bloom.setAttribute('aria-hidden','true');
        const x=box?box.left+box.width/2:innerWidth/2;
    bloom.style.setProperty('--kkpt-dawn-delay',(-elapsed)+'ms');
         const y=box?box.top+box.height/2:innerHeight/2;
    bloom.innerHTML='<i class="kkpt-dawn-dark-rim"></i>'+
        const prelude=document.createElement('div');
      '<i class="kkpt-dawn-bright-rim"></i>'+
        prelude.id='kkpt-fate-prelude';
      '<i class="kkpt-dawn-cloud-sweep"></i>';
        prelude.setAttribute('aria-hidden','true');
    root.append(bloom);
        const isDark=targetPage==='Kuronata';
    setTimeout(()=>{
        prelude.className=isDark?'kkpt-fate-dark':'kkpt-fate-light';
      bloom.remove();
        prelude.style.setProperty('--kkpt-fate-x',x+'px');
      try{
        prelude.style.setProperty('--kkpt-fate-y',y+'px');
         if(Number(sessionStorage.getItem(DAWN_HALO_KEY))===timestamp)
        prelude.style.setProperty('--kkpt-fate-dx',(innerWidth/2-x)+'px');
          sessionStorage.removeItem(DAWN_HALO_KEY);
        prelude.style.setProperty('--kkpt-fate-dy',(innerHeight/2-y)+'px');
      }catch(_){}
        prelude.style.setProperty('--kkpt-fate-dx15',((innerWidth/2-x)*.15)+'px');
    },Math.max(0,7000-elapsed));
        prelude.style.setProperty('--kkpt-fate-dy15',((innerHeight/2-y)*.15)+'px');
  }
         prelude.style.setProperty('--kkpt-fate-dx91',((innerWidth/2-x)*.91)+'px');
  function createRebirthVisual(){
         prelude.style.setProperty('--kkpt-fate-dy91',((innerHeight/2-y)*.91)+'px');
    const layer=document.createElement('div');
         prelude.innerHTML='<div class="kkpt-fate-orbit">'+
    layer.id='kkpt-rebirth-visual';
            '<i class="kkpt-fate-aura"></i>'+
    layer.setAttribute('aria-hidden','true');
            '<i class="kkpt-fate-ring kkpt-fate-ring-a"></i>'+
    const drizzle=document.createElement('div');
            '<i class="kkpt-fate-ring kkpt-fate-ring-b"></i>'+
    drizzle.className='kkpt-rebirth-rain';
            '<i class="kkpt-fate-arc"></i>'+
    const rainbow=document.createElementNS('http://www.w3.org/2000/svg','svg');
            '<i class="kkpt-fate-spark"></i>'+
    rainbow.setAttribute('class','kkpt-rebirth-rainbow');
            '<i class="kkpt-fate-wave"></i>'+
    rainbow.setAttribute('viewBox','0 0 1000 650');
            fateSigilMarkup(isDark)+'</div>';
    rainbow.setAttribute('preserveAspectRatio','none');
        // The sigil is a viewport overlay. Keeping it outside the zoomed
    const svgNS='http://www.w3.org/2000/svg';
        // Wiki body makes the clicked gear travel directly to true center.
    const defs=document.createElementNS(svgNS,'defs');
        root.append(prelude);
    const gradient=document.createElementNS(svgNS,'linearGradient');
        fatePrelude=prelude;
    gradient.setAttribute('id','kkpt-rebirth-muted-arc');
        // Only an emergency timeout: the sigil must remain visible while
    gradient.setAttribute('x1','0%');
        // waiting for a delayed soundtrack cue. No empty gap before fire.
    gradient.setAttribute('x2','100%');
        setTimeout(()=>{
    const haloColors=['#c7e5ff','#e4f7e6','#f7ebd9','#e7e0f5'];
            if(fatePrelude===prelude)clearFatePrelude();
    haloColors.forEach((color,i)=>{
            else prelude.remove();
      const stop=document.createElementNS(svgNS,'stop');
         },30000);
      stop.setAttribute('offset',(i*100/3).toFixed(1)+'%');
      stop.setAttribute('stop-color',color);
      gradient.append(stop);
    });
    defs.append(gradient);
    const arc=document.createElementNS(svgNS,'path');
    arc.setAttribute('d','M -100 625 Q 500 -175 1100 625');
    arc.setAttribute('fill','none');
    arc.setAttribute('stroke','url(#kkpt-rebirth-muted-arc)');
    arc.setAttribute('stroke-width','36');
    rainbow.append(defs,arc);
    const light=document.createElement('div');
    light.className='kkpt-rebirth-sunlight';
    const essence=document.createElement('div');
    essence.className='kkpt-rebirth-essence';
    const count=innerWidth<700?14:24;
    for(let i=0;i<count;i++){
      const mote=document.createElement('i');
      mote.className='kkpt-rebirth-mote';
      mote.style.left=(5+Math.random()*88).toFixed(1)+'%';
      mote.style.top=(22+Math.random()*73).toFixed(1)+'%';
      mote.style.animationDelay=(-Math.random()*2.8).toFixed(2)+'s';
      mote.style.animationDuration=(2.3+Math.random()*1.8).toFixed(2)+'s';
      essence.append(mote);
    }
    layer.append(drizzle,light,rainbow,essence);
    document.body.append(layer);
    let last=-1,alive=true;
    return {
      update(t){
         if(!alive||Math.abs(t-last)<.018&&t<1)return;
        last=t;
        layer.style.clipPath='inset(0 '+
          (Math.max(0,1-t)*100).toFixed(2)+'% 0 0)';
        drizzle.style.opacity=(.30*Math.pow(1-t,1.4)).toFixed(3);
         light.style.opacity=(Math.max(0,Math.min(1,(t-.12)/.67))*.47).toFixed(3);
        rainbow.style.opacity=(Math.max(0,Math.min(1,(t-.48)/.40))*.105).toFixed(3);
        essence.style.opacity=(Math.max(0,Math.min(1,(t-.22)/.55))*.72).toFixed(3);
      },
      finish(){
         alive=false;layer.classList.add('kkpt-rebirth-finish');
        setTimeout(()=>layer.remove(),1250);
      },
      dispose(){alive=false;layer.remove();}
    };
  }
  function clearReturnScore(score){
    if(!score)return;
    clearInterval(score.displayTimer);
    score.hud?.remove();
    score.beatScene?.dispose();
    score.aftermath?.settle();
    try{
      score.audio.pause();
      score.audio.removeAttribute('src');score.audio.load();
    }catch(_){}
    if(returnScore===score)returnScore=null;
    if(score.config?.id==='marenoia')removeScoreOmen();
  }
  function watchScore(score){
    const audio=score.audio,config=score.config;
    score.displayTimer=setInterval(()=>{
      if(audio.ended||audio.error||audio.currentTime>=config.stopAt-.018){
        clearReturnScore(score);return;
      }
      const elapsed=Math.max(0,audio.currentTime-config.startAt);
      const remaining=Math.max(0,config.stopAt-audio.currentTime);
      const fadeIn=config.fadeInSeconds?
        Math.min(1,elapsed/config.fadeInSeconds):1;
      const fadeOut=config.fadeSeconds?
        Math.min(1,remaining/config.fadeSeconds):1;
      const volume=.78*Math.min(fadeIn,fadeOut);
      if(Math.abs(audio.volume-volume)>.0008)audio.volume=volume;
    },60);
    audio.addEventListener('ended',()=>clearReturnScore(score),{once:true});
    audio.addEventListener('error',()=>{
      score.failed=true;clearReturnScore(score);
    },{once:true});
  }
  function createScore(config){
    const audio=new Audio(config.url);
    audio.preload='auto';audio.playsInline=true;
    audio.volume=0;audio.muted=userWantsMute();
    const score={audio,config,failed:false,blocked:false,
      displayTimer:null,hud:null,playPromise:null};
    returnScore=score;
    mountAudioToggle(score);
    watchScore(score);
    return score;
  }
  function armReturnScore(targetPage){
    const config=SCORES[targetPage];
    if(!config||targetPage===basePage)return null;
    if(returnScore&&!returnScore.failed&&
      returnScore.config.id===config.id)return returnScore;
    if(returnScore)clearReturnScore(returnScore);
    const score=createScore(config);
    try{score.audio.currentTime=config.startAt;}catch(_){}
    score.audio.addEventListener('loadedmetadata',()=>{
      if(score.audio.currentTime<config.startAt-.2){
        try{score.audio.currentTime=config.startAt;}catch(_){}
      }
    },{once:true});
    startScoreOmen(score);
    try{
      score.playPromise=Promise.resolve(score.audio.play()).then(()=>{
        return true;
      },err=>{
        console.warn('[KKPT] Soundtrack playback blocked:',err);
         score.failed=true;clearReturnScore(score);return false;
      });
    }catch(err){
      console.warn('[KKPT] Soundtrack playback failed:',err);
      score.failed=true;clearReturnScore(score);
      score.playPromise=Promise.resolve(false);
     }
     }
 
     return score;
     // Actual transient analysis of the hosted Marenoia recording, 22.05 kHz
  }
    // mono / spectral-flux + low-band kick envelope. A main beat repeats
  function awaitScoreTime(score,seconds){
    // roughly every 0.439 seconds (~137 BPM), with salient attacks near
    if(!score||score.failed)return Promise.resolve(false);
    // 31.775, 33.561, 33.751 and 34.250s. React to audio.currentTime:
    return new Promise(resolve=>{
    // wall-clock timers would desynchronize during MP3 buffering/seeking.
      const audio=score.audio;
    const MARENOIA_TRANSIENTS=[
      let timer=null,lastTime=audio.currentTime;
        [28.453,.56],[28.682,.58],[28.892,.76],[29.121,.64],
      let lastMovement=performance.now();
        [29.341,.72],[29.570,.48],[29.780,.87],[30.009,.93],
      let settled=false;
        [30.438,.44],[30.658,.77],[30.887,.83],[31.356,.68],
      const done=ok=>{
        [31.556,.79],[31.775,.93],[32.364,.29],[32.883,.36],
        if(settled)return;
        [33.192,.38],[33.561,1],[33.751,.97],[34.250,.86],
        settled=true;clearInterval(timer);
        [34.469,.54],[35.317,.86],[35.786,.69]
        audio.removeEventListener('timeupdate',check);
    ];
        audio.removeEventListener('ended',check);
    // The fire segment only gets lightning. The pressure / dark vignette
        audio.removeEventListener('error',check);
    // belongs to the AFTERMATH and survives the real page navigation.
        resolve(ok);
    function createMarenoiaBeatScene(score){
      };
        if(score.config.id!=='marenoia')return null;
      const check=()=>{
        const lightningLayer=document.createElement('div');
        if(audio.currentTime>=seconds-.018){done(true);return;}
        lightningLayer.id='kkpt-lightning-layer';
        if(score.failed||audio.error||audio.ended){done(false);return;}
        lightningLayer.setAttribute('aria-hidden','true');
        if(audio.currentTime>lastTime+.015){
        document.body.append(lightningLayer);
          lastTime=audio.currentTime;lastMovement=performance.now();
        let live=true,raf=0,lastTick=0,nextBeat=0,progress=0;
        let lastLightning=-100;
        function lightning(t,strength){
            if(!live||t<.11||t>.96)return;
            const w=Math.max(1,innerWidth),h=Math.max(1,innerHeight);
            const rng=(a,b)=>a+Math.random()*(b-a);
            const edge=w*(1-t);
            let x=Math.min(w*.93,Math.max(edge+35,edge+(w-edge)*.53));
            const top=rng(-70,-16),bottom=rng(h*.46,h*.80);
            const points=[[x,top]];
            const count=9;
            for(let i=1;i<=count;i++){
                const yy=top+(bottom-top)*i/count;
                const xx=x+rng(-39,39)*(i/count)+Math.sin(i*2.4)*14;
                points.push([Math.max(10,Math.min(w-10,xx)),yy]);
            }
            const path=points.map((p,i)=>(i?'L':'M')+
                p[0].toFixed(1)+','+p[1].toFixed(1)).join(' ');
            const branchAt=Math.floor(rng(3,7));
            const p=points[branchAt];
            const branch='M'+p[0].toFixed(1)+','+p[1].toFixed(1)+
                ' l'+rng(-46,46).toFixed(1)+','+rng(45,86).toFixed(1)+
                ' l'+rng(-28,28).toFixed(1)+','+rng(38,71).toFixed(1);
            const ns='http://www.w3.org/2000/svg';
            const svg=document.createElementNS(ns,'svg');
            svg.setAttribute('class','kkpt-storm-bolt');
            svg.setAttribute('viewBox','0 0 '+w+' '+h);
            svg.setAttribute('preserveAspectRatio','none');
            svg.style.opacity=String(Math.min(1,.46+strength*.42));
            svg.style.clipPath='inset(0 0 0 '+
                Math.max(0,(1-t)*100).toFixed(2)+'%)';
            const addPath=(d,klass)=>{
                const el=document.createElementNS(ns,'path');
                el.setAttribute('d',d);el.setAttribute('class',klass);
                svg.append(el);
            };
            addPath(path,'kkpt-bolt-haze');
            addPath(path,'kkpt-bolt-core');
            addPath(branch,'kkpt-bolt-branch');
            lightningLayer.append(svg);
            setTimeout(()=>svg.remove(),390);
         }
         }
         function tick(now){
         if(performance.now()-lastMovement>20000)done(false);
            if(!live)return;
      };
            raf=requestAnimationFrame(tick);
      audio.addEventListener('timeupdate',check);
            if(now-lastTick<30)return;lastTick=now;
      audio.addEventListener('ended',check);
            const audio=score.audio;
      audio.addEventListener('error',check);
            const audioTime=audio.currentTime;
      timer=setInterval(check,30);
            while(nextBeat<MARENOIA_TRANSIENTS.length&&
      check();
                MARENOIA_TRANSIENTS[nextBeat][0]<=audioTime){
    });
                const [moment,strength]=MARENOIA_TRANSIENTS[nextBeat++];
  }
                if(audioTime-moment<.22){
  function saveReturnScoreHandoff(score){
                    if(progress>.11&&strength>.81&&moment-lastLightning>.18){
    if(!score||score.failed||score.audio.ended||
                        lightning(progress,strength);
      score.audio.currentTime>=score.config.stopAt-.15)return;
                        lastLightning=moment;
    try{
                    }
      sessionStorage.setItem(SCORE_KEY,JSON.stringify({
                }
         stamp:Date.now(),time:score.audio.currentTime,
            }
         target:score.config.target,
         }
        expires:Date.now()+115000,revision:42
        raf=requestAnimationFrame(tick);
      }));
         return {
      if(score.config.id==='marenoia'){
            update(t){progress=t;},
        sessionStorage.setItem(AFTER_DARK_KEY,'1');
            finish(){
      }
                live=false;cancelAnimationFrame(raf);
    }catch(err){
                setTimeout(()=>lightningLayer.remove(),450);
      console.warn('[KKPT] Cannot preserve soundtrack position:',err);
            },
            dispose(){
                live=false;cancelAnimationFrame(raf);
                lightningLayer.remove();
            }
        };
     }
     }
 
  }
    // Measured with the submitted MP3: heavy low-frequency accents repeat
  function restoreReturnScoreHandoff(){
     // every 8 beats (~3.532 s at 135.9 BPM). Each was 1.6–2.1x the
     if(window!==window.top)return false;
     // median low-band RMS of the seven intervening beats. Never trigger
     let saved=null;
     // a full-screen pressure pump on all of the fast intermediate notes.
     try{
    const AFTER_DARK_KEY='kkpt:dark-afterglow:41';
      saved=JSON.parse(sessionStorage.getItem(SCORE_KEY));
    const DAWN_HALO_KEY='kkpt:dawn-cloudbreak:41';
      sessionStorage.removeItem(SCORE_KEY);
     const MARENOIA_EIGHT_BEAT_KICKS=[
     }catch(_){}
        35.311,38.843,42.375,45.907,49.429
    if(!saved||saved.revision!==42||saved.target!==basePage||
     ];
      Date.now()>saved.expires)return false;
     // Offline 0.25s RMS contour, normalized by the passage's 95th
     const config=SCORES[saved.target];
    // percentile. Tracks long-form dynamics without cross-origin
     if(!config)return false;
    // WebAudio sample access (which may be forbidden by CORS).
     const currentTarget=()=>saved.time+
     const MARENOIA_LOUDNESS=[
      (Date.now()-saved.stamp)/1000;
        .61,.71,.98,1,.85,.96,.92,.72,.57,.54,.45,.46,
    if(currentTarget()>=config.stopAt-.15)return false;
        .53,.54,.56,.63,.92,1,.88,.95,.94,.77,.61,.61,
    const score=createScore(config);
        .52,.41,.46,.48,.44,.50,.81,1,.92,.98,.90,.80,
    const audio=score.audio;
        .63,.58,.54,.61,.65,.67,.65,.69,.83,1,.90,.93,
    let attempted=false,blockedAt=null;
        .94,.81,.65,.65,.66,.65,.62,.64,.60,.58,.70,1,
     function playFromPosition(){
        .91,.90,.95,.83,.62,.58,.46,.54,.47,.57,.55,.63
      const requested=blockedAt===null?currentTarget():blockedAt;
    ];
      const position=Math.min(config.stopAt-.12,requested);
     function marenoiaLoudness(time){
      if(position>=config.stopAt-.15){clearReturnScore(score);return;}
        const pos=Math.max(0,Math.min(MARENOIA_LOUDNESS.length-1,
      try{audio.currentTime=Math.max(config.startAt,position);}catch(_){}
            (time-34.75)*4));
      Promise.resolve(audio.play()).then(()=>{
        const i=Math.floor(pos),j=Math.min(MARENOIA_LOUDNESS.length-1,i+1);
        blockedAt=null;
         return MARENOIA_LOUDNESS[i]*(1-(pos-i))+
         score.blocked=false;updateAudioToggle(score);
            MARENOIA_LOUDNESS[j]*(pos-i);
      }).catch(err=>{
        console.info('[KKPT] Browser requires an audio click:',err);
        blockedAt=position;score.blocked=true;
        updateAudioToggle(score);
      });
     }
     }
     function marenoiaDeepPulse(age){
     score.resume=()=>{
         // A 420ms inhale, 300ms hold and ~2.2s heavy exhale.
      if(blockedAt!==null){
         // One pressure wave per eight beats, not a rapid heartbeat.
         try{audio.currentTime=blockedAt;}catch(_){}
        if(age<0||age>=2.98)return 0;
      }
        if(age<.42)return smooth(age/.42);
      return Promise.resolve(audio.play()).then(value=>{
        if(age<.72)return 1;
         blockedAt=null;return value;
        return 1-smooth((age-.72)/2.26);
      });
    };
    const autoplay=()=>{
      if(attempted)return;attempted=true;
      playFromPosition();
    };
    audio.addEventListener('loadedmetadata',autoplay,{once:true});
    audio.load();
    setTimeout(()=>{if(audio.readyState>=1)autoplay();},1100);
    return true;
  }
  let active = false; // true only during the visual handoff; destination navigates natively
  let animating = false;
  let gearPending = false;
  let ready = false;
  let loading = null;
  let button, labelTitle, labelSub, labelIcon, frame;
  let otherTitle = otherPage + ' - Blockland Wiki';
  function whichPage() { return active ? otherPage : basePage; }
  function destination() { return active ? basePage : otherPage; }
  function isDark(page) { return page === 'Kuronata'; }
  function pathPage() {
    const url = new URL(location.href);
    if (url.pathname.startsWith('/wiki/')) {
      try { return decodeURIComponent(url.pathname.slice(6)).replace(/_/g, ' '); }
      catch (e) { return ''; }
     }
     }
     let kuronataAftermath=null;
     return url.searchParams.get('title') || '';
    function mountKuronataAftermath(score){
  }
        if(kuronataAftermath){
  function updateButton() {
            if(score)kuronataAftermath.attach(score);
    if (!button) return;
            return kuronataAftermath;
    const to = destination();
        }
    button.href = to === basePage ? baseUrl : otherUrl.href;
        const layer=document.createElement('div');
    labelTitle.textContent = to.toUpperCase();
        layer.id='kkpt-beat-scene';
    labelSub.textContent = isDark(to) ? 'BURN THE SKY' : 'RECLAIM THE SKY';
        layer.setAttribute('aria-hidden','true');
    labelIcon.textContent = isDark(to) ? '✦' : '❄';
        layer.className='kkpt-beat-after kkpt-beat-idle';
    button.classList.toggle('kkpt-dark', isDark(whichPage()));
        layer.innerHTML='<div class="kkpt-beat-focus"></div>'+
    button.setAttribute('aria-label', isDark(to) ? '燃尽天空,进入 Kuronata' : '驱散黑暗,回到 Kuranoka');
            '<div class="kkpt-beat-lens"></div>'+
  }
            '<div class="kkpt-beat-fringe"></div>'+
  function wikiLinkPage(anchor, doc) {
            '<div class="kkpt-beat-rupture"></div>'+
    try {
            '<svg class="kkpt-beat-scars" viewBox="0 0 1440 900" '+
      const url = new URL(anchor.href, doc.baseURI);
            'preserveAspectRatio="none" aria-hidden="true">'+
      if (url.origin !== location.origin) return null;
            '<path d="M-20 340L126 317 203 355 294 339 383 382 477 352 605 395 728 365 842 399 982 371 1125 397 1264 351 1460 390" />'+
      const action = url.searchParams.get('action');
            '<path d="M-20 635L158 605 294 646 404 612 547 668 679 622 846 653 991 621 1149 668 1277 636 1460 660" />'+
      if (action && action !== 'view') return null;
            '<path d="M203 355l-19 33 27 39M842 399l30 34-20 47M404 612l-44 42 23 29M1149 668l39 41-18 32"/>'+
      let title = url.searchParams.get('title');
            '</svg>';
      if (!title) {
        // Put the vignette outside the page-scale camera so its
        const matched = url.pathname.match(/^\/wiki\/([^/?#]+)\/?$/);
        // darkness covers the WHOLE viewport even while the body zooms.
        if (matched) title = decodeURIComponent(matched[1]);
        root.append(layer);
      }
        let live=true,raf=0,lastTick=0,source=null,lastKick=-100;
      title = (title || '').replace(/_/g, ' ');
        let cameraReleaseTimer=null;
      return title === 'Kuranoka' || title === 'Kuronata' ? title : null;
        let smoothPressure=0,index=0;
    } catch (_) {
        const releaseCamera=()=>{
      return null;
            root.style.setProperty('--kkpt-camera-zoom','1');
            clearTimeout(cameraReleaseTimer);
            cameraReleaseTimer=setTimeout(()=>{
                if(!live)root.classList.remove('kkpt-camera-running');
            },420);
        };
        const quiet=()=>{
            layer.style.setProperty('--kkpt-beat-dark','.60');
            layer.style.setProperty('--kkpt-beat-scale','1.025');
            layer.style.setProperty('--kkpt-beat-blur','0.28px');
            layer.style.setProperty('--kkpt-beat-glitch','.12');
            layer.style.setProperty('--kkpt-beat-shear','0px');
            layer.style.setProperty('--kkpt-beat-shear-reverse','0px');
            layer.style.setProperty('--kkpt-beat-scars','.035');
            root.style.setProperty('--kkpt-camera-zoom','1');
            root.style.setProperty('--kkpt-camera-focus-y',
                (scrollY+innerHeight*.5).toFixed(1)+'px');
            root.style.setProperty('--kkpt-chroma-red','0px');
            root.style.setProperty('--kkpt-chroma-shadow','0px');
        };
        function tick(now){
            if(!live)return;
            raf=requestAnimationFrame(tick);
            if(now-lastTick<38)return;
            const dt=Math.min(.12,Math.max(.038,(now-lastTick)/1000));
            lastTick=now;
            if(!source||source.failed||source.audio.ended||
                source.audio.currentTime>=source.config.stopAt-.02){
                source=null;layer.classList.add('kkpt-beat-idle');
                quiet();live=false;cancelAnimationFrame(raf);
                releaseCamera();
                return;
            }
            const audioTime=source.audio.currentTime;
            while(index<MARENOIA_EIGHT_BEAT_KICKS.length&&
                MARENOIA_EIGHT_BEAT_KICKS[index]<=audioTime){
                lastKick=MARENOIA_EIGHT_BEAT_KICKS[index++];
            }
            // The recording's actual loudness and 18s player fade BOTH
            // attenuate the pump as the music settles down.
            const actualGain=Number(source.audio.volume);
            const playerGain=Number.isFinite(actualGain)
                ? Math.max(0,Math.min(1,actualGain/.78)):1;
            const soundStrength=(.48+.52*marenoiaLoudness(audioTime))*
                playerGain;
            const targetPressure=marenoiaDeepPulse(audioTime-lastKick)*
                soundStrength;
            const follow=1-Math.exp(-dt/.21);
            smoothPressure+=(targetPressure-smoothPressure)*follow;
            const pressure=smoothPressure;
            root.classList.add('kkpt-camera-running');
            layer.style.setProperty('--kkpt-beat-dark',
                (.60+.38*pressure).toFixed(3));
            layer.style.setProperty('--kkpt-beat-scale',
                (1.025-.175*pressure).toFixed(4));
            layer.style.setProperty('--kkpt-beat-blur',
                (.28+2.6*pressure).toFixed(2)+'px');
            layer.style.setProperty('--kkpt-beat-glitch',
                (.12+.63*pressure).toFixed(3));
            const shear=Math.sin(index*1.47)*pressure*9.0;
            layer.style.setProperty('--kkpt-beat-shear',
                shear.toFixed(2)+'px');
            layer.style.setProperty('--kkpt-beat-shear-reverse',
                (-shear*.86).toFixed(2)+'px');
            layer.style.setProperty('--kkpt-beat-scars',
                (.035+.76*pressure).toFixed(3));
            // V40 camera breathes IN then exhales to its natural size.
            // Zoom is never below 1.0: no blue/uncovered viewport edges.
            // The transform belongs to the whole body (Wiki chrome,
            // sidebar, imagery and text), not only #mw-content-text.
            root.style.setProperty('--kkpt-camera-zoom',
                (1.008+.054*pressure).toFixed(4));
            root.style.setProperty('--kkpt-camera-focus-y',
                (scrollY+innerHeight*.5).toFixed(1)+'px');
            root.style.setProperty('--kkpt-chroma-red',
                (-4.4*pressure).toFixed(2)+'px');
            root.style.setProperty('--kkpt-chroma-shadow',
                (5.6*pressure).toFixed(2)+'px');
        }
        quiet();
        kuronataAftermath={
            attach(nextScore){
                source=nextScore;
                nextScore.aftermath=kuronataAftermath;
                if(live){cancelAnimationFrame(raf);}
                clearTimeout(cameraReleaseTimer);
                live=true;lastTick=0;lastKick=-100;smoothPressure=0;
                index=0;
                layer.classList.remove('kkpt-beat-idle');
                raf=requestAnimationFrame(tick);
            },
            settle(){
                source=null;live=false;cancelAnimationFrame(raf);
                layer.classList.add('kkpt-beat-idle');quiet();
                releaseCamera();
            },
            fadeToDawn(){
                source=null;live=false;cancelAnimationFrame(raf);
                releaseCamera();
                layer.classList.add('kkpt-beat-outro');
                const old=layer;
                setTimeout(()=>{
                    old.remove();
                    if(kuronataAftermath?.layer===old)kuronataAftermath=null;
                },2400);
            },
            dispose(){
                source=null;live=false;cancelAnimationFrame(raf);
                clearTimeout(cameraReleaseTimer);
                root.classList.remove('kkpt-camera-running');
                root.style.removeProperty('--kkpt-camera-zoom');
                root.style.removeProperty('--kkpt-camera-focus-y');
                root.style.removeProperty('--kkpt-chroma-red');
                root.style.removeProperty('--kkpt-chroma-shadow');
                layer.remove();kuronataAftermath=null;
            },
            layer
        };
        if(score)kuronataAftermath.attach(score);
        return kuronataAftermath;
     }
     }
    function createCloudbreakAfterglow(continueFromNavigation=false){
  }
        const previous=document.getElementById('kkpt-cloudbreak');
  function decoratePortalLinks(doc, targetPage, changedRoots) {
        previous?.remove();
    const content = doc.querySelector('#mw-content-text,.mw-parser-output');
        let timestamp=Date.now();
    if (!content) return;
        if(continueFromNavigation){
    const decorate=anchor=>{
            try{timestamp=Number(sessionStorage.getItem(DAWN_HALO_KEY));}
      if(anchor.classList.contains('kkpt-fate-gear'))return;
            catch(_){return;}
      if (wikiLinkPage(anchor, doc) !== targetPage) return;
            if(!Number.isFinite(timestamp)||timestamp<=0)return;
      anchor.classList.add('kkpt-portal-link');
        }else{
      anchor.classList.toggle('kkpt-portal-red', targetPage === 'Kuronata');
            try{sessionStorage.setItem(DAWN_HALO_KEY,String(timestamp));}
      anchor.classList.toggle('kkpt-portal-blue', targetPage === 'Kuranoka');
            catch(_){}
      const caption = (anchor.textContent || '').trim();
        }
      if (caption) anchor.dataset.kkptText = caption.slice(0, 120);
        const elapsed=Math.max(0,Date.now()-timestamp);
      anchor.setAttribute('aria-label', caption + ',切换至 ' + targetPage);
        if(elapsed>6900){
    };
            try{sessionStorage.removeItem(DAWN_HALO_KEY);}catch(_){}
    for(const node of changedRoots||[content]){
            return;
      if(node.matches?.('a[href]'))decorate(node);
        }
      node.querySelectorAll?.('a[href]').forEach(decorate);
        const bloom=document.createElement('div');
        bloom.id='kkpt-cloudbreak';
        bloom.setAttribute('aria-hidden','true');
        bloom.style.setProperty('--kkpt-dawn-delay',(-elapsed)+'ms');
        bloom.innerHTML='<i class="kkpt-dawn-dark-rim"></i>'+
            '<i class="kkpt-dawn-bright-rim"></i>'+
            '<i class="kkpt-dawn-cloud-sweep"></i>';
        root.append(bloom);
        setTimeout(()=>{
            bloom.remove();
            try{
                if(Number(sessionStorage.getItem(DAWN_HALO_KEY))===timestamp)
                    sessionStorage.removeItem(DAWN_HALO_KEY);
            }catch(_){}
        },Math.max(0,7000-elapsed));
     }
     }
    function createRebirthVisual(){
  }
        // Hope replaces ash: a barely perceptible rainbow after the rain.
  function watchPortalLinks(doc, targetPage) {
        // No numbered / nested six-band rainbow; just one diffuse halo.
    decoratePortalLinks(doc, targetPage);
        // All are compositor layers; nothing is rasterized on every frame.
    if (doc.__kkptPortalObserver) return;
        const layer=document.createElement('div');
    const root = doc.querySelector('#mw-content-text,.mw-parser-output');
        layer.id='kkpt-rebirth-visual';
    if (!root) return;
        layer.setAttribute('aria-hidden','true');
    let pending=false;
        const drizzle=document.createElement('div');
    const changed=new Set();
        drizzle.className='kkpt-rebirth-rain';
    const observer = new MutationObserver(records => {
        const rainbow=document.createElementNS('http://www.w3.org/2000/svg','svg');
      for(const record of records){
        rainbow.setAttribute('class','kkpt-rebirth-rainbow');
         for(const node of record.addedNodes){
        rainbow.setAttribute('viewBox','0 0 1000 650');
          if(node.nodeType===1)changed.add(node);
        rainbow.setAttribute('preserveAspectRatio','none');
          else if(node.parentElement?.matches('a[href]'))
        const svgNS='http://www.w3.org/2000/svg';
             changed.add(node.parentElement);
        const defs=document.createElementNS(svgNS,'defs');
        const gradient=document.createElementNS(svgNS,'linearGradient');
        gradient.setAttribute('id','kkpt-rebirth-muted-arc');
        gradient.setAttribute('x1','0%');
        gradient.setAttribute('x2','100%');
        const haloColors=['#c7e5ff','#e4f7e6','#f7ebd9','#e7e0f5'];
        haloColors.forEach((color,i)=>{
            const stop=document.createElementNS(svgNS,'stop');
            stop.setAttribute('offset',(i*100/3).toFixed(1)+'%');
            stop.setAttribute('stop-color',color);
            gradient.append(stop);
        });
        defs.append(gradient);
        const arc=document.createElementNS(svgNS,'path');
         arc.setAttribute('d','M -100 625 Q 500 -175 1100 625');
        arc.setAttribute('fill','none');
        arc.setAttribute('stroke','url(#kkpt-rebirth-muted-arc)');
        arc.setAttribute('stroke-width','36');
        rainbow.append(defs,arc);
        const light=document.createElement('div');
        light.className='kkpt-rebirth-sunlight';
        const essence=document.createElement('div');
        essence.className='kkpt-rebirth-essence';
        const count=innerWidth<700?14:24;
        for(let i=0;i<count;i++){
            const mote=document.createElement('i');
            mote.className='kkpt-rebirth-mote';
            mote.style.left=(5+Math.random()*88).toFixed(1)+'%';
            mote.style.top=(22+Math.random()*73).toFixed(1)+'%';
            mote.style.animationDelay=(-Math.random()*2.8).toFixed(2)+'s';
             mote.style.animationDuration=(2.3+Math.random()*1.8).toFixed(2)+'s';
            essence.append(mote);
         }
         }
        layer.append(drizzle,light,rainbow,essence);
      }
        document.body.append(layer);
      if(pending)return;
        let last=-1,alive=true;
      pending=true;
        return {
      setTimeout(()=>{
            update(t){
        pending=false;
                if(!alive||Math.abs(t-last)<.018&&t<1)return;
        const roots=[...changed];changed.clear();
                last=t;
        if(roots.length)decoratePortalLinks(doc,targetPage,roots);
                layer.style.clipPath='inset(0 '+
      },60);
                    (Math.max(0,1-t)*100).toFixed(2)+'% 0 0)';
    });
                drizzle.style.opacity=(.30*Math.pow(1-t,1.4)).toFixed(3);
    observer.observe(root, {childList:true,subtree:true});
                light.style.opacity=(Math.max(0,Math.min(1,(t-.12)/.67))*.47).toFixed(3);
    doc.__kkptPortalObserver = observer;
                rainbow.style.opacity=(Math.max(0,Math.min(1,(t-.48)/.40))*.105).toFixed(3);
  }
                essence.style.opacity=(Math.max(0,Math.min(1,(t-.22)/.55))*.72).toFixed(3);
  function isPlainClick(event) {
            },
    return !event.defaultPrevented && event.button === 0 &&
            finish(){
      !event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
                alive=false;layer.classList.add('kkpt-rebirth-finish');
  }
                setTimeout(()=>layer.remove(),1250);
  function findAnchor(event, doc) {
            },
    const target = event.target;
            dispose(){alive=false;layer.remove();}
    return target && target.nodeType === 1 &&
        };
      typeof target.closest === 'function'
      ? target.closest('a[href],button.kkpt-fate-gear') : null;
  }
  function portalClick(event, doc, targetPage, nextActive) {
    if (!isPlainClick(event)) return false;
    const anchor = findAnchor(event, doc);
    const gear=anchor?.classList.contains('kkpt-fate-gear')&&
      anchor.dataset.kkptTargetPage===targetPage;
    if (!anchor || (anchor.target && anchor.target !== '_self') ||
      anchor.hasAttribute('download') ||
      (!gear&&wikiLinkPage(anchor, doc)!==targetPage)) return false;
    event.preventDefault();
    if(animating||gearPending)return true;
    const score=armReturnScore(targetPage);
    if(targetPage==='Kuronata'&&score&&!score.beatScene){
      score.beatScene=createMarenoiaBeatScene(score);
     }
     }
     function clearReturnScore(score){
     startFatePrelude(anchor,targetPage);
        if(!score)return;
    primeSound(); // Synchronous click activation unlocks Web Audio.
        clearInterval(score.displayTimer);
    if(gear) {
        score.hud?.remove();
      beginFateSwap(anchor,nextActive);
        score.beatScene?.dispose();
    } else {
        score.aftermath?.settle();
      requestSwap(nextActive);
        try{
            score.audio.pause();
            score.audio.removeAttribute('src');score.audio.load();
        }catch(_){}
        if(returnScore===score)returnScore=null;
        if(score.config?.id==='marenoia')removeScoreOmen();
     }
     }
     function watchScore(score){
     return true;
        const audio=score.audio,config=score.config;
  }
        score.displayTimer=setInterval(()=>{
  function beginFateSwap(anchor,nextActive) {
            if(audio.ended||audio.error||audio.currentTime>=config.stopAt-.018){
    if(animating||gearPending||nextActive===active)return;
                clearReturnScore(score);return;
    gearPending=true;
            }
    anchor.classList.add('kkpt-fate-gear-turning');
            const elapsed=Math.max(0,audio.currentTime-config.startAt);
    requestAnimationFrame(()=>{
            const remaining=Math.max(0,config.stopAt-audio.currentTime);
      gearPending=false;
            const fadeIn=config.fadeInSeconds?
      requestSwap(nextActive);
                Math.min(1,elapsed/config.fadeInSeconds):1;
    });
            const fadeOut=config.fadeSeconds?
    setTimeout(()=>anchor.classList.remove('kkpt-fate-gear-turning'),3300);
                Math.min(1,remaining/config.fadeSeconds):1;
  }
            audio.volume=.78*Math.min(fadeIn,fadeOut);
  function fateGearShape() {
        },60);
    const parts=[];
        audio.addEventListener('ended',()=>clearReturnScore(score),{once:true});
    for(let i=0;i<48;i++) {
         audio.addEventListener('error',()=>{
      const a=i/48*Math.PI*2-Math.PI/2;
            score.failed=true;clearReturnScore(score);
      const r=(i%4===0||i%4===3)?21:16.8;
        },{once:true});
      parts.push((i?'L':'M')+(24+Math.cos(a)*r).toFixed(2)+' '+
         (24+Math.sin(a)*r).toFixed(2));
     }
     }
     function createScore(config){
     return parts.join(' ')+' Z';
        const audio=new Audio(config.url);
  }
         audio.preload='auto';audio.playsInline=true;
  function mountFateGear(doc,targetPage) {
        audio.volume=0;audio.muted=userWantsMute();
    if(doc.querySelector('.kkpt-fate-gear'))return;
        const score={audio,config,failed:false,blocked:false,
    const content=doc.querySelector('#mw-content-text,.mw-parser-output');
            displayTimer:null,hud:null,playPromise:null};
    if(!content)return;
        returnScore=score;
    const box=content.querySelector('table.infobox.vcard,table.infobox');
         mountAudioToggle(score);
    const host=box?.caption ||
         watchScore(score);
      box?.querySelector('th.infobox-above,th.infobox-title,th[colspan]')||
        return score;
      doc.querySelector('#firstHeading .mw-page-title-main,'+
         '.mw-page-title-main,#firstHeading');
    if(!host)return;
    host.classList.add('kkpt-fate-host');
    const anchor=doc.createElement('button');
    anchor.type='button';
    anchor.className='kkpt-fate-gear';
    anchor.dataset.kkptTargetPage=targetPage;
    const gearLabel=targetPage==='Kuronata'
      ?'命运的齿轮 · 唤醒 Kuronata':'命运的齿轮 · 回到 Kuranoka';
    anchor.setAttribute('aria-label',gearLabel);
    anchor.innerHTML='<svg class="kkpt-fate-gear-svg" viewBox="0 0 48 48" '+
      'aria-hidden="true" focusable="false"><path d="'+fateGearShape()+
      '"/><circle cx="24" cy="24" r="8.5"/>'+
      '<circle cx="24" cy="24" r="3"/></svg>';
    host.appendChild(anchor);
  }
  function backgroundUrls(doc) {
    const urls = new Set();
    const selector = [doc.documentElement, doc.body];
    try {
      doc.querySelectorAll('.background-theme,[class*="background"],[style*="background"],#mw-content-text,.citizen-page-container')
        .forEach((el, i) => { if (i < 18) selector.push(el); });
    } catch (e) { /* use html/body */ }
    function add(value) {
      if (!value) return;
      const pattern = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
      let match;
      while ((match = pattern.exec(value))) {
         try { urls.add(new URL(match[2], doc.baseURI).href); } catch (e) { /* ignore */ }
      }
      if (/^https?:\/\/\S+\.(?:jpg|jpeg|png|webp|avif)(?:\?\S*)?$/i.test(value)) {
         try { urls.add(new URL(value, doc.baseURI).href); } catch (e) { /* ignore */ }
      }
     }
     }
     function armReturnScore(targetPage){
     selector.forEach(el => {
        const config=SCORES[targetPage];
      if (!el) return;
        if(!config||targetPage===basePage)return null;
      try {
        if(returnScore&&!returnScore.failed&&
         [null, '::before', '::after'].forEach(pseudo => {
            returnScore.config.id===config.id)return returnScore;
          const style = doc.defaultView.getComputedStyle(el, pseudo);
        if(returnScore)clearReturnScore(returnScore);
          if (!style) return;
        const score=createScore(config);
          add(style.backgroundImage);
        // Seek straight into the selected three-second transition prelude.
          add(style.getPropertyValue('--bg-image'));
        try{score.audio.currentTime=config.startAt;}catch(_){}
          add(style.getPropertyValue('--background-image'));
         score.audio.addEventListener('loadedmetadata',()=>{
            if(score.audio.currentTime<config.startAt-.2){
                try{score.audio.currentTime=config.startAt;}catch(_){}
            }
        },{once:true});
        startScoreOmen(score);
        try{
            score.playPromise=Promise.resolve(score.audio.play()).then(()=>{
                return true;
            },err=>{
                console.warn('[KKPT] Soundtrack playback blocked:',err);
                score.failed=true;clearReturnScore(score);return false;
            });
        }catch(err){
            console.warn('[KKPT] Soundtrack playback failed:',err);
            score.failed=true;clearReturnScore(score);
            score.playPromise=Promise.resolve(false);
        }
        return score;
    }
    function awaitScoreTime(score,seconds){
        if(!score||score.failed)return Promise.resolve(false);
        return new Promise(resolve=>{
            const audio=score.audio;
            let timer=null,lastTime=audio.currentTime;
            let lastMovement=performance.now();
            let settled=false;
            const done=ok=>{
                if(settled)return;
                settled=true;clearInterval(timer);
                audio.removeEventListener('timeupdate',check);
                audio.removeEventListener('ended',check);
                audio.removeEventListener('error',check);
                resolve(ok);
            };
            const check=()=>{
                if(audio.currentTime>=seconds-.018){done(true);return;}
                if(score.failed||audio.error||audio.ended){done(false);return;}
                if(audio.currentTime>lastTime+.015){
                    lastTime=audio.currentTime;lastMovement=performance.now();
                }
                // A stalled soundtrack cannot trap navigation indefinitely.
                if(performance.now()-lastMovement>20000)done(false);
            };
            audio.addEventListener('timeupdate',check);
            audio.addEventListener('ended',check);
            audio.addEventListener('error',check);
            timer=setInterval(check,75);
            check();
         });
         });
     }
        if (el.dataset) add(el.dataset.bgImage);
     function saveReturnScoreHandoff(score){
      } catch (e) { /* style not yet ready */ }
         if(!score||score.failed||score.audio.ended||
     });
            score.audio.currentTime>=score.config.stopAt-.15)return;
     return [...urls];
        try{
  }
            sessionStorage.setItem(SCORE_KEY,JSON.stringify({
  function warmImage(url) {
                stamp:Date.now(),time:score.audio.currentTime,
    if (imagePool.has(url)) return imagePool.get(url).promise;
                target:score.config.target,
    const image = new Image();
                expires:Date.now()+115000,revision:41
    const promise = new Promise(resolve => {
            }));
      let finished = false;
            if(score.config.id==='marenoia'){
      const timeout = setTimeout(() => done(false), 5000);
                sessionStorage.setItem(AFTER_DARK_KEY,'1');
      function done(ok) {
            }
         if (finished) return;
         }catch(err){
        finished = true;
            console.warn('[KKPT] Cannot preserve soundtrack position:',err);
        clearTimeout(timeout);
        resolve(ok);
      }
      image.onload = () => done(true);
      image.onerror = () => done(false);
      image.src = url;
      if (image.complete) done(image.naturalWidth > 0);
    }).then(ok => ok && image.decode
      ? Promise.race([image.decode().then(() => true).catch(() => true),sleep(900).then(() => true)])
      : ok);
    imagePool.set(url, { image, promise });
    return promise;
  }
  async function warmBackgrounds(doc) {
    const urls = backgroundUrls(doc);
    await Promise.allSettled(urls.map(warmImage));
  }
  function wireFrame() {
    try {
      const doc = frame.contentDocument;
      if (!doc) return;
      otherTitle = doc.title || otherTitle;
      doc.documentElement.classList.add('kkpt-frame-inactive');
      watchPortalLinks(doc, basePage);
      mountFateGear(doc, basePage);
      if (doc.__kkptFrameBridge) return;
      doc.__kkptFrameBridge = true;
      doc.addEventListener('click', event => {
        if (!active || !isPlainClick(event)) return;
        if (portalClick(event, doc, basePage, false)) return;
        const el = findAnchor(event, doc);
        if (!el || (el.target && el.target !== '_self')) return;
        const href = el.href;
        if (!href || href.startsWith('javascript:')) return;
        const next = new URL(href, doc.baseURI);
        const currentFrame = new URL(frame.contentWindow.location.href);
         if (next.origin === currentFrame.origin &&
          next.pathname === currentFrame.pathname &&
          next.search === currentFrame.search && next.hash) {
          history.replaceState(history.state, '', next.href);
          return;
         }
         }
    }
         event.preventDefault();
    function restoreReturnScoreHandoff(){
         navigateRealPage(next.href);
         if(window!==window.top)return false;
      }, true);
        let saved=null;
      doc.addEventListener('submit', event => {
         try{
         if (active && event.target instanceof doc.defaultView.HTMLFormElement) {
            saved=JSON.parse(sessionStorage.getItem(SCORE_KEY));
          event.target.target = '_top';
            sessionStorage.removeItem(SCORE_KEY);
        }catch(_){}
        if(!saved||saved.revision!==41||saved.target!==basePage||
            Date.now()>saved.expires)return false;
        const config=SCORES[saved.target];
        if(!config)return false;
        const currentTarget=()=>saved.time+
            (Date.now()-saved.stamp)/1000;
         if(currentTarget()>=config.stopAt-.15)return false;
        const score=createScore(config);
        const audio=score.audio;
        let attempted=false,blockedAt=null;
        function playFromPosition(){
            const requested=blockedAt===null?currentTarget():blockedAt;
            const position=Math.min(config.stopAt-.12,requested);
            if(position>=config.stopAt-.15){clearReturnScore(score);return;}
            try{audio.currentTime=Math.max(config.startAt,position);}catch(_){}
            Promise.resolve(audio.play()).then(()=>{
                blockedAt=null;
                score.blocked=false;updateAudioToggle(score);
            }).catch(err=>{
                // The only fallback is the SAME small icon, no audio popup.
                console.info('[KKPT] Browser requires an audio click:',err);
                blockedAt=position;score.blocked=true;
                updateAudioToggle(score);
            });
         }
         }
        score.resume=()=>{
      }, true);
            if(blockedAt!==null){
    } catch (err) {
                try{audio.currentTime=blockedAt;}catch(_){}
      console.warn('[KuranokaPageTransition] Frame link bridge unavailable:', err);
            }
            return Promise.resolve(audio.play()).then(value=>{
                blockedAt=null;return value;
            });
        };
        const autoplay=()=>{
            if(attempted)return;attempted=true;
            playFromPosition();
        };
        audio.addEventListener('loadedmetadata',autoplay,{once:true});
        audio.load();
        setTimeout(()=>{if(audio.readyState>=1)autoplay();},1100);
        return true;
     }
     }
 
  }
     let active = false; // true only during the visual handoff; destination navigates natively
  function warmFrame() {
     let animating = false;
     if (loading) return loading;
     let gearPending = false;
     frame = document.createElement('iframe');
     let ready = false;
     frame.id = 'kkpt-preview';
     let loading = null;
     frame.src = otherUrl.href; // set BEFORE insertion, avoids extra initial history hop
     let button, labelTitle, labelSub, labelIcon, frame;
     frame.title = otherPage + ' 预载页面';
     let otherTitle = otherPage + ' - Blockland Wiki';
     frame.tabIndex = -1;
 
     frame.setAttribute('aria-hidden', 'true');
     function whichPage() { return active ? otherPage : basePage; }
     frame.setAttribute('loading', 'eager');
     function destination() { return active ? basePage : otherPage; }
     frame.style.clipPath = hideClip;
     function isDark(page) { return page === 'Kuronata'; }
     frame.style.visibility = 'hidden';
     function pathPage() {
     const targetBackdrop=worldBackdrops[otherPage];
        const url = new URL(location.href);
    if(targetBackdrop){
        if (url.pathname.startsWith('/wiki/')) {
      frame.style.backgroundImage='url("'+targetBackdrop+'")';
            try { return decodeURIComponent(url.pathname.slice(6)).replace(/_/g, ' '); }
      frame.style.backgroundPosition='center center';
            catch (e) { return ''; }
      frame.style.backgroundSize='cover';
        }
      frame.style.backgroundRepeat='no-repeat';
        return url.searchParams.get('title') || '';
     }
     }
 
    loading = new Promise(resolve => {
    function updateButton() {
      let settled = false;
         if (!button) return;
      let preparing = false;
         const to = destination();
      let poll = null;
         button.href = to === basePage ? baseUrl : otherUrl.href;
      function finish(ok, reason) {
        labelTitle.textContent = to.toUpperCase();
         if (settled) return;
         labelSub.textContent = isDark(to) ? 'BURN THE SKY' : 'RECLAIM THE SKY';
         settled = true;
        labelIcon.textContent = isDark(to) ? '✦' : '❄';
        clearInterval(poll);
        button.classList.toggle('kkpt-dark', isDark(whichPage()));
         ready = ok;
        button.setAttribute('aria-label', isDark(to) ? '燃尽天空,进入 Kuronata' : '驱散黑暗,回到 Kuranoka');
        if (!ok) console.warn('[KuranokaPageTransition] Preview:', reason);
        resolve(ok);
      }
      async function inspect() {
        if (settled || preparing) return;
        try {
          const doc = frame.contentDocument;
          if (!doc || !doc.querySelector('#mw-content-text,.mw-parser-output')) return;
          preparing = true;
          clearInterval(poll);
          await sleep(320); // Background.js / wiki style scripts
          await Promise.race([warmBackgrounds(doc), sleep(3600)]);
          await sleep(120);
          await Promise.race([warmBackgrounds(doc), sleep(1300)]);
          frame.contentWindow.scrollTo(0, 0);
          wireFrame();
          finish(true);
         } catch (err) { finish(false, String(err)); }
      }
      frame.addEventListener('load', () => {
        if (!settled && !preparing) inspect();
      });
      frame.addEventListener('error', () => finish(false,'iframe 网络加载错误'));
      document.body.append(frame);
      poll = setInterval(inspect, 130);
      setTimeout(() => { if (!settled && !preparing) finish(false,'18 秒仍未找到目标正文'); }, 18000);
    });
    warmBackgrounds(document).catch(() => {});
    setTimeout(() => warmBackgrounds(document).catch(() => {}), 700);
    return loading;
  }
  let atmosphereAssetCache=null;
  const atmosphereNoiseCache=new Map();
  function createAtmosphere(canvas,buildOneOnly=false) {
    const ctx = canvas.getContext('2d', {alpha:true});
    const rand = n => {
      const cached=atmosphereNoiseCache.get(n);
      if(cached!==undefined)return cached;
      const x = Math.sin(n*127.1+311.7)*43758.5453123;
      const value=x-Math.floor(x);
      atmosphereNoiseCache.set(n,value);
      return value;
    };
    const clamp01 = n => Math.max(0,Math.min(1,n));
    function valueNoise(x,y,seed) {
      const ix=Math.floor(x), iy=Math.floor(y);
      let fx=x-ix,fy=y-iy;
      fx=fx*fx*(3-2*fx);fy=fy*fy*(3-2*fy);
      const h=(a,b)=>rand(a*171.3+b*513.7+seed*89.1);
      const a=h(ix,iy)*(1-fx)+h(ix+1,iy)*fx;
      const b=h(ix,iy+1)*(1-fx)+h(ix+1,iy+1)*fx;
      return a*(1-fy)+b*fy;
     }
     }
 
     function smokeTexture(seed,dark) {
     // Accept the two standard MediaWiki link forms, while rejecting
      const tex=document.createElement('canvas');
    // edit/action links, external sites and similarly named articles.
      tex.width=tex.height=112;
    function wikiLinkPage(anchor, doc) {
      const g=tex.getContext('2d');
         try {
      const data=g.createImageData(112,112);
            const url = new URL(anchor.href, doc.baseURI);
      for(let y=0;y<112;y++) for(let x=0;x<112;x++){
            if (url.origin !== location.origin) return null;
         const u=(x-56)/56,v=(y-56)/56;
            const action = url.searchParams.get('action');
        const curl=valueNoise(u*2.9+11,v*2.9+1,seed);
            if (action && action !== 'view') return null;
        const curl2=valueNoise(u*3.4+3,v*3.4+7,seed+7);
            let title = url.searchParams.get('title');
        const wx=u+.47*(curl-.5),wy=v+.40*(curl2-.5);
            if (!title) {
        const radius=Math.sqrt(wx*wx*.91+wy*wy*1.13);
                const matched = url.pathname.match(/^\/wiki\/([^/?#]+)\/?$/);
        const envelope=clamp01((1.05-radius)/.56);
                if (matched) title = decodeURIComponent(matched[1]);
        const low=valueNoise(u*4.1+4,v*4.1-9,seed+2);
            }
        const high=valueNoise(u*11.4-3,v*11.4+6,seed+5);
            title = (title || '').replace(/_/g, ' ');
        const fibers=valueNoise(u*22.0+1,v*16.0+5,seed+9);
            return title === 'Kuranoka' || title === 'Kuronata' ? title : null;
        const density=clamp01((low*.62+high*.28+fibers*.10-.28)*2.0)*
         } catch (_) {
          envelope*envelope;
            return null;
        const i=(y*112+x)*4;
        const variation=low*28+high*15;
        if(dark){
          data.data[i]=5+variation*.37;
          data.data[i+1]=5+variation*.33;
          data.data[i+2]=8+variation*.38;
         }else{
          data.data[i]=44+variation;
          data.data[i+1]=91+variation;
          data.data[i+2]=125+variation;
         }
         }
        data.data[i+3]=Math.round(240*density);
      }
      g.putImageData(data,0,0);
      return tex;
     }
     }
 
     function glowTexture(seed,mode) {
     function decoratePortalLinks(doc, targetPage) {
      const tex=document.createElement('canvas');
        // Use only the article: sidebar, edit tools and site-wide links
      tex.width=96;tex.height=128;
        // must retain their usual navigation behavior.
      const g=tex.getContext('2d');
        const content = doc.querySelector('#mw-content-text,.mw-parser-output');
      const gradient=g.createRadialGradient(48,68,0,48,68,64);
        if (!content) return;
      const colors=mode==='fire'
         content.querySelectorAll('a[href]').forEach(anchor => {
         ? [[0,'#fffef1'],[.14,'#fff4bb'],[.34,'#ffd04d'],
            if(anchor.classList.contains('kkpt-fate-gear'))return;
          [.55,'#ff841bcc'],[.80,'#bc290749'],[1,'#6e030000']]
            if (wikiLinkPage(anchor, doc) !== targetPage) return;
        : mode==='blue'
            anchor.classList.add('kkpt-portal-link');
        ? [[0,'#ffffff'],[.19,'#f1ffff'],[.40,'#a2efff'],
            anchor.classList.toggle('kkpt-portal-red', targetPage === 'Kuronata');
          [.68,'#5298ff99'],[1,'#345bf000']]
            anchor.classList.toggle('kkpt-portal-blue', targetPage === 'Kuranoka');
        : [[0,'#ffffff'],[.19,'#fff8cd'],[.49,'#ff9b28cc'],
            const caption = (anchor.textContent || '').trim();
          [1,'#ff380000']];
            if (caption) anchor.dataset.kkptText = caption.slice(0, 120);
      colors.forEach(([p,c])=>gradient.addColorStop(p,c));
            anchor.setAttribute('aria-label', caption + ',切换至 ' + targetPage);
      g.fillStyle=gradient;g.fillRect(0,0,96,128);
        });
      const img=g.getImageData(0,0,96,128);
      for(let y=0;y<128;y++)for(let x=0;x<96;x++){
        const u=(x-48)/48,v=(y-64)/64;
        const cloud=valueNoise(u*4+seed*1.9,v*4+seed*.7,seed+18);
        const fiber=valueNoise(u*11+3,v*8+17,seed+41);
        const shifted=u+.18*(cloud-.5);
        const irregular=clamp01((1.15-Math.abs(shifted))/.52)*
          clamp01((1.05-Math.abs(v+.12*(fiber-.5)))/.42);
        const n=.59+.77*cloud+.28*fiber;
        const alphaIndex=(y*96+x)*4+3;
        img.data[alphaIndex]=Math.round(img.data[alphaIndex]*
          irregular*clamp01(n));
      }
      g.putImageData(img,0,0);
      return tex;
     }
     }
 
    const cache=atmosphereAssetCache ||= {
     function watchPortalLinks(doc, targetPage) {
      sootSprites:[],mistSprites:[],FIRE_GLOW:[],BLUE_GLOW:[],
        decoratePortalLinks(doc, targetPage);
      EMBER_GLOW:null
        if (doc.__kkptPortalObserver) return;
    };
        const root = doc.querySelector('#mw-content-text,.mw-parser-output');
     function buildNextAsset(){
        if (!root) return;
      let n=cache.sootSprites.length;
        let pending=false;
      if(n<4){cache.sootSprites.push(smokeTexture(n+17,true));return true;}
        const observer = new MutationObserver(() => {
      n=cache.mistSprites.length;
            if(pending)return;
      if(n<4){cache.mistSprites.push(smokeTexture(n+51,false));return true;}
            pending=true;
      n=cache.FIRE_GLOW.length;
            setTimeout(()=>{
      if(n<4){cache.FIRE_GLOW.push(glowTexture(n+1,'fire'));return true;}
                pending=false;
      n=cache.BLUE_GLOW.length;
                decoratePortalLinks(doc,targetPage);
      if(n<4){cache.BLUE_GLOW.push(glowTexture(n+8,'blue'));return true;}
            },120);
      if(!cache.EMBER_GLOW){
        });
         cache.EMBER_GLOW=glowTexture(21,'ember');return true;
         observer.observe(root, {childList:true,subtree:true});
      }
        doc.__kkptPortalObserver = observer;
      return false;
     }
     }
 
    if(buildOneOnly)return buildNextAsset();
     function isPlainClick(event) {
     while(buildNextAsset()){}
        return !event.defaultPrevented && event.button === 0 &&
    const {sootSprites,mistSprites,FIRE_GLOW,BLUE_GLOW,EMBER_GLOW}=cache;
            !event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
    const combustion=[];
     }
    const residualSoot=[];
 
    const tornPaper=[];
     function findAnchor(event, doc) {
     let lastElapsed=0,spawnRemainder=0,sootRemainder=0,shardRemainder=0;
        const target = event.target;
     function advanceCombustion(seconds,progress,dark,elapsed,w,h) {
        return target && target.nodeType === 1 &&
      const dt=Math.min(.065,Math.max(0,elapsed-lastElapsed));
            typeof target.closest === 'function'
      lastElapsed=elapsed;
            ? target.closest('a[href],button.kkpt-fate-gear') : null;
      const live=progress>.01&&progress<.985;
    }
      if(live){
 
        const flow=(dark?132:64)*Math.min(1.5,h/720)*
    function portalClick(event, doc, targetPage, nextActive) {
          (.75+.25*Math.sin(Math.PI*progress));
        if (!isPlainClick(event)) return false;
        spawnRemainder+=dt*flow;
        const anchor = findAnchor(event, doc);
        const births=Math.min(65,Math.floor(spawnRemainder));
        const gear=anchor?.classList.contains('kkpt-fate-gear')&&
        spawnRemainder-=births;
             anchor.dataset.kkptTargetPage===targetPage;
        for(let j=0;j<births&&combustion.length<1320;j++){
        if (!anchor || (anchor.target && anchor.target !== '_self') ||
          const y=Math.random()*h;
             anchor.hasAttribute('download') ||
          const edge=edgeAt(y,seconds,progress,w);
             (!gear&&wikiLinkPage(anchor, doc)!==targetPage)) return false;
          const r=Math.random();
        event.preventDefault();
          const kind=dark
        if(animating||gearPending)return true;
            ? r<.38?'flame':r<.91?'ember':'ash'
        // Both directions start playback during the trusted user gesture.
             : r<.37?'glow':r<.81?'leaf':'pollen';
        const score=armReturnScore(targetPage);
          const small=kind==='ember'||kind==='pollen';
        if(targetPage==='Kuronata'&&score&&!score.beatScene){
          const soot=kind==='ash';
             score.beatScene=createMarenoiaBeatScene(score);
          const life=dark
            ? (soot?.7+Math.random()*1.1:
              small?.9+Math.random()*1.55:.42+Math.random()*.76)
            : 1.15+Math.random()*1.55;
          combustion.push({
             kind,x:edge+(dark?1:-1)*(Math.random()*47-8),
             y:y+(Math.random()-.5)*25,
            vx:(dark?1:-1)*(dark?(small?65+Math.random()*245:
              14+Math.random()*154):
              16+Math.random()*69),
            vy:-(dark?(small?12+Math.random()*190:
              32+Math.random()*171):12+Math.random()*57),
            life,remaining:life,
            radius:dark
              ? (soot?1.2+Math.random()*2.7:
                small?2+Math.random()*5:9+Math.random()*19)
              : kind==='glow'?26+Math.random()*49:
                kind==='leaf'?4+Math.random()*6:
                2+Math.random()*3,
            angle:(Math.random()-.5)*1.1,
            sprite:(Math.random()*4)|0,
             seed:Math.random()*6.28
          });
         }
         }
         startFatePrelude(anchor,targetPage);
         if(dark) {
        primeSound(); // Synchronous click activation unlocks Web Audio.
          sootRemainder+=dt*(h<680?32:46);
        if(gear) {
          const sootBirths=Math.min(11,Math.floor(sootRemainder));
             beginFateSwap(anchor,nextActive);
          sootRemainder-=sootBirths;
        } else {
          for(let i=0;i<sootBirths&&residualSoot.length<(w<700?92:155);i++){
             requestSwap(nextActive);
            const y=Math.random()*h;
            const life=2.15+Math.random()*1.60;
            residualSoot.push({
              x:edgeAt(y,seconds,progress,w)+5+Math.random()*83,
              y:y+(Math.random()-.5)*48,
              vx:8+Math.random()*39,vy:-18-Math.random()*83,
              size:155+Math.random()*195,life,remaining:life,
              sprite:(Math.random()*4)|0,
              spin:(Math.random()-.5)*.8,
              seed:Math.random()*6.28
             });
          }
          shardRemainder+=dt*(h<680?18:30);
          const shardBirths=Math.min(7,Math.floor(shardRemainder));
          shardRemainder-=shardBirths;
          for(let i=0;i<shardBirths&&tornPaper.length<175;i++){
            const y=Math.random()*h;
            const life=.90+Math.random()*1.85;
            const points=8+Math.floor(Math.random()*7);
            const outline=Array.from({length:points},(_,j)=>({
              a:Math.PI*2*j/points+(Math.random()-.5)*.50,
              r:.25+Math.random()*1.06
            }));
             tornPaper.push({
              x:edgeAt(y,seconds,progress,w)+(Math.random()-.20)*58,
              y, vx:55+Math.random()*295,
              vy:-190+Math.random()*270,
              gravity:90+Math.random()*150,
              radius:2+Math.pow(Math.random(),2.5)*22,
              angle:Math.random()*6.28,
              spin:(Math.random()-.5)*7,
              outline,life,remaining:life,
              seed:Math.random()*6.28
            });
          }
         }
         }
         return true;
      }
      for(let i=combustion.length-1;i>=0;i--){
        const p=combustion[i];
        p.remaining-=dt;
        if(p.remaining<=0){combustion.splice(i,1);continue;}
        p.x+=(p.vx+Math.sin(elapsed*(dark?5:1.6)+p.seed)*
          (dark?22:24))*dt;
        p.y+=p.vy*dt;
        p.vy-=(dark?17:7)*dt;
      }
      for(let i=residualSoot.length-1;i>=0;i--){
        const p=residualSoot[i];
        p.remaining-=dt;
        if(p.remaining<=0){residualSoot.splice(i,1);continue;}
        p.x+=(p.vx+Math.sin(elapsed*2+p.seed)*27)*dt;
        p.y+=p.vy*dt;
        p.size+=32*dt;
      }
      for(let i=tornPaper.length-1;i>=0;i--){
        const p=tornPaper[i];
        p.remaining-=dt;
        if(p.remaining<=0){tornPaper.splice(i,1);continue;}
        p.x+=(p.vx+Math.sin(elapsed*4+p.seed)*42)*dt;
        p.y+=p.vy*dt;
        p.vy+=p.gravity*dt;
         p.angle+=p.spin*dt;
      }
     }
     }
 
    const particles=Array.from({length:innerWidth<700?40:62},(_,i)=>({
     // The Infobox gear begins to turn before the actual world changes.
      seed:rand(i*4+71),height:rand(i*5+13),
     function beginFateSwap(anchor,nextActive) {
      lifetime:.5+rand(i*8+37)*.7,
        if(animating||gearPending||nextActive===active)return;
      phase:rand(i*9+21),sprite:i%4,
        gearPending=true;
      scale:.72+rand(i*7+9)*1.25,
         anchor.classList.add('kkpt-fate-gear-turning');
      bend:rand(i*15+23)*6.28
         // The gear and the world run on the SAME audio timeline. A 2s
    }));
         // unobserved setTimeout used to create an awkward silent gap.
    const motes=Array.from({length:innerWidth<700?56:115},(_,i)=>({
         requestAnimationFrame(()=>{
      y:rand(i*3+17),phase:rand(i*9+43),
            gearPending=false;
      speed:100+rand(i*12+5)*340,
            requestSwap(nextActive);
      drift:82+rand(i*15+2)*330,
         });
      length:3+rand(i*17+7)*16
         setTimeout(()=>anchor.classList.remove('kkpt-fate-gear-turning'),3300);
     }));
    const shockDebris=Array.from({length:innerWidth<700?32:66},(_,i)=>({
      angle:rand(i*17+131)*Math.PI*2,
      velocity:190+rand(i*11+51)*900,
      seed:rand(i*5+79),
      length:6+rand(i*7+17)*26
    }));
     function pressureWave(x,y,radius,power,dark,time) {
      if(power<=.005)return;
      const core=ctx.createRadialGradient(x,y,0,x,y,radius*.75);
      if(dark){
         core.addColorStop(0,'rgba(255,231,169,'+(.20*power)+')');
         core.addColorStop(.17,'rgba(253,88,18,'+(.21*power)+')');
         core.addColorStop(.60,'rgba(106,16,12,'+(.09*power)+')');
         core.addColorStop(1,'rgba(30,0,0,0)');
      }else{
        core.addColorStop(0,'rgba(237,255,255,'+(.24*power)+')');
         core.addColorStop(.26,'rgba(98,193,255,'+(.18*power)+')');
         core.addColorStop(1,'rgba(0,29,100,0)');
      }
      ctx.globalCompositeOperation='screen';
      ctx.fillStyle=core;
      ctx.fillRect(x-radius,y-radius,radius*2,radius*2);
      ctx.globalCompositeOperation='source-over';
     }
     }
 
     return function drawAtmosphere(seconds,progress,dark,elapsed,cells) {
     function fateGearShape() {
      const w=innerWidth,h=innerHeight;
        const parts=[];
      const scale=dark?(w<700?.43:.55):(w<700?.35:.45);
         for(let i=0;i<48;i++) {
      const bw=Math.max(1,Math.round(w*scale));
            const a=i/48*Math.PI*2-Math.PI/2;
      const bh=Math.max(1,Math.round(h*scale));
            const r=(i%4===0||i%4===3)?21:16.8;
      if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;}
            parts.push((i?'L':'M')+(24+Math.cos(a)*r).toFixed(2)+' '+
      ctx.setTransform(scale,0,0,scale,0,0);
                (24+Math.sin(a)*r).toFixed(2));
      ctx.clearRect(0,0,w,h);
      const envelope=Math.sin(Math.PI*progress);
      const fade=clamp01(progress/.075)*clamp01((1-progress)/.065);
      advanceCombustion(seconds,progress,dark,elapsed,w,h);
      const sprites=dark?sootSprites:mistSprites;
      const direction=dark?1:-1;
      ctx.globalCompositeOperation='source-over';
      const bands=dark?Math.max(8,Math.ceil(h/116)):
         Math.max(6,Math.ceil(h/136));
      for(let i=0;i<bands;i++){
        const baseY=(i+.15+rand(i*11+39)*.70)*h/bands;
        const y=baseY+31*Math.sin(seconds*1.6+i*1.47);
        const x=edgeAt(y,seconds,progress,w)+direction*
          (dark?76+rand(i*19+6)*260:120)+
          Math.sin(seconds*.93+i*1.3)*37;
        const width=dark?170+rand(i*29+12)*220:250;
        const height=dark?165+rand(i*7+5)*170:245;
        ctx.save();
        ctx.globalAlpha=(dark?.34:.21)*fade*(.55+.45*envelope);
        ctx.translate(x,y);
        ctx.rotate(.27*Math.sin(i*2.1+seconds*.9));
        ctx.drawImage(sprites[i%4],-width/2,-height/2,width,height);
        ctx.restore();
      }
      for(let particleIndex=0;particleIndex<particles.length;
        particleIndex+=dark?1:2){
        const item=particles[particleIndex];
        const age=((elapsed/item.lifetime+item.phase)%1);
        const baseY=item.height*h;
        const y=baseY-age*(dark?230:128)+(Math.sin(elapsed*2+item.bend)*15);
        if(y< -230||y>h+230)continue;
        const frontier=edgeAt(baseY,seconds,progress,w);
        const x=frontier+direction*(age*(dark?225:135)+
          30*Math.sin(age*6.3+item.bend));
        const size=(dark?174:145)*item.scale*(.68+age*1.9);
        const opacity=(dark?.43:.26)*Math.sin(Math.PI*age)*fade*
          (.55+.45*envelope);
        if(opacity<=.005)continue;
        ctx.save();
        ctx.globalAlpha=opacity;
        ctx.translate(x,y);
        ctx.rotate((item.bend-3.14)*.14+age*.85*direction);
        ctx.scale(1.18,.84);
        ctx.drawImage(sprites[item.sprite],-size/2,-size/2,size,size);
        ctx.restore();
      }
      if(dark){
        ctx.save();
        ctx.globalCompositeOperation='source-over';
        for(const p of residualSoot){
          const age=1-p.remaining/p.life;
          const opacity=clamp01(age/.14)*
            Math.pow(1-age,.49)*(.66+.24*Math.sin(p.seed*3)**2)*fade;
          if(opacity<.009)continue;
          ctx.save();
          ctx.globalAlpha=opacity;
          ctx.translate(p.x,p.y);
          ctx.rotate(p.spin+age*.33*Math.sin(p.seed));
          ctx.scale(1.20,.92);
          const s=p.size*(1+age*.68);
          ctx.drawImage(sootSprites[p.sprite],-s/2,-s/2,s,s);
          ctx.restore();
         }
         }
         return parts.join(' ')+' Z';
         ctx.restore();
    }
      }else{
 
         ctx.save();
    function mountFateGear(doc,targetPage) {
         ctx.globalCompositeOperation='screen';
         if(doc.querySelector('.kkpt-fate-gear'))return;
         for(let n=0;n<2;n++){
         const content=doc.querySelector('#mw-content-text,.mw-parser-output');
          const yy=(n+.64)*h/2.25+Math.sin(seconds*.8+n*2)*30;
         if(!content)return;
          const xx=edgeAt(yy,seconds,progress,w)-
        // The fate wheel stays inside the native Infobox caption, but is
             (88+55*Math.sin(n*2.1+seconds*.7));
        // positioned independently: it must NEVER push the title off-center.
          const radius=145+65*Math.sin(seconds*.7+n*1.7)**2;
        const box=content.querySelector('table.infobox.vcard,table.infobox');
          const bloom=ctx.createRadialGradient(xx,yy,8,xx,yy,radius);
        const host=box?.caption ||
          bloom.addColorStop(0,'rgba(250,255,255,.10)');
            box?.querySelector('th.infobox-above,th.infobox-title,th[colspan]')||
          bloom.addColorStop(.33,'rgba(164,237,255,.07)');
             doc.querySelector('#firstHeading .mw-page-title-main,'+
          bloom.addColorStop(.74,'rgba(117,198,255,.04)');
                '.mw-page-title-main,#firstHeading');
          bloom.addColorStop(1,'rgba(117,198,255,0)');
        if(!host)return;
          ctx.globalAlpha=fade;
        host.classList.add('kkpt-fate-host');
          ctx.fillStyle=bloom;
        const anchor=doc.createElement('button');
          ctx.fillRect(xx-radius,yy-radius,radius*2,radius*2);
        anchor.type='button';
        anchor.className='kkpt-fate-gear';
        // A native button cannot be mistaken for an article link by
        // MediaWiki Popups, including in previews loaded into iframes.
        anchor.dataset.kkptTargetPage=targetPage;
        const gearLabel=targetPage==='Kuronata'
            ?'命运的齿轮 · 唤醒 Kuronata':'命运的齿轮 · 回到 Kuranoka';
        anchor.setAttribute('aria-label',gearLabel);
        anchor.innerHTML='<svg class="kkpt-fate-gear-svg" viewBox="0 0 48 48" '+
            'aria-hidden="true" focusable="false"><path d="'+fateGearShape()+
            '"/><circle cx="24" cy="24" r="8.5"/>'+
            '<circle cx="24" cy="24" r="3"/></svg>';
        host.appendChild(anchor);
 
        // Fixed corner placement: no title measurements, ResizeObserver
        // or reflow during the animation.
    }
 
    // Read backgrounds after background-template code populates --bg-image.
    function backgroundUrls(doc) {
        const urls = new Set();
        const selector = [doc.documentElement, doc.body];
        try {
            doc.querySelectorAll('.background-theme,[class*="background"],[style*="background"],#mw-content-text,.citizen-page-container')
                .forEach((el, i) => { if (i < 18) selector.push(el); });
        } catch (e) { /* use html/body */ }
        function add(value) {
            if (!value) return;
            const pattern = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
            let match;
            while ((match = pattern.exec(value))) {
                try { urls.add(new URL(match[2], doc.baseURI).href); } catch (e) { /* ignore */ }
            }
            if (/^https?:\/\/\S+\.(?:jpg|jpeg|png|webp|avif)(?:\?\S*)?$/i.test(value)) {
                try { urls.add(new URL(value, doc.baseURI).href); } catch (e) { /* ignore */ }
            }
         }
         }
         selector.forEach(el => {
         ctx.restore();
            if (!el) return;
      }
            try {
      for(const moment of (dark?[.10,.48]:[])){
                [null, '::before', '::after'].forEach(pseudo => {
        const age=(progress-moment)/.125;
                    const style = doc.defaultView.getComputedStyle(el, pseudo);
        if(age<0||age>1)continue;
                    if (!style) return;
        const intensity=(1-age)*(1-age)*fade;
                    add(style.backgroundImage);
        ctx.globalCompositeOperation='screen';
                    add(style.getPropertyValue('--bg-image'));
        ctx.fillStyle=dark
                    add(style.getPropertyValue('--background-image'));
          ? 'rgba(255,81,26,'+(.095*intensity)+')'
                });
          : 'rgba(150,219,255,'+(.080*intensity)+')';
                if (el.dataset) add(el.dataset.bgImage);
         ctx.fillRect(0,0,w,h);
            } catch (e) { /* style not yet ready */ }
         ctx.globalCompositeOperation='source-over';
        });
         const x=edgeAt(h*.51,seconds,progress,w);
         return [...urls];
         const y=h*.51;
    }
        const radius=95+age*Math.max(w,h)*.9;
 
        pressureWave(x,y,radius,intensity,dark,seconds);
    function warmImage(url) {
        ctx.globalAlpha=(dark?.20:.28)*intensity;
         if (imagePool.has(url)) return imagePool.get(url).promise;
         ctx.lineWidth=(dark?2.5:2)*Math.max(.4,1-age);
         const image = new Image();
        ctx.strokeStyle=dark?'#ffb36c':'#b5eaff';
         const promise = new Promise(resolve => {
         ctx.beginPath();
            let finished = false;
         for(let i=0;i<=110;i++){
            const timeout = setTimeout(() => done(false), 5000);
          const a=i/110*Math.PI*2;
            function done(ok) {
          const jag=1+.03*Math.sin(a*29+seconds*12)+
                if (finished) return;
             .05*Math.sin(a*13-seconds*5);
                finished = true;
          const rx=Math.cos(a)*radius*jag,ry=Math.sin(a)*radius*jag;
                clearTimeout(timeout);
          if(i===0)ctx.moveTo(x+rx,y+ry);else ctx.lineTo(x+rx,y+ry);
                resolve(ok);
            }
            image.onload = () => done(true);
            image.onerror = () => done(false);
            image.src = url;
            if (image.complete) done(image.naturalWidth > 0);
         }).then(ok => ok && image.decode
            ? Promise.race([image.decode().then(() => true).catch(() => true),sleep(900).then(() => true)])
            : ok);
         imagePool.set(url, { image, promise });
         return promise;
    }
 
    async function warmBackgrounds(doc) {
        const urls = backgroundUrls(doc);
        await Promise.allSettled(urls.map(warmImage));
    }
 
    function wireFrame() {
        try {
            const doc = frame.contentDocument;
            if (!doc) return;
            otherTitle = doc.title || otherTitle;
            // The preview exists only for the animated reveal. Pausing
            // its hidden ambience avoids doubling ongoing render work.
            doc.documentElement.classList.add('kkpt-frame-inactive');
             watchPortalLinks(doc, basePage);
            mountFateGear(doc, basePage);
            if (doc.__kkptFrameBridge) return;
            doc.__kkptFrameBridge = true;
 
            // Non-anchor navigation should leave the iframe and open normally
            // in the top window, so the browser's URL doesn't become stale.
            doc.addEventListener('click', event => {
                if (!active || !isPlainClick(event)) return;
                if (portalClick(event, doc, basePage, false)) return;
                const el = findAnchor(event, doc);
                if (!el || (el.target && el.target !== '_self')) return;
                const href = el.href;
                if (!href || href.startsWith('javascript:')) return;
                const next = new URL(href, doc.baseURI);
                // Same-document fragments stay inside the temporary preview.
                const currentFrame = new URL(frame.contentWindow.location.href);
                if (next.origin === currentFrame.origin &&
                    next.pathname === currentFrame.pathname &&
                    next.search === currentFrame.search && next.hash) {
                    history.replaceState(history.state, '', next.href);
                    return;
                }
                event.preventDefault();
                navigateRealPage(next.href);
            }, true);
 
            doc.addEventListener('submit', event => {
                if (active && event.target instanceof doc.defaultView.HTMLFormElement) {
                    event.target.target = '_top';
                }
            }, true);
        } catch (err) {
            console.warn('[KuranokaPageTransition] Frame link bridge unavailable:', err);
         }
         }
    }
        ctx.stroke();
 
         ctx.globalAlpha=1;
    function warmFrame() {
         if(dark){
         if (loading) return loading;
        ctx.globalCompositeOperation='screen';
         frame = document.createElement('iframe');
        for(const bit of shockDebris){
        frame.id = 'kkpt-preview';
          const f=age*(.5+bit.seed*.5);
        frame.src = otherUrl.href; // set BEFORE insertion, avoids extra initial history hop
          const travel=bit.velocity*f;
        frame.title = otherPage + ' 预载页面';
          const px=x+Math.cos(bit.angle)*travel;
        frame.tabIndex = -1;
          const py=y+Math.sin(bit.angle)*travel;
        frame.setAttribute('aria-hidden', 'true');
          const vx=Math.cos(bit.angle)*bit.length*(1-age);
        frame.setAttribute('loading', 'eager');
          const vy=Math.sin(bit.angle)*bit.length*(1-age);
        frame.style.clipPath = hideClip;
          ctx.globalAlpha=(dark?.82:.68)*intensity*(.3+bit.seed*.7);
        // Load all resources in advance, but don't composite the hidden
          ctx.strokeStyle=dark?'#ffdd9b':'#e0faff';
        // full-screen Wiki until its first transition frame.
          ctx.lineWidth=.6+bit.seed*1.3;
        frame.style.visibility = 'hidden';
          ctx.beginPath();
        // Never expose a plain cyan rectangle while the destination Wiki
          ctx.moveTo(px,py);
        // page is still painting. The image is the same native world photo.
          ctx.lineTo(px-vx,py-vy);
        const targetBackdrop=worldBackdrops[otherPage];
          ctx.stroke();
        if(targetBackdrop){
        }
            frame.style.backgroundImage='url("'+targetBackdrop+'")';
            frame.style.backgroundPosition='center center';
            frame.style.backgroundSize='cover';
            frame.style.backgroundRepeat='no-repeat';
         }
         }
 
         ctx.globalCompositeOperation='source-over';
         loading = new Promise(resolve => {
        ctx.globalAlpha=1;
            let settled = false;
      }
            let preparing = false;
      if(dark){
            let poll = null;
        ctx.save();
            function finish(ok, reason) {
        ctx.globalCompositeOperation='screen';
                if (settled) return;
        const count=Math.ceil(h/17);
                settled = true;
        for(let j=0;j<count;j++){
                clearInterval(poll);
          const seed=rand(j*13+167);
                ready = ok;
          const y=(j+seed*.78)*11.5-5;
                if (!ok) console.warn('[KuranokaPageTransition] Preview:', reason);
          const x=edgeAt(y,seconds,progress,w);
                resolve(ok);
          if(x < -90 || x > w+90)continue;
            }
          const ripple=Math.sin(seconds*(3.4+rand(j*11+4)*2.4)+
            async function inspect() {
            j*2.09+seed*3.1);
                if (settled || preparing) return;
          const heat=clamp01(.58+ripple*.34+
                try {
            .16*Math.sin(j*1.17-seconds*7.1));
                    const doc = frame.contentDocument;
          if(heat<.34)continue;
                    if (!doc || !doc.querySelector('#mw-content-text,.mw-parser-output')) return;
          const length=(22+rand(j*23+19)*75)*(.5+heat*.94);
                    preparing = true;
          const bend=.18+rand(j*17+42)*.66;
                    clearInterval(poll);
          const tipX=x-length*(.76+.19*seed);
                    await sleep(320); // Background.js / wiki style scripts
          const tipY=y-length*bend;
                    await Promise.race([warmBackgrounds(doc), sleep(3600)]);
          const thick=(3.2+rand(j*31+11)*10.3)*(.5+heat);
                    await sleep(120);
          const gradient=ctx.createLinearGradient(x,y,tipX,tipY);
                    await Promise.race([warmBackgrounds(doc), sleep(1300)]);
          gradient.addColorStop(0,'rgba(255,255,242,.99)');
                    frame.contentWindow.scrollTo(0, 0);
          gradient.addColorStop(.28,'rgba(255,224,84,.96)');
                    wireFrame();
          gradient.addColorStop(.59,'rgba(255,76,10,.64)');
                    finish(true);
          gradient.addColorStop(1,'rgba(182,17,6,0)');
                } catch (err) { finish(false, String(err)); }
          ctx.beginPath();
            }
          ctx.moveTo(x+2,y+thick*.5);
            frame.addEventListener('load', () => {
          ctx.bezierCurveTo(
                if (!settled && !preparing) inspect();
            x-length*.22,y-thick*.25,
            });
            tipX+length*.36,tipY+thick*.68,tipX,tipY);
            frame.addEventListener('error', () => finish(false,'iframe 网络加载错误'));
          ctx.bezierCurveTo(
            document.body.append(frame);
             tipX+length*.32,tipY-thick*.22,
            poll = setInterval(inspect, 130);
             x-length*.17,y-thick*.74,
            setTimeout(() => { if (!settled && !preparing) finish(false,'18 秒仍未找到目标正文'); }, 18000);
             x+2,y-thick*.65);
        });
          ctx.closePath();
 
          ctx.globalAlpha=Math.min(1,heat*1.08)*fade;
        warmBackgrounds(document).catch(() => {});
          ctx.fillStyle=gradient;
        setTimeout(() => warmBackgrounds(document).catch(() => {}), 700);
          ctx.shadowBlur=0;
        return loading;
          ctx.fill();
    }
          if(heat>.66){
 
             ctx.globalAlpha=(heat-.54)*1.40*fade;
 
             ctx.strokeStyle='#fffce0';ctx.lineWidth=.9+heat*1.1;
    // Shared, low-resolution atmospherics: return of V14's dark smoke,
             ctx.beginPath();ctx.moveTo(x,y);
    // long-lived torn paper, shockwaves, blue-white petals and pollen.
             ctx.quadraticCurveTo(x-length*.3,y-length*.11,
    // Only the combustion CURTAIN is replaced by the bright V2 engine.
              tipX+length*.19,tipY+thick*.18);
    let atmosphereAssetCache=null;
             ctx.stroke();
    const atmosphereNoiseCache=new Map();
          }
    function createAtmosphere(canvas,buildOneOnly=false) {
        const ctx = canvas.getContext('2d', {alpha:true});
        const rand = n => {
            const cached=atmosphereNoiseCache.get(n);
             if(cached!==undefined)return cached;
             const x = Math.sin(n*127.1+311.7)*43758.5453123;
             const value=x-Math.floor(x);
            atmosphereNoiseCache.set(n,value);
            return value;
        };
        const clamp01 = n => Math.max(0,Math.min(1,n));
        function valueNoise(x,y,seed) {
            const ix=Math.floor(x), iy=Math.floor(y);
            let fx=x-ix,fy=y-iy;
             fx=fx*fx*(3-2*fx);fy=fy*fy*(3-2*fy);
             const h=(a,b)=>rand(a*171.3+b*513.7+seed*89.1);
             const a=h(ix,iy)*(1-fx)+h(ix+1,iy)*fx;
             const b=h(ix,iy+1)*(1-fx)+h(ix+1,iy+1)*fx;
             return a*(1-fy)+b*fy;
         }
         }
         function smokeTexture(seed,dark) {
         ctx.restore();
            const tex=document.createElement('canvas');
      }
            tex.width=tex.height=112;
      ctx.save();
            const g=tex.getContext('2d');
      ctx.beginPath();
            const data=g.createImageData(112,112);
      for(let y=-12,i=0;y<=h+12;y+=3.6,i++){
            for(let y=0;y<112;y++) for(let x=0;x<112;x++){
        const x=edgeAt(y,seconds,progress,w);
                const u=(x-56)/56,v=(y-56)/56;
        if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
                const curl=valueNoise(u*2.9+11,v*2.9+1,seed);
      }
                const curl2=valueNoise(u*3.4+3,v*3.4+7,seed+7);
      ctx.lineCap='round';ctx.lineJoin='round';
                const wx=u+.47*(curl-.5),wy=v+.40*(curl2-.5);
      if(dark){
                const radius=Math.sqrt(wx*wx*.91+wy*wy*1.13);
        ctx.strokeStyle='rgba(9,5,7,.81)';
                const envelope=clamp01((1.05-radius)/.56);
        ctx.lineWidth=18;ctx.shadowBlur=0;
                const low=valueNoise(u*4.1+4,v*4.1-9,seed+2);
        ctx.stroke();
                const high=valueNoise(u*11.4-3,v*11.4+6,seed+5);
        ctx.strokeStyle='rgba(210,45,9,.53)';
                const fibers=valueNoise(u*22.0+1,v*16.0+5,seed+9);
        ctx.lineWidth=6;
                const density=clamp01((low*.62+high*.28+fibers*.10-.28)*2.0)*
        ctx.stroke();
                    envelope*envelope;
      }else{
                const i=(y*112+x)*4;
        ctx.strokeStyle='rgba(27,95,190,.22)';
                const variation=low*28+high*15;
        ctx.lineWidth=27;ctx.shadowColor='#56bdff';ctx.shadowBlur=17;
                if(dark){
        ctx.stroke();
                    data.data[i]=5+variation*.37;
        ctx.strokeStyle='rgba(132,225,255,.51)';
                    data.data[i+1]=5+variation*.33;
        ctx.lineWidth=11;ctx.shadowColor='#8aeaff';ctx.shadowBlur=11;
                    data.data[i+2]=8+variation*.38;
        ctx.stroke();
                }else{
        ctx.strokeStyle='rgba(249,255,255,.65)';
                    data.data[i]=44+variation;
        ctx.lineWidth=2.7;ctx.shadowBlur=4;
                    data.data[i+1]=91+variation;
        ctx.stroke();
                    data.data[i+2]=125+variation;
      }
                }
      ctx.restore();
                data.data[i+3]=Math.round(240*density);
      if(dark){
            }
        ctx.save();
            g.putImageData(data,0,0);
        ctx.globalCompositeOperation='screen';
            return tex;
        ctx.lineCap='round';
        for(let y=-8;y<h+8;y+=5.8){
          const intensity=clamp01(.58+
            .29*Math.sin(y*.054+seconds*3.7)+
            .21*Math.sin(y*.147-seconds*8.9)+
            .13*Math.sin(y*.337+seconds*16.4));
          if(intensity<.47)continue;
          const x=edgeAt(y,seconds,progress,w);
          const nextX=edgeAt(y+5.8,seconds,progress,w);
          ctx.globalAlpha=intensity*fade;
          ctx.lineWidth=1.4+intensity*4.6;
          ctx.shadowBlur=0;
          ctx.strokeStyle=intensity>.75?'#fff8d7':
            intensity>.58?'#ffd369':'#ff831b';
          ctx.beginPath();ctx.moveTo(x,y);
          ctx.lineTo(nextX,y+5.8);ctx.stroke();
         }
         }
         // V2-inspired white-hot additive sprites, but with noise-eroded
         ctx.restore();
        // silhouettes instead of repetitive smooth, elliptical balls.
        ctx.save();
        function glowTexture(seed,mode) {
        ctx.globalCompositeOperation='screen';
            const tex=document.createElement('canvas');
        ctx.shadowBlur=0;
            tex.width=96;tex.height=128;
        for (let i=0; i<cells.length; i++) {
            const g=tex.getContext('2d');
          const c=cells[i];
            const gradient=g.createRadialGradient(48,68,0,48,68,64);
          ctx.globalAlpha=(.42+.34*c.heat)*fade;
            const colors=mode==='fire'
          ctx.strokeStyle=c.heat>.73?'#fff6bf':
                ? [[0,'#fffef1'],[.14,'#fff4bb'],[.34,'#ffd04d'],
            c.heat>.38?'#ffd166':'#ff621c';
                  [.55,'#ff841bcc'],[.80,'#bc290749'],[1,'#6e030000']]
          ctx.lineWidth=.8+Math.min(2.6,c.radius*.24);
                : mode==='blue'
          const n=c.points.length;
                ? [[0,'#ffffff'],[.19,'#f1ffff'],[.40,'#a2efff'],
          ctx.beginPath();
                  [.68,'#5298ff99'],[1,'#345bf000']]
          for (let j=0;j<n;j++) {
                : [[0,'#ffffff'],[.19,'#fff8cd'],[.49,'#ff9b28cc'],
            if ((j+i*3)%5>2)continue;
                  [1,'#ff380000']];
            const a=c.points[j],b=c.points[(j+1)%n];
            colors.forEach(([p,c])=>gradient.addColorStop(p,c));
            ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);
            g.fillStyle=gradient;g.fillRect(0,0,96,128);
          }
            const img=g.getImageData(0,0,96,128);
          ctx.stroke();
            for(let y=0;y<128;y++)for(let x=0;x<96;x++){
                const u=(x-48)/48,v=(y-64)/64;
                const cloud=valueNoise(u*4+seed*1.9,v*4+seed*.7,seed+18);
                const fiber=valueNoise(u*11+3,v*8+17,seed+41);
                const shifted=u+.18*(cloud-.5);
                const irregular=clamp01((1.15-Math.abs(shifted))/.52)*
                    clamp01((1.05-Math.abs(v+.12*(fiber-.5)))/.42);
                const n=.59+.77*cloud+.28*fiber;
                const alphaIndex=(y*96+x)*4+3;
                img.data[alphaIndex]=Math.round(img.data[alphaIndex]*
                    irregular*clamp01(n));
            }
            g.putImageData(img,0,0);
            return tex;
         }
         }
         // V25: the profiler found one 270ms long task here. Build the
         ctx.restore();
        // EXACT SAME 17 textures in separate tasks, never all at once.
         ctx.save();
         // The stable noise cache avoids repeating the costly sin hash.
         for(let j=0;j<Math.ceil(h/10);j++){
        const cache=atmosphereAssetCache ||= {
          const y=(j+rand(j*5+61)*.8)*10;
            sootSprites:[],mistSprites:[],FIRE_GLOW:[],BLUE_GLOW:[],
          const x=edgeAt(y,seconds,progress,w)-
            EMBER_GLOW:null
             (5+rand(j*17+38)*24);
        };
          const size=.5+rand(j*29+11)*2.5;
         function buildNextAsset(){
          const visible=.25+.59*rand(j*13+7);
            let n=cache.sootSprites.length;
          ctx.globalAlpha=visible*fade;
            if(n<4){cache.sootSprites.push(smokeTexture(n+17,true));return true;}
          ctx.fillStyle='#150b0a';
            n=cache.mistSprites.length;
          ctx.beginPath();
             if(n<4){cache.mistSprites.push(smokeTexture(n+51,false));return true;}
          ctx.ellipse(x,y,size*.71,size,rand(j*7+31)*3.14,
            n=cache.FIRE_GLOW.length;
             0,Math.PI*2);
            if(n<4){cache.FIRE_GLOW.push(glowTexture(n+1,'fire'));return true;}
          ctx.fill();
            n=cache.BLUE_GLOW.length;
            if(n<4){cache.BLUE_GLOW.push(glowTexture(n+8,'blue'));return true;}
             if(!cache.EMBER_GLOW){
                cache.EMBER_GLOW=glowTexture(21,'ember');return true;
            }
            return false;
         }
         }
         if(buildOneOnly)return buildNextAsset();
         ctx.restore();
        // Normally prewarming is already complete before a user clicks.
      }
        // The fallback also works if an animation is started immediately.
      ctx.save();
        while(buildNextAsset()){}
      ctx.globalCompositeOperation='lighter';
        const {sootSprites,mistSprites,FIRE_GLOW,BLUE_GLOW,EMBER_GLOW}=cache;
      for(const p of combustion){
        const combustion=[];
         if(p.kind==='ash'||p.kind==='leaf'||p.kind==='pollen')continue;
         const residualSoot=[];
         const age=1-p.remaining/p.life;
        const tornPaper=[];
        const opacity=clamp01(p.remaining/.22)*
        let lastElapsed=0,spawnRemainder=0,sootRemainder=0,shardRemainder=0;
          clamp01(age/.08)*(1-age*.30)*
         function advanceCombustion(seconds,progress,dark,elapsed,w,h) {
          (p.kind==='ember'?.74:p.kind==='glow'?.13:.66)*fade*
            const dt=Math.min(.065,Math.max(0,elapsed-lastElapsed));
          (dark?Math.exp(-Math.max(0,p.x-
            lastElapsed=elapsed;
            edgeAt(p.y,seconds,progress,w))/70):1);
            const live=progress>.01&&progress<.985;
        if(opacity<.012)continue;
            if(live){
        ctx.save();
                // More long-lived sparks than V25, far below V31's 370/s.
        ctx.globalAlpha=opacity;
                // Blue petals are softer/less dense: fewer layers noticeably
        ctx.translate(p.x,p.y);
                // reduce reverse-direction Canvas work without touching fire.
        ctx.rotate(p.angle+Math.sin(elapsed*4+p.seed)*.13);
                const flow=(dark?132:64)*Math.min(1.5,h/720)*
        const large=p.kind==='flame';
                    (.75+.25*Math.sin(Math.PI*progress));
        const sx=p.radius*(p.kind==='glow'?1.7:large?.96:1.32)*(1+age*.32);
                spawnRemainder+=dt*flow;
        const sy=p.radius*(p.kind==='glow'?1.4:large?1.78:1.35)*(1+age*.57);
                const births=Math.min(65,Math.floor(spawnRemainder));
        const sprite=p.kind==='flame'?FIRE_GLOW[p.sprite]:
                spawnRemainder-=births;
          p.kind==='glow'?BLUE_GLOW[p.sprite]:EMBER_GLOW;
                for(let j=0;j<births&&combustion.length<1320;j++){
        ctx.drawImage(sprite,-sx,-sy,sx*2,sy*2);
                    const y=Math.random()*h;
        ctx.restore();
                    const edge=edgeAt(y,seconds,progress,w);
      }
                    const r=Math.random();
      ctx.restore();
                    const kind=dark
      if(!dark){
                        ? r<.38?'flame':r<.91?'ember':'ash'
        ctx.save();
                        : r<.37?'glow':r<.81?'leaf':'pollen';
        ctx.globalCompositeOperation='screen';
                    const small=kind==='ember'||kind==='pollen';
        for(const p of combustion){
                    const soot=kind==='ash';
          if(p.kind!=='leaf'&&p.kind!=='pollen')continue;
                    const life=dark
          const age=1-p.remaining/p.life;
                        ? (soot?.7+Math.random()*1.1:
          const opacity=clamp01(age/.17)*clamp01(p.remaining/.55)*
                            small?.9+Math.random()*1.55:.42+Math.random()*.76)
            (p.kind==='leaf'?.47:.28)*fade;
                        : 1.15+Math.random()*1.55;
          if(opacity<.012)continue;
                    combustion.push({
          ctx.save();
                        kind,x:edge+(dark?1:-1)*(Math.random()*47-8),
          ctx.globalAlpha=opacity;
                        y:y+(Math.random()-.5)*25,
          ctx.translate(p.x+11*Math.sin(elapsed*1.6+p.seed),p.y);
                        vx:(dark?1:-1)*(dark?(small?65+Math.random()*245:
          ctx.rotate(p.angle+age*1.65*Math.sin(p.seed*2.5));
                            14+Math.random()*154):
          if(p.kind==='leaf'){
                            16+Math.random()*69),
            const r=p.radius;
                        vy:-(dark?(small?12+Math.random()*190:
            ctx.shadowBlur=5;
                            32+Math.random()*171):12+Math.random()*57),
            ctx.shadowColor='#bbf4ff';
                        life,remaining:life,
            ctx.fillStyle=p.sprite%2?'#e6ffff':'#ffffff';
                        radius:dark
            ctx.beginPath();
                            ? (soot?1.2+Math.random()*2.7:
            ctx.moveTo(0,-r*1.65);
                                small?2+Math.random()*5:9+Math.random()*19)
            ctx.bezierCurveTo(r*.97,-r*.48,r*.85,r*.60,0,r*1.35);
                            : kind==='glow'?26+Math.random()*49:
            ctx.bezierCurveTo(-r*.92,r*.28,-r*.94,-r*.57,0,-r*1.65);
                                kind==='leaf'?4+Math.random()*6:
             ctx.fill();
                                2+Math.random()*3,
            ctx.globalAlpha=opacity*.38;
                        angle:(Math.random()-.5)*1.1,
            ctx.strokeStyle='#70c9ea';
                        sprite:(Math.random()*4)|0,
            ctx.lineWidth=.65;
                        seed:Math.random()*6.28
             ctx.beginPath();ctx.moveTo(0,-r*1.15);
                    });
            ctx.quadraticCurveTo(-r*.17,0,0,r*.95);ctx.stroke();
                }
          }else{
                if(dark) {
             const r=p.radius;
                    // Unlike the traveling front, these black smoke plumes
            ctx.globalAlpha=opacity*.72;
                    // are born in world-space and remain over burnt paper.
            ctx.drawImage(BLUE_GLOW[p.sprite],-r*3,-r*3,r*6,r*6);
                    sootRemainder+=dt*(h<680?32:46);
          }
                    const sootBirths=Math.min(11,Math.floor(sootRemainder));
          ctx.restore();
                    sootRemainder-=sootBirths;
                    for(let i=0;i<sootBirths&&residualSoot.length<(w<700?92:155);i++){
                        const y=Math.random()*h;
                        const life=2.15+Math.random()*1.60;
                        residualSoot.push({
                            x:edgeAt(y,seconds,progress,w)+5+Math.random()*83,
                            y:y+(Math.random()-.5)*48,
                            vx:8+Math.random()*39,vy:-18-Math.random()*83,
                            size:155+Math.random()*195,life,remaining:life,
                            sprite:(Math.random()*4)|0,
                            spin:(Math.random()-.5)*.8,
                            seed:Math.random()*6.28
                        });
                    }
                    // Charcoal sheets tear off in irregular polygons, not
                    // squares or narrow glitch streaks.
                    shardRemainder+=dt*(h<680?18:30);
                    const shardBirths=Math.min(7,Math.floor(shardRemainder));
                    shardRemainder-=shardBirths;
                    for(let i=0;i<shardBirths&&tornPaper.length<175;i++){
                        const y=Math.random()*h;
                        const life=.90+Math.random()*1.85;
                        const points=8+Math.floor(Math.random()*7);
                        const outline=Array.from({length:points},(_,j)=>({
                            a:Math.PI*2*j/points+(Math.random()-.5)*.50,
                            r:.25+Math.random()*1.06
                        }));
                        tornPaper.push({
                            x:edgeAt(y,seconds,progress,w)+(Math.random()-.20)*58,
                            y, vx:55+Math.random()*295,
                            vy:-190+Math.random()*270,
                            gravity:90+Math.random()*150,
                            radius:2+Math.pow(Math.random(),2.5)*22,
                            angle:Math.random()*6.28,
                            spin:(Math.random()-.5)*7,
                            outline,life,remaining:life,
                            seed:Math.random()*6.28
                        });
                    }
                }
            }
             for(let i=combustion.length-1;i>=0;i--){
                const p=combustion[i];
                p.remaining-=dt;
                if(p.remaining<=0){combustion.splice(i,1);continue;}
                p.x+=(p.vx+Math.sin(elapsed*(dark?5:1.6)+p.seed)*
                    (dark?22:24))*dt;
                p.y+=p.vy*dt;
                p.vy-=(dark?17:7)*dt;
             }
            for(let i=residualSoot.length-1;i>=0;i--){
                const p=residualSoot[i];
                p.remaining-=dt;
                if(p.remaining<=0){residualSoot.splice(i,1);continue;}
                p.x+=(p.vx+Math.sin(elapsed*2+p.seed)*27)*dt;
                p.y+=p.vy*dt;
                p.size+=32*dt;
            }
             for(let i=tornPaper.length-1;i>=0;i--){
                const p=tornPaper[i];
                p.remaining-=dt;
                if(p.remaining<=0){tornPaper.splice(i,1);continue;}
                p.x+=(p.vx+Math.sin(elapsed*4+p.seed)*42)*dt;
                p.y+=p.vy*dt;
                p.vy+=p.gravity*dt;
                p.angle+=p.spin*dt;
            }
         }
         }
         const particles=Array.from({length:innerWidth<700?40:62},(_,i)=>({
         ctx.restore();
            seed:rand(i*4+71),height:rand(i*5+13),
      }
            lifetime:.5+rand(i*8+37)*.7,
      ctx.save();
            phase:rand(i*9+21),sprite:i%4,
      ctx.globalCompositeOperation='source-over';
            scale:.72+rand(i*7+9)*1.25,
      for(const p of combustion){
            bend:rand(i*15+23)*6.28
        if(p.kind!=='ash')continue;
        }));
        ctx.globalAlpha=clamp01(p.remaining/.25)*.7*fade;
         const motes=Array.from({length:innerWidth<700?56:115},(_,i)=>({
        ctx.fillStyle='#190d10';
            y:rand(i*3+17),phase:rand(i*9+43),
        ctx.save();ctx.translate(p.x,p.y);ctx.rotate(elapsed*2+p.angle);
            speed:100+rand(i*12+5)*340,
         ctx.fillRect(-p.radius*.5,-p.radius*.5,p.radius,p.radius*1.8);
            drift:82+rand(i*15+2)*330,
        ctx.restore();
            length:3+rand(i*17+7)*16
      }
         }));
      ctx.restore();
         const shockDebris=Array.from({length:innerWidth<700?32:66},(_,i)=>({
      if(dark){
            angle:rand(i*17+131)*Math.PI*2,
         ctx.save();
            velocity:190+rand(i*11+51)*900,
         ctx.globalCompositeOperation='source-over';
            seed:rand(i*5+79),
        for(const p of tornPaper){
            length:6+rand(i*7+17)*26
          const age=1-p.remaining/p.life;
        }));
          const opacity=clamp01(age/.08)*clamp01(p.remaining/.4)*fade;
        function pressureWave(x,y,radius,power,dark,time) {
          if(opacity<.01)continue;
             if(power<=.005)return;
          ctx.save();
             const core=ctx.createRadialGradient(x,y,0,x,y,radius*.75);
          ctx.globalAlpha=opacity;
             if(dark){
          ctx.translate(p.x,p.y);
                core.addColorStop(0,'rgba(255,231,169,'+(.20*power)+')');
          ctx.rotate(p.angle);
                core.addColorStop(.17,'rgba(253,88,18,'+(.21*power)+')');
          ctx.beginPath();
                core.addColorStop(.60,'rgba(106,16,12,'+(.09*power)+')');
          p.outline.forEach((v,i)=>{
                core.addColorStop(1,'rgba(30,0,0,0)');
             const px=Math.cos(v.a)*v.r*p.radius;
            }else{
             const py=Math.sin(v.a)*v.r*p.radius*.74;
                core.addColorStop(0,'rgba(237,255,255,'+(.24*power)+')');
             if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);
                core.addColorStop(.26,'rgba(98,193,255,'+(.18*power)+')');
          });
                core.addColorStop(1,'rgba(0,29,100,0)');
          ctx.closePath();
             }
          ctx.shadowBlur=7;
             ctx.globalCompositeOperation='screen';
          ctx.shadowColor='#090506';
             ctx.fillStyle=core;
          ctx.fillStyle=p.seed>3?'#190d11':'#302025';
             ctx.fillRect(x-radius,y-radius,radius*2,radius*2);
          ctx.fill();
             ctx.globalCompositeOperation='source-over';
          ctx.shadowBlur=0;
          ctx.lineWidth=.8+Math.sin(p.seed)**2*.8;
          ctx.strokeStyle=age<.48?'rgba(255,107,35,.48)':
            'rgba(49,20,19,.47)';
          ctx.stroke();
          if(p.seed>4.5&&age<.52){
             ctx.globalAlpha*=.45*(1-age);
             ctx.strokeStyle='#c5e7f7';
             ctx.beginPath();
             ctx.moveTo(-p.radius*.42,-p.radius*.16);
            ctx.quadraticCurveTo(0,p.radius*.25,p.radius*.28,0);
             ctx.stroke();
          }
          ctx.restore();
         }
         }
         return function drawAtmosphere(seconds,progress,dark,elapsed,cells) {
         ctx.restore();
            const w=innerWidth,h=innerHeight;
      }
            // Dawn uses soft-focus sprites, so 0.45 is visually sufficient.
      if(dark){ctx.save();
            // This cuts ~33% of full-screen pixels compared with 0.55.
      ctx.globalCompositeOperation='screen';
            const scale=dark?(w<700?.43:.55):(w<700?.35:.45);
      for(const mote of motes){
            const bw=Math.max(1,Math.round(w*scale));
        const age=(elapsed*.74+mote.phase)%1;
            const bh=Math.max(1,Math.round(h*scale));
        const yy=mote.y*h-age*mote.speed+Math.sin(age*12+mote.phase*8)*9;
            if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;}
        const x=edgeAt(yy,seconds,progress,w)+direction*age*mote.drift;
            ctx.setTransform(scale,0,0,scale,0,0);
        if(x<-25||x>w+25||yy<-25||yy>h+25)continue;
            ctx.clearRect(0,0,w,h);
        const opacity=(1-age)*fade*.89;
            const envelope=Math.sin(Math.PI*progress);
        ctx.globalAlpha=opacity;
            const fade=clamp01(progress/.075)*clamp01((1-progress)/.065);
        ctx.strokeStyle='#ffb45d';
            advanceCombustion(seconds,progress,dark,elapsed,w,h);
        ctx.lineWidth=.8;
            const sprites=dark?sootSprites:mistSprites;
        ctx.beginPath();
            // New-world side: red fire trails to the right when burning,
        ctx.moveTo(x,yy);
            // blue dawn trails to the left when returning.
        ctx.lineTo(x-direction*mote.length*.4,yy+mote.length);
            const direction=dark?1:-1;
        ctx.stroke();
            // Broken smoke banks with different depths and sizes. The old
      }
            // aligned rows created an artificial horizontal fog ribbon.
      ctx.restore();}
            ctx.globalCompositeOperation='source-over';
    };
            const bands=dark?Math.max(8,Math.ceil(h/116)):
  }
                Math.max(6,Math.ceil(h/136));
  let atmosphereWarmPromise=null;
            for(let i=0;i<bands;i++){
  function prewarmAtmosphere() {
                const baseY=(i+.15+rand(i*11+39)*.70)*h/bands;
    if(atmosphereWarmPromise)return atmosphereWarmPromise;
                const y=baseY+31*Math.sin(seconds*1.6+i*1.47);
    atmosphereWarmPromise=new Promise(resolve=>{
                const x=edgeAt(y,seconds,progress,w)+direction*
      const scratchCanvas=document.createElement('canvas');
                    (dark?76+rand(i*19+6)*260:120)+
      const schedule=()=>{
                    Math.sin(seconds*.93+i*1.3)*37;
        if(typeof requestIdleCallback==='function')
                const width=dark?170+rand(i*29+12)*220:250;
          requestIdleCallback(step,{timeout:70});
                const height=dark?165+rand(i*7+5)*170:245;
        else setTimeout(step,24);
                ctx.save();
      };
                ctx.globalAlpha=(dark?.34:.21)*fade*(.55+.45*envelope);
      function step(){
                ctx.translate(x,y);
        if(atmosphereAssetCache?.EMBER_GLOW){resolve();return;}
                ctx.rotate(.27*Math.sin(i*2.1+seconds*.9));
        createAtmosphere(scratchCanvas,true);
                ctx.drawImage(sprites[i%4],-width/2,-height/2,width,height);
        if(atmosphereAssetCache?.EMBER_GLOW){resolve();return;}
                ctx.restore();
        schedule();
            }
      }
            // Independent turbulent billows peel away and rise.
      setTimeout(schedule,40);
            for(let particleIndex=0;particleIndex<particles.length;
    });
                particleIndex+=dark?1:2){
    return atmosphereWarmPromise;
                const item=particles[particleIndex];
  }
                const age=((elapsed/item.lifetime+item.phase)%1);
  let atmosphereEdge=null;
                const baseY=item.height*h;
  function edgeAt(y,seconds,progress,width){
                const y=baseY-age*(dark?230:128)+(Math.sin(elapsed*2+item.bend)*15);
    return atmosphereEdge
                if(y< -230||y>h+230)continue;
      ? atmosphereEdge(y)
                const frontier=edgeAt(baseY,seconds,progress,w);
      : makeEdge(progress,seconds,Boolean(window.__kkptVisualDark),width)(y);
                const x=frontier+direction*(age*(dark?225:135)+
  }
                    30*Math.sin(age*6.3+item.bend));
  let sprites=null;
                const size=(dark?174:145)*item.scale*(.68+age*1.9);
  function makeSprites() {
                const opacity=(dark?.43:.26)*Math.sin(Math.PI*age)*fade*
    if (sprites) return sprites;
                    (.55+.45*envelope);
    function radial(stops) {
                if(opacity<=.005)continue;
      const el=document.createElement('canvas');
                ctx.save();
      el.width=el.height=96;
                ctx.globalAlpha=opacity;
      const c=el.getContext('2d');
                ctx.translate(x,y);
      const g=c.createRadialGradient(48,48,0,48,48,48);
                ctx.rotate((item.bend-3.14)*.14+age*.85*direction);
      stops.forEach(pair=>g.addColorStop(pair[0],pair[1]));
                ctx.scale(1.18,.84);
      c.fillStyle=g;c.fillRect(0,0,96,96);
                ctx.drawImage(sprites[item.sprite],-size/2,-size/2,size,size);
      return el;
                ctx.restore();
    }
            }
    sprites={
            // Residual devastation: coal-black plumes stay pinned to their
      flame:radial([[0,'#fffbed'],[.18,'#fff2a0'],
            // original burn positions, expanding long after the tear passes.
        [.43,'#ffb42b'],[.70,'#ff360088'],[1,'#8f050000']]),
            // Composited *before* fire, so the hot core cuts through smoke.
      ember:radial([[0,'#ffffff'],[.15,'#fff6a8'],
            if(dark){
        [.44,'#ff9900cc'],[1,'#ef280000']]),
                ctx.save();
      smoke:radial([[0,'#080608dd'],[.30,'#100b10bb'],
                ctx.globalCompositeOperation='source-over';
        [.62,'#160f1677'],[1,'#08050800']]),
                for(const p of residualSoot){
      sky:radial([[0,'#ffffff'],[.16,'#e6ffff'],
                    const age=1-p.remaining/p.life;
        [.46,'#94deffe0'],[1,'#4ab5ff00']]),
                    const opacity=clamp01(age/.14)*
      mist:radial([[0,'#f2fdffb0'],[.4,'#abdffff0'],
                        Math.pow(1-age,.49)*(.66+.24*Math.sin(p.seed*3)**2)*fade;
        [1,'#a9e7ff00']])
                    if(opacity<.009)continue;
    };
                    ctx.save();
    return sprites;
                    ctx.globalAlpha=opacity;
  }
                    ctx.translate(p.x,p.y);
  function makeEdge(t,seconds,dark,w) {
                    ctx.rotate(p.spin+age*.33*Math.sin(p.seed));
    const center=dark
                    ctx.scale(1.20,.92);
      ? w+105-(w+210)*smooth(t)
                    const s=p.size*(1+age*.68);
      : -105+(w+210)*smooth(t);
                    ctx.drawImage(sootSprites[p.sprite],-s/2,-s/2,s,s);
    const wobble=Math.sin(Math.PI*t);
                    ctx.restore();
    return y=>center+wobble*(
                }
      17*Math.sin(y*.023+seconds*5.5)+
                ctx.restore();
      9*Math.sin(y*.063-seconds*8.4)+
            }else{
      4*Math.sin(y*.13+seconds*12)
                // The inverse is a slow, organic unfurling: broad milky
    );
                // radiance behind the rebuilt sky, not a cold explosion.
  }
                ctx.save();
  function createGpuEmbers(){
                ctx.globalCompositeOperation='screen';
    const canvas=document.createElement('canvas');
                for(let n=0;n<2;n++){
    canvas.id='kkpt-webgl';
                    const yy=(n+.64)*h/2.25+Math.sin(seconds*.8+n*2)*30;
    canvas.setAttribute('aria-hidden','true');
                    const xx=edgeAt(yy,seconds,progress,w)-
    let gl,program,buffer;
                        (88+55*Math.sin(n*2.1+seconds*.7));
    try{
                    const radius=145+65*Math.sin(seconds*.7+n*1.7)**2;
      gl=canvas.getContext('webgl2',{
                    const bloom=ctx.createRadialGradient(xx,yy,8,xx,yy,radius);
        alpha:true,antialias:false,depth:false,stencil:false,
                    bloom.addColorStop(0,'rgba(250,255,255,.10)');
        premultipliedAlpha:false,preserveDrawingBuffer:false,
                    bloom.addColorStop(.33,'rgba(164,237,255,.07)');
        powerPreference:'low-power'
                    bloom.addColorStop(.74,'rgba(117,198,255,.04)');
      });
                    bloom.addColorStop(1,'rgba(117,198,255,0)');
      if(!gl)throw new Error('WebGL2 context unavailable');
                    ctx.globalAlpha=fade;
      const vert=`#version 300 es
                    ctx.fillStyle=bloom;
        precision mediump float;
                    ctx.fillRect(xx-radius,yy-radius,radius*2,radius*2);
        layout(location=0) in vec4 aSpark;
                }
        uniform vec2 uSize;
                ctx.restore();
        uniform float uScale;
            }
        out float vOpacity;
            // The impact happens at ignition and again as the rupture reaches
        void main(){
            // the middle of the screen; pulses are intentionally fleeting.
          vec2 xy=aSpark.xy/uSize*2.0-1.0;
            for(const moment of (dark?[.10,.48]:[])){
          gl_Position=vec4(xy.x,-xy.y,0.0,1.0);
                const age=(progress-moment)/.125;
          gl_PointSize=aSpark.z*uScale;
                if(age<0||age>1)continue;
          vOpacity=aSpark.w;
                const intensity=(1-age)*(1-age)*fade;
        }`;
                // A single-frame-ish exposure bloom punches through
      const frag=`#version 300 es
                // the entire page; the tone is red or icy blue by direction.
        precision mediump float;
                ctx.globalCompositeOperation='screen';
        in float vOpacity;
                ctx.fillStyle=dark
        out vec4 fragColor;
                    ? 'rgba(255,81,26,'+(.095*intensity)+')'
        void main(){
                    : 'rgba(150,219,255,'+(.080*intensity)+')';
          vec2 p=gl_PointCoord*2.0-1.0;
                ctx.fillRect(0,0,w,h);
          float r=length(p);
                ctx.globalCompositeOperation='source-over';
          if(r>=1.0)discard;
                const x=edgeAt(h*.51,seconds,progress,w);
          float a=pow(1.0-r,1.65)*vOpacity;
                const y=h*.51;
          vec3 c=mix(vec3(1.0,.30,.035),
                const radius=95+age*Math.max(w,h)*.9;
            vec3(1.0,.93,.57),smoothstep(.40,.92,1.0-r));
                pressureWave(x,y,radius,intensity,dark,seconds);
          fragColor=vec4(c,a);
                ctx.globalAlpha=(dark?.20:.28)*intensity;
        }`;
                ctx.lineWidth=(dark?2.5:2)*Math.max(.4,1-age);
      function shader(type,source){
                ctx.strokeStyle=dark?'#ffb36c':'#b5eaff';
        const s=gl.createShader(type);
                ctx.beginPath();
        gl.shaderSource(s,source);gl.compileShader(s);
                for(let i=0;i<=110;i++){
        if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))
                    const a=i/110*Math.PI*2;
          throw new Error(gl.getShaderInfoLog(s));
                    const jag=1+.03*Math.sin(a*29+seconds*12)+
        return s;
                        .05*Math.sin(a*13-seconds*5);
      }
                    const rx=Math.cos(a)*radius*jag,ry=Math.sin(a)*radius*jag;
      const vs=shader(gl.VERTEX_SHADER,vert);
                    if(i===0)ctx.moveTo(x+rx,y+ry);else ctx.lineTo(x+rx,y+ry);
      const fs=shader(gl.FRAGMENT_SHADER,frag);
                }
      program=gl.createProgram();
                ctx.stroke();
      gl.attachShader(program,vs);gl.attachShader(program,fs);
                ctx.globalAlpha=1;
      gl.linkProgram(program);
                // Only combustion throws straight, fiery projectile trails.
      gl.deleteShader(vs);gl.deleteShader(fs);
                // Return-to-blue has no streaks or sparks whatsoever.
      if(!gl.getProgramParameter(program,gl.LINK_STATUS))
                if(dark){
        throw new Error(gl.getProgramInfoLog(program));
                  ctx.globalCompositeOperation='screen';
      buffer=gl.createBuffer();
                  for(const bit of shockDebris){
      gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
                    const f=age*(.5+bit.seed*.5);
      gl.bufferData(gl.ARRAY_BUFFER,170*4*4,gl.DYNAMIC_DRAW);
                    const travel=bit.velocity*f;
      gl.enableVertexAttribArray(0);
                    const px=x+Math.cos(bit.angle)*travel;
      gl.vertexAttribPointer(0,4,gl.FLOAT,false,0,0);
                    const py=y+Math.sin(bit.angle)*travel;
      gl.useProgram(program);
                    const vx=Math.cos(bit.angle)*bit.length*(1-age);
      gl.enable(gl.BLEND);
                    const vy=Math.sin(bit.angle)*bit.length*(1-age);
      gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
                    ctx.globalAlpha=(dark?.82:.68)*intensity*(.3+bit.seed*.7);
      gl.disable(gl.DEPTH_TEST);
                    ctx.strokeStyle=dark?'#ffdd9b':'#e0faff';
    }catch(err){
                    ctx.lineWidth=.6+bit.seed*1.3;
      console.warn('[KKPT] Ember-only WebGL disabled:',err);
                    ctx.beginPath();
      canvas.remove();
                    ctx.moveTo(px,py);
      return null;
                    ctx.lineTo(px-vx,py-vy);
    }
                    ctx.stroke();
    document.body.append(canvas);
                  }
    const sizeUniform=gl.getUniformLocation(program,'uSize');
                }
    const scaleUniform=gl.getUniformLocation(program,'uScale');
                ctx.globalCompositeOperation='source-over';
    const max=innerWidth<700?80:170;
                ctx.globalAlpha=1;
    const data=new Float32Array(max*4);
            }
    const sparks=[];
            // Real flame tongues break OUT of the burn front into the sky.
    let carry=0,disabled=false,lastW=0,lastH=0;
            // They are thin, tapered Bezier ribbons with an unstable pulse,
    canvas.addEventListener('webglcontextlost',event=>{
            // rather than round glowing particles or a uniform bright line.
      event.preventDefault();disabled=true;
            if(dark){
      window.__kkptRenderer='Canvas2D (GPU context lost)';
                ctx.save();
    },{once:true});
                ctx.globalCompositeOperation='screen';
    const rand=(min,max)=>min+Math.random()*(max-min);
                const count=Math.ceil(h/17);
    function draw(edge,seconds,dt,fade,progress){
                for(let j=0;j<count;j++){
      if(disabled)return;
                    const seed=rand(j*13+167);
      const w=innerWidth,h=innerHeight;
                    const y=(j+seed*.78)*11.5-5;
      const scale=w<700?.67:.75;
                    const x=edgeAt(y,seconds,progress,w);
      const cw=Math.round(w*scale),ch=Math.round(h*scale);
                    if(x < -90 || x > w+90)continue;
      if(cw!==lastW||ch!==lastH){
                    const ripple=Math.sin(seconds*(3.4+rand(j*11+4)*2.4)+
        canvas.width=cw;canvas.height=ch;
                        j*2.09+seed*3.1);
        gl.viewport(0,0,cw,ch);lastW=cw;lastH=ch;
                    const heat=clamp01(.58+ripple*.34+
      }
                        .16*Math.sin(j*1.17-seconds*7.1));
      gl.clearColor(0,0,0,0);
                    if(heat<.34)continue;
      gl.clear(gl.COLOR_BUFFER_BIT);
                    const length=(22+rand(j*23+19)*75)*(.5+heat*.94);
      gl.useProgram(program);
                    const bend=.18+rand(j*17+42)*.66;
      gl.uniform2f(sizeUniform,w,h);
                    const tipX=x-length*(.76+.19*seed);
      gl.uniform1f(scaleUniform,scale);
                    const tipY=y-length*bend;
      carry+=dt*(w<700?27:56)*Math.max(0,fade);
                    const thick=(3.2+rand(j*31+11)*10.3)*(.5+heat);
      const births=Math.min(5,Math.floor(carry));carry-=births;
                    const gradient=ctx.createLinearGradient(x,y,tipX,tipY);
      for(let i=0;i<births&&sparks.length<max;i++){
                    gradient.addColorStop(0,'rgba(255,255,242,.99)');
        const y=rand(0,h),life=rand(.55,1.65);
                    gradient.addColorStop(.28,'rgba(255,224,84,.96)');
        sparks.push({x:edge(y)+rand(-7,23),y,
                    gradient.addColorStop(.59,'rgba(255,76,10,.64)');
          vx:rand(48,265),vy:rand(-195,-26),life,left:life,
                    gradient.addColorStop(1,'rgba(182,17,6,0)');
          size:rand(3,13),alpha:rand(.24,.60)});
                    ctx.beginPath();
      }
                    ctx.moveTo(x+2,y+thick*.5);
      let count=0;
                    ctx.bezierCurveTo(
      for(let i=sparks.length-1;i>=0;i--){
                        x-length*.22,y-thick*.25,
        const p=sparks[i];
                        tipX+length*.36,tipY+thick*.68,tipX,tipY);
        p.left-=dt;
                    ctx.bezierCurveTo(
        if(p.left<=0){sparks[i]=sparks[sparks.length-1];sparks.pop();continue;}
                        tipX+length*.32,tipY-thick*.22,
        p.x+=p.vx*dt;p.y+=p.vy*dt;
                        x-length*.17,y-thick*.74,
        if(p.x < -30||p.x>w+30||p.y < -30||p.y>h+30)continue;
                        x+2,y-thick*.65);
        const offset=count++*4;
                    ctx.closePath();
        data[offset]=p.x;data[offset+1]=p.y;
                    ctx.globalAlpha=Math.min(1,heat*1.08)*fade;
        data[offset+2]=p.size;
                    ctx.fillStyle=gradient;
        data[offset+3]=p.alpha*fade*Math.min(1,p.left/.24);
                    ctx.shadowBlur=0;
      }
                    ctx.fill();
      if(!count)return;
                    if(heat>.66){
      gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
                        ctx.globalAlpha=(heat-.54)*1.40*fade;
      gl.bufferSubData(gl.ARRAY_BUFFER,0,data.subarray(0,count*4));
                        ctx.strokeStyle='#fffce0';ctx.lineWidth=.9+heat*1.1;
      gl.drawArrays(gl.POINTS,0,count);
                        ctx.beginPath();ctx.moveTo(x,y);
    }
                        ctx.quadraticCurveTo(x-length*.3,y-length*.11,
    function dispose(){
                            tipX+length*.19,tipY+thick*.18);
      disabled=true;
                        ctx.stroke();
      if(!gl.isContextLost()){
                    }
        gl.deleteBuffer(buffer);gl.deleteProgram(program);
                }
      }
                ctx.restore();
      canvas.remove();
            }
    }
 
    return {canvas,draw,dispose};
            // Carbonized paper has a fine, ragged edge. The charcoal layer
  }
            // persists continuously, but the white-hot core does NOT.
  function clipFrame(edge) {
            ctx.save();
    const w=innerWidth,h=innerHeight;
            ctx.beginPath();
    const rows=window.__kkptVisualDark?
            for(let y=-12,i=0;y<=h+12;y+=3.6,i++){
      Math.max(24,Math.ceil(h/30)):
                const x=edgeAt(y,seconds,progress,w);
      Math.max(16,Math.ceil(h/61));
                if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
    const pts=[];
            }
    for(let i=rows;i>=0;i--) {
            ctx.lineCap='round';ctx.lineJoin='round';
      const y=h*i/rows;
            if(dark){
      pts.push(edge(y).toFixed(1)+'px '+y.toFixed(1)+'px');
                ctx.strokeStyle='rgba(9,5,7,.81)';
    }
                ctx.lineWidth=18;ctx.shadowBlur=0;
    frame.style.clipPath=clipSide==='right'
                ctx.stroke();
      ? 'polygon(100% 0,100% 100%,'+pts.join(',')+')'
                ctx.strokeStyle='rgba(210,45,9,.53)';
      : 'polygon(0 0,0 100%,'+pts.join(',')+')';
                ctx.lineWidth=6;
  }
                ctx.stroke();
  function canvasFire(canvas,dark,duration,drawAtmosphere,done,gpuEmbers,score,extraVisuals) {
            }else{
    const ctx=dark&&canvas
                ctx.strokeStyle='rgba(27,95,190,.22)';
      ? canvas.getContext('2d',{alpha:true,desynchronized:true})
                ctx.lineWidth=27;ctx.shadowColor='#56bdff';ctx.shadowBlur=17;
      : null;
                ctx.stroke();
    if(dark&&!ctx){done();return;}
                ctx.strokeStyle='rgba(132,225,255,.51)';
    const art=dark?makeSprites():null;
                ctx.lineWidth=11;ctx.shadowColor='#8aeaff';ctx.shadowBlur=11;
    const w=innerWidth,h=innerHeight;
                ctx.stroke();
    if(ctx){
                ctx.strokeStyle='rgba(249,255,255,.65)';
      const scale=Math.min(window.devicePixelRatio||1,w<650?.69:.80);
                ctx.lineWidth=2.7;ctx.shadowBlur=4;
      canvas.width=Math.max(1,Math.round(w*scale));
                ctx.stroke();
      canvas.height=Math.max(1,Math.round(h*scale));
            }
      ctx.setTransform(scale,0,0,scale,0,0);
            ctx.restore();
    }
            if(dark){
    const rand=(a,b)=>a+Math.random()*(b-a);
                // Intermittent ignition cells: varying width/color/intensity
    const mobile=w<650;
                // make the bright edge resemble burning fibres, not neon.
    const maxCount=mobile?160:250;
                ctx.save();
    const p=[];
                ctx.globalCompositeOperation='screen';
    const accum={flame:0,ember:0,shard:0,sky:0,mist:0,pollen:0};
                ctx.lineCap='round';
    const rates=dark
                for(let y=-8;y<h+8;y+=5.8){
      ? {flame:mobile?80:105,ember:mobile?72:96,
                    const intensity=clamp01(.58+
        shard:mobile?26:42}
                        .29*Math.sin(y*.054+seconds*3.7)+
      : {sky:mobile?50:80,mist:mobile?13:20,pollen:mobile?27:50};
                        .21*Math.sin(y*.147-seconds*8.9)+
    const started=performance.now();
                        .13*Math.sin(y*.337+seconds*16.4));
    let previous=started;
                    if(intensity<.47)continue;
    let lastPaint=0;
                    const x=edgeAt(y,seconds,progress,w);
    function spawn(kind,edge,seconds) {
                    const nextX=edgeAt(y+5.8,seconds,progress,w);
      if(p.length>=maxCount)return;
                    ctx.globalAlpha=intensity*fade;
      const y=rand(-25,h+25);
                    ctx.lineWidth=1.4+intensity*4.6;
      const x=edge(y);
                    ctx.shadowBlur=0;
      const type=kind;
                    ctx.strokeStyle=intensity>.75?'#fff8d7':
      const smoke=type==='smoke';
                        intensity>.58?'#ffd369':'#ff831b';
      const shard=type==='shard';
                    ctx.beginPath();ctx.moveTo(x,y);
      const ember=type==='ember'||type==='pollen';
                    ctx.lineTo(nextX,y+5.8);ctx.stroke();
      const blue=type==='sky'||type==='mist'||type==='pollen';
                }
      let radius,life,vx,vy;
                ctx.restore();
      if(shard){
                // Fragmented ignition on real microscopic perforations.
        radius=rand(4,18);life=rand(.6,1.65);
                // Missing arc sections stop them resembling punched circles.
        vx=rand(-130,65);vy=rand(-125,30);
                ctx.save();
      }else if(smoke){
                ctx.globalCompositeOperation='screen';
        radius=rand(32,85);life=rand(1.1,2.5);
                ctx.shadowBlur=0;
        vx=rand(-20,65);vy=rand(-88,-22);
                for (let i=0; i<cells.length; i++) {
      }else if(type==='mist'){
                    const c=cells[i];
        radius=rand(45,100);life=rand(.9,1.8);
                    ctx.globalAlpha=(.42+.34*c.heat)*fade;
        vx=rand(-35,45);vy=rand(-75,-15);
                    ctx.strokeStyle=c.heat>.73?'#fff6bf':
      }else if(type==='flame'){
                        c.heat>.38?'#ffd166':'#ff621c';
        radius=rand(20,57);life=rand(.3,.82);
                    ctx.lineWidth=.8+Math.min(2.6,c.radius*.24);
        vx=rand(-100,30);vy=rand(-180,-45);
                    const n=c.points.length;
      }else{
                    ctx.beginPath();
        radius=ember?rand(2.5,9):rand(18,47);
                    for (let j=0;j<n;j++) {
        life=ember?rand(.45,1.1):rand(.35,.85);
                        if ((j+i*3)%5>2)continue;
        vx=rand(-95,75);vy=rand(-150,-25);
                        const a=c.points[j],b=c.points[(j+1)%n];
      }
                        ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);
      p.push({
                    }
        type,life,left:life,
                    ctx.stroke();
        x:x+rand(smoke?12:shard?-35:-27,smoke?140:shard?36:64),
                }
        y:y+rand(-14,14),vx,vy,radius,
                ctx.restore();
        angle:rand(-Math.PI,Math.PI),spin:rand(-5,5),
                // Small carbon flecks stick to the still-blue paper just
        drift:rand(-.65,.65),blue
                // ahead of the bite; no large rectangular glitch debris.
      });
                ctx.save();
    }
                for(let j=0;j<Math.ceil(h/10);j++){
    function spawnAtRate(dt,edge,seconds,strength){
                    const y=(j+rand(j*5+61)*.8)*10;
      for(const type in rates){
                    const x=edgeAt(y,seconds,progress,w)-
        accum[type]+=rates[type]*dt*strength;
                        (5+rand(j*17+38)*24);
        const n=Math.min(11,Math.floor(accum[type]));
                    const size=.5+rand(j*29+11)*2.5;
        accum[type]-=n;
                    const visible=.25+.59*rand(j*13+7);
        for(let i=0;i<n;i++)spawn(type,edge,seconds);
                    ctx.globalAlpha=visible*fade;
      }
                    ctx.fillStyle='#150b0a';
                    ctx.beginPath();
                    ctx.ellipse(x,y,size*.71,size,rand(j*7+31)*3.14,
                        0,Math.PI*2);
                    ctx.fill();
                }
                ctx.restore();
            }
            // The key property V3-V6 lost: particles remain at their
            // birth position while the front advances. A wide, incandescent
            // WAKE builds behind the tearing page instead of following
            // it as one thin, plastic-looking stripe.
            ctx.save();
            ctx.globalCompositeOperation='lighter';
            for(const p of combustion){
                if(p.kind==='ash'||p.kind==='leaf'||p.kind==='pollen')continue;
                const age=1-p.remaining/p.life;
                const opacity=clamp01(p.remaining/.22)*
                    clamp01(age/.08)*(1-age*.30)*
                    (p.kind==='ember'?.74:p.kind==='glow'?.13:.66)*fade*
                    (dark?Math.exp(-Math.max(0,p.x-
                        edgeAt(p.y,seconds,progress,w))/70):1);
                if(opacity<.012)continue;
                ctx.save();
                ctx.globalAlpha=opacity;
                ctx.translate(p.x,p.y);
                ctx.rotate(p.angle+Math.sin(elapsed*4+p.seed)*.13);
                const large=p.kind==='flame';
                const sx=p.radius*(p.kind==='glow'?1.7:large?.96:1.32)*(1+age*.32);
                const sy=p.radius*(p.kind==='glow'?1.4:large?1.78:1.35)*(1+age*.57);
                const sprite=p.kind==='flame'?FIRE_GLOW[p.sprite]:
                    p.kind==='glow'?BLUE_GLOW[p.sprite]:EMBER_GLOW;
                ctx.drawImage(sprite,-sx,-sy,sx*2,sy*2);
                ctx.restore();
            }
            ctx.restore();
            // Rebirth uses soft, rotating, organic leaf/petal silhouettes,
            // minute circular pollen and spacious white-blue bokeh. No
            // rectangular ions, no warm textures and NO glowing streaks.
            if(!dark){
                ctx.save();
                ctx.globalCompositeOperation='screen';
                for(const p of combustion){
                    if(p.kind!=='leaf'&&p.kind!=='pollen')continue;
                    const age=1-p.remaining/p.life;
                    const opacity=clamp01(age/.17)*clamp01(p.remaining/.55)*
                        (p.kind==='leaf'?.47:.28)*fade;
                    if(opacity<.012)continue;
                    ctx.save();
                    ctx.globalAlpha=opacity;
                    ctx.translate(p.x+11*Math.sin(elapsed*1.6+p.seed),p.y);
                    ctx.rotate(p.angle+age*1.65*Math.sin(p.seed*2.5));
                    if(p.kind==='leaf'){
                        const r=p.radius;
                        ctx.shadowBlur=5;
                        ctx.shadowColor='#bbf4ff';
                        ctx.fillStyle=p.sprite%2?'#e6ffff':'#ffffff';
                        ctx.beginPath();
                        ctx.moveTo(0,-r*1.65);
                        ctx.bezierCurveTo(r*.97,-r*.48,r*.85,r*.60,0,r*1.35);
                        ctx.bezierCurveTo(-r*.92,r*.28,-r*.94,-r*.57,0,-r*1.65);
                        ctx.fill();
                        ctx.globalAlpha=opacity*.38;
                        ctx.strokeStyle='#70c9ea';
                        ctx.lineWidth=.65;
                        ctx.beginPath();ctx.moveTo(0,-r*1.15);
                        ctx.quadraticCurveTo(-r*.17,0,0,r*.95);ctx.stroke();
                    }else{
                        const r=p.radius;
                        // Reuse the cached soft sprite; a fresh radial
                        // gradient for every pollen mote per frame was costly.
                        ctx.globalAlpha=opacity*.72;
                        ctx.drawImage(BLUE_GLOW[p.sprite],-r*3,-r*3,r*6,r*6);
                    }
                    ctx.restore();
                }
                ctx.restore();
            }
            // Minute black flecks, never rectangular shards on the blue side.
            ctx.save();
            ctx.globalCompositeOperation='source-over';
            for(const p of combustion){
                if(p.kind!=='ash')continue;
                ctx.globalAlpha=clamp01(p.remaining/.25)*.7*fade;
                ctx.fillStyle='#190d10';
                ctx.save();ctx.translate(p.x,p.y);ctx.rotate(elapsed*2+p.angle);
                ctx.fillRect(-p.radius*.5,-p.radius*.5,p.radius,p.radius*1.8);
                ctx.restore();
            }
            ctx.restore();
            // Irregular burnt page sections tumble away and reveal their
            // blackened underside, with an occasional incandescent rim.
            if(dark){
                ctx.save();
                ctx.globalCompositeOperation='source-over';
                for(const p of tornPaper){
                    const age=1-p.remaining/p.life;
                    const opacity=clamp01(age/.08)*clamp01(p.remaining/.4)*fade;
                    if(opacity<.01)continue;
                    ctx.save();
                    ctx.globalAlpha=opacity;
                    ctx.translate(p.x,p.y);
                    ctx.rotate(p.angle);
                    ctx.beginPath();
                    p.outline.forEach((v,i)=>{
                        const px=Math.cos(v.a)*v.r*p.radius;
                        const py=Math.sin(v.a)*v.r*p.radius*.74;
                        if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);
                    });
                    ctx.closePath();
                    ctx.shadowBlur=7;
                    ctx.shadowColor='#090506';
                    ctx.fillStyle=p.seed>3?'#190d11':'#302025';
                    ctx.fill();
                    ctx.shadowBlur=0;
                    ctx.lineWidth=.8+Math.sin(p.seed)**2*.8;
                    ctx.strokeStyle=age<.48?'rgba(255,107,35,.48)':
                        'rgba(49,20,19,.47)';
                    ctx.stroke();
                    // Thin blue-white unburnt fibers still visible on a
                    // few ripped faces, before the back chars completely.
                    if(p.seed>4.5&&age<.52){
                        ctx.globalAlpha*=.45*(1-age);
                        ctx.strokeStyle='#c5e7f7';
                        ctx.beginPath();
                        ctx.moveTo(-p.radius*.42,-p.radius*.16);
                        ctx.quadraticCurveTo(0,p.radius*.25,p.radius*.28,0);
                        ctx.stroke();
                    }
                    ctx.restore();
                }
                ctx.restore();
            }
            // Sharp, directional embers / crystalline fragments, not blobs.
            if(dark){ctx.save();
            ctx.globalCompositeOperation='screen';
            for(const mote of motes){
                const age=(elapsed*.74+mote.phase)%1;
                const yy=mote.y*h-age*mote.speed+Math.sin(age*12+mote.phase*8)*9;
                const x=edgeAt(yy,seconds,progress,w)+direction*age*mote.drift;
                if(x<-25||x>w+25||yy<-25||yy>h+25)continue;
                const opacity=(1-age)*fade*.89;
                ctx.globalAlpha=opacity;
                ctx.strokeStyle='#ffb45d';
                ctx.lineWidth=.8;
                ctx.beginPath();
                ctx.moveTo(x,yy);
                ctx.lineTo(x-direction*mote.length*.4,yy+mote.length);
                ctx.stroke();
            }
            ctx.restore();}
        };
     }
     }
 
     function advance(dt){
    let atmosphereWarmPromise=null;
      for(let i=p.length-1;i>=0;i--) {
     function prewarmAtmosphere() {
        const q=p[i];
        if(atmosphereWarmPromise)return atmosphereWarmPromise;
        q.left-=dt;
        atmosphereWarmPromise=new Promise(resolve=>{
        if(q.left<=0){p[i]=p[p.length-1];p.pop();continue;}
            const scratchCanvas=document.createElement('canvas');
        q.x+=q.vx*dt;
            function step(){
        q.y+=q.vy*dt;
                if(atmosphereAssetCache?.EMBER_GLOW){resolve();return;}
        q.vx+=q.drift*dt*35;
                // Exactly one texture per task: no more 270ms blocking call.
        q.vy-=10*dt;
                createAtmosphere(scratchCanvas,true);
         q.angle+=q.spin*dt;
                if(atmosphereAssetCache?.EMBER_GLOW){resolve();return;}
      }
                setTimeout(step,24);
            }
            setTimeout(step,40);
         });
        return atmosphereWarmPromise;
     }
     }
 
     function spriteDraw(image,x,y,sx,sy,alpha){
    let atmosphereEdge=null;
      ctx.globalAlpha=alpha;
     function edgeAt(y,seconds,progress,width){
      ctx.drawImage(image,x-sx,y-sy,sx*2,sy*2);
        return atmosphereEdge
            ? atmosphereEdge(y)
            : makeEdge(progress,seconds,Boolean(window.__kkptVisualDark),width)(y);
     }
     }
 
     function drawSmoke(fade){
     // Original V2's moving flame sprites and three bright contours, rebuilt
      ctx.globalCompositeOperation='source-over';
    // with bounded particle counts. No WebGL, no full-screen noise pass,
      for(const q of p){
    // no 1,000-point polygon or per-frame image-texture upload.
         if(q.type!=='smoke'&&q.type!=='mist')continue;
    let sprites=null;
        const age=1-q.left/q.life;
    function makeSprites() {
        const radius=q.radius*(1+age*.98);
         if (sprites) return sprites;
         const alpha=(q.type==='smoke'?.76:.32)*
        function radial(stops) {
          Math.min(1,age*5)*Math.min(1,q.left/.55)*fade;
            const el=document.createElement('canvas');
        spriteDraw(q.type==='smoke'?art.smoke:art.mist,
            el.width=el.height=96;
          q.x,q.y,radius*1.7,radius*1.3,alpha);
            const c=el.getContext('2d');
      }
            const g=c.createRadialGradient(48,48,0,48,48,48);
      ctx.globalAlpha=1;
            stops.forEach(pair=>g.addColorStop(pair[0],pair[1]));
            c.fillStyle=g;c.fillRect(0,0,96,96);
            return el;
         }
        sprites={
            flame:radial([[0,'#fffbed'],[.18,'#fff2a0'],
                [.43,'#ffb42b'],[.70,'#ff360088'],[1,'#8f050000']]),
            ember:radial([[0,'#ffffff'],[.15,'#fff6a8'],
                [.44,'#ff9900cc'],[1,'#ef280000']]),
            smoke:radial([[0,'#080608dd'],[.30,'#100b10bb'],
                [.62,'#160f1677'],[1,'#08050800']]),
            sky:radial([[0,'#ffffff'],[.16,'#e6ffff'],
                [.46,'#94deffe0'],[1,'#4ab5ff00']]),
            mist:radial([[0,'#f2fdffb0'],[.4,'#abdffff0'],
                [1,'#a9e7ff00']])
        };
        return sprites;
     }
     }
 
    function drawShards(fade) {
    // V2's simple advancing boundary. The reader asked for the
      if(!dark)return;
    // vibrant fire curtain, not a mathematically fractal tearing wire.
      ctx.save();
    function makeEdge(t,seconds,dark,w) {
      ctx.globalCompositeOperation='source-over';
         const center=dark
      for(const q of p){
            ? w+105-(w+210)*smooth(t)
        if(q.type!=='shard')continue;
            : -105+(w+210)*smooth(t);
         const age=1-q.left/q.life;
         const wobble=Math.sin(Math.PI*t);
        const a=Math.min(1,q.left/.38)*(.90-age*.28)*fade;
         return y=>center+wobble*(
        ctx.save();
            17*Math.sin(y*.023+seconds*5.5)+
        ctx.translate(q.x,q.y);
            9*Math.sin(y*.063-seconds*8.4)+
         ctx.rotate(q.angle);
            4*Math.sin(y*.13+seconds*12)
         ctx.globalAlpha=a;
         );
        const r=q.radius;
        ctx.beginPath();
        ctx.moveTo(-r*.9,-r*.3);
        ctx.lineTo(r*.7,-r*.75);
        ctx.lineTo(r*.48,r*.42);
        ctx.lineTo(-r*.35,r*.8);
        ctx.closePath();
        ctx.fillStyle=age<.3?'#371d22':'#140d13';
        ctx.fill();
        ctx.strokeStyle=age<.42?'#f57d3c':'#5a2c2b';
        ctx.lineWidth=age<.42?1.6:.75;
        ctx.stroke();
         ctx.restore();
      }
      ctx.restore();
     }
     }
 
     function drawCore(edge,seconds,t,fade){
     // GPU-only ember flecks: ONE small draw call at most 30fps.
      const waviness=Math.sin(Math.PI*t);
    // The white-hot V25 fire, the smoke and the dawn stay unmodified 2D.
      ctx.save();
    // No fullscreen pixel shader, FBM, flame quads or cyan rectangle.
      ctx.beginPath();
    function createGpuEmbers(){
      const count=Math.max(35,Math.ceil(h/19));
        const canvas=document.createElement('canvas');
      for(let i=0;i<=count;i++){
        canvas.id='kkpt-webgl';
        const y=h*i/count;
        canvas.setAttribute('aria-hidden','true');
        if(i===0)ctx.moveTo(edge(y),y);
        let gl,program,buffer;
        else ctx.lineTo(edge(y),y);
        try{
      }
            gl=canvas.getContext('webgl2',{
      ctx.lineCap='round';
                alpha:true,antialias:false,depth:false,stencil:false,
      ctx.lineJoin='round';
                premultipliedAlpha:false,preserveDrawingBuffer:false,
      if(dark){
                powerPreference:'low-power'
        ctx.globalCompositeOperation='lighter';
            });
        ctx.globalAlpha=fade;
            if(!gl)throw new Error('WebGL2 context unavailable');
        ctx.shadowColor='#ff3903';
            const vert=`#version 300 es
        ctx.shadowBlur=25;
                precision mediump float;
        ctx.strokeStyle='rgba(255,55,5,.82)';
                layout(location=0) in vec4 aSpark;
        ctx.lineWidth=43;
                uniform vec2 uSize;
        ctx.stroke();
                uniform float uScale;
        ctx.shadowBlur=16;
                out float vOpacity;
        ctx.strokeStyle='#ff9b17';
                void main(){
        ctx.lineWidth=20;
                    vec2 xy=aSpark.xy/uSize*2.0-1.0;
        ctx.stroke();
                    gl_Position=vec4(xy.x,-xy.y,0.0,1.0);
        ctx.shadowBlur=7;
                    gl_PointSize=aSpark.z*uScale;
        ctx.strokeStyle='#ffe45e';
                    vOpacity=aSpark.w;
        ctx.lineWidth=9;
                }`;
        ctx.stroke();
            const frag=`#version 300 es
        ctx.shadowBlur=4;
                precision mediump float;
        ctx.strokeStyle='#fffcde';
                in float vOpacity;
        ctx.lineWidth=3.8;
                out vec4 fragColor;
        ctx.stroke();
                void main(){
      }else{
                    vec2 p=gl_PointCoord*2.0-1.0;
        ctx.globalAlpha=fade*.64;
                    float r=length(p);
        ctx.shadowColor='#8ccfff';
                    if(r>=1.0)discard;
        ctx.shadowBlur=29;
                    float a=pow(1.0-r,1.65)*vOpacity;
        ctx.strokeStyle='rgba(139,219,255,.68)';
                    vec3 c=mix(vec3(1.0,.30,.035),
        ctx.lineWidth=28;
                        vec3(1.0,.93,.57),smoothstep(.40,.92,1.0-r));
        ctx.stroke();
                    fragColor=vec4(c,a);
        ctx.shadowBlur=10;
                }`;
        ctx.strokeStyle='#e7f8ff';
            function shader(type,source){
        ctx.lineWidth=9;
                const s=gl.createShader(type);
        ctx.stroke();
                gl.shaderSource(s,source);gl.compileShader(s);
        ctx.shadowBlur=0;
                if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))
        ctx.strokeStyle='white';
                    throw new Error(gl.getShaderInfoLog(s));
        ctx.lineWidth=2;
                return s;
        ctx.stroke();
            }
      }
            const vs=shader(gl.VERTEX_SHADER,vert);
      ctx.restore();
            const fs=shader(gl.FRAGMENT_SHADER,frag);
      if(dark){
            program=gl.createProgram();
        ctx.save();
            gl.attachShader(program,vs);gl.attachShader(program,fs);
        ctx.globalCompositeOperation='lighter';
            gl.linkProgram(program);
        for(let y=-70;y<h+80;y+=48){
            gl.deleteShader(vs);gl.deleteShader(fs);
          const x=edge(y);
            if(!gl.getProgramParameter(program,gl.LINK_STATUS))
          const pulse=.65+.35*Math.sin(seconds*8.5+y*.083);
                throw new Error(gl.getProgramInfoLog(program));
          spriteDraw(art.flame,x+rand(-8,30),y,77,70,
            buffer=gl.createBuffer();
             fade*(.26+.16*pulse));
            gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
            gl.bufferData(gl.ARRAY_BUFFER,170*4*4,gl.DYNAMIC_DRAW);
            gl.enableVertexAttribArray(0);
            gl.vertexAttribPointer(0,4,gl.FLOAT,false,0,0);
            gl.useProgram(program);
            gl.enable(gl.BLEND);
            gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
            gl.disable(gl.DEPTH_TEST);
        }catch(err){
            console.warn('[KKPT] Ember-only WebGL disabled:',err);
             canvas.remove();
            return null;
         }
         }
         document.body.append(canvas);
         for(let y=-20;y<h+52;y+=36){
        const sizeUniform=gl.getUniformLocation(program,'uSize');
          const x=edge(y);
        const scaleUniform=gl.getUniformLocation(program,'uScale');
          const flicker=Math.sin(seconds*13+y*.19);
        const max=innerWidth<700?80:170;
          const length=52+30*flicker+17*Math.sin(seconds*6-y*.11);
        const data=new Float32Array(max*4);
          const lean=15+18*Math.sin(seconds*9+y*.069);
        const sparks=[];
          ctx.globalAlpha=.62*fade;
        let carry=0,disabled=false,lastW=0,lastH=0;
          ctx.fillStyle='#ff5705';
        canvas.addEventListener('webglcontextlost',event=>{
          ctx.beginPath();
            event.preventDefault();disabled=true;
          ctx.moveTo(x+18,y+19);
            window.__kkptRenderer='Canvas2D (GPU context lost)';
          ctx.bezierCurveTo(x-18,y+2,x-lean-24,y-length*.57,
        },{once:true});
             x-lean,y-length);
        const rand=(min,max)=>min+Math.random()*(max-min);
          ctx.bezierCurveTo(x+2,y-length*.57,x+13,y-12,x+18,y+19);
        function draw(edge,seconds,dt,fade,progress){
          ctx.fill();
             if(disabled)return;
          ctx.globalAlpha=.58*fade;
            const w=innerWidth,h=innerHeight;
          ctx.fillStyle='#ffdb58';
            const scale=w<700?.67:.75;
          ctx.beginPath();
            const cw=Math.round(w*scale),ch=Math.round(h*scale);
          ctx.moveTo(x+7,y+8);
            if(cw!==lastW||ch!==lastH){
          ctx.quadraticCurveTo(x-10,y-length*.33,
                canvas.width=cw;canvas.height=ch;
             x-lean*.8,y-length*.72);
                gl.viewport(0,0,cw,ch);lastW=cw;lastH=ch;
          ctx.quadraticCurveTo(x+3,y-length*.30,x+8,y+8);
            }
          ctx.fill();
            gl.clearColor(0,0,0,0);
          ctx.globalAlpha=.38*fade;
            gl.clear(gl.COLOR_BUFFER_BIT);
          ctx.fillStyle='#fffced';
            gl.useProgram(program);
          ctx.beginPath();
            gl.uniform2f(sizeUniform,w,h);
          ctx.moveTo(x+3,y+5);
            gl.uniform1f(scaleUniform,scale);
          ctx.quadraticCurveTo(x-5,y-length*.2,
            carry+=dt*(w<700?27:56)*Math.max(0,fade);
            x-lean*.5,y-length*.48);
             const births=Math.min(5,Math.floor(carry));carry-=births;
          ctx.quadraticCurveTo(x+4,y-length*.19,x+5,y+5);
            for(let i=0;i<births&&sparks.length<max;i++){
          ctx.fill();
                const y=rand(0,h),life=rand(.55,1.65);
                sparks.push({x:edge(y)+rand(-7,23),y,
                    vx:rand(48,265),vy:rand(-195,-26),life,left:life,
                    size:rand(3,13),alpha:rand(.24,.60)});
            }
            let count=0;
            for(let i=sparks.length-1;i>=0;i--){
                const p=sparks[i];
                p.left-=dt;
                if(p.left<=0){sparks[i]=sparks[sparks.length-1];sparks.pop();continue;}
                p.x+=p.vx*dt;p.y+=p.vy*dt;
                if(p.x < -30||p.x>w+30||p.y < -30||p.y>h+30)continue;
                const offset=count++*4;
                data[offset]=p.x;data[offset+1]=p.y;
                data[offset+2]=p.size;
                data[offset+3]=p.alpha*fade*Math.min(1,p.left/.24);
            }
            if(!count)return;
            gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
            gl.bufferSubData(gl.ARRAY_BUFFER,0,data.subarray(0,count*4));
            gl.drawArrays(gl.POINTS,0,count);
         }
         }
         function dispose(){
         ctx.restore();
            disabled=true;
      }else{
            if(!gl.isContextLost()){
        ctx.save();
                gl.deleteBuffer(buffer);gl.deleteProgram(program);
        ctx.globalCompositeOperation='lighter';
            }
        for(let y=-50;y<h+55;y+=88){
            canvas.remove();
          spriteDraw(art.sky,edge(y),y,89,109,.27*fade);
         }
         }
         return {canvas,draw,dispose};
         ctx.restore();
      }
     }
     }
 
    function drawBrightParticles(fade) {
    // Clip the ACTUAL preloaded Wiki frame, precisely where the fire is.
      ctx.save();
    function clipFrame(edge) {
      ctx.globalCompositeOperation='lighter';
         const w=innerWidth,h=innerHeight;
      for(const q of p){
         const rows=window.__kkptVisualDark?
        if(q.type==='smoke'||q.type==='mist'||q.type==='shard')continue;
            Math.max(24,Math.ceil(h/30)):
        const age=1-q.left/q.life;
            Math.max(16,Math.ceil(h/61));
        const brightness=Math.min(1,q.left/.25)*(1-age*.25)*fade;
        const pts=[];
        let artName='flame',sx=q.radius,sy=q.radius;
        for(let i=rows;i>=0;i--) {
        if(q.type==='ember'){artName='ember';sx*=1.2;sy*=1.2;}
            const y=h*i/rows;
        else if(q.type==='pollen'){artName='sky';sx*=1.15;sy*=1.15;}
            pts.push(edge(y).toFixed(1)+'px '+y.toFixed(1)+'px');
         else if(q.type==='sky'){artName='sky';sx*=1.1;sy*=1.55;}
         }
         else {sx*=.9;sy*=1.65;}
         frame.style.clipPath=clipSide==='right'
        spriteDraw(art[artName],q.x,q.y,sx,sy,
            ? 'polygon(100% 0,100% 100%,'+pts.join(',')+')'
          brightness*(q.type==='ember'?.93:.78));
            : 'polygon(0 0,0 100%,'+pts.join(',')+')';
      }
      ctx.restore();
      ctx.globalAlpha=1;
      ctx.globalCompositeOperation='source-over';
    }
    function paint(now) {
      const t=score
        ? Math.max(0,Math.min(1,(score.audio.currentTime-
          score.config.fireStart)/
          (score.config.fireEnd-score.config.fireStart)))
        : Math.min(1,(now-started)/duration);
      extraVisuals?.update(t);
      const seconds=now/1000;
      const edge=makeEdge(t,seconds,dark,w);
      const paintStep=dark?(mobile?35:32):(mobile?52:41);
      if(now-lastPaint<paintStep && t<1){
        requestAnimationFrame(paint);return;
      }
      atmosphereEdge=edge;
      window.__kkptVisualDark=dark;
      clipFrame(edge);
      const dt=Math.min(.070,(now-previous)/1000);
      previous=now;lastPaint=now;
      const fade=Math.min(1,t/.055,(1-t)/.045);
      drawAtmosphere(seconds,Math.min(t,.925),dark,(now-started)/1000,[]);
      if(dark&&gpuEmbers)gpuEmbers.draw(edge,seconds,dt,fade,t);
      if(dark){
         advance(dt);
         if(t<1)spawnAtRate(dt,edge,seconds,Math.max(0,fade));
        ctx.clearRect(0,0,w,h);
        drawShards(fade);
        drawCore(edge,seconds,t,fade);
        drawBrightParticles(fade);
      }
      if(t<1)requestAnimationFrame(paint);
      else done();
     }
     }
 
     requestAnimationFrame(paint);
     function canvasFire(canvas,dark,duration,drawAtmosphere,done,gpuEmbers,score,extraVisuals) {
  }
        const ctx=dark&&canvas
  function setDisplayed(nextActive) {
            ? canvas.getContext('2d',{alpha:true,desynchronized:true})
    active=nextActive;
            : null;
    frame.classList.toggle('kkpt-active',active);
        if(dark&&!ctx){done();return;}
    document.body.classList.toggle('kkpt-other-active',active);
        const art=dark?makeSprites():null;
    frame.style.clipPath=active?'inset(0)':hideClip;
        const w=innerWidth,h=innerHeight;
    frame.style.visibility=active?'visible':'hidden';
        if(ctx){
    frame.style.pointerEvents='';
            const scale=Math.min(window.devicePixelRatio||1,w<650?.69:.80);
    frame.tabIndex=active?0:-1;
            canvas.width=Math.max(1,Math.round(w*scale));
    frame.setAttribute('aria-hidden',active?'false':'true');
            canvas.height=Math.max(1,Math.round(h*scale));
    document.title=active?otherTitle:baseTitle;
            ctx.setTransform(scale,0,0,scale,0,0);
    showTheme((active?otherPage:basePage)==='Kuronata');
        }
    root.classList.toggle('kkpt-world-suspended',basePage==='Kuronata'&&active);
 
    try{
        const rand=(a,b)=>a+Math.random()*(b-a);
      frame.contentDocument?.documentElement.classList.toggle(
        const mobile=w<650;
        'kkpt-frame-inactive',!active);
        const maxCount=mobile?160:250;
    }catch(_){}
        const p=[];
    updateButton();
        const accum={flame:0,ember:0,shard:0,sky:0,mist:0,pollen:0};
  }
        const rates=dark
  async function animateSwap(nextActive) {
            ? {flame:mobile?80:105,ember:mobile?72:96,
    if(animating)return;
              shard:mobile?26:42}
    animating=true;
            : {sky:mobile?50:80,mist:mobile?13:20,pollen:mobile?27:50};
    button.classList.add('kkpt-busy');
        const started=performance.now();
    labelSub.textContent='PREPARING THE OTHER SIDE...';
        let previous=started;
    const dark=(nextActive?otherPage:basePage)==='Kuronata';
        let lastPaint=0;
    let score=returnScore?.config?.target===otherPage?
        let lastClip=0;
      returnScore:null;
        function spawn(kind,edge,seconds) {
    if(!loading)warmFrame();
            if(p.length>=maxCount)return;
    const preparation=Promise.all([
            const y=rand(-25,h+25);
      Promise.race([loading,sleep(score?350:1100)]),
            const x=edge(y);
      score?prewarmAtmosphere():
            const type=kind;
        Promise.race([prewarmAtmosphere(),sleep(160)])
            const smoke=type==='smoke';
    ]);
            const shard=type==='shard';
    if(score){
            const ember=type==='ember'||type==='pollen';
      const started=await Promise.race([
            const blue=type==='sky'||type==='mist'||type==='pollen';
        score.playPromise,sleep(6500).then(()=>false)
            let radius,life,vx,vy;
      ]);
            if(shard){
      const onCue=started ? await Promise.all([
                radius=rand(4,18);life=rand(.6,1.65);
        preparation,awaitScoreTime(score,score.config.fireStart)
                vx=rand(-130,65);vy=rand(-125,30);
      ]).then(([,hit])=>hit) : false;
            }else if(smoke){
      if(!onCue){
                radius=rand(32,85);life=rand(1.1,2.5);
        await preparation;
                vx=rand(-20,65);vy=rand(-88,-22);
        clearReturnScore(score);
            }else if(type==='mist'){
        score=null; // Preserve the exact V25 visuals on audio failure.
                radius=rand(45,100);life=rand(.9,1.8);
      }
                vx=rand(-35,45);vy=rand(-75,-15);
    } else await preparation;
            }else if(type==='flame'){
    removeScoreOmen();
                radius=rand(20,57);life=rand(.3,.82);
    frame.style.visibility='visible';
                vx=rand(-100,30);vy=rand(-180,-45);
    frame.classList.add('kkpt-animating');
            }else{
    await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
                radius=ember?rand(2.5,9):rand(18,47);
    labelSub.textContent=dark?'THE SKY IS BURNING':'DAWN IS BREAKING';
                life=ember?rand(.45,1.1):rand(.35,.85);
    if(!dark)showTheme(false);
                vx=rand(-95,75);vy=rand(-150,-25);
    frame.classList.remove('kkpt-active');
            }
    frame.style.pointerEvents='none';
            p.push({
    frame.style.clipPath=active?'inset(0)':hideClip;
                type,life,left:life,
    const atmosphere=document.createElement('canvas');
                x:x+rand(smoke?12:shard?-35:-27,smoke?140:shard?36:64),
    atmosphere.id='kkpt-atmosphere';
                y:y+rand(-14,14),vx,vy,radius,
    atmosphere.setAttribute('aria-hidden','true');
                angle:rand(-Math.PI,Math.PI),spin:rand(-5,5),
    document.body.append(atmosphere);
                drift:rand(-.65,.65),blue
    const drawAtmosphere=createAtmosphere(atmosphere);
            });
    const canvas=dark?document.createElement('canvas'):null;
        }
    if(canvas){
        function spawnAtRate(dt,edge,seconds,strength){
      canvas.id='kkpt-vfx';
            for(const type in rates){
      canvas.setAttribute('aria-hidden','true');
                accum[type]+=rates[type]*dt*strength;
      document.body.append(canvas);
                const n=Math.min(11,Math.floor(accum[type]));
                accum[type]-=n;
                for(let i=0;i<n;i++)spawn(type,edge,seconds);
            }
        }
        function advance(dt){
            for(let i=p.length-1;i>=0;i--) {
                const q=p[i];
                q.left-=dt;
                if(q.left<=0){p[i]=p[p.length-1];p.pop();continue;}
                q.x+=q.vx*dt;
                q.y+=q.vy*dt;
                q.vx+=q.drift*dt*35;
                q.vy-=10*dt;
                q.angle+=q.spin*dt;
            }
        }
        function spriteDraw(image,x,y,sx,sy,alpha){
            ctx.globalAlpha=alpha;
            ctx.drawImage(image,x-sx,y-sy,sx*2,sy*2);
        }
        function drawSmoke(fade){
            ctx.globalCompositeOperation='source-over';
            for(const q of p){
                if(q.type!=='smoke'&&q.type!=='mist')continue;
                const age=1-q.left/q.life;
                const radius=q.radius*(1+age*.98);
                const alpha=(q.type==='smoke'?.76:.32)*
                    Math.min(1,age*5)*Math.min(1,q.left/.55)*fade;
                spriteDraw(q.type==='smoke'?art.smoke:art.mist,
                    q.x,q.y,radius*1.7,radius*1.3,alpha);
            }
            ctx.globalAlpha=1;
        }
        function drawShards(fade) {
            if(!dark)return;
            ctx.save();
            ctx.globalCompositeOperation='source-over';
            for(const q of p){
                if(q.type!=='shard')continue;
                const age=1-q.left/q.life;
                const a=Math.min(1,q.left/.38)*(.90-age*.28)*fade;
                ctx.save();
                ctx.translate(q.x,q.y);
                ctx.rotate(q.angle);
                ctx.globalAlpha=a;
                const r=q.radius;
                ctx.beginPath();
                ctx.moveTo(-r*.9,-r*.3);
                ctx.lineTo(r*.7,-r*.75);
                ctx.lineTo(r*.48,r*.42);
                ctx.lineTo(-r*.35,r*.8);
                ctx.closePath();
                ctx.fillStyle=age<.3?'#371d22':'#140d13';
                ctx.fill();
                ctx.strokeStyle=age<.42?'#f57d3c':'#5a2c2b';
                ctx.lineWidth=age<.42?1.6:.75;
                ctx.stroke();
                ctx.restore();
            }
            ctx.restore();
        }
        function drawCore(edge,seconds,t,fade){
            const waviness=Math.sin(Math.PI*t);
            ctx.save();
            ctx.beginPath();
            const count=Math.max(35,Math.ceil(h/19));
            for(let i=0;i<=count;i++){
                const y=h*i/count;
                if(i===0)ctx.moveTo(edge(y),y);
                else ctx.lineTo(edge(y),y);
            }
            ctx.lineCap='round';
            ctx.lineJoin='round';
            if(dark){
                ctx.globalCompositeOperation='lighter';
                ctx.globalAlpha=fade;
                ctx.shadowColor='#ff3903';
                ctx.shadowBlur=25;
                ctx.strokeStyle='rgba(255,55,5,.82)';
                ctx.lineWidth=43;
                ctx.stroke();
                ctx.shadowBlur=16;
                ctx.strokeStyle='#ff9b17';
                ctx.lineWidth=20;
                ctx.stroke();
                ctx.shadowBlur=7;
                ctx.strokeStyle='#ffe45e';
                ctx.lineWidth=9;
                ctx.stroke();
                ctx.shadowBlur=4;
                ctx.strokeStyle='#fffcde';
                ctx.lineWidth=3.8;
                ctx.stroke();
            }else{
                ctx.globalAlpha=fade*.64;
                ctx.shadowColor='#8ccfff';
                ctx.shadowBlur=29;
                ctx.strokeStyle='rgba(139,219,255,.68)';
                ctx.lineWidth=28;
                ctx.stroke();
                ctx.shadowBlur=10;
                ctx.strokeStyle='#e7f8ff';
                ctx.lineWidth=9;
                ctx.stroke();
                ctx.shadowBlur=0;
                ctx.strokeStyle='white';
                ctx.lineWidth=2;
                ctx.stroke();
            }
            ctx.restore();
 
            // Dense GOLD / WHITE rolling fire, including on both sides
            // of the border. These are the luminous shapes from V2.
            if(dark){
                ctx.save();
                ctx.globalCompositeOperation='lighter';
                for(let y=-70;y<h+80;y+=48){
                    const x=edge(y);
                    const pulse=.65+.35*Math.sin(seconds*8.5+y*.083);
                    spriteDraw(art.flame,x+rand(-8,30),y,77,70,
                        fade*(.26+.16*pulse));
                }
                for(let y=-20;y<h+52;y+=36){
                    const x=edge(y);
                    const flicker=Math.sin(seconds*13+y*.19);
                    const length=52+30*flicker+17*Math.sin(seconds*6-y*.11);
                    const lean=15+18*Math.sin(seconds*9+y*.069);
                    ctx.globalAlpha=.62*fade;
                    ctx.fillStyle='#ff5705';
                    ctx.beginPath();
                    ctx.moveTo(x+18,y+19);
                    ctx.bezierCurveTo(x-18,y+2,x-lean-24,y-length*.57,
                        x-lean,y-length);
                    ctx.bezierCurveTo(x+2,y-length*.57,x+13,y-12,x+18,y+19);
                    ctx.fill();
                    ctx.globalAlpha=.58*fade;
                    ctx.fillStyle='#ffdb58';
                    ctx.beginPath();
                    ctx.moveTo(x+7,y+8);
                    ctx.quadraticCurveTo(x-10,y-length*.33,
                        x-lean*.8,y-length*.72);
                    ctx.quadraticCurveTo(x+3,y-length*.30,x+8,y+8);
                    ctx.fill();
                    ctx.globalAlpha=.38*fade;
                    ctx.fillStyle='#fffced';
                    ctx.beginPath();
                    ctx.moveTo(x+3,y+5);
                    ctx.quadraticCurveTo(x-5,y-length*.2,
                        x-lean*.5,y-length*.48);
                    ctx.quadraticCurveTo(x+4,y-length*.19,x+5,y+5);
                    ctx.fill();
                }
                ctx.restore();
            }else{
                ctx.save();
                ctx.globalCompositeOperation='lighter';
                for(let y=-50;y<h+55;y+=88){
                    spriteDraw(art.sky,edge(y),y,89,109,.27*fade);
                }
                ctx.restore();
            }
        }
        function drawBrightParticles(fade) {
            ctx.save();
            ctx.globalCompositeOperation='lighter';
            for(const q of p){
                if(q.type==='smoke'||q.type==='mist'||q.type==='shard')continue;
                const age=1-q.left/q.life;
                const brightness=Math.min(1,q.left/.25)*(1-age*.25)*fade;
                let artName='flame',sx=q.radius,sy=q.radius;
                if(q.type==='ember'){artName='ember';sx*=1.2;sy*=1.2;}
                else if(q.type==='pollen'){artName='sky';sx*=1.15;sy*=1.15;}
                else if(q.type==='sky'){artName='sky';sx*=1.1;sy*=1.55;}
                else {sx*=.9;sy*=1.65;}
                spriteDraw(art[artName],q.x,q.y,sx,sy,
                    brightness*(q.type==='ember'?.93:.78));
            }
            ctx.restore();
            ctx.globalAlpha=1;
            ctx.globalCompositeOperation='source-over';
        }
        function paint(now) {
            // Both original V25 curtains follow their OWN song cue clock.
            // Framerate drops cannot move 00:33 / 02:39 off the midpoints.
            const t=score
                ? Math.max(0,Math.min(1,(score.audio.currentTime-
                    score.config.fireStart)/
                    (score.config.fireEnd-score.config.fireStart)))
                : Math.min(1,(now-started)/duration);
            extraVisuals?.update(t);
            const seconds=now/1000;
            const edge=makeEdge(t,seconds,dark,w);
            // Clip-path polygon reconstruction can cause expensive iframe
            // compositor work. It runs at a bounded rate independent of rAF.
            atmosphereEdge=edge;
            window.__kkptVisualDark=dark;
            const clipStep=dark?(mobile?33:23):(mobile?43:34);
            if(now-lastClip>=clipStep||t>=1){
                clipFrame(edge);
                lastClip=now;
            }
            const paintStep=dark?(mobile?35:32):(mobile?52:41);
            if(now-lastPaint<paintStep && t<1){
                requestAnimationFrame(paint);return;
            }
            const dt=Math.min(.070,(now-previous)/1000);
            previous=now;lastPaint=now;
            const fade=Math.min(1,t/.055,(1-t)/.045);
            // Restore V14's broad smoke banks, long-lived char, pressure
            // waves and delicate blue leaves. V2 only paints the FIRE.
            drawAtmosphere(seconds,Math.min(t,.925),dark,(now-started)/1000,[]);
            if(dark&&gpuEmbers)gpuEmbers.draw(edge,seconds,dt,fade,t);
            if(dark){
                advance(dt);
                if(t<1)spawnAtRate(dt,edge,seconds,Math.max(0,fade));
                ctx.clearRect(0,0,w,h);
                drawShards(fade);
                drawCore(edge,seconds,t,fade);
                drawBrightParticles(fade);
            }
            if(t<1)requestAnimationFrame(paint);
            else done();
        }
        requestAnimationFrame(paint);
     }
     }
 
     const gpuEmbers=dark?createGpuEmbers():null;
     function setDisplayed(nextActive) {
    const extraVisuals=dark?(score?.beatScene||null):createRebirthVisual();
        active=nextActive;
    window.__kkptRenderer=dark
        frame.classList.toggle('kkpt-active',active);
      ? gpuEmbers?'WebGL2 embers + original V25 Canvas fire':
        document.body.classList.toggle('kkpt-other-active',active);
         'Original V25 Canvas fire (WebGL fallback)'
        frame.style.clipPath=active?'inset(0)':hideClip;
      : 'Original V25 Canvas dawn';
         frame.style.visibility=active?'visible':'hidden';
    const duration=dark?3450:3100;
        frame.style.pointerEvents='';
    if(!score)transitionSound(dark,duration);
        frame.tabIndex=active?0:-1;
    const curtainFinished=new Promise(resolve=>canvasFire(
        frame.setAttribute('aria-hidden',active?'false':'true');
      canvas,dark,duration,drawAtmosphere,resolve,gpuEmbers,
        document.title=active?otherTitle:baseTitle;
      score,extraVisuals));
        showTheme((active?otherPage:basePage)==='Kuronata');
    requestAnimationFrame(releaseFatePrelude);
        // Stop the invisible Kuronata ambience in either document.
    await curtainFinished;
        root.classList.toggle('kkpt-world-suspended',basePage==='Kuronata'&&active);
    setDisplayed(nextActive);
        try{
    if(dark){
            frame.contentDocument?.documentElement.classList.toggle(
      try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
                'kkpt-frame-inactive',!active);
      mountKuronataAftermath(score);
        }catch(_){}
    }else{
        updateButton();
      kuronataAftermath?.fadeToDawn();
      createCloudbreakAfterglow();
     }
     }
 
     extraVisuals?.finish();
     async function animateSwap(nextActive) {
    if(canvas){
        if(animating)return;
      canvas.style.opacity='0';
        animating=true;
      setTimeout(()=>canvas.remove(),210);
        button.classList.add('kkpt-busy');
        labelSub.textContent='PREPARING THE OTHER SIDE...';
        const dark=(nextActive?otherPage:basePage)==='Kuronata';
        let score=returnScore?.config?.target===otherPage?
            returnScore:null;
        if(!loading)warmFrame();
        // Never make the music wait for the network or an expensive texture.
        // The iframe already has a matching photographic fallback; reveal
        // it under the original V25 curtain even if HTML loads late.
        await Promise.race([loading,sleep(score?350:1100)]);
        await Promise.race([prewarmAtmosphere(),sleep(160)]);
        if(score){
            const started=await Promise.race([
                score.playPromise,sleep(6500).then(()=>false)
            ]);
            if(!started||!await awaitScoreTime(score,
                score.config.fireStart)){
                clearReturnScore(score);
                score=null; // Preserve the exact V25 visuals on audio failure.
            }
        }
        removeScoreOmen();
        frame.style.visibility='visible';
        frame.classList.add('kkpt-animating');
        // Give the now-visible destination its own first composited frame.
        await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
 
        labelSub.textContent=dark?'THE SKY IS BURNING':'DAWN IS BREAKING';
        // When returning to the blue page, force the Day palette
        // BEFORE unmasking it. The dark iframe still fully covers it.
        if(!dark)showTheme(false);
        frame.classList.remove('kkpt-active');
        frame.style.pointerEvents='none';
        frame.style.clipPath=active?'inset(0)':hideClip;
 
        const atmosphere=document.createElement('canvas');
        atmosphere.id='kkpt-atmosphere';
        atmosphere.setAttribute('aria-hidden','true');
        document.body.append(atmosphere);
        const drawAtmosphere=createAtmosphere(atmosphere);
        const canvas=dark?document.createElement('canvas'):null;
        if(canvas){
            canvas.id='kkpt-vfx';
            canvas.setAttribute('aria-hidden','true');
            document.body.append(canvas);
        }
        const gpuEmbers=dark?createGpuEmbers():null;
        const extraVisuals=dark?(score?.beatScene||null):createRebirthVisual();
        window.__kkptRenderer=dark
            ? gpuEmbers?'WebGL2 embers + original V25 Canvas fire':
                'Original V25 Canvas fire (WebGL fallback)'
            : 'Original V25 Canvas dawn';
        const duration=dark?3450:3100;
        if(!score)transitionSound(dark,duration);
        const curtainFinished=new Promise(resolve=>canvasFire(
            canvas,dark,duration,drawAtmosphere,resolve,gpuEmbers,
            score,extraVisuals));
        // canvasFire already queued its FIRST paint() rAF. This next
        // rAF releases the held seal in that very first painted frame.
        requestAnimationFrame(releaseFatePrelude);
        await curtainFinished;
 
        setDisplayed(nextActive);
        if(dark){
            try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
            mountKuronataAftermath(score);
        }else{
            // Fade the shadow away while a pearl-white edge radiates out
            // of the just-opened sky, then let that light evaporate.
            kuronataAftermath?.fadeToDawn();
            createCloudbreakAfterglow();
        }
        extraVisuals?.finish();
        if(canvas){
            canvas.style.opacity='0';
            setTimeout(()=>canvas.remove(),210);
        }
        if(gpuEmbers){
            gpuEmbers.canvas.style.transition='opacity 280ms ease-out';
            gpuEmbers.canvas.style.opacity='0';
            setTimeout(()=>gpuEmbers.dispose(),320);
        }
        // The last frame holds living smoke and drifting fragments.
        // Let it disperse with a lightweight compositor fade AFTER the
        // destination is revealed; no costly extra canvas render loop.
        const tail=dark?2000:1320;
        atmosphere.style.transition='opacity '+tail+
            'ms ease-out, transform '+tail+'ms ease-out';
        await new Promise(resolve=>requestAnimationFrame(resolve));
        atmosphere.style.opacity='0';
        atmosphere.style.transform='translate3d(0,-22px,0) scale(1.035)';
        await sleep(tail+70);
        atmosphere.remove();
        animating=false;
        button.classList.remove('kkpt-busy','kkpt-quiet');
        // The iframe was only the moving scenery during the reveal.
        // Hand off to the ACTUAL Wiki page: native sidebars, scrollbar,
        // history, refresh, accessibility and links all work normally.
        saveReturnScoreHandoff(score);
        navigateRealPage(otherUrl.href);
     }
     }
 
     if(gpuEmbers){
     function requestSwap(nextActive) {
      gpuEmbers.canvas.style.transition='opacity 280ms ease-out';
        if (animating || nextActive === active) return;
      gpuEmbers.canvas.style.opacity='0';
        if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
      setTimeout(()=>gpuEmbers.dispose(),320);
            if(otherPage==='Kuronata'){
                try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
            }
            saveReturnScoreHandoff(returnScore);
            navigateRealPage(otherUrl.href);
            return;
        }
        animateSwap(nextActive).catch(err => {
            console.error('[KuranokaPageTransition] V41:',err);
            removeScoreOmen();
            clearFatePrelude();
            document.getElementById('kkpt-vfx')?.remove();
            document.getElementById('kkpt-atmosphere')?.remove();
            document.getElementById('kkpt-webgl')?.remove();
            document.getElementById('kkpt-rebirth-visual')?.remove();
            returnScore?.beatScene?.dispose();
            document.getElementById('kkpt-lightning-layer')?.remove();
            if (frame) frame.style.filter = '';
            if((nextActive?otherPage:basePage)==='Kuronata'){
                try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
            }
            saveReturnScoreHandoff(returnScore);
            navigateRealPage(nextActive ? otherUrl.href : baseUrl);
        });
     }
     }
 
    const tail=dark?2000:1320;
     function createButton() {
     atmosphere.style.transition='opacity '+tail+
        if (!document.body || document.getElementById('kkpt-toggle')) return;
      'ms ease-out, transform '+tail+'ms ease-out';
        button=document.createElement('a');
    await new Promise(resolve=>requestAnimationFrame(resolve));
        button.id='kkpt-toggle';
    atmosphere.style.opacity='0';
        labelIcon=document.createElement('span');
    atmosphere.style.transform='translate3d(0,-22px,0) scale(1.035)';
        labelIcon.className='kkpt-icon';
    await sleep(tail+70);
        labelIcon.setAttribute('aria-hidden','true');
    atmosphere.remove();
        const text=document.createElement('span');
    animating=false;
        text.className='kkpt-text';
    button.classList.remove('kkpt-busy','kkpt-quiet');
        labelTitle=document.createElement('span');
    saveReturnScoreHandoff(score);
        labelSub=document.createElement('span');
    navigateRealPage(otherUrl.href);
        labelSub.className='kkpt-sub';
  }
        text.append(labelTitle,labelSub);
  function requestSwap(nextActive) {
        button.append(labelIcon,text);
    if (animating || nextActive === active) return;
        document.body.append(button);
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
        button.hidden=!SHOW_FLOATING_BUTTON;
      if(otherPage==='Kuronata'){
        updateButton();
        try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
        watchPortalLinks(document, otherPage);
      }
        mountFateGear(document,otherPage);
      saveReturnScoreHandoff(returnScore);
        const continuingScore=restoreReturnScoreHandoff();
      navigateRealPage(otherUrl.href);
        if(basePage==='Kuranoka')createCloudbreakAfterglow(true);
      return;
        if(basePage==='Kuronata'){
            let hadDarkHandoff=false;
            try{hadDarkHandoff=sessionStorage.getItem(AFTER_DARK_KEY)==='1';}
            catch(_){}
            if(continuingScore||hadDarkHandoff){
                const aftermath=mountKuronataAftermath(returnScore);
                // The departing preview already carried the dark lens.
                // Do not replay the 850ms fade-in on the actual URL:
                // that briefly lightened then darkened the whole page.
                aftermath.layer.classList.add('kkpt-beat-resumed');
            }
        }
        if(continuingScore)setTimeout(warmFrame,3000);
        else warmFrame();
        prewarmAtmosphere();
        preloadRebirthPad();
 
        button.addEventListener('click', event => {
            if (!isPlainClick(event)) return;
            event.preventDefault();
            armReturnScore(otherPage);
            primeSound();
            requestSwap(!active);
        });
     }
     }
 
    animateSwap(nextActive).catch(err => {
    // A plain [[Kuronata|your mysterious text]] now triggers everything.
      console.error('[KuranokaPageTransition] V42:',err);
    // The inverse [[Kuranoka|...]] uses the same transition, then real navigation.
      removeScoreOmen();
    document.addEventListener('click', event => {
      clearFatePrelude();
        if (active) return;
      document.getElementById('kkpt-vfx')?.remove();
        portalClick(event, document, otherPage, true);
      document.getElementById('kkpt-atmosphere')?.remove();
    }, true);
      document.getElementById('kkpt-webgl')?.remove();
    mw.hook('wikipage.content').add(() => {
      document.getElementById('kkpt-rebirth-visual')?.remove();
        if (document.readyState !== 'loading') watchPortalLinks(document, otherPage);
      returnScore?.beatScene?.dispose();
      document.getElementById('kkpt-lightning-layer')?.remove();
      if (frame) frame.style.filter = '';
      if((nextActive?otherPage:basePage)==='Kuronata'){
        try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
      }
      saveReturnScoreHandoff(returnScore);
      navigateRealPage(nextActive ? otherUrl.href : baseUrl);
     });
     });
 
  }
     // Browser history is now real document history. A bfcache return
  function createButton() {
     // must restore THIS document, not leave its animated preview visible.
     if (!document.body || document.getElementById('kkpt-toggle')) return;
     window.addEventListener('pageshow', event => {
     button=document.createElement('a');
        if (!event.persisted) return;
     button.id='kkpt-toggle';
        animating=false;
    labelIcon=document.createElement('span');
        document.getElementById('kkpt-vfx')?.remove();
    labelIcon.className='kkpt-icon';
        document.getElementById('kkpt-atmosphere')?.remove();
    labelIcon.setAttribute('aria-hidden','true');
        document.getElementById('kkpt-rebirth-visual')?.remove();
    const text=document.createElement('span');
        document.getElementById('kkpt-cloudbreak')?.remove();
    text.className='kkpt-text';
        document.getElementById('kkpt-beat-scene')?.remove();
    labelTitle=document.createElement('span');
        kuronataAftermath?.dispose();
    labelSub=document.createElement('span');
        document.getElementById('kkpt-lightning-layer')?.remove();
    labelSub.className='kkpt-sub';
        if(frame){
    text.append(labelTitle,labelSub);
            frame.style.filter='';
    button.append(labelIcon,text);
            setDisplayed(false);
    document.body.append(button);
            frame.classList.remove('kkpt-animating');
    button.hidden=!SHOW_FLOATING_BUTTON;
        }
    updateButton();
        if(basePage==='Kuronata'){
    watchPortalLinks(document, otherPage);
            let entered=false;
    mountFateGear(document,otherPage);
            try{entered=sessionStorage.getItem(AFTER_DARK_KEY)==='1';}
    const continuingScore=restoreReturnScoreHandoff();
            catch(_){}
    if(basePage==='Kuranoka')createCloudbreakAfterglow(true);
            if(entered){
    if(basePage==='Kuronata'){
                const aftermath=mountKuronataAftermath(returnScore);
      let hadDarkHandoff=false;
                aftermath.layer.classList.add('kkpt-beat-resumed');
      try{hadDarkHandoff=sessionStorage.getItem(AFTER_DARK_KEY)==='1';}
            }
      catch(_){}
        }
      if(continuingScore||hadDarkHandoff){
        button?.classList.remove('kkpt-busy','kkpt-quiet');
        const aftermath=mountKuronataAftermath(returnScore);
        aftermath.layer.classList.add('kkpt-beat-resumed');
      }
    }
    if(continuingScore)setTimeout(warmFrame,3000);
    else warmFrame();
    prewarmAtmosphere();
    preloadRebirthPad();
    button.addEventListener('click', event => {
      if (!isPlainClick(event)) return;
      event.preventDefault();
      armReturnScore(otherPage);
      primeSound();
      requestSwap(!active);
     });
     });
 
  }
     if (document.readyState === 'loading') {
  document.addEventListener('click', event => {
        document.addEventListener('DOMContentLoaded',createButton,{once:true});
    if (active) return;
    } else {
    portalClick(event, document, otherPage, true);
         createButton();
  }, true);
  mw.hook('wikipage.content').add(() => {
     if (document.readyState !== 'loading') watchPortalLinks(document, otherPage);
  });
  window.addEventListener('pageshow', event => {
    if (!event.persisted) return;
    animating=false;
    document.getElementById('kkpt-vfx')?.remove();
    document.getElementById('kkpt-atmosphere')?.remove();
    document.getElementById('kkpt-rebirth-visual')?.remove();
    document.getElementById('kkpt-cloudbreak')?.remove();
    document.getElementById('kkpt-beat-scene')?.remove();
    kuronataAftermath?.dispose();
    document.getElementById('kkpt-lightning-layer')?.remove();
    if(frame){
      frame.style.filter='';
      setDisplayed(false);
      frame.classList.remove('kkpt-animating');
    }
    if(basePage==='Kuronata'){
      let entered=false;
      try{entered=sessionStorage.getItem(AFTER_DARK_KEY)==='1';}
      catch(_){}
      if(entered){
         const aftermath=mountKuronataAftermath(returnScore);
        aftermath.layer.classList.add('kkpt-beat-resumed');
      }
     }
     }
    button?.classList.remove('kkpt-busy','kkpt-quiet');
  });
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded',createButton,{once:true});
  } else {
    createButton();
  }
}());
}());

2026年10月24日 (六) 18:18的最新版本

/* ============================================================
  Kuranoka / Kuronata — Page Transition V42 / V25 faithful restore
  V25 ORIGINAL fire edge, translucent fire pockets, smoke and dawn.
  Lightweight WebGL2 is restricted to subtle ember points, NEVER the
  fire curtain nor the reverse transition. Carbon sheets are 2D.
  Both cues start three seconds before the V25 curtains, fade in over
  those three seconds, then fade OUT over 18 seconds after reveal.
  Only this page-scoped JS/CSS; never modify Citizen or loader.js.
  Real page navigation after the transition; iframe is visual-only.
  ============================================================ */
(function initKkpt(retry) {
  'use strict';
  retry=retry||0;
  if (!window.mw || !mw.config || typeof mw.config.get!=='function') {
    if(retry<40)setTimeout(()=>initKkpt(retry+1),100);
    return;
  }
  const pathMatch = location.pathname.match(/\/wiki\/(Kuranoka|Kuronata)\/?$/);
  const urlPage = pathMatch ? pathMatch[1]
    : new URLSearchParams(location.search).get('title');
  const basePage = ['Kuranoka','Kuronata'].includes(urlPage)
    ? urlPage : mw.config.get('wgPageName');
  const action = new URLSearchParams(location.search).get('action') || 'view';
  if (!['Kuranoka','Kuronata'].includes(basePage) || action !== 'view') return;
  const root=document.documentElement;
  const themeNames=['skin-theme-clientpref-day','skin-theme-clientpref-os',
    'skin-theme-clientpref-night','skin-theme-clientpref-black',
    'skin-citizen-light'];
  let wantsNight=basePage==='Kuronata';
  let changingTheme=false;
  function syncTheme(){
    if(changingTheme)return;
    const wanted=wantsNight?'skin-theme-clientpref-night':
      'skin-theme-clientpref-day';
    const shadow=wantsNight?'kkpt-kuronata-dark':null;
    const unexpected=themeNames.filter(c=>c!==wanted)
      .concat(wantsNight?[]:['kkpt-kuronata-dark']);
    if(root.classList.contains(wanted)&&
      (!shadow||root.classList.contains(shadow))&&
      unexpected.every(c=>!root.classList.contains(c))){
      return;
    }
    changingTheme=true;
    root.classList.remove(...themeNames,'kkpt-kuronata-dark');
    root.classList.add(wanted);
    if(shadow)root.classList.add(shadow);
    changingTheme=false;
  }
  function showTheme(dark){wantsNight=dark;syncTheme();}
  syncTheme();
  new MutationObserver(syncTheme).observe(root,{
    attributes:true,attributeFilter:['class']
  });
  function ensureWorldStyles(){
    if(!document.head || document.getElementById('kkpt-css-retry'))return;
    try{
      if(getComputedStyle(root).getPropertyValue('--kkpt-css-v42').trim()==='ready')return;
    }catch(_){}
    const link=document.createElement('link');
    link.id='kkpt-css-retry';
    link.rel='stylesheet';
    const target=new URL('/index.php',location.origin);
    target.searchParams.set('title','MediaWiki:KuranokaPageTransition.css');
    target.searchParams.set('action','raw');
    target.searchParams.set('ctype','text/css');
    target.searchParams.set('kkptv','42');
    link.href=target.href;
    document.head.appendChild(link);
  }
  setTimeout(ensureWorldStyles,1200);
  function navigateRealPage(href){
    window.location.assign(href);
  }
  function installKuronataAfterglow() {
    if(basePage!=='Kuronata'||window.__kkptDarkExtrasReady)return;
    window.__kkptDarkExtrasReady=true;
    const prefersReduced=window.matchMedia('(prefers-reduced-motion: reduce)');
    const spoilerOverlays=new WeakMap();
    const renderedSpoilerLines=(node)=>{
      const lines=[];
      const walk=document.createTreeWalker(node,NodeFilter.SHOW_TEXT);
      const range=document.createRange();
      let text,budget=0;
      while((text=walk.nextNode())&&budget<340){
        for(let i=0;i<text.length&&budget<340;i++,budget++){
          const ch=text.nodeValue[i];
          if(ch==='\n'||ch==='\r')continue;
          range.setStart(text,i);
          range.setEnd(text,i+1);
          const r=range.getBoundingClientRect();
          if(!r.width||!r.height)continue;
          let line=lines.find(row=>Math.abs(row.top-r.top)<3);
          if(!line){
            line={top:r.top,left:r.left,right:r.right,
              height:r.height,text:''};
            lines.push(line);
          }
          line.left=Math.min(line.left,r.left);
          line.right=Math.max(line.right,r.right);
          line.height=Math.max(line.height,r.height);
          line.text+=ch;
        }
      }
      range.detach?.();
      return lines.sort((a,b)=>a.top-b.top);
    };
    const spoilerFault=(node)=>{
      if(prefersReduced.matches)return;
      const rects=node.getClientRects();
      if(rects.length<2)return;
      const lines=renderedSpoilerLines(node);
      if(lines.length<2)return;
      node.setAttribute('data-kkpt-multiline','1');
      spoilerOverlays.get(node)?.remove();
      const overlay=document.createElement('div');
      overlay.className='kkpt-heimu-fault';
      overlay.setAttribute('aria-hidden','true');
      const font=getComputedStyle(node);
      for(const row of lines){
        const fragment=document.createElement('span');
        fragment.className='kkpt-heimu-fault-line';
        fragment.dataset.copy=row.text;
        fragment.style.left=row.left+'px';
        fragment.style.top=row.top+'px';
        fragment.style.width=(row.right-row.left+6)+'px';
        fragment.style.height=(row.height+4)+'px';
        fragment.style.fontFamily=font.fontFamily;
        fragment.style.fontSize=font.fontSize;
        fragment.style.fontWeight=font.fontWeight;
        fragment.style.fontStyle=font.fontStyle;
        fragment.style.lineHeight=font.lineHeight;
        fragment.style.letterSpacing=font.letterSpacing;
        overlay.append(fragment);
      }
      document.body.append(overlay);
      spoilerOverlays.set(node,overlay);
    };
    const clearSpoilerFault=(node)=>{
      const overlay=spoilerOverlays.get(node);
      if(!overlay)return;
      spoilerOverlays.delete(node);
      overlay.classList.add('kkpt-fault-away');
      setTimeout(()=>overlay.remove(),210);
    };
    const decorateSpoilers=()=>{
      document.querySelectorAll('span.heimu').forEach(node=>{
        if(node.dataset.kkptBound)return;
        node.dataset.kkptBound='1';
        const label=(node.textContent||'').replace(/\s+/g,' ').trim();
        if(label&&label.length<=240&&!node.children.length)
          node.setAttribute('data-kkpt-glitch',label);
        node.addEventListener('mouseenter',()=>spoilerFault(node));
        node.addEventListener('mouseleave',()=>clearSpoilerFault(node));
      });
    };
    const decorateLinks=()=>{
      const article=document.querySelector('#mw-content-text,.mw-parser-output');
      if(!article)return;
      article.querySelectorAll('a[href]').forEach(link=>{
        if(link.dataset.kkptLinkReady ||
          link.classList.contains('kkpt-fate-gear') ||
          link.querySelector('img,svg,video'))return;
        link.dataset.kkptLinkReady='1';
        link.classList.add('kkpt-link-glitch');
        try{
          const url=new URL(link.href,document.baseURI);
          const path=url.pathname.match(/\/wiki\/([^/?#]+)/);
          const name=url.searchParams.get('title')||
            (path?decodeURIComponent(path[1]):'');
          if(url.origin===location.origin &&
            name.replace(/_/g,' ')==='Kuranoka'){
            link.classList.add('kkpt-link-origin');
            const label=(link.textContent||'').trim();
            if(!link.children.length &&
              link.getClientRects().length===1 &&
              label.length>0&&label.length<100)
              link.dataset.kkptLinkCopy=label;
          }
        }catch(_){}
      });
    };
    const decorateIdentityMentions=()=>{
      const article=document.querySelector('#mw-content-text,.mw-parser-output');
      if(!article)return;
      const hasIdentity=/Kuranoka|Kurano\s+Kanata|天野[ \u00a0\u3000]*彼方/i;
      const exactIdentity=/Kuranoka|Kurano\s+Kanata|天野[ \u00a0\u3000]*彼方/gi;
      const walker=document.createTreeWalker(article,NodeFilter.SHOW_TEXT,{
        acceptNode(node){
          if(!node.nodeValue||!hasIdentity.test(node.nodeValue))
            return NodeFilter.FILTER_REJECT;
          const parent=node.parentElement;
          if(!parent||parent.closest('script,style,textarea,code,pre,'+
            'input,button,select,[contenteditable="true"],'+
            '.kkpt-identity-glitch,.kkpt-heimu-fault,'+
            'span.heimu,a.kkpt-link-origin,'+
            '#kkpt-afterglow,#kkpt-toggle'))
            return NodeFilter.FILTER_REJECT;
          return NodeFilter.FILTER_ACCEPT;
        }
      });
      const matches=[];
      let node;
      while((node=walker.nextNode())&&matches.length<1800)matches.push(node);
      let index=0;
      for(const textNode of matches){
        const raw=textNode.nodeValue;
        const fragment=document.createDocumentFragment();
        let previous=0,match;
        exactIdentity.lastIndex=0;
        while((match=exactIdentity.exec(raw))){
          if(match.index>previous)
            fragment.append(document.createTextNode(raw.slice(previous,match.index)));
          const el=document.createElement('span');
          el.className='kkpt-identity-glitch';
          el.dataset.kkptIdentity=match[0];
          el.textContent=match[0];
          el.style.setProperty('--kkpt-identity-delay',
            (-((index++*7)%23)*.21).toFixed(2)+'s');
          fragment.append(el);
          previous=match.index+match[0].length;
        }
        if(previous<raw.length)
          fragment.append(document.createTextNode(raw.slice(previous)));
        textNode.replaceWith(fragment);
      }
    };
    const createAmbience=()=>{
      if(prefersReduced.matches||document.getElementById('kkpt-afterglow')||!document.body)return;
      const overlay=document.createElement('div');
      overlay.id='kkpt-afterglow';
      overlay.setAttribute('aria-hidden','true');
      const el=(type,i)=>{
        const part=document.createElement('span');
        part.className=type==='soot'?'kkpt-soot-cell':'kkpt-ash-cell';
        const smoke=type==='soot';
        const fromLeft=i%2===0;
        part.style.left=(smoke
          ? (fromLeft?-26+Math.random()*22:79+Math.random()*21)
          : Math.random()*100)+'%';
        part.style.top=(smoke?8+Math.random()*94:15+Math.random()*94)+'%';
        part.style.setProperty('--size',(smoke?220+Math.random()*240:2+Math.random()*4).toFixed(1)+'px');
        part.style.setProperty('--delay',(-Math.random()*(smoke?29:18)).toFixed(2)+'s');
        part.style.setProperty('--duration',(smoke?18+Math.random()*14:8+Math.random()*11).toFixed(2)+'s');
        part.style.setProperty('--wind',((Math.random()-.5)*(smoke?100:230)).toFixed(1)+'px');
        return part;
      };
      for(let i=0;i<(innerWidth<700?8:16);i++)overlay.append(el('soot',i));
      for(let i=0;i<30;i++)overlay.append(el('ash',i));
      document.body.append(overlay);
    };
    const init=()=>{
      decorateSpoilers();
      decorateLinks();
      decorateIdentityMentions();
      createAmbience();
      mw.hook('wikipage.content').add(()=>{
        decorateSpoilers();
        decorateLinks();
        decorateIdentityMentions();
      });
    };
    if(document.readyState==='loading')
      document.addEventListener('DOMContentLoaded',init,{once:true});
    else init();
    const pulse=()=>{
      const inactive=document.hidden||prefersReduced.matches||
        root.classList.contains('kkpt-frame-inactive')||
        root.classList.contains('kkpt-world-suspended')||
        (window!==window.top&&window.frameElement?.getAttribute('aria-hidden')==='true');
      if(!inactive){
        root.style.setProperty('--kkpt-rift-y',(14+Math.random()*70).toFixed(1)+'%');
        root.classList.add('kkpt-rift');
        window.setTimeout(()=>root.classList.remove('kkpt-rift'),390);
      }
      window.setTimeout(pulse,5000+Math.random()*15000);
    };
    window.setTimeout(pulse,5000+Math.random()*15000);
  }
  installKuronataAfterglow();
  if (window!==window.top || window.__kkptV12) return;
  window.__kkptV12=true;
  window.__kkptVersion='42.0';
  const SHOW_FLOATING_BUTTON=false;
  const otherPage=basePage==='Kuranoka'?'Kuronata':'Kuranoka';
  const baseUrl=location.href;
  const baseTitle=document.title;
  const otherUrl=new URL('/wiki/'+encodeURIComponent(otherPage),location.href);
  const clipSide=basePage==='Kuranoka'?'right':'left';
  const hideClip=clipSide==='right'?'inset(0 0 0 100%)':'inset(0 100% 0 0)';
  const sleep=ms=>new Promise(r=>setTimeout(r,ms));
  const smooth=t=>{t=Math.max(0,Math.min(1,t));
    return t*t*t*(t*(t*6-15)+10);};
  const imagePool=new Map();
  const worldBackdrops={
    Kuranoka:'https://blockland.wiki/images/0/05/%E7%A7%AF%E9%9B%A8%E4%BA%911.png',
    Kuronata:'https://blockland.wiki/images/a/ab/%E9%BB%84%E6%98%8F%EF%BC%8C%E9%BB%91%E4%BA%91.png'
  };
  function primeWorldBackdrops(){
    const current=worldBackdrops[basePage];
    if(current&&document.head){
      const link=document.createElement('link');
      link.rel='preload';
      link.as='image';
      link.href=current;
      link.setAttribute('fetchpriority','high');
      link.dataset.kkptBackgroundPreload='1';
      document.head.appendChild(link);
      warmImage(current).catch(()=>{});
    }
    setTimeout(()=>{
      const next=worldBackdrops[otherPage];
      if(next)warmImage(next).catch(()=>{});
    },300);
  }
  primeWorldBackdrops();
  const ENABLE_TRANSITION_AUDIO = true;
  const FIRE_VOLUME = .30;
  const DAWN_VOLUME = .20;
  let soundContext=null,fireBuffer=null,activeSound=null;
  const REBIRTH_PAD_URL=
    'https://upload.wikimedia.org/wikipedia/commons/e/ed/Padsynth_example_synth_piano.ogg';
  let padBytesPromise=null,padDecodePromise=null,padBuffer=null;
  function preloadRebirthPad(){
    if(padBytesPromise)return padBytesPromise;
    padBytesPromise=fetch(REBIRTH_PAD_URL,{mode:'cors',cache:'force-cache'})
      .then(resp=>{
        if(!resp.ok)throw new Error('PAD HTTP '+resp.status);
        return resp.arrayBuffer();
      })
      .catch(err=>{
        console.debug('[KKPT] PAD file unavailable:',err);
        return null;
      });
    return padBytesPromise;
  }
  function decodeRebirthPad(ctx){
    if(padBuffer)return Promise.resolve(padBuffer);
    if(!padDecodePromise){
      padDecodePromise=preloadRebirthPad()
        .then(bytes=>bytes?ctx.decodeAudioData(bytes.slice(0)):null)
        .then(result=>(padBuffer=result))
        .catch(err=>{
          console.debug('[KKPT] Cannot decode PAD:',err);
          return null;
        });
    }
    return padDecodePromise;
  }
  function playRebirthPadFallback(durationMs){
    const sound=new Audio(REBIRTH_PAD_URL);
    sound.volume=0;
    sound.play().then(()=>{
      const begin=performance.now();
      const timer=setInterval(()=>{
        const t=Math.min(1,(performance.now()-begin)/durationMs);
        sound.volume=Math.max(0,Math.min(DAWN_VOLUME,
          DAWN_VOLUME*Math.min(t/.27,(1-t)/.26)));
        if(t>=1){clearInterval(timer);sound.pause();sound.src='';}
      },80);
    }).catch(err=>console.debug('[KKPT] PAD playback unavailable:',err));
  }
  function primeSound(){
    if(!ENABLE_TRANSITION_AUDIO)return;
    try{
      const AudioEngine=window.AudioContext||window.webkitAudioContext;
      if(!AudioEngine)return;
      if(!soundContext)soundContext=new AudioEngine();
      if(soundContext.state==='suspended')soundContext.resume().catch(()=>{});
      decodeRebirthPad(soundContext);
    }catch(err){console.debug('[KKPT] Audio unavailable:',err);}
  }
  function createFireAudio(ctx){
    const length=Math.round(ctx.sampleRate*4.5);
    const buffer=ctx.createBuffer(1,length,ctx.sampleRate);
    const data=buffer.getChannelData(0);
    let low=0,middle=0;
    for(let i=0;i<length;i++){
      const random=Math.random()*2-1;
      low=low*.992+random*.008;
      middle=middle*.92+random*.08;
      data[i]=Math.max(-1,Math.min(1,
        low*3.9+middle*.33+random*.055));
    }
    for(let k=0;k<350;k++){
      const at=Math.floor(Math.random()*(length-1500));
      const decay=36+Math.random()*330;
      const duration=Math.min(1400,Math.ceil(decay*4));
      const power=.20+Math.pow(Math.random(),3)*.68;
      for(let j=0;j<duration;j++){
        const env=Math.exp(-j/decay);
        data[at+j]=Math.max(-1,Math.min(1,data[at+j]+
          (Math.random()*2-1)*power*env));
      }
    }
    return buffer;
  }
  function transitionSound(dark,durationMs){
    const ctx=soundContext;
    if(!ctx||ctx.state!=='running')return;
    const start=ctx.currentTime+.018;
    const span=durationMs/1000;
    if(activeSound){
      const old=activeSound.gain;
      old.cancelScheduledValues(start);
      old.setTargetAtTime(.0001,start,.05);
    }
    const master=ctx.createGain();
    master.gain.setValueAtTime(.0001,start);
    master.gain.linearRampToValueAtTime(dark?FIRE_VOLUME:DAWN_VOLUME,
      start+(dark?.75:1.10));
    master.gain.setValueAtTime(dark?FIRE_VOLUME:DAWN_VOLUME,
      start+span-(dark?.88:1.0));
    master.gain.exponentialRampToValueAtTime(.0001,start+span);
    master.connect(ctx.destination);
    activeSound={gain:master};
    if(dark){
      if(!fireBuffer)fireBuffer=createFireAudio(ctx);
      const src=ctx.createBufferSource();
      src.buffer=fireBuffer;src.loop=true;
      const lowpass=ctx.createBiquadFilter();
      lowpass.type='lowpass';lowpass.frequency.value=2050;
      const bass=ctx.createBiquadFilter();
      bass.type='highpass';bass.frequency.value=55;
      src.connect(lowpass);lowpass.connect(bass);bass.connect(master);
      src.start(start);src.stop(start+span+.12);
    }else{
      decodeRebirthPad(ctx).then(buffer=>{
        if(!buffer){playRebirthPadFallback(durationMs);return;}
        const at=Math.max(start,ctx.currentTime+.020);
        if(at>start+span-.55)return;
        const player=ctx.createBufferSource();
        player.buffer=buffer;
        player.connect(master);
        player.start(at,Math.max(0,at-start)+.95);
        player.stop(start+span);
      }).catch(()=>{});
    }
    setTimeout(()=>{
      if(activeSound&&activeSound.gain===master)activeSound=null;
      try{master.disconnect();}catch(_){}
    },durationMs+350);
  }
  const SCORE_KEY='kkpt:score-handoff:42';
  const SCORE_MUTE_KEY='kkpt:score-muted';
  const SCORES={
    Kuronata:{
      id:'marenoia',target:'Kuronata',
      url:'https://blkl.fun/wp-content/uploads/2026/10/Marenoia.mp3?kkpt_audio=v35',
      startAt:28.275,fireStart:31.275,fireEnd:34.725,
      stopAt:52.725,fadeInSeconds:3,fadeSeconds:18
    },
    Kuranoka:{
      id:'anotherme',target:'Kuranoka',
      url:'https://blkl.fun/wp-content/uploads/2026/10/AnotherMe.mp3?kkpt_audio=20261024_043627',
      startAt:154.45,fireStart:157.45,fireEnd:160.55,
      stopAt:178.55,fadeInSeconds:3,fadeSeconds:18
    }
  };
  let returnScore=null;
  let scoreOmen=null;
  let fatePrelude=null;
  function userWantsMute(){
    try{return sessionStorage.getItem(SCORE_MUTE_KEY)==='1';}
    catch(_){return false;}
  }
  const AUDIO_ICONS={
    speaker:'<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="M15 9a5 5 0 0 1 0 6"/><path d="M18 6a9 9 0 0 1 0 12"/>',
    mute:'<path d="M11 5 6 9H3v6h3l5 4V5Z"/><path d="m16 9 5 6m0-6-5 6"/>',
    play:'<path d="m8 5 11 7-11 7V5Z"/>'
  };
  function updateAudioToggle(score){
    if(!score?.hud)return;
    const paused=score.blocked||score.audio.paused;
    const icon=paused?'play':score.audio.muted?'mute':'speaker';
    score.hud.innerHTML='<svg viewBox="0 0 24 24" fill="none" '+
      'stroke="currentColor" stroke-width="1.8" stroke-linecap="round" '+
      'stroke-linejoin="round" aria-hidden="true">'+AUDIO_ICONS[icon]+'</svg>';
    score.hud.setAttribute('aria-label',paused?'恢复播放':
      score.audio.muted?'取消静音':'静音');
    score.hud.setAttribute('aria-pressed',String(score.audio.muted));
    score.hud.classList.toggle('kkpt-audio-needs-resume',paused);
  }
  function mountAudioToggle(score){
    const old=document.getElementById('kkpt-audio-toggle');
    old?.remove();
    const hud=document.createElement('button');
    hud.type='button';hud.id='kkpt-audio-toggle';
    hud.setAttribute('aria-label','静音');
    score.hud=hud;
    document.body.append(hud);
    const audio=score.audio;
    hud.addEventListener('click',()=>{
      if(audio.paused||score.blocked){
        score.blocked=false;
        const resume=typeof score.resume==='function'?
          score.resume():audio.play();
        Promise.resolve(resume).then(()=>{
          updateAudioToggle(score);
        }).catch(err=>{
          console.info('[KKPT] Audio resume blocked:',err);
          score.blocked=true;updateAudioToggle(score);
        });
        updateAudioToggle(score);
        return;
      }
      audio.muted=!audio.muted;
      try{sessionStorage.setItem(SCORE_MUTE_KEY,audio.muted?'1':'0');}
      catch(_){}
      updateAudioToggle(score);
    });
    audio.addEventListener('play',()=>updateAudioToggle(score));
    audio.addEventListener('pause',()=>updateAudioToggle(score));
    updateAudioToggle(score);
  }
  function removeScoreOmen(){
    if(scoreOmen){
      const old=scoreOmen;
      const opacity=getComputedStyle(old).opacity;
      scoreOmen=null;
      old.style.opacity=opacity;
      old.classList.add('kkpt-omen-closing');
      requestAnimationFrame(()=>requestAnimationFrame(()=>{
        old.style.opacity='0';
      }));
      setTimeout(()=>old.remove(),1540);
    }
    document.body.classList.remove('kkpt-omen-running');
  }
  function startScoreOmen(score){
    if(score?.config?.id!=='marenoia'||scoreOmen)return;
    const omen=document.createElement('div');
    omen.id='kkpt-omen';omen.setAttribute('aria-hidden','true');
    omen.innerHTML='<i class="kkpt-omen-glow"></i>'+
      '<i class="kkpt-omen-line"></i>';
    document.body.append(omen);
    scoreOmen=omen;
    document.body.classList.add('kkpt-omen-running');
  }
  function clearFatePrelude(){
    fatePrelude?.remove();fatePrelude=null;
  }
  function releaseFatePrelude(){
    if(!fatePrelude)return;
    const node=fatePrelude;
    node.classList.add('kkpt-fate-merging');
    requestAnimationFrame(()=>{if(node.isConnected)node.style.opacity='0';});
    setTimeout(()=>{
      if(fatePrelude===node)clearFatePrelude();
      else node.remove();
    },1700);
  }
  function fateSigilMarkup(isDark){
    const at=(r,d)=>{
      const a=(d-90)*Math.PI/180;
      return [(150+Math.cos(a)*r).toFixed(2),
        (150+Math.sin(a)*r).toFixed(2)];
    };
    const ring=(r,cls)=>'<circle class="'+cls+
      '" cx="150" cy="150" r="'+r+'"/>';
    const stroke=(a,b,cls)=>'<path class="'+cls+
      '" d="M'+a.join(' ')+' L'+b.join(' ')+'"/>';
    const poly=(points,cls)=>'<polygon class="'+cls+'" points="'+
      points.map(p=>p.join(',')).join(' ')+'"/>';
    const outer=ring(133,'kkpt-sigil-outer')+
      ring(128,'kkpt-sigil-orbit')+
      ring(121,'kkpt-sigil-ghost')+
      ring(105,'kkpt-sigil-seal')+
      ring(100,'kkpt-sigil-trace')+
      ring(83,'kkpt-sigil-ghost');
    const glyphs=Array.from({length:48},(_,i)=>{
      const angle=i*7.5;
      const major=i%4===0;
      return stroke(at(121,angle),at(major?132:i%2?125:128,angle),
        major?'kkpt-sigil-mark kkpt-sigil-major':'kkpt-sigil-mark');
    }).join('');
    const cardinal=Array.from({length:8},(_,i)=>{
      const a=i*45;
      return poly([at(99,a),at(111,a+4.2),at(120,a),at(111,a-4.2)],
        'kkpt-sigil-diamond')+
        stroke(at(85,a),at(96,a),'kkpt-sigil-needle');
    }).join('');
    const darkDrawing=
      ring(54,'kkpt-sigil-core-orbit')+
      ring(43,'kkpt-sigil-inner-orbit')+
      '<path class="kkpt-sigil-dark-mark" d="M150 115L170 150 150 185 130 150Z"/>'+
      '<path class="kkpt-sigil-dark-cut" d="M150 130L156 150 150 170 144 150Z"/>'+
      '<path class="kkpt-sigil-axis" d="M150 96V108M150 192V204"/>';
    const hopeDrawing=
      ring(54,'kkpt-sigil-core-orbit')+
      ring(43,'kkpt-sigil-inner-orbit')+
      '<path class="kkpt-sigil-light-mark" d="M150 119L158 142 181 150 158 158 150 181 142 158 119 150 142 142Z"/>'+
      '<circle class="kkpt-sigil-center" cx="150" cy="150" r="7"/>'+
      '<path class="kkpt-sigil-axis" d="M150 96V108M150 192V204"/>';
    return '<svg class="kkpt-fate-sigil '+(isDark?'kkpt-sigil-ominous':
      'kkpt-sigil-hope')+'" viewBox="0 0 300 300" '+
      'xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'+
      '<g class="kkpt-sigil-engraving" fill="none" stroke="'+
        (isDark?'#ff8c9b':'#f7edcf')+'" stroke-width="1.3">'+
        outer+glyphs+cardinal+
        (isDark?darkDrawing:hopeDrawing)+'</g></svg>';
  }
  function startFatePrelude(anchor,targetPage){
    clearFatePrelude();
    const box=anchor?.getBoundingClientRect?.();
    const x=box?box.left+box.width/2:innerWidth/2;
    const y=box?box.top+box.height/2:innerHeight/2;
    const prelude=document.createElement('div');
    prelude.id='kkpt-fate-prelude';
    prelude.setAttribute('aria-hidden','true');
    const isDark=targetPage==='Kuronata';
    prelude.className=isDark?'kkpt-fate-dark':'kkpt-fate-light';
    prelude.style.setProperty('--kkpt-fate-x',x+'px');
    prelude.style.setProperty('--kkpt-fate-y',y+'px');
    prelude.style.setProperty('--kkpt-fate-dx',(innerWidth/2-x)+'px');
    prelude.style.setProperty('--kkpt-fate-dy',(innerHeight/2-y)+'px');
    prelude.style.setProperty('--kkpt-fate-dx15',((innerWidth/2-x)*.15)+'px');
    prelude.style.setProperty('--kkpt-fate-dy15',((innerHeight/2-y)*.15)+'px');
    prelude.style.setProperty('--kkpt-fate-dx91',((innerWidth/2-x)*.91)+'px');
    prelude.style.setProperty('--kkpt-fate-dy91',((innerHeight/2-y)*.91)+'px');
    prelude.innerHTML='<div class="kkpt-fate-orbit">'+
      '<i class="kkpt-fate-aura"></i>'+
      '<i class="kkpt-fate-ring kkpt-fate-ring-a"></i>'+
      '<i class="kkpt-fate-ring kkpt-fate-ring-b"></i>'+
      '<i class="kkpt-fate-arc"></i>'+
      '<i class="kkpt-fate-spark"></i>'+
      '<i class="kkpt-fate-wave"></i>'+
      fateSigilMarkup(isDark)+'</div>';
    root.append(prelude);
    fatePrelude=prelude;
    setTimeout(()=>{
      if(fatePrelude===prelude)clearFatePrelude();
      else prelude.remove();
    },30000);
  }
  const MARENOIA_TRANSIENTS=[
    [28.453,.56],[28.682,.58],[28.892,.76],[29.121,.64],
    [29.341,.72],[29.570,.48],[29.780,.87],[30.009,.93],
    [30.438,.44],[30.658,.77],[30.887,.83],[31.356,.68],
    [31.556,.79],[31.775,.93],[32.364,.29],[32.883,.36],
    [33.192,.38],[33.561,1],[33.751,.97],[34.250,.86],
    [34.469,.54],[35.317,.86],[35.786,.69]
  ];
  function createMarenoiaBeatScene(score){
    if(score.config.id!=='marenoia')return null;
    const lightningLayer=document.createElement('div');
    lightningLayer.id='kkpt-lightning-layer';
    lightningLayer.setAttribute('aria-hidden','true');
    document.body.append(lightningLayer);
    let live=true,raf=0,lastTick=0,nextBeat=0,progress=0;
    let lastLightning=-100;
    function lightning(t,strength){
      if(!live||t<.11||t>.96)return;
      const w=Math.max(1,innerWidth),h=Math.max(1,innerHeight);
      const rng=(a,b)=>a+Math.random()*(b-a);
      const edge=w*(1-t);
      let x=Math.min(w*.93,Math.max(edge+35,edge+(w-edge)*.53));
      const top=rng(-70,-16),bottom=rng(h*.46,h*.80);
      const points=[[x,top]];
      const count=9;
      for(let i=1;i<=count;i++){
        const yy=top+(bottom-top)*i/count;
        const xx=x+rng(-39,39)*(i/count)+Math.sin(i*2.4)*14;
        points.push([Math.max(10,Math.min(w-10,xx)),yy]);
      }
      const path=points.map((p,i)=>(i?'L':'M')+
        p[0].toFixed(1)+','+p[1].toFixed(1)).join(' ');
      const branchAt=Math.floor(rng(3,7));
      const p=points[branchAt];
      const branch='M'+p[0].toFixed(1)+','+p[1].toFixed(1)+
        ' l'+rng(-46,46).toFixed(1)+','+rng(45,86).toFixed(1)+
        ' l'+rng(-28,28).toFixed(1)+','+rng(38,71).toFixed(1);
      const ns='http://www.w3.org/2000/svg';
      const svg=document.createElementNS(ns,'svg');
      svg.setAttribute('class','kkpt-storm-bolt');
      svg.setAttribute('viewBox','0 0 '+w+' '+h);
      svg.setAttribute('preserveAspectRatio','none');
      svg.style.opacity=String(Math.min(1,.46+strength*.42));
      svg.style.clipPath='inset(0 0 0 '+
        Math.max(0,(1-t)*100).toFixed(2)+'%)';
      const addPath=(d,klass)=>{
        const el=document.createElementNS(ns,'path');
        el.setAttribute('d',d);el.setAttribute('class',klass);
        svg.append(el);
      };
      addPath(path,'kkpt-bolt-haze');
      addPath(path,'kkpt-bolt-core');
      addPath(branch,'kkpt-bolt-branch');
      lightningLayer.append(svg);
      setTimeout(()=>svg.remove(),390);
    }
    function tick(now){
      if(!live)return;
      raf=requestAnimationFrame(tick);
      if(now-lastTick<30)return;lastTick=now;
      const audio=score.audio;
      const audioTime=audio.currentTime;
      while(nextBeat<MARENOIA_TRANSIENTS.length&&
        MARENOIA_TRANSIENTS[nextBeat][0]<=audioTime){
        const [moment,strength]=MARENOIA_TRANSIENTS[nextBeat++];
        if(audioTime-moment<.22){
          if(progress>.11&&strength>.81&&moment-lastLightning>.18){
            lightning(progress,strength);
            lastLightning=moment;
          }
        }
      }
    }
    raf=requestAnimationFrame(tick);
    return {
      update(t){progress=t;},
      finish(){
        live=false;cancelAnimationFrame(raf);
        setTimeout(()=>lightningLayer.remove(),450);
      },
      dispose(){
        live=false;cancelAnimationFrame(raf);
        lightningLayer.remove();
      }
    };
  }
  const AFTER_DARK_KEY='kkpt:dark-afterglow:42';
  const DAWN_HALO_KEY='kkpt:dawn-cloudbreak:42';
  const MARENOIA_EIGHT_BEAT_KICKS=[
    35.311,38.843,42.375,45.907,49.429
  ];
  const MARENOIA_LOUDNESS=[
    .61,.71,.98,1,.85,.96,.92,.72,.57,.54,.45,.46,
    .53,.54,.56,.63,.92,1,.88,.95,.94,.77,.61,.61,
    .52,.41,.46,.48,.44,.50,.81,1,.92,.98,.90,.80,
    .63,.58,.54,.61,.65,.67,.65,.69,.83,1,.90,.93,
    .94,.81,.65,.65,.66,.65,.62,.64,.60,.58,.70,1,
    .91,.90,.95,.83,.62,.58,.46,.54,.47,.57,.55,.63
  ];
  function marenoiaLoudness(time){
    const pos=Math.max(0,Math.min(MARENOIA_LOUDNESS.length-1,
      (time-34.75)*4));
    const i=Math.floor(pos),j=Math.min(MARENOIA_LOUDNESS.length-1,i+1);
    return MARENOIA_LOUDNESS[i]*(1-(pos-i))+
      MARENOIA_LOUDNESS[j]*(pos-i);
  }
  function marenoiaDeepPulse(age){
    if(age<0||age>=2.98)return 0;
    if(age<.42)return smooth(age/.42);
    if(age<.72)return 1;
    return 1-smooth((age-.72)/2.26);
  }
  let kuronataAftermath=null;
  function mountKuronataAftermath(score){
    if(kuronataAftermath){
      if(score)kuronataAftermath.attach(score);
      return kuronataAftermath;
    }
    const layer=document.createElement('div');
    layer.id='kkpt-beat-scene';
    layer.setAttribute('aria-hidden','true');
    layer.className='kkpt-beat-after kkpt-beat-idle';
    layer.innerHTML='<div class="kkpt-beat-focus"></div>'+
      '<div class="kkpt-beat-lens"></div>'+
      '<div class="kkpt-beat-fringe"></div>'+
      '<div class="kkpt-beat-rupture"></div>'+
      '<svg class="kkpt-beat-scars" viewBox="0 0 1440 900" '+
      'preserveAspectRatio="none" aria-hidden="true">'+
      '<path d="M-20 340L126 317 203 355 294 339 383 382 477 352 605 395 728 365 842 399 982 371 1125 397 1264 351 1460 390" />'+
      '<path d="M-20 635L158 605 294 646 404 612 547 668 679 622 846 653 991 621 1149 668 1277 636 1460 660" />'+
      '<path d="M203 355l-19 33 27 39M842 399l30 34-20 47M404 612l-44 42 23 29M1149 668l39 41-18 32"/>'+
      '</svg>';
    root.append(layer);
    let live=true,raf=0,idleTimer=0,lastTick=0,source=null,lastKick=-100;
    let cameraReleaseTimer=null;
    let smoothPressure=0,index=0;
    const layerValues=Object.create(null),rootValues=Object.create(null);
    const setFx=(node,cache,name,value)=>{
      if(cache[name]===value)return;
      cache[name]=value;
      node.style.setProperty(name,value);
    };
    const setLayer=(name,value)=>setFx(layer,layerValues,name,value);
    const setRoot=(name,value)=>setFx(root,rootValues,name,value);
    const stopTicks=()=>{
      cancelAnimationFrame(raf);
      clearTimeout(idleTimer);
      idleTimer=0;
    };
    const queueTick=(delay=0)=>{
      if(!delay){raf=requestAnimationFrame(tick);return;}
      idleTimer=setTimeout(()=>{
        idleTimer=0;
        if(live)raf=requestAnimationFrame(tick);
      },delay);
    };
    const releaseCamera=()=>{
      setRoot('--kkpt-camera-zoom','1');
      clearTimeout(cameraReleaseTimer);
      cameraReleaseTimer=setTimeout(()=>{
        if(!live)root.classList.remove('kkpt-camera-running');
      },420);
    };
    const quiet=()=>{
      setLayer('--kkpt-beat-dark','.60');
      setLayer('--kkpt-beat-scale','1.025');
      setLayer('--kkpt-beat-blur','0.28px');
      setLayer('--kkpt-beat-glitch','.12');
      setLayer('--kkpt-beat-shear','0px');
      setLayer('--kkpt-beat-shear-reverse','0px');
      setLayer('--kkpt-beat-scars','.035');
      setRoot('--kkpt-camera-zoom','1');
      setRoot('--kkpt-camera-focus-y',
        (scrollY+innerHeight*.5).toFixed(1)+'px');
      setRoot('--kkpt-chroma-red','0px');
      setRoot('--kkpt-chroma-shadow','0px');
    };
    function tick(now){
      if(!live)return;
      if(now-lastTick<38){
        queueTick(Math.max(0,38-(now-lastTick)));
        return;
      }
      const dt=Math.min(.12,Math.max(.038,(now-lastTick)/1000));
      lastTick=now;
      if(!source||source.failed||source.audio.ended||
        source.audio.currentTime>=source.config.stopAt-.02){
        source=null;layer.classList.add('kkpt-beat-idle');
        quiet();live=false;stopTicks();
        releaseCamera();
        return;
      }
      const audioTime=source.audio.currentTime;
      while(index<MARENOIA_EIGHT_BEAT_KICKS.length&&
        MARENOIA_EIGHT_BEAT_KICKS[index]<=audioTime){
        lastKick=MARENOIA_EIGHT_BEAT_KICKS[index++];
      }
      const actualGain=Number(source.audio.volume);
      const playerGain=Number.isFinite(actualGain)
        ? Math.max(0,Math.min(1,actualGain/.78)):1;
      const soundStrength=(.48+.52*marenoiaLoudness(audioTime))*
        playerGain;
      const targetPressure=marenoiaDeepPulse(audioTime-lastKick)*
        soundStrength;
      const follow=1-Math.exp(-dt/.21);
      smoothPressure+=(targetPressure-smoothPressure)*follow;
      const pressure=smoothPressure;
      root.classList.add('kkpt-camera-running');
      setLayer('--kkpt-beat-dark',
        (.60+.38*pressure).toFixed(2));
      setLayer('--kkpt-beat-scale',
        (1.025-.175*pressure).toFixed(3));
      setLayer('--kkpt-beat-blur',
        (.28+2.6*pressure).toFixed(1)+'px');
      setLayer('--kkpt-beat-glitch',
        (.12+.63*pressure).toFixed(2));
      const shear=Math.sin(index*1.47)*pressure*9.0;
      setLayer('--kkpt-beat-shear',
        shear.toFixed(1)+'px');
      setLayer('--kkpt-beat-shear-reverse',
        (-shear*.86).toFixed(1)+'px');
      setLayer('--kkpt-beat-scars',
        (.035+.76*pressure).toFixed(2));
      setRoot('--kkpt-camera-zoom',
        (1.008+.054*pressure).toFixed(4));
      setRoot('--kkpt-camera-focus-y',
        (scrollY+innerHeight*.5).toFixed(1)+'px');
      setRoot('--kkpt-chroma-red',
        (-4.4*pressure).toFixed(1)+'px');
      setRoot('--kkpt-chroma-shadow',
        (5.6*pressure).toFixed(1)+'px');
      const nextKick=MARENOIA_EIGHT_BEAT_KICKS[index]??Infinity;
      if(pressure<.003&&targetPressure===0&&nextKick-audioTime>.22){
        queueTick(90);
      }else queueTick(38);
    }
    quiet();
    kuronataAftermath={
      attach(nextScore){
        source=nextScore;
        nextScore.aftermath=kuronataAftermath;
        stopTicks();
        clearTimeout(cameraReleaseTimer);
        live=true;lastTick=0;lastKick=-100;smoothPressure=0;
        index=0;
        layer.classList.remove('kkpt-beat-idle');
        raf=requestAnimationFrame(tick);
      },
      settle(){
        source=null;live=false;stopTicks();
        layer.classList.add('kkpt-beat-idle');quiet();
        releaseCamera();
      },
      fadeToDawn(){
        source=null;live=false;stopTicks();
        releaseCamera();
        layer.classList.add('kkpt-beat-outro');
        const old=layer;
        setTimeout(()=>{
          old.remove();
          if(kuronataAftermath?.layer===old)kuronataAftermath=null;
        },2400);
      },
      dispose(){
        source=null;live=false;stopTicks();
        clearTimeout(cameraReleaseTimer);
        root.classList.remove('kkpt-camera-running');
        root.style.removeProperty('--kkpt-camera-zoom');
        root.style.removeProperty('--kkpt-camera-focus-y');
        root.style.removeProperty('--kkpt-chroma-red');
        root.style.removeProperty('--kkpt-chroma-shadow');
        layer.remove();kuronataAftermath=null;
      },
      layer
    };
    if(score)kuronataAftermath.attach(score);
    return kuronataAftermath;
  }
  function createCloudbreakAfterglow(continueFromNavigation=false){
    const previous=document.getElementById('kkpt-cloudbreak');
    previous?.remove();
    let timestamp=Date.now();
    if(continueFromNavigation){
      try{timestamp=Number(sessionStorage.getItem(DAWN_HALO_KEY));}
      catch(_){return;}
      if(!Number.isFinite(timestamp)||timestamp<=0)return;
    }else{
      try{sessionStorage.setItem(DAWN_HALO_KEY,String(timestamp));}
      catch(_){}
    }
    const elapsed=Math.max(0,Date.now()-timestamp);
    if(elapsed>6900){
      try{sessionStorage.removeItem(DAWN_HALO_KEY);}catch(_){}
      return;
    }
    const bloom=document.createElement('div');
    bloom.id='kkpt-cloudbreak';
    bloom.setAttribute('aria-hidden','true');
    bloom.style.setProperty('--kkpt-dawn-delay',(-elapsed)+'ms');
    bloom.innerHTML='<i class="kkpt-dawn-dark-rim"></i>'+
      '<i class="kkpt-dawn-bright-rim"></i>'+
      '<i class="kkpt-dawn-cloud-sweep"></i>';
    root.append(bloom);
    setTimeout(()=>{
      bloom.remove();
      try{
        if(Number(sessionStorage.getItem(DAWN_HALO_KEY))===timestamp)
          sessionStorage.removeItem(DAWN_HALO_KEY);
      }catch(_){}
    },Math.max(0,7000-elapsed));
  }
  function createRebirthVisual(){
    const layer=document.createElement('div');
    layer.id='kkpt-rebirth-visual';
    layer.setAttribute('aria-hidden','true');
    const drizzle=document.createElement('div');
    drizzle.className='kkpt-rebirth-rain';
    const rainbow=document.createElementNS('http://www.w3.org/2000/svg','svg');
    rainbow.setAttribute('class','kkpt-rebirth-rainbow');
    rainbow.setAttribute('viewBox','0 0 1000 650');
    rainbow.setAttribute('preserveAspectRatio','none');
    const svgNS='http://www.w3.org/2000/svg';
    const defs=document.createElementNS(svgNS,'defs');
    const gradient=document.createElementNS(svgNS,'linearGradient');
    gradient.setAttribute('id','kkpt-rebirth-muted-arc');
    gradient.setAttribute('x1','0%');
    gradient.setAttribute('x2','100%');
    const haloColors=['#c7e5ff','#e4f7e6','#f7ebd9','#e7e0f5'];
    haloColors.forEach((color,i)=>{
      const stop=document.createElementNS(svgNS,'stop');
      stop.setAttribute('offset',(i*100/3).toFixed(1)+'%');
      stop.setAttribute('stop-color',color);
      gradient.append(stop);
    });
    defs.append(gradient);
    const arc=document.createElementNS(svgNS,'path');
    arc.setAttribute('d','M -100 625 Q 500 -175 1100 625');
    arc.setAttribute('fill','none');
    arc.setAttribute('stroke','url(#kkpt-rebirth-muted-arc)');
    arc.setAttribute('stroke-width','36');
    rainbow.append(defs,arc);
    const light=document.createElement('div');
    light.className='kkpt-rebirth-sunlight';
    const essence=document.createElement('div');
    essence.className='kkpt-rebirth-essence';
    const count=innerWidth<700?14:24;
    for(let i=0;i<count;i++){
      const mote=document.createElement('i');
      mote.className='kkpt-rebirth-mote';
      mote.style.left=(5+Math.random()*88).toFixed(1)+'%';
      mote.style.top=(22+Math.random()*73).toFixed(1)+'%';
      mote.style.animationDelay=(-Math.random()*2.8).toFixed(2)+'s';
      mote.style.animationDuration=(2.3+Math.random()*1.8).toFixed(2)+'s';
      essence.append(mote);
    }
    layer.append(drizzle,light,rainbow,essence);
    document.body.append(layer);
    let last=-1,alive=true;
    return {
      update(t){
        if(!alive||Math.abs(t-last)<.018&&t<1)return;
        last=t;
        layer.style.clipPath='inset(0 '+
          (Math.max(0,1-t)*100).toFixed(2)+'% 0 0)';
        drizzle.style.opacity=(.30*Math.pow(1-t,1.4)).toFixed(3);
        light.style.opacity=(Math.max(0,Math.min(1,(t-.12)/.67))*.47).toFixed(3);
        rainbow.style.opacity=(Math.max(0,Math.min(1,(t-.48)/.40))*.105).toFixed(3);
        essence.style.opacity=(Math.max(0,Math.min(1,(t-.22)/.55))*.72).toFixed(3);
      },
      finish(){
        alive=false;layer.classList.add('kkpt-rebirth-finish');
        setTimeout(()=>layer.remove(),1250);
      },
      dispose(){alive=false;layer.remove();}
    };
  }
  function clearReturnScore(score){
    if(!score)return;
    clearInterval(score.displayTimer);
    score.hud?.remove();
    score.beatScene?.dispose();
    score.aftermath?.settle();
    try{
      score.audio.pause();
      score.audio.removeAttribute('src');score.audio.load();
    }catch(_){}
    if(returnScore===score)returnScore=null;
    if(score.config?.id==='marenoia')removeScoreOmen();
  }
  function watchScore(score){
    const audio=score.audio,config=score.config;
    score.displayTimer=setInterval(()=>{
      if(audio.ended||audio.error||audio.currentTime>=config.stopAt-.018){
        clearReturnScore(score);return;
      }
      const elapsed=Math.max(0,audio.currentTime-config.startAt);
      const remaining=Math.max(0,config.stopAt-audio.currentTime);
      const fadeIn=config.fadeInSeconds?
        Math.min(1,elapsed/config.fadeInSeconds):1;
      const fadeOut=config.fadeSeconds?
        Math.min(1,remaining/config.fadeSeconds):1;
      const volume=.78*Math.min(fadeIn,fadeOut);
      if(Math.abs(audio.volume-volume)>.0008)audio.volume=volume;
    },60);
    audio.addEventListener('ended',()=>clearReturnScore(score),{once:true});
    audio.addEventListener('error',()=>{
      score.failed=true;clearReturnScore(score);
    },{once:true});
  }
  function createScore(config){
    const audio=new Audio(config.url);
    audio.preload='auto';audio.playsInline=true;
    audio.volume=0;audio.muted=userWantsMute();
    const score={audio,config,failed:false,blocked:false,
      displayTimer:null,hud:null,playPromise:null};
    returnScore=score;
    mountAudioToggle(score);
    watchScore(score);
    return score;
  }
  function armReturnScore(targetPage){
    const config=SCORES[targetPage];
    if(!config||targetPage===basePage)return null;
    if(returnScore&&!returnScore.failed&&
      returnScore.config.id===config.id)return returnScore;
    if(returnScore)clearReturnScore(returnScore);
    const score=createScore(config);
    try{score.audio.currentTime=config.startAt;}catch(_){}
    score.audio.addEventListener('loadedmetadata',()=>{
      if(score.audio.currentTime<config.startAt-.2){
        try{score.audio.currentTime=config.startAt;}catch(_){}
      }
    },{once:true});
    startScoreOmen(score);
    try{
      score.playPromise=Promise.resolve(score.audio.play()).then(()=>{
        return true;
      },err=>{
        console.warn('[KKPT] Soundtrack playback blocked:',err);
        score.failed=true;clearReturnScore(score);return false;
      });
    }catch(err){
      console.warn('[KKPT] Soundtrack playback failed:',err);
      score.failed=true;clearReturnScore(score);
      score.playPromise=Promise.resolve(false);
    }
    return score;
  }
  function awaitScoreTime(score,seconds){
    if(!score||score.failed)return Promise.resolve(false);
    return new Promise(resolve=>{
      const audio=score.audio;
      let timer=null,lastTime=audio.currentTime;
      let lastMovement=performance.now();
      let settled=false;
      const done=ok=>{
        if(settled)return;
        settled=true;clearInterval(timer);
        audio.removeEventListener('timeupdate',check);
        audio.removeEventListener('ended',check);
        audio.removeEventListener('error',check);
        resolve(ok);
      };
      const check=()=>{
        if(audio.currentTime>=seconds-.018){done(true);return;}
        if(score.failed||audio.error||audio.ended){done(false);return;}
        if(audio.currentTime>lastTime+.015){
          lastTime=audio.currentTime;lastMovement=performance.now();
        }
        if(performance.now()-lastMovement>20000)done(false);
      };
      audio.addEventListener('timeupdate',check);
      audio.addEventListener('ended',check);
      audio.addEventListener('error',check);
      timer=setInterval(check,30);
      check();
    });
  }
  function saveReturnScoreHandoff(score){
    if(!score||score.failed||score.audio.ended||
      score.audio.currentTime>=score.config.stopAt-.15)return;
    try{
      sessionStorage.setItem(SCORE_KEY,JSON.stringify({
        stamp:Date.now(),time:score.audio.currentTime,
        target:score.config.target,
        expires:Date.now()+115000,revision:42
      }));
      if(score.config.id==='marenoia'){
        sessionStorage.setItem(AFTER_DARK_KEY,'1');
      }
    }catch(err){
      console.warn('[KKPT] Cannot preserve soundtrack position:',err);
    }
  }
  function restoreReturnScoreHandoff(){
    if(window!==window.top)return false;
    let saved=null;
    try{
      saved=JSON.parse(sessionStorage.getItem(SCORE_KEY));
      sessionStorage.removeItem(SCORE_KEY);
    }catch(_){}
    if(!saved||saved.revision!==42||saved.target!==basePage||
      Date.now()>saved.expires)return false;
    const config=SCORES[saved.target];
    if(!config)return false;
    const currentTarget=()=>saved.time+
      (Date.now()-saved.stamp)/1000;
    if(currentTarget()>=config.stopAt-.15)return false;
    const score=createScore(config);
    const audio=score.audio;
    let attempted=false,blockedAt=null;
    function playFromPosition(){
      const requested=blockedAt===null?currentTarget():blockedAt;
      const position=Math.min(config.stopAt-.12,requested);
      if(position>=config.stopAt-.15){clearReturnScore(score);return;}
      try{audio.currentTime=Math.max(config.startAt,position);}catch(_){}
      Promise.resolve(audio.play()).then(()=>{
        blockedAt=null;
        score.blocked=false;updateAudioToggle(score);
      }).catch(err=>{
        console.info('[KKPT] Browser requires an audio click:',err);
        blockedAt=position;score.blocked=true;
        updateAudioToggle(score);
      });
    }
    score.resume=()=>{
      if(blockedAt!==null){
        try{audio.currentTime=blockedAt;}catch(_){}
      }
      return Promise.resolve(audio.play()).then(value=>{
        blockedAt=null;return value;
      });
    };
    const autoplay=()=>{
      if(attempted)return;attempted=true;
      playFromPosition();
    };
    audio.addEventListener('loadedmetadata',autoplay,{once:true});
    audio.load();
    setTimeout(()=>{if(audio.readyState>=1)autoplay();},1100);
    return true;
  }
  let active = false; // true only during the visual handoff; destination navigates natively
  let animating = false;
  let gearPending = false;
  let ready = false;
  let loading = null;
  let button, labelTitle, labelSub, labelIcon, frame;
  let otherTitle = otherPage + ' - Blockland Wiki';
  function whichPage() { return active ? otherPage : basePage; }
  function destination() { return active ? basePage : otherPage; }
  function isDark(page) { return page === 'Kuronata'; }
  function pathPage() {
    const url = new URL(location.href);
    if (url.pathname.startsWith('/wiki/')) {
      try { return decodeURIComponent(url.pathname.slice(6)).replace(/_/g, ' '); }
      catch (e) { return ''; }
    }
    return url.searchParams.get('title') || '';
  }
  function updateButton() {
    if (!button) return;
    const to = destination();
    button.href = to === basePage ? baseUrl : otherUrl.href;
    labelTitle.textContent = to.toUpperCase();
    labelSub.textContent = isDark(to) ? 'BURN THE SKY' : 'RECLAIM THE SKY';
    labelIcon.textContent = isDark(to) ? '✦' : '❄';
    button.classList.toggle('kkpt-dark', isDark(whichPage()));
    button.setAttribute('aria-label', isDark(to) ? '燃尽天空,进入 Kuronata' : '驱散黑暗,回到 Kuranoka');
  }
  function wikiLinkPage(anchor, doc) {
    try {
      const url = new URL(anchor.href, doc.baseURI);
      if (url.origin !== location.origin) return null;
      const action = url.searchParams.get('action');
      if (action && action !== 'view') return null;
      let title = url.searchParams.get('title');
      if (!title) {
        const matched = url.pathname.match(/^\/wiki\/([^/?#]+)\/?$/);
        if (matched) title = decodeURIComponent(matched[1]);
      }
      title = (title || '').replace(/_/g, ' ');
      return title === 'Kuranoka' || title === 'Kuronata' ? title : null;
    } catch (_) {
      return null;
    }
  }
  function decoratePortalLinks(doc, targetPage, changedRoots) {
    const content = doc.querySelector('#mw-content-text,.mw-parser-output');
    if (!content) return;
    const decorate=anchor=>{
      if(anchor.classList.contains('kkpt-fate-gear'))return;
      if (wikiLinkPage(anchor, doc) !== targetPage) return;
      anchor.classList.add('kkpt-portal-link');
      anchor.classList.toggle('kkpt-portal-red', targetPage === 'Kuronata');
      anchor.classList.toggle('kkpt-portal-blue', targetPage === 'Kuranoka');
      const caption = (anchor.textContent || '').trim();
      if (caption) anchor.dataset.kkptText = caption.slice(0, 120);
      anchor.setAttribute('aria-label', caption + ',切换至 ' + targetPage);
    };
    for(const node of changedRoots||[content]){
      if(node.matches?.('a[href]'))decorate(node);
      node.querySelectorAll?.('a[href]').forEach(decorate);
    }
  }
  function watchPortalLinks(doc, targetPage) {
    decoratePortalLinks(doc, targetPage);
    if (doc.__kkptPortalObserver) return;
    const root = doc.querySelector('#mw-content-text,.mw-parser-output');
    if (!root) return;
    let pending=false;
    const changed=new Set();
    const observer = new MutationObserver(records => {
      for(const record of records){
        for(const node of record.addedNodes){
          if(node.nodeType===1)changed.add(node);
          else if(node.parentElement?.matches('a[href]'))
            changed.add(node.parentElement);
        }
      }
      if(pending)return;
      pending=true;
      setTimeout(()=>{
        pending=false;
        const roots=[...changed];changed.clear();
        if(roots.length)decoratePortalLinks(doc,targetPage,roots);
      },60);
    });
    observer.observe(root, {childList:true,subtree:true});
    doc.__kkptPortalObserver = observer;
  }
  function isPlainClick(event) {
    return !event.defaultPrevented && event.button === 0 &&
      !event.ctrlKey && !event.metaKey && !event.altKey && !event.shiftKey;
  }
  function findAnchor(event, doc) {
    const target = event.target;
    return target && target.nodeType === 1 &&
      typeof target.closest === 'function'
      ? target.closest('a[href],button.kkpt-fate-gear') : null;
  }
  function portalClick(event, doc, targetPage, nextActive) {
    if (!isPlainClick(event)) return false;
    const anchor = findAnchor(event, doc);
    const gear=anchor?.classList.contains('kkpt-fate-gear')&&
      anchor.dataset.kkptTargetPage===targetPage;
    if (!anchor || (anchor.target && anchor.target !== '_self') ||
      anchor.hasAttribute('download') ||
      (!gear&&wikiLinkPage(anchor, doc)!==targetPage)) return false;
    event.preventDefault();
    if(animating||gearPending)return true;
    const score=armReturnScore(targetPage);
    if(targetPage==='Kuronata'&&score&&!score.beatScene){
      score.beatScene=createMarenoiaBeatScene(score);
    }
    startFatePrelude(anchor,targetPage);
    primeSound(); // Synchronous click activation unlocks Web Audio.
    if(gear) {
      beginFateSwap(anchor,nextActive);
    } else {
      requestSwap(nextActive);
    }
    return true;
  }
  function beginFateSwap(anchor,nextActive) {
    if(animating||gearPending||nextActive===active)return;
    gearPending=true;
    anchor.classList.add('kkpt-fate-gear-turning');
    requestAnimationFrame(()=>{
      gearPending=false;
      requestSwap(nextActive);
    });
    setTimeout(()=>anchor.classList.remove('kkpt-fate-gear-turning'),3300);
  }
  function fateGearShape() {
    const parts=[];
    for(let i=0;i<48;i++) {
      const a=i/48*Math.PI*2-Math.PI/2;
      const r=(i%4===0||i%4===3)?21:16.8;
      parts.push((i?'L':'M')+(24+Math.cos(a)*r).toFixed(2)+' '+
        (24+Math.sin(a)*r).toFixed(2));
    }
    return parts.join(' ')+' Z';
  }
  function mountFateGear(doc,targetPage) {
    if(doc.querySelector('.kkpt-fate-gear'))return;
    const content=doc.querySelector('#mw-content-text,.mw-parser-output');
    if(!content)return;
    const box=content.querySelector('table.infobox.vcard,table.infobox');
    const host=box?.caption ||
      box?.querySelector('th.infobox-above,th.infobox-title,th[colspan]')||
      doc.querySelector('#firstHeading .mw-page-title-main,'+
        '.mw-page-title-main,#firstHeading');
    if(!host)return;
    host.classList.add('kkpt-fate-host');
    const anchor=doc.createElement('button');
    anchor.type='button';
    anchor.className='kkpt-fate-gear';
    anchor.dataset.kkptTargetPage=targetPage;
    const gearLabel=targetPage==='Kuronata'
      ?'命运的齿轮 · 唤醒 Kuronata':'命运的齿轮 · 回到 Kuranoka';
    anchor.setAttribute('aria-label',gearLabel);
    anchor.innerHTML='<svg class="kkpt-fate-gear-svg" viewBox="0 0 48 48" '+
      'aria-hidden="true" focusable="false"><path d="'+fateGearShape()+
      '"/><circle cx="24" cy="24" r="8.5"/>'+
      '<circle cx="24" cy="24" r="3"/></svg>';
    host.appendChild(anchor);
  }
  function backgroundUrls(doc) {
    const urls = new Set();
    const selector = [doc.documentElement, doc.body];
    try {
      doc.querySelectorAll('.background-theme,[class*="background"],[style*="background"],#mw-content-text,.citizen-page-container')
        .forEach((el, i) => { if (i < 18) selector.push(el); });
    } catch (e) { /* use html/body */ }
    function add(value) {
      if (!value) return;
      const pattern = /url\(\s*(['"]?)(.*?)\1\s*\)/gi;
      let match;
      while ((match = pattern.exec(value))) {
        try { urls.add(new URL(match[2], doc.baseURI).href); } catch (e) { /* ignore */ }
      }
      if (/^https?:\/\/\S+\.(?:jpg|jpeg|png|webp|avif)(?:\?\S*)?$/i.test(value)) {
        try { urls.add(new URL(value, doc.baseURI).href); } catch (e) { /* ignore */ }
      }
    }
    selector.forEach(el => {
      if (!el) return;
      try {
        [null, '::before', '::after'].forEach(pseudo => {
          const style = doc.defaultView.getComputedStyle(el, pseudo);
          if (!style) return;
          add(style.backgroundImage);
          add(style.getPropertyValue('--bg-image'));
          add(style.getPropertyValue('--background-image'));
        });
        if (el.dataset) add(el.dataset.bgImage);
      } catch (e) { /* style not yet ready */ }
    });
    return [...urls];
  }
  function warmImage(url) {
    if (imagePool.has(url)) return imagePool.get(url).promise;
    const image = new Image();
    const promise = new Promise(resolve => {
      let finished = false;
      const timeout = setTimeout(() => done(false), 5000);
      function done(ok) {
        if (finished) return;
        finished = true;
        clearTimeout(timeout);
        resolve(ok);
      }
      image.onload = () => done(true);
      image.onerror = () => done(false);
      image.src = url;
      if (image.complete) done(image.naturalWidth > 0);
    }).then(ok => ok && image.decode
      ? Promise.race([image.decode().then(() => true).catch(() => true),sleep(900).then(() => true)])
      : ok);
    imagePool.set(url, { image, promise });
    return promise;
  }
  async function warmBackgrounds(doc) {
    const urls = backgroundUrls(doc);
    await Promise.allSettled(urls.map(warmImage));
  }
  function wireFrame() {
    try {
      const doc = frame.contentDocument;
      if (!doc) return;
      otherTitle = doc.title || otherTitle;
      doc.documentElement.classList.add('kkpt-frame-inactive');
      watchPortalLinks(doc, basePage);
      mountFateGear(doc, basePage);
      if (doc.__kkptFrameBridge) return;
      doc.__kkptFrameBridge = true;
      doc.addEventListener('click', event => {
        if (!active || !isPlainClick(event)) return;
        if (portalClick(event, doc, basePage, false)) return;
        const el = findAnchor(event, doc);
        if (!el || (el.target && el.target !== '_self')) return;
        const href = el.href;
        if (!href || href.startsWith('javascript:')) return;
        const next = new URL(href, doc.baseURI);
        const currentFrame = new URL(frame.contentWindow.location.href);
        if (next.origin === currentFrame.origin &&
          next.pathname === currentFrame.pathname &&
          next.search === currentFrame.search && next.hash) {
          history.replaceState(history.state, '', next.href);
          return;
        }
        event.preventDefault();
        navigateRealPage(next.href);
      }, true);
      doc.addEventListener('submit', event => {
        if (active && event.target instanceof doc.defaultView.HTMLFormElement) {
          event.target.target = '_top';
        }
      }, true);
    } catch (err) {
      console.warn('[KuranokaPageTransition] Frame link bridge unavailable:', err);
    }
  }
  function warmFrame() {
    if (loading) return loading;
    frame = document.createElement('iframe');
    frame.id = 'kkpt-preview';
    frame.src = otherUrl.href; // set BEFORE insertion, avoids extra initial history hop
    frame.title = otherPage + ' 预载页面';
    frame.tabIndex = -1;
    frame.setAttribute('aria-hidden', 'true');
    frame.setAttribute('loading', 'eager');
    frame.style.clipPath = hideClip;
    frame.style.visibility = 'hidden';
    const targetBackdrop=worldBackdrops[otherPage];
    if(targetBackdrop){
      frame.style.backgroundImage='url("'+targetBackdrop+'")';
      frame.style.backgroundPosition='center center';
      frame.style.backgroundSize='cover';
      frame.style.backgroundRepeat='no-repeat';
    }
    loading = new Promise(resolve => {
      let settled = false;
      let preparing = false;
      let poll = null;
      function finish(ok, reason) {
        if (settled) return;
        settled = true;
        clearInterval(poll);
        ready = ok;
        if (!ok) console.warn('[KuranokaPageTransition] Preview:', reason);
        resolve(ok);
      }
      async function inspect() {
        if (settled || preparing) return;
        try {
          const doc = frame.contentDocument;
          if (!doc || !doc.querySelector('#mw-content-text,.mw-parser-output')) return;
          preparing = true;
          clearInterval(poll);
          await sleep(320); // Background.js / wiki style scripts
          await Promise.race([warmBackgrounds(doc), sleep(3600)]);
          await sleep(120);
          await Promise.race([warmBackgrounds(doc), sleep(1300)]);
          frame.contentWindow.scrollTo(0, 0);
          wireFrame();
          finish(true);
        } catch (err) { finish(false, String(err)); }
      }
      frame.addEventListener('load', () => {
        if (!settled && !preparing) inspect();
      });
      frame.addEventListener('error', () => finish(false,'iframe 网络加载错误'));
      document.body.append(frame);
      poll = setInterval(inspect, 130);
      setTimeout(() => { if (!settled && !preparing) finish(false,'18 秒仍未找到目标正文'); }, 18000);
    });
    warmBackgrounds(document).catch(() => {});
    setTimeout(() => warmBackgrounds(document).catch(() => {}), 700);
    return loading;
  }
  let atmosphereAssetCache=null;
  const atmosphereNoiseCache=new Map();
  function createAtmosphere(canvas,buildOneOnly=false) {
    const ctx = canvas.getContext('2d', {alpha:true});
    const rand = n => {
      const cached=atmosphereNoiseCache.get(n);
      if(cached!==undefined)return cached;
      const x = Math.sin(n*127.1+311.7)*43758.5453123;
      const value=x-Math.floor(x);
      atmosphereNoiseCache.set(n,value);
      return value;
    };
    const clamp01 = n => Math.max(0,Math.min(1,n));
    function valueNoise(x,y,seed) {
      const ix=Math.floor(x), iy=Math.floor(y);
      let fx=x-ix,fy=y-iy;
      fx=fx*fx*(3-2*fx);fy=fy*fy*(3-2*fy);
      const h=(a,b)=>rand(a*171.3+b*513.7+seed*89.1);
      const a=h(ix,iy)*(1-fx)+h(ix+1,iy)*fx;
      const b=h(ix,iy+1)*(1-fx)+h(ix+1,iy+1)*fx;
      return a*(1-fy)+b*fy;
    }
    function smokeTexture(seed,dark) {
      const tex=document.createElement('canvas');
      tex.width=tex.height=112;
      const g=tex.getContext('2d');
      const data=g.createImageData(112,112);
      for(let y=0;y<112;y++) for(let x=0;x<112;x++){
        const u=(x-56)/56,v=(y-56)/56;
        const curl=valueNoise(u*2.9+11,v*2.9+1,seed);
        const curl2=valueNoise(u*3.4+3,v*3.4+7,seed+7);
        const wx=u+.47*(curl-.5),wy=v+.40*(curl2-.5);
        const radius=Math.sqrt(wx*wx*.91+wy*wy*1.13);
        const envelope=clamp01((1.05-radius)/.56);
        const low=valueNoise(u*4.1+4,v*4.1-9,seed+2);
        const high=valueNoise(u*11.4-3,v*11.4+6,seed+5);
        const fibers=valueNoise(u*22.0+1,v*16.0+5,seed+9);
        const density=clamp01((low*.62+high*.28+fibers*.10-.28)*2.0)*
          envelope*envelope;
        const i=(y*112+x)*4;
        const variation=low*28+high*15;
        if(dark){
          data.data[i]=5+variation*.37;
          data.data[i+1]=5+variation*.33;
          data.data[i+2]=8+variation*.38;
        }else{
          data.data[i]=44+variation;
          data.data[i+1]=91+variation;
          data.data[i+2]=125+variation;
        }
        data.data[i+3]=Math.round(240*density);
      }
      g.putImageData(data,0,0);
      return tex;
    }
    function glowTexture(seed,mode) {
      const tex=document.createElement('canvas');
      tex.width=96;tex.height=128;
      const g=tex.getContext('2d');
      const gradient=g.createRadialGradient(48,68,0,48,68,64);
      const colors=mode==='fire'
        ? [[0,'#fffef1'],[.14,'#fff4bb'],[.34,'#ffd04d'],
          [.55,'#ff841bcc'],[.80,'#bc290749'],[1,'#6e030000']]
        : mode==='blue'
        ? [[0,'#ffffff'],[.19,'#f1ffff'],[.40,'#a2efff'],
          [.68,'#5298ff99'],[1,'#345bf000']]
        : [[0,'#ffffff'],[.19,'#fff8cd'],[.49,'#ff9b28cc'],
          [1,'#ff380000']];
      colors.forEach(([p,c])=>gradient.addColorStop(p,c));
      g.fillStyle=gradient;g.fillRect(0,0,96,128);
      const img=g.getImageData(0,0,96,128);
      for(let y=0;y<128;y++)for(let x=0;x<96;x++){
        const u=(x-48)/48,v=(y-64)/64;
        const cloud=valueNoise(u*4+seed*1.9,v*4+seed*.7,seed+18);
        const fiber=valueNoise(u*11+3,v*8+17,seed+41);
        const shifted=u+.18*(cloud-.5);
        const irregular=clamp01((1.15-Math.abs(shifted))/.52)*
          clamp01((1.05-Math.abs(v+.12*(fiber-.5)))/.42);
        const n=.59+.77*cloud+.28*fiber;
        const alphaIndex=(y*96+x)*4+3;
        img.data[alphaIndex]=Math.round(img.data[alphaIndex]*
          irregular*clamp01(n));
      }
      g.putImageData(img,0,0);
      return tex;
    }
    const cache=atmosphereAssetCache ||= {
      sootSprites:[],mistSprites:[],FIRE_GLOW:[],BLUE_GLOW:[],
      EMBER_GLOW:null
    };
    function buildNextAsset(){
      let n=cache.sootSprites.length;
      if(n<4){cache.sootSprites.push(smokeTexture(n+17,true));return true;}
      n=cache.mistSprites.length;
      if(n<4){cache.mistSprites.push(smokeTexture(n+51,false));return true;}
      n=cache.FIRE_GLOW.length;
      if(n<4){cache.FIRE_GLOW.push(glowTexture(n+1,'fire'));return true;}
      n=cache.BLUE_GLOW.length;
      if(n<4){cache.BLUE_GLOW.push(glowTexture(n+8,'blue'));return true;}
      if(!cache.EMBER_GLOW){
        cache.EMBER_GLOW=glowTexture(21,'ember');return true;
      }
      return false;
    }
    if(buildOneOnly)return buildNextAsset();
    while(buildNextAsset()){}
    const {sootSprites,mistSprites,FIRE_GLOW,BLUE_GLOW,EMBER_GLOW}=cache;
    const combustion=[];
    const residualSoot=[];
    const tornPaper=[];
    let lastElapsed=0,spawnRemainder=0,sootRemainder=0,shardRemainder=0;
    function advanceCombustion(seconds,progress,dark,elapsed,w,h) {
      const dt=Math.min(.065,Math.max(0,elapsed-lastElapsed));
      lastElapsed=elapsed;
      const live=progress>.01&&progress<.985;
      if(live){
        const flow=(dark?132:64)*Math.min(1.5,h/720)*
          (.75+.25*Math.sin(Math.PI*progress));
        spawnRemainder+=dt*flow;
        const births=Math.min(65,Math.floor(spawnRemainder));
        spawnRemainder-=births;
        for(let j=0;j<births&&combustion.length<1320;j++){
          const y=Math.random()*h;
          const edge=edgeAt(y,seconds,progress,w);
          const r=Math.random();
          const kind=dark
            ? r<.38?'flame':r<.91?'ember':'ash'
            : r<.37?'glow':r<.81?'leaf':'pollen';
          const small=kind==='ember'||kind==='pollen';
          const soot=kind==='ash';
          const life=dark
            ? (soot?.7+Math.random()*1.1:
              small?.9+Math.random()*1.55:.42+Math.random()*.76)
            : 1.15+Math.random()*1.55;
          combustion.push({
            kind,x:edge+(dark?1:-1)*(Math.random()*47-8),
            y:y+(Math.random()-.5)*25,
            vx:(dark?1:-1)*(dark?(small?65+Math.random()*245:
              14+Math.random()*154):
              16+Math.random()*69),
            vy:-(dark?(small?12+Math.random()*190:
              32+Math.random()*171):12+Math.random()*57),
            life,remaining:life,
            radius:dark
              ? (soot?1.2+Math.random()*2.7:
                small?2+Math.random()*5:9+Math.random()*19)
              : kind==='glow'?26+Math.random()*49:
                kind==='leaf'?4+Math.random()*6:
                2+Math.random()*3,
            angle:(Math.random()-.5)*1.1,
            sprite:(Math.random()*4)|0,
            seed:Math.random()*6.28
          });
        }
        if(dark) {
          sootRemainder+=dt*(h<680?32:46);
          const sootBirths=Math.min(11,Math.floor(sootRemainder));
          sootRemainder-=sootBirths;
          for(let i=0;i<sootBirths&&residualSoot.length<(w<700?92:155);i++){
            const y=Math.random()*h;
            const life=2.15+Math.random()*1.60;
            residualSoot.push({
              x:edgeAt(y,seconds,progress,w)+5+Math.random()*83,
              y:y+(Math.random()-.5)*48,
              vx:8+Math.random()*39,vy:-18-Math.random()*83,
              size:155+Math.random()*195,life,remaining:life,
              sprite:(Math.random()*4)|0,
              spin:(Math.random()-.5)*.8,
              seed:Math.random()*6.28
            });
          }
          shardRemainder+=dt*(h<680?18:30);
          const shardBirths=Math.min(7,Math.floor(shardRemainder));
          shardRemainder-=shardBirths;
          for(let i=0;i<shardBirths&&tornPaper.length<175;i++){
            const y=Math.random()*h;
            const life=.90+Math.random()*1.85;
            const points=8+Math.floor(Math.random()*7);
            const outline=Array.from({length:points},(_,j)=>({
              a:Math.PI*2*j/points+(Math.random()-.5)*.50,
              r:.25+Math.random()*1.06
            }));
            tornPaper.push({
              x:edgeAt(y,seconds,progress,w)+(Math.random()-.20)*58,
              y, vx:55+Math.random()*295,
              vy:-190+Math.random()*270,
              gravity:90+Math.random()*150,
              radius:2+Math.pow(Math.random(),2.5)*22,
              angle:Math.random()*6.28,
              spin:(Math.random()-.5)*7,
              outline,life,remaining:life,
              seed:Math.random()*6.28
            });
          }
        }
      }
      for(let i=combustion.length-1;i>=0;i--){
        const p=combustion[i];
        p.remaining-=dt;
        if(p.remaining<=0){combustion.splice(i,1);continue;}
        p.x+=(p.vx+Math.sin(elapsed*(dark?5:1.6)+p.seed)*
          (dark?22:24))*dt;
        p.y+=p.vy*dt;
        p.vy-=(dark?17:7)*dt;
      }
      for(let i=residualSoot.length-1;i>=0;i--){
        const p=residualSoot[i];
        p.remaining-=dt;
        if(p.remaining<=0){residualSoot.splice(i,1);continue;}
        p.x+=(p.vx+Math.sin(elapsed*2+p.seed)*27)*dt;
        p.y+=p.vy*dt;
        p.size+=32*dt;
      }
      for(let i=tornPaper.length-1;i>=0;i--){
        const p=tornPaper[i];
        p.remaining-=dt;
        if(p.remaining<=0){tornPaper.splice(i,1);continue;}
        p.x+=(p.vx+Math.sin(elapsed*4+p.seed)*42)*dt;
        p.y+=p.vy*dt;
        p.vy+=p.gravity*dt;
        p.angle+=p.spin*dt;
      }
    }
    const particles=Array.from({length:innerWidth<700?40:62},(_,i)=>({
      seed:rand(i*4+71),height:rand(i*5+13),
      lifetime:.5+rand(i*8+37)*.7,
      phase:rand(i*9+21),sprite:i%4,
      scale:.72+rand(i*7+9)*1.25,
      bend:rand(i*15+23)*6.28
    }));
    const motes=Array.from({length:innerWidth<700?56:115},(_,i)=>({
      y:rand(i*3+17),phase:rand(i*9+43),
      speed:100+rand(i*12+5)*340,
      drift:82+rand(i*15+2)*330,
      length:3+rand(i*17+7)*16
    }));
    const shockDebris=Array.from({length:innerWidth<700?32:66},(_,i)=>({
      angle:rand(i*17+131)*Math.PI*2,
      velocity:190+rand(i*11+51)*900,
      seed:rand(i*5+79),
      length:6+rand(i*7+17)*26
    }));
    function pressureWave(x,y,radius,power,dark,time) {
      if(power<=.005)return;
      const core=ctx.createRadialGradient(x,y,0,x,y,radius*.75);
      if(dark){
        core.addColorStop(0,'rgba(255,231,169,'+(.20*power)+')');
        core.addColorStop(.17,'rgba(253,88,18,'+(.21*power)+')');
        core.addColorStop(.60,'rgba(106,16,12,'+(.09*power)+')');
        core.addColorStop(1,'rgba(30,0,0,0)');
      }else{
        core.addColorStop(0,'rgba(237,255,255,'+(.24*power)+')');
        core.addColorStop(.26,'rgba(98,193,255,'+(.18*power)+')');
        core.addColorStop(1,'rgba(0,29,100,0)');
      }
      ctx.globalCompositeOperation='screen';
      ctx.fillStyle=core;
      ctx.fillRect(x-radius,y-radius,radius*2,radius*2);
      ctx.globalCompositeOperation='source-over';
    }
    return function drawAtmosphere(seconds,progress,dark,elapsed,cells) {
      const w=innerWidth,h=innerHeight;
      const scale=dark?(w<700?.43:.55):(w<700?.35:.45);
      const bw=Math.max(1,Math.round(w*scale));
      const bh=Math.max(1,Math.round(h*scale));
      if(canvas.width!==bw||canvas.height!==bh){canvas.width=bw;canvas.height=bh;}
      ctx.setTransform(scale,0,0,scale,0,0);
      ctx.clearRect(0,0,w,h);
      const envelope=Math.sin(Math.PI*progress);
      const fade=clamp01(progress/.075)*clamp01((1-progress)/.065);
      advanceCombustion(seconds,progress,dark,elapsed,w,h);
      const sprites=dark?sootSprites:mistSprites;
      const direction=dark?1:-1;
      ctx.globalCompositeOperation='source-over';
      const bands=dark?Math.max(8,Math.ceil(h/116)):
        Math.max(6,Math.ceil(h/136));
      for(let i=0;i<bands;i++){
        const baseY=(i+.15+rand(i*11+39)*.70)*h/bands;
        const y=baseY+31*Math.sin(seconds*1.6+i*1.47);
        const x=edgeAt(y,seconds,progress,w)+direction*
          (dark?76+rand(i*19+6)*260:120)+
          Math.sin(seconds*.93+i*1.3)*37;
        const width=dark?170+rand(i*29+12)*220:250;
        const height=dark?165+rand(i*7+5)*170:245;
        ctx.save();
        ctx.globalAlpha=(dark?.34:.21)*fade*(.55+.45*envelope);
        ctx.translate(x,y);
        ctx.rotate(.27*Math.sin(i*2.1+seconds*.9));
        ctx.drawImage(sprites[i%4],-width/2,-height/2,width,height);
        ctx.restore();
      }
      for(let particleIndex=0;particleIndex<particles.length;
        particleIndex+=dark?1:2){
        const item=particles[particleIndex];
        const age=((elapsed/item.lifetime+item.phase)%1);
        const baseY=item.height*h;
        const y=baseY-age*(dark?230:128)+(Math.sin(elapsed*2+item.bend)*15);
        if(y< -230||y>h+230)continue;
        const frontier=edgeAt(baseY,seconds,progress,w);
        const x=frontier+direction*(age*(dark?225:135)+
          30*Math.sin(age*6.3+item.bend));
        const size=(dark?174:145)*item.scale*(.68+age*1.9);
        const opacity=(dark?.43:.26)*Math.sin(Math.PI*age)*fade*
          (.55+.45*envelope);
        if(opacity<=.005)continue;
        ctx.save();
        ctx.globalAlpha=opacity;
        ctx.translate(x,y);
        ctx.rotate((item.bend-3.14)*.14+age*.85*direction);
        ctx.scale(1.18,.84);
        ctx.drawImage(sprites[item.sprite],-size/2,-size/2,size,size);
        ctx.restore();
      }
      if(dark){
        ctx.save();
        ctx.globalCompositeOperation='source-over';
        for(const p of residualSoot){
          const age=1-p.remaining/p.life;
          const opacity=clamp01(age/.14)*
            Math.pow(1-age,.49)*(.66+.24*Math.sin(p.seed*3)**2)*fade;
          if(opacity<.009)continue;
          ctx.save();
          ctx.globalAlpha=opacity;
          ctx.translate(p.x,p.y);
          ctx.rotate(p.spin+age*.33*Math.sin(p.seed));
          ctx.scale(1.20,.92);
          const s=p.size*(1+age*.68);
          ctx.drawImage(sootSprites[p.sprite],-s/2,-s/2,s,s);
          ctx.restore();
        }
        ctx.restore();
      }else{
        ctx.save();
        ctx.globalCompositeOperation='screen';
        for(let n=0;n<2;n++){
          const yy=(n+.64)*h/2.25+Math.sin(seconds*.8+n*2)*30;
          const xx=edgeAt(yy,seconds,progress,w)-
            (88+55*Math.sin(n*2.1+seconds*.7));
          const radius=145+65*Math.sin(seconds*.7+n*1.7)**2;
          const bloom=ctx.createRadialGradient(xx,yy,8,xx,yy,radius);
          bloom.addColorStop(0,'rgba(250,255,255,.10)');
          bloom.addColorStop(.33,'rgba(164,237,255,.07)');
          bloom.addColorStop(.74,'rgba(117,198,255,.04)');
          bloom.addColorStop(1,'rgba(117,198,255,0)');
          ctx.globalAlpha=fade;
          ctx.fillStyle=bloom;
          ctx.fillRect(xx-radius,yy-radius,radius*2,radius*2);
        }
        ctx.restore();
      }
      for(const moment of (dark?[.10,.48]:[])){
        const age=(progress-moment)/.125;
        if(age<0||age>1)continue;
        const intensity=(1-age)*(1-age)*fade;
        ctx.globalCompositeOperation='screen';
        ctx.fillStyle=dark
          ? 'rgba(255,81,26,'+(.095*intensity)+')'
          : 'rgba(150,219,255,'+(.080*intensity)+')';
        ctx.fillRect(0,0,w,h);
        ctx.globalCompositeOperation='source-over';
        const x=edgeAt(h*.51,seconds,progress,w);
        const y=h*.51;
        const radius=95+age*Math.max(w,h)*.9;
        pressureWave(x,y,radius,intensity,dark,seconds);
        ctx.globalAlpha=(dark?.20:.28)*intensity;
        ctx.lineWidth=(dark?2.5:2)*Math.max(.4,1-age);
        ctx.strokeStyle=dark?'#ffb36c':'#b5eaff';
        ctx.beginPath();
        for(let i=0;i<=110;i++){
          const a=i/110*Math.PI*2;
          const jag=1+.03*Math.sin(a*29+seconds*12)+
            .05*Math.sin(a*13-seconds*5);
          const rx=Math.cos(a)*radius*jag,ry=Math.sin(a)*radius*jag;
          if(i===0)ctx.moveTo(x+rx,y+ry);else ctx.lineTo(x+rx,y+ry);
        }
        ctx.stroke();
        ctx.globalAlpha=1;
        if(dark){
         ctx.globalCompositeOperation='screen';
         for(const bit of shockDebris){
          const f=age*(.5+bit.seed*.5);
          const travel=bit.velocity*f;
          const px=x+Math.cos(bit.angle)*travel;
          const py=y+Math.sin(bit.angle)*travel;
          const vx=Math.cos(bit.angle)*bit.length*(1-age);
          const vy=Math.sin(bit.angle)*bit.length*(1-age);
          ctx.globalAlpha=(dark?.82:.68)*intensity*(.3+bit.seed*.7);
          ctx.strokeStyle=dark?'#ffdd9b':'#e0faff';
          ctx.lineWidth=.6+bit.seed*1.3;
          ctx.beginPath();
          ctx.moveTo(px,py);
          ctx.lineTo(px-vx,py-vy);
          ctx.stroke();
         }
        }
        ctx.globalCompositeOperation='source-over';
        ctx.globalAlpha=1;
      }
      if(dark){
        ctx.save();
        ctx.globalCompositeOperation='screen';
        const count=Math.ceil(h/17);
        for(let j=0;j<count;j++){
          const seed=rand(j*13+167);
          const y=(j+seed*.78)*11.5-5;
          const x=edgeAt(y,seconds,progress,w);
          if(x < -90 || x > w+90)continue;
          const ripple=Math.sin(seconds*(3.4+rand(j*11+4)*2.4)+
            j*2.09+seed*3.1);
          const heat=clamp01(.58+ripple*.34+
            .16*Math.sin(j*1.17-seconds*7.1));
          if(heat<.34)continue;
          const length=(22+rand(j*23+19)*75)*(.5+heat*.94);
          const bend=.18+rand(j*17+42)*.66;
          const tipX=x-length*(.76+.19*seed);
          const tipY=y-length*bend;
          const thick=(3.2+rand(j*31+11)*10.3)*(.5+heat);
          const gradient=ctx.createLinearGradient(x,y,tipX,tipY);
          gradient.addColorStop(0,'rgba(255,255,242,.99)');
          gradient.addColorStop(.28,'rgba(255,224,84,.96)');
          gradient.addColorStop(.59,'rgba(255,76,10,.64)');
          gradient.addColorStop(1,'rgba(182,17,6,0)');
          ctx.beginPath();
          ctx.moveTo(x+2,y+thick*.5);
          ctx.bezierCurveTo(
            x-length*.22,y-thick*.25,
            tipX+length*.36,tipY+thick*.68,tipX,tipY);
          ctx.bezierCurveTo(
            tipX+length*.32,tipY-thick*.22,
            x-length*.17,y-thick*.74,
            x+2,y-thick*.65);
          ctx.closePath();
          ctx.globalAlpha=Math.min(1,heat*1.08)*fade;
          ctx.fillStyle=gradient;
          ctx.shadowBlur=0;
          ctx.fill();
          if(heat>.66){
            ctx.globalAlpha=(heat-.54)*1.40*fade;
            ctx.strokeStyle='#fffce0';ctx.lineWidth=.9+heat*1.1;
            ctx.beginPath();ctx.moveTo(x,y);
            ctx.quadraticCurveTo(x-length*.3,y-length*.11,
              tipX+length*.19,tipY+thick*.18);
            ctx.stroke();
          }
        }
        ctx.restore();
      }
      ctx.save();
      ctx.beginPath();
      for(let y=-12,i=0;y<=h+12;y+=3.6,i++){
        const x=edgeAt(y,seconds,progress,w);
        if(i===0)ctx.moveTo(x,y);else ctx.lineTo(x,y);
      }
      ctx.lineCap='round';ctx.lineJoin='round';
      if(dark){
        ctx.strokeStyle='rgba(9,5,7,.81)';
        ctx.lineWidth=18;ctx.shadowBlur=0;
        ctx.stroke();
        ctx.strokeStyle='rgba(210,45,9,.53)';
        ctx.lineWidth=6;
        ctx.stroke();
      }else{
        ctx.strokeStyle='rgba(27,95,190,.22)';
        ctx.lineWidth=27;ctx.shadowColor='#56bdff';ctx.shadowBlur=17;
        ctx.stroke();
        ctx.strokeStyle='rgba(132,225,255,.51)';
        ctx.lineWidth=11;ctx.shadowColor='#8aeaff';ctx.shadowBlur=11;
        ctx.stroke();
        ctx.strokeStyle='rgba(249,255,255,.65)';
        ctx.lineWidth=2.7;ctx.shadowBlur=4;
        ctx.stroke();
      }
      ctx.restore();
      if(dark){
        ctx.save();
        ctx.globalCompositeOperation='screen';
        ctx.lineCap='round';
        for(let y=-8;y<h+8;y+=5.8){
          const intensity=clamp01(.58+
            .29*Math.sin(y*.054+seconds*3.7)+
            .21*Math.sin(y*.147-seconds*8.9)+
            .13*Math.sin(y*.337+seconds*16.4));
          if(intensity<.47)continue;
          const x=edgeAt(y,seconds,progress,w);
          const nextX=edgeAt(y+5.8,seconds,progress,w);
          ctx.globalAlpha=intensity*fade;
          ctx.lineWidth=1.4+intensity*4.6;
          ctx.shadowBlur=0;
          ctx.strokeStyle=intensity>.75?'#fff8d7':
            intensity>.58?'#ffd369':'#ff831b';
          ctx.beginPath();ctx.moveTo(x,y);
          ctx.lineTo(nextX,y+5.8);ctx.stroke();
        }
        ctx.restore();
        ctx.save();
        ctx.globalCompositeOperation='screen';
        ctx.shadowBlur=0;
        for (let i=0; i<cells.length; i++) {
          const c=cells[i];
          ctx.globalAlpha=(.42+.34*c.heat)*fade;
          ctx.strokeStyle=c.heat>.73?'#fff6bf':
            c.heat>.38?'#ffd166':'#ff621c';
          ctx.lineWidth=.8+Math.min(2.6,c.radius*.24);
          const n=c.points.length;
          ctx.beginPath();
          for (let j=0;j<n;j++) {
            if ((j+i*3)%5>2)continue;
            const a=c.points[j],b=c.points[(j+1)%n];
            ctx.moveTo(a[0],a[1]);ctx.lineTo(b[0],b[1]);
          }
          ctx.stroke();
        }
        ctx.restore();
        ctx.save();
        for(let j=0;j<Math.ceil(h/10);j++){
          const y=(j+rand(j*5+61)*.8)*10;
          const x=edgeAt(y,seconds,progress,w)-
            (5+rand(j*17+38)*24);
          const size=.5+rand(j*29+11)*2.5;
          const visible=.25+.59*rand(j*13+7);
          ctx.globalAlpha=visible*fade;
          ctx.fillStyle='#150b0a';
          ctx.beginPath();
          ctx.ellipse(x,y,size*.71,size,rand(j*7+31)*3.14,
            0,Math.PI*2);
          ctx.fill();
        }
        ctx.restore();
      }
      ctx.save();
      ctx.globalCompositeOperation='lighter';
      for(const p of combustion){
        if(p.kind==='ash'||p.kind==='leaf'||p.kind==='pollen')continue;
        const age=1-p.remaining/p.life;
        const opacity=clamp01(p.remaining/.22)*
          clamp01(age/.08)*(1-age*.30)*
          (p.kind==='ember'?.74:p.kind==='glow'?.13:.66)*fade*
          (dark?Math.exp(-Math.max(0,p.x-
            edgeAt(p.y,seconds,progress,w))/70):1);
        if(opacity<.012)continue;
        ctx.save();
        ctx.globalAlpha=opacity;
        ctx.translate(p.x,p.y);
        ctx.rotate(p.angle+Math.sin(elapsed*4+p.seed)*.13);
        const large=p.kind==='flame';
        const sx=p.radius*(p.kind==='glow'?1.7:large?.96:1.32)*(1+age*.32);
        const sy=p.radius*(p.kind==='glow'?1.4:large?1.78:1.35)*(1+age*.57);
        const sprite=p.kind==='flame'?FIRE_GLOW[p.sprite]:
          p.kind==='glow'?BLUE_GLOW[p.sprite]:EMBER_GLOW;
        ctx.drawImage(sprite,-sx,-sy,sx*2,sy*2);
        ctx.restore();
      }
      ctx.restore();
      if(!dark){
        ctx.save();
        ctx.globalCompositeOperation='screen';
        for(const p of combustion){
          if(p.kind!=='leaf'&&p.kind!=='pollen')continue;
          const age=1-p.remaining/p.life;
          const opacity=clamp01(age/.17)*clamp01(p.remaining/.55)*
            (p.kind==='leaf'?.47:.28)*fade;
          if(opacity<.012)continue;
          ctx.save();
          ctx.globalAlpha=opacity;
          ctx.translate(p.x+11*Math.sin(elapsed*1.6+p.seed),p.y);
          ctx.rotate(p.angle+age*1.65*Math.sin(p.seed*2.5));
          if(p.kind==='leaf'){
            const r=p.radius;
            ctx.shadowBlur=5;
            ctx.shadowColor='#bbf4ff';
            ctx.fillStyle=p.sprite%2?'#e6ffff':'#ffffff';
            ctx.beginPath();
            ctx.moveTo(0,-r*1.65);
            ctx.bezierCurveTo(r*.97,-r*.48,r*.85,r*.60,0,r*1.35);
            ctx.bezierCurveTo(-r*.92,r*.28,-r*.94,-r*.57,0,-r*1.65);
            ctx.fill();
            ctx.globalAlpha=opacity*.38;
            ctx.strokeStyle='#70c9ea';
            ctx.lineWidth=.65;
            ctx.beginPath();ctx.moveTo(0,-r*1.15);
            ctx.quadraticCurveTo(-r*.17,0,0,r*.95);ctx.stroke();
          }else{
            const r=p.radius;
            ctx.globalAlpha=opacity*.72;
            ctx.drawImage(BLUE_GLOW[p.sprite],-r*3,-r*3,r*6,r*6);
          }
          ctx.restore();
        }
        ctx.restore();
      }
      ctx.save();
      ctx.globalCompositeOperation='source-over';
      for(const p of combustion){
        if(p.kind!=='ash')continue;
        ctx.globalAlpha=clamp01(p.remaining/.25)*.7*fade;
        ctx.fillStyle='#190d10';
        ctx.save();ctx.translate(p.x,p.y);ctx.rotate(elapsed*2+p.angle);
        ctx.fillRect(-p.radius*.5,-p.radius*.5,p.radius,p.radius*1.8);
        ctx.restore();
      }
      ctx.restore();
      if(dark){
        ctx.save();
        ctx.globalCompositeOperation='source-over';
        for(const p of tornPaper){
          const age=1-p.remaining/p.life;
          const opacity=clamp01(age/.08)*clamp01(p.remaining/.4)*fade;
          if(opacity<.01)continue;
          ctx.save();
          ctx.globalAlpha=opacity;
          ctx.translate(p.x,p.y);
          ctx.rotate(p.angle);
          ctx.beginPath();
          p.outline.forEach((v,i)=>{
            const px=Math.cos(v.a)*v.r*p.radius;
            const py=Math.sin(v.a)*v.r*p.radius*.74;
            if(i===0)ctx.moveTo(px,py);else ctx.lineTo(px,py);
          });
          ctx.closePath();
          ctx.shadowBlur=7;
          ctx.shadowColor='#090506';
          ctx.fillStyle=p.seed>3?'#190d11':'#302025';
          ctx.fill();
          ctx.shadowBlur=0;
          ctx.lineWidth=.8+Math.sin(p.seed)**2*.8;
          ctx.strokeStyle=age<.48?'rgba(255,107,35,.48)':
            'rgba(49,20,19,.47)';
          ctx.stroke();
          if(p.seed>4.5&&age<.52){
            ctx.globalAlpha*=.45*(1-age);
            ctx.strokeStyle='#c5e7f7';
            ctx.beginPath();
            ctx.moveTo(-p.radius*.42,-p.radius*.16);
            ctx.quadraticCurveTo(0,p.radius*.25,p.radius*.28,0);
            ctx.stroke();
          }
          ctx.restore();
        }
        ctx.restore();
      }
      if(dark){ctx.save();
      ctx.globalCompositeOperation='screen';
      for(const mote of motes){
        const age=(elapsed*.74+mote.phase)%1;
        const yy=mote.y*h-age*mote.speed+Math.sin(age*12+mote.phase*8)*9;
        const x=edgeAt(yy,seconds,progress,w)+direction*age*mote.drift;
        if(x<-25||x>w+25||yy<-25||yy>h+25)continue;
        const opacity=(1-age)*fade*.89;
        ctx.globalAlpha=opacity;
        ctx.strokeStyle='#ffb45d';
        ctx.lineWidth=.8;
        ctx.beginPath();
        ctx.moveTo(x,yy);
        ctx.lineTo(x-direction*mote.length*.4,yy+mote.length);
        ctx.stroke();
      }
      ctx.restore();}
    };
  }
  let atmosphereWarmPromise=null;
  function prewarmAtmosphere() {
    if(atmosphereWarmPromise)return atmosphereWarmPromise;
    atmosphereWarmPromise=new Promise(resolve=>{
      const scratchCanvas=document.createElement('canvas');
      const schedule=()=>{
        if(typeof requestIdleCallback==='function')
          requestIdleCallback(step,{timeout:70});
        else setTimeout(step,24);
      };
      function step(){
        if(atmosphereAssetCache?.EMBER_GLOW){resolve();return;}
        createAtmosphere(scratchCanvas,true);
        if(atmosphereAssetCache?.EMBER_GLOW){resolve();return;}
        schedule();
      }
      setTimeout(schedule,40);
    });
    return atmosphereWarmPromise;
  }
  let atmosphereEdge=null;
  function edgeAt(y,seconds,progress,width){
    return atmosphereEdge
      ? atmosphereEdge(y)
      : makeEdge(progress,seconds,Boolean(window.__kkptVisualDark),width)(y);
  }
  let sprites=null;
  function makeSprites() {
    if (sprites) return sprites;
    function radial(stops) {
      const el=document.createElement('canvas');
      el.width=el.height=96;
      const c=el.getContext('2d');
      const g=c.createRadialGradient(48,48,0,48,48,48);
      stops.forEach(pair=>g.addColorStop(pair[0],pair[1]));
      c.fillStyle=g;c.fillRect(0,0,96,96);
      return el;
    }
    sprites={
      flame:radial([[0,'#fffbed'],[.18,'#fff2a0'],
        [.43,'#ffb42b'],[.70,'#ff360088'],[1,'#8f050000']]),
      ember:radial([[0,'#ffffff'],[.15,'#fff6a8'],
        [.44,'#ff9900cc'],[1,'#ef280000']]),
      smoke:radial([[0,'#080608dd'],[.30,'#100b10bb'],
        [.62,'#160f1677'],[1,'#08050800']]),
      sky:radial([[0,'#ffffff'],[.16,'#e6ffff'],
        [.46,'#94deffe0'],[1,'#4ab5ff00']]),
      mist:radial([[0,'#f2fdffb0'],[.4,'#abdffff0'],
        [1,'#a9e7ff00']])
    };
    return sprites;
  }
  function makeEdge(t,seconds,dark,w) {
    const center=dark
      ? w+105-(w+210)*smooth(t)
      : -105+(w+210)*smooth(t);
    const wobble=Math.sin(Math.PI*t);
    return y=>center+wobble*(
      17*Math.sin(y*.023+seconds*5.5)+
       9*Math.sin(y*.063-seconds*8.4)+
       4*Math.sin(y*.13+seconds*12)
    );
  }
  function createGpuEmbers(){
    const canvas=document.createElement('canvas');
    canvas.id='kkpt-webgl';
    canvas.setAttribute('aria-hidden','true');
    let gl,program,buffer;
    try{
      gl=canvas.getContext('webgl2',{
        alpha:true,antialias:false,depth:false,stencil:false,
        premultipliedAlpha:false,preserveDrawingBuffer:false,
        powerPreference:'low-power'
      });
      if(!gl)throw new Error('WebGL2 context unavailable');
      const vert=`#version 300 es
        precision mediump float;
        layout(location=0) in vec4 aSpark;
        uniform vec2 uSize;
        uniform float uScale;
        out float vOpacity;
        void main(){
          vec2 xy=aSpark.xy/uSize*2.0-1.0;
          gl_Position=vec4(xy.x,-xy.y,0.0,1.0);
          gl_PointSize=aSpark.z*uScale;
          vOpacity=aSpark.w;
        }`;
      const frag=`#version 300 es
        precision mediump float;
        in float vOpacity;
        out vec4 fragColor;
        void main(){
          vec2 p=gl_PointCoord*2.0-1.0;
          float r=length(p);
          if(r>=1.0)discard;
          float a=pow(1.0-r,1.65)*vOpacity;
          vec3 c=mix(vec3(1.0,.30,.035),
            vec3(1.0,.93,.57),smoothstep(.40,.92,1.0-r));
          fragColor=vec4(c,a);
        }`;
      function shader(type,source){
        const s=gl.createShader(type);
        gl.shaderSource(s,source);gl.compileShader(s);
        if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))
          throw new Error(gl.getShaderInfoLog(s));
        return s;
      }
      const vs=shader(gl.VERTEX_SHADER,vert);
      const fs=shader(gl.FRAGMENT_SHADER,frag);
      program=gl.createProgram();
      gl.attachShader(program,vs);gl.attachShader(program,fs);
      gl.linkProgram(program);
      gl.deleteShader(vs);gl.deleteShader(fs);
      if(!gl.getProgramParameter(program,gl.LINK_STATUS))
        throw new Error(gl.getProgramInfoLog(program));
      buffer=gl.createBuffer();
      gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
      gl.bufferData(gl.ARRAY_BUFFER,170*4*4,gl.DYNAMIC_DRAW);
      gl.enableVertexAttribArray(0);
      gl.vertexAttribPointer(0,4,gl.FLOAT,false,0,0);
      gl.useProgram(program);
      gl.enable(gl.BLEND);
      gl.blendFunc(gl.SRC_ALPHA,gl.ONE_MINUS_SRC_ALPHA);
      gl.disable(gl.DEPTH_TEST);
    }catch(err){
      console.warn('[KKPT] Ember-only WebGL disabled:',err);
      canvas.remove();
      return null;
    }
    document.body.append(canvas);
    const sizeUniform=gl.getUniformLocation(program,'uSize');
    const scaleUniform=gl.getUniformLocation(program,'uScale');
    const max=innerWidth<700?80:170;
    const data=new Float32Array(max*4);
    const sparks=[];
    let carry=0,disabled=false,lastW=0,lastH=0;
    canvas.addEventListener('webglcontextlost',event=>{
      event.preventDefault();disabled=true;
      window.__kkptRenderer='Canvas2D (GPU context lost)';
    },{once:true});
    const rand=(min,max)=>min+Math.random()*(max-min);
    function draw(edge,seconds,dt,fade,progress){
      if(disabled)return;
      const w=innerWidth,h=innerHeight;
      const scale=w<700?.67:.75;
      const cw=Math.round(w*scale),ch=Math.round(h*scale);
      if(cw!==lastW||ch!==lastH){
        canvas.width=cw;canvas.height=ch;
        gl.viewport(0,0,cw,ch);lastW=cw;lastH=ch;
      }
      gl.clearColor(0,0,0,0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      gl.useProgram(program);
      gl.uniform2f(sizeUniform,w,h);
      gl.uniform1f(scaleUniform,scale);
      carry+=dt*(w<700?27:56)*Math.max(0,fade);
      const births=Math.min(5,Math.floor(carry));carry-=births;
      for(let i=0;i<births&&sparks.length<max;i++){
        const y=rand(0,h),life=rand(.55,1.65);
        sparks.push({x:edge(y)+rand(-7,23),y,
          vx:rand(48,265),vy:rand(-195,-26),life,left:life,
          size:rand(3,13),alpha:rand(.24,.60)});
      }
      let count=0;
      for(let i=sparks.length-1;i>=0;i--){
        const p=sparks[i];
        p.left-=dt;
        if(p.left<=0){sparks[i]=sparks[sparks.length-1];sparks.pop();continue;}
        p.x+=p.vx*dt;p.y+=p.vy*dt;
        if(p.x < -30||p.x>w+30||p.y < -30||p.y>h+30)continue;
        const offset=count++*4;
        data[offset]=p.x;data[offset+1]=p.y;
        data[offset+2]=p.size;
        data[offset+3]=p.alpha*fade*Math.min(1,p.left/.24);
      }
      if(!count)return;
      gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
      gl.bufferSubData(gl.ARRAY_BUFFER,0,data.subarray(0,count*4));
      gl.drawArrays(gl.POINTS,0,count);
    }
    function dispose(){
      disabled=true;
      if(!gl.isContextLost()){
        gl.deleteBuffer(buffer);gl.deleteProgram(program);
      }
      canvas.remove();
    }
    return {canvas,draw,dispose};
  }
  function clipFrame(edge) {
    const w=innerWidth,h=innerHeight;
    const rows=window.__kkptVisualDark?
      Math.max(24,Math.ceil(h/30)):
      Math.max(16,Math.ceil(h/61));
    const pts=[];
    for(let i=rows;i>=0;i--) {
      const y=h*i/rows;
      pts.push(edge(y).toFixed(1)+'px '+y.toFixed(1)+'px');
    }
    frame.style.clipPath=clipSide==='right'
      ? 'polygon(100% 0,100% 100%,'+pts.join(',')+')'
      : 'polygon(0 0,0 100%,'+pts.join(',')+')';
  }
  function canvasFire(canvas,dark,duration,drawAtmosphere,done,gpuEmbers,score,extraVisuals) {
    const ctx=dark&&canvas
      ? canvas.getContext('2d',{alpha:true,desynchronized:true})
      : null;
    if(dark&&!ctx){done();return;}
    const art=dark?makeSprites():null;
    const w=innerWidth,h=innerHeight;
    if(ctx){
      const scale=Math.min(window.devicePixelRatio||1,w<650?.69:.80);
      canvas.width=Math.max(1,Math.round(w*scale));
      canvas.height=Math.max(1,Math.round(h*scale));
      ctx.setTransform(scale,0,0,scale,0,0);
    }
    const rand=(a,b)=>a+Math.random()*(b-a);
    const mobile=w<650;
    const maxCount=mobile?160:250;
    const p=[];
    const accum={flame:0,ember:0,shard:0,sky:0,mist:0,pollen:0};
    const rates=dark
      ? {flame:mobile?80:105,ember:mobile?72:96,
        shard:mobile?26:42}
      : {sky:mobile?50:80,mist:mobile?13:20,pollen:mobile?27:50};
    const started=performance.now();
    let previous=started;
    let lastPaint=0;
    function spawn(kind,edge,seconds) {
      if(p.length>=maxCount)return;
      const y=rand(-25,h+25);
      const x=edge(y);
      const type=kind;
      const smoke=type==='smoke';
      const shard=type==='shard';
      const ember=type==='ember'||type==='pollen';
      const blue=type==='sky'||type==='mist'||type==='pollen';
      let radius,life,vx,vy;
      if(shard){
        radius=rand(4,18);life=rand(.6,1.65);
        vx=rand(-130,65);vy=rand(-125,30);
      }else if(smoke){
        radius=rand(32,85);life=rand(1.1,2.5);
        vx=rand(-20,65);vy=rand(-88,-22);
      }else if(type==='mist'){
        radius=rand(45,100);life=rand(.9,1.8);
        vx=rand(-35,45);vy=rand(-75,-15);
      }else if(type==='flame'){
        radius=rand(20,57);life=rand(.3,.82);
        vx=rand(-100,30);vy=rand(-180,-45);
      }else{
        radius=ember?rand(2.5,9):rand(18,47);
        life=ember?rand(.45,1.1):rand(.35,.85);
        vx=rand(-95,75);vy=rand(-150,-25);
      }
      p.push({
        type,life,left:life,
        x:x+rand(smoke?12:shard?-35:-27,smoke?140:shard?36:64),
        y:y+rand(-14,14),vx,vy,radius,
        angle:rand(-Math.PI,Math.PI),spin:rand(-5,5),
        drift:rand(-.65,.65),blue
      });
    }
    function spawnAtRate(dt,edge,seconds,strength){
      for(const type in rates){
        accum[type]+=rates[type]*dt*strength;
        const n=Math.min(11,Math.floor(accum[type]));
        accum[type]-=n;
        for(let i=0;i<n;i++)spawn(type,edge,seconds);
      }
    }
    function advance(dt){
      for(let i=p.length-1;i>=0;i--) {
        const q=p[i];
        q.left-=dt;
        if(q.left<=0){p[i]=p[p.length-1];p.pop();continue;}
        q.x+=q.vx*dt;
        q.y+=q.vy*dt;
        q.vx+=q.drift*dt*35;
        q.vy-=10*dt;
        q.angle+=q.spin*dt;
      }
    }
    function spriteDraw(image,x,y,sx,sy,alpha){
      ctx.globalAlpha=alpha;
      ctx.drawImage(image,x-sx,y-sy,sx*2,sy*2);
    }
    function drawSmoke(fade){
      ctx.globalCompositeOperation='source-over';
      for(const q of p){
        if(q.type!=='smoke'&&q.type!=='mist')continue;
        const age=1-q.left/q.life;
        const radius=q.radius*(1+age*.98);
        const alpha=(q.type==='smoke'?.76:.32)*
          Math.min(1,age*5)*Math.min(1,q.left/.55)*fade;
        spriteDraw(q.type==='smoke'?art.smoke:art.mist,
          q.x,q.y,radius*1.7,radius*1.3,alpha);
      }
      ctx.globalAlpha=1;
    }
    function drawShards(fade) {
      if(!dark)return;
      ctx.save();
      ctx.globalCompositeOperation='source-over';
      for(const q of p){
        if(q.type!=='shard')continue;
        const age=1-q.left/q.life;
        const a=Math.min(1,q.left/.38)*(.90-age*.28)*fade;
        ctx.save();
        ctx.translate(q.x,q.y);
        ctx.rotate(q.angle);
        ctx.globalAlpha=a;
        const r=q.radius;
        ctx.beginPath();
        ctx.moveTo(-r*.9,-r*.3);
        ctx.lineTo(r*.7,-r*.75);
        ctx.lineTo(r*.48,r*.42);
        ctx.lineTo(-r*.35,r*.8);
        ctx.closePath();
        ctx.fillStyle=age<.3?'#371d22':'#140d13';
        ctx.fill();
        ctx.strokeStyle=age<.42?'#f57d3c':'#5a2c2b';
        ctx.lineWidth=age<.42?1.6:.75;
        ctx.stroke();
        ctx.restore();
      }
      ctx.restore();
    }
    function drawCore(edge,seconds,t,fade){
      const waviness=Math.sin(Math.PI*t);
      ctx.save();
      ctx.beginPath();
      const count=Math.max(35,Math.ceil(h/19));
      for(let i=0;i<=count;i++){
        const y=h*i/count;
        if(i===0)ctx.moveTo(edge(y),y);
        else ctx.lineTo(edge(y),y);
      }
      ctx.lineCap='round';
      ctx.lineJoin='round';
      if(dark){
        ctx.globalCompositeOperation='lighter';
        ctx.globalAlpha=fade;
        ctx.shadowColor='#ff3903';
        ctx.shadowBlur=25;
        ctx.strokeStyle='rgba(255,55,5,.82)';
        ctx.lineWidth=43;
        ctx.stroke();
        ctx.shadowBlur=16;
        ctx.strokeStyle='#ff9b17';
        ctx.lineWidth=20;
        ctx.stroke();
        ctx.shadowBlur=7;
        ctx.strokeStyle='#ffe45e';
        ctx.lineWidth=9;
        ctx.stroke();
        ctx.shadowBlur=4;
        ctx.strokeStyle='#fffcde';
        ctx.lineWidth=3.8;
        ctx.stroke();
      }else{
        ctx.globalAlpha=fade*.64;
        ctx.shadowColor='#8ccfff';
        ctx.shadowBlur=29;
        ctx.strokeStyle='rgba(139,219,255,.68)';
        ctx.lineWidth=28;
        ctx.stroke();
        ctx.shadowBlur=10;
        ctx.strokeStyle='#e7f8ff';
        ctx.lineWidth=9;
        ctx.stroke();
        ctx.shadowBlur=0;
        ctx.strokeStyle='white';
        ctx.lineWidth=2;
        ctx.stroke();
      }
      ctx.restore();
      if(dark){
        ctx.save();
        ctx.globalCompositeOperation='lighter';
        for(let y=-70;y<h+80;y+=48){
          const x=edge(y);
          const pulse=.65+.35*Math.sin(seconds*8.5+y*.083);
          spriteDraw(art.flame,x+rand(-8,30),y,77,70,
            fade*(.26+.16*pulse));
        }
        for(let y=-20;y<h+52;y+=36){
          const x=edge(y);
          const flicker=Math.sin(seconds*13+y*.19);
          const length=52+30*flicker+17*Math.sin(seconds*6-y*.11);
          const lean=15+18*Math.sin(seconds*9+y*.069);
          ctx.globalAlpha=.62*fade;
          ctx.fillStyle='#ff5705';
          ctx.beginPath();
          ctx.moveTo(x+18,y+19);
          ctx.bezierCurveTo(x-18,y+2,x-lean-24,y-length*.57,
            x-lean,y-length);
          ctx.bezierCurveTo(x+2,y-length*.57,x+13,y-12,x+18,y+19);
          ctx.fill();
          ctx.globalAlpha=.58*fade;
          ctx.fillStyle='#ffdb58';
          ctx.beginPath();
          ctx.moveTo(x+7,y+8);
          ctx.quadraticCurveTo(x-10,y-length*.33,
            x-lean*.8,y-length*.72);
          ctx.quadraticCurveTo(x+3,y-length*.30,x+8,y+8);
          ctx.fill();
          ctx.globalAlpha=.38*fade;
          ctx.fillStyle='#fffced';
          ctx.beginPath();
          ctx.moveTo(x+3,y+5);
          ctx.quadraticCurveTo(x-5,y-length*.2,
            x-lean*.5,y-length*.48);
          ctx.quadraticCurveTo(x+4,y-length*.19,x+5,y+5);
          ctx.fill();
        }
        ctx.restore();
      }else{
        ctx.save();
        ctx.globalCompositeOperation='lighter';
        for(let y=-50;y<h+55;y+=88){
          spriteDraw(art.sky,edge(y),y,89,109,.27*fade);
        }
        ctx.restore();
      }
    }
    function drawBrightParticles(fade) {
      ctx.save();
      ctx.globalCompositeOperation='lighter';
      for(const q of p){
        if(q.type==='smoke'||q.type==='mist'||q.type==='shard')continue;
        const age=1-q.left/q.life;
        const brightness=Math.min(1,q.left/.25)*(1-age*.25)*fade;
        let artName='flame',sx=q.radius,sy=q.radius;
        if(q.type==='ember'){artName='ember';sx*=1.2;sy*=1.2;}
        else if(q.type==='pollen'){artName='sky';sx*=1.15;sy*=1.15;}
        else if(q.type==='sky'){artName='sky';sx*=1.1;sy*=1.55;}
        else {sx*=.9;sy*=1.65;}
        spriteDraw(art[artName],q.x,q.y,sx,sy,
          brightness*(q.type==='ember'?.93:.78));
      }
      ctx.restore();
      ctx.globalAlpha=1;
      ctx.globalCompositeOperation='source-over';
    }
    function paint(now) {
      const t=score
        ? Math.max(0,Math.min(1,(score.audio.currentTime-
          score.config.fireStart)/
          (score.config.fireEnd-score.config.fireStart)))
        : Math.min(1,(now-started)/duration);
      extraVisuals?.update(t);
      const seconds=now/1000;
      const edge=makeEdge(t,seconds,dark,w);
      const paintStep=dark?(mobile?35:32):(mobile?52:41);
      if(now-lastPaint<paintStep && t<1){
        requestAnimationFrame(paint);return;
      }
      atmosphereEdge=edge;
      window.__kkptVisualDark=dark;
      clipFrame(edge);
      const dt=Math.min(.070,(now-previous)/1000);
      previous=now;lastPaint=now;
      const fade=Math.min(1,t/.055,(1-t)/.045);
      drawAtmosphere(seconds,Math.min(t,.925),dark,(now-started)/1000,[]);
      if(dark&&gpuEmbers)gpuEmbers.draw(edge,seconds,dt,fade,t);
      if(dark){
        advance(dt);
        if(t<1)spawnAtRate(dt,edge,seconds,Math.max(0,fade));
        ctx.clearRect(0,0,w,h);
        drawShards(fade);
        drawCore(edge,seconds,t,fade);
        drawBrightParticles(fade);
      }
      if(t<1)requestAnimationFrame(paint);
      else done();
    }
    requestAnimationFrame(paint);
  }
  function setDisplayed(nextActive) {
    active=nextActive;
    frame.classList.toggle('kkpt-active',active);
    document.body.classList.toggle('kkpt-other-active',active);
    frame.style.clipPath=active?'inset(0)':hideClip;
    frame.style.visibility=active?'visible':'hidden';
    frame.style.pointerEvents='';
    frame.tabIndex=active?0:-1;
    frame.setAttribute('aria-hidden',active?'false':'true');
    document.title=active?otherTitle:baseTitle;
    showTheme((active?otherPage:basePage)==='Kuronata');
    root.classList.toggle('kkpt-world-suspended',basePage==='Kuronata'&&active);
    try{
      frame.contentDocument?.documentElement.classList.toggle(
        'kkpt-frame-inactive',!active);
    }catch(_){}
    updateButton();
  }
  async function animateSwap(nextActive) {
    if(animating)return;
    animating=true;
    button.classList.add('kkpt-busy');
    labelSub.textContent='PREPARING THE OTHER SIDE...';
    const dark=(nextActive?otherPage:basePage)==='Kuronata';
    let score=returnScore?.config?.target===otherPage?
      returnScore:null;
    if(!loading)warmFrame();
    const preparation=Promise.all([
      Promise.race([loading,sleep(score?350:1100)]),
      score?prewarmAtmosphere():
        Promise.race([prewarmAtmosphere(),sleep(160)])
    ]);
    if(score){
      const started=await Promise.race([
        score.playPromise,sleep(6500).then(()=>false)
      ]);
      const onCue=started ? await Promise.all([
        preparation,awaitScoreTime(score,score.config.fireStart)
      ]).then(([,hit])=>hit) : false;
      if(!onCue){
        await preparation;
        clearReturnScore(score);
        score=null; // Preserve the exact V25 visuals on audio failure.
      }
    } else await preparation;
    removeScoreOmen();
    frame.style.visibility='visible';
    frame.classList.add('kkpt-animating');
    await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
    labelSub.textContent=dark?'THE SKY IS BURNING':'DAWN IS BREAKING';
    if(!dark)showTheme(false);
    frame.classList.remove('kkpt-active');
    frame.style.pointerEvents='none';
    frame.style.clipPath=active?'inset(0)':hideClip;
    const atmosphere=document.createElement('canvas');
    atmosphere.id='kkpt-atmosphere';
    atmosphere.setAttribute('aria-hidden','true');
    document.body.append(atmosphere);
    const drawAtmosphere=createAtmosphere(atmosphere);
    const canvas=dark?document.createElement('canvas'):null;
    if(canvas){
      canvas.id='kkpt-vfx';
      canvas.setAttribute('aria-hidden','true');
      document.body.append(canvas);
    }
    const gpuEmbers=dark?createGpuEmbers():null;
    const extraVisuals=dark?(score?.beatScene||null):createRebirthVisual();
    window.__kkptRenderer=dark
      ? gpuEmbers?'WebGL2 embers + original V25 Canvas fire':
        'Original V25 Canvas fire (WebGL fallback)'
      : 'Original V25 Canvas dawn';
    const duration=dark?3450:3100;
    if(!score)transitionSound(dark,duration);
    const curtainFinished=new Promise(resolve=>canvasFire(
      canvas,dark,duration,drawAtmosphere,resolve,gpuEmbers,
      score,extraVisuals));
    requestAnimationFrame(releaseFatePrelude);
    await curtainFinished;
    setDisplayed(nextActive);
    if(dark){
      try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
      mountKuronataAftermath(score);
    }else{
      kuronataAftermath?.fadeToDawn();
      createCloudbreakAfterglow();
    }
    extraVisuals?.finish();
    if(canvas){
      canvas.style.opacity='0';
      setTimeout(()=>canvas.remove(),210);
    }
    if(gpuEmbers){
      gpuEmbers.canvas.style.transition='opacity 280ms ease-out';
      gpuEmbers.canvas.style.opacity='0';
      setTimeout(()=>gpuEmbers.dispose(),320);
    }
    const tail=dark?2000:1320;
    atmosphere.style.transition='opacity '+tail+
      'ms ease-out, transform '+tail+'ms ease-out';
    await new Promise(resolve=>requestAnimationFrame(resolve));
    atmosphere.style.opacity='0';
    atmosphere.style.transform='translate3d(0,-22px,0) scale(1.035)';
    await sleep(tail+70);
    atmosphere.remove();
    animating=false;
    button.classList.remove('kkpt-busy','kkpt-quiet');
    saveReturnScoreHandoff(score);
    navigateRealPage(otherUrl.href);
  }
  function requestSwap(nextActive) {
    if (animating || nextActive === active) return;
    if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
      if(otherPage==='Kuronata'){
        try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
      }
      saveReturnScoreHandoff(returnScore);
      navigateRealPage(otherUrl.href);
      return;
    }
    animateSwap(nextActive).catch(err => {
      console.error('[KuranokaPageTransition] V42:',err);
      removeScoreOmen();
      clearFatePrelude();
      document.getElementById('kkpt-vfx')?.remove();
      document.getElementById('kkpt-atmosphere')?.remove();
      document.getElementById('kkpt-webgl')?.remove();
      document.getElementById('kkpt-rebirth-visual')?.remove();
      returnScore?.beatScene?.dispose();
      document.getElementById('kkpt-lightning-layer')?.remove();
      if (frame) frame.style.filter = '';
      if((nextActive?otherPage:basePage)==='Kuronata'){
        try{sessionStorage.setItem(AFTER_DARK_KEY,'1');}catch(_){}
      }
      saveReturnScoreHandoff(returnScore);
      navigateRealPage(nextActive ? otherUrl.href : baseUrl);
    });
  }
  function createButton() {
    if (!document.body || document.getElementById('kkpt-toggle')) return;
    button=document.createElement('a');
    button.id='kkpt-toggle';
    labelIcon=document.createElement('span');
    labelIcon.className='kkpt-icon';
    labelIcon.setAttribute('aria-hidden','true');
    const text=document.createElement('span');
    text.className='kkpt-text';
    labelTitle=document.createElement('span');
    labelSub=document.createElement('span');
    labelSub.className='kkpt-sub';
    text.append(labelTitle,labelSub);
    button.append(labelIcon,text);
    document.body.append(button);
    button.hidden=!SHOW_FLOATING_BUTTON;
    updateButton();
    watchPortalLinks(document, otherPage);
    mountFateGear(document,otherPage);
    const continuingScore=restoreReturnScoreHandoff();
    if(basePage==='Kuranoka')createCloudbreakAfterglow(true);
    if(basePage==='Kuronata'){
      let hadDarkHandoff=false;
      try{hadDarkHandoff=sessionStorage.getItem(AFTER_DARK_KEY)==='1';}
      catch(_){}
      if(continuingScore||hadDarkHandoff){
        const aftermath=mountKuronataAftermath(returnScore);
        aftermath.layer.classList.add('kkpt-beat-resumed');
      }
    }
    if(continuingScore)setTimeout(warmFrame,3000);
    else warmFrame();
    prewarmAtmosphere();
    preloadRebirthPad();
    button.addEventListener('click', event => {
      if (!isPlainClick(event)) return;
      event.preventDefault();
      armReturnScore(otherPage);
      primeSound();
      requestSwap(!active);
    });
  }
  document.addEventListener('click', event => {
    if (active) return;
    portalClick(event, document, otherPage, true);
  }, true);
  mw.hook('wikipage.content').add(() => {
    if (document.readyState !== 'loading') watchPortalLinks(document, otherPage);
  });
  window.addEventListener('pageshow', event => {
    if (!event.persisted) return;
    animating=false;
    document.getElementById('kkpt-vfx')?.remove();
    document.getElementById('kkpt-atmosphere')?.remove();
    document.getElementById('kkpt-rebirth-visual')?.remove();
    document.getElementById('kkpt-cloudbreak')?.remove();
    document.getElementById('kkpt-beat-scene')?.remove();
    kuronataAftermath?.dispose();
    document.getElementById('kkpt-lightning-layer')?.remove();
    if(frame){
      frame.style.filter='';
      setDisplayed(false);
      frame.classList.remove('kkpt-animating');
    }
    if(basePage==='Kuronata'){
      let entered=false;
      try{entered=sessionStorage.getItem(AFTER_DARK_KEY)==='1';}
      catch(_){}
      if(entered){
        const aftermath=mountKuronataAftermath(returnScore);
        aftermath.layer.classList.add('kkpt-beat-resumed');
      }
    }
    button?.classList.remove('kkpt-busy','kkpt-quiet');
  });
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded',createButton,{once:true});
  } else {
    createButton();
  }
}());