AbemaTV Screen Comment Scroller

AbemaTV のコメントをニコニコ風にスクロールさせます。

当前为 2018-12-07 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        AbemaTV Screen Comment Scroller
// @namespace   knoa.jp
// @description AbemaTV のコメントをニコニコ風にスクロールさせます。
// @include     https://abema.tv/*
// @version     2.10.3
// @grant       none
// ==/UserScript==

// console.log('AbemaTV? => hireMe()');
(function(){
  const SCRIPTNAME = 'ScreenCommentScroller';
  const DEBUG = false;/*
  [update]
  アベマの仕様変更に伴いコメント入力まわりを修正。

  [to do]
  コメ入力フォーム
  新着スクロール

  タイムシフトでのコメント対応(プレミアム限定?)

  たまにしか起きない?Chromeで起きやすい?
    ウィンドウ最大化に追随し切れてない?
    フルスクリーンの状態が狂うことが?

  連携ないし投稿ボタン押したら再度コメ欄にフォーカス当ててあげたい

  [to research]
  Windowsで画面最下部マウスで耐えられないので、最下部に1pxマウス休めエリアを作る?
    たぶん最上部でOK、だけどポインタ合わせにくいか
    ホバー中でもn秒経過で閉じるようにする?
  最小フォントサイズ可変対応

  [possible]
  4:3の時にずらせる?
  greasemonkey 4 系対応
  ユーザーブロックアイコン(秒数と差し替わる)のアニメーション
  ピクチャインピクチャはアベマが公式にやるべきだろうけど、やらないままブラウザが任意要素に対応したら実装しようか

  [requests]
  設定のナビゲーションに「マウスを近づけたら表示する」
  設定のスクロールコメントに「画面下部の専用領域に流す」「高さ(%)」
  設定の一覧コメントに「コメントをひとつずつ表示する」
  一覧コメントの横幅「%」以外も指定可能に

  [not to do]
  新着コメント緑ボタン後の表示は現状簡単にはアニメーションさせられない
  */
  if(window === top && console.time) console.time(SCRIPTNAME);
  const CONFIGS = {
    /* スクロールコメント */
    maxlines:        {TYPE: 'int',    DEFAULT: 10  },/*最大行数(文字サイズ連動)*/
    linemargin:      {TYPE: 'float',  DEFAULT: 0.20},/*行間(比率)*/
    transparency:    {TYPE: 'float',  DEFAULT: 75.0},/*透明度(%)*/
    owidth:          {TYPE: 'float',  DEFAULT: 0.10},/*縁取りの太さ(比率)*/
    duration:        {TYPE: 'float',  DEFAULT: 5.00},/*横断にかける秒数*/
    maxcomments:     {TYPE: 'int',    DEFAULT: 100 },/*最大同時表示数*/
    font:            {TYPE: 'string', DEFAULT: ''  },/*フォント指定*/
    /* 一覧コメント */
    l_hide:          {TYPE: 'bool',   DEFAULT: 1   },/*操作していない時は画面外に隠す*/
    l_overlay:       {TYPE: 'bool',   DEFAULT: 1   },/*映像に重ねる*/
    l_showtime:      {TYPE: 'bool',   DEFAULT: 1   },/*投稿時刻を表示する*/
    l_width:         {TYPE: 'float',  DEFAULT: 16.5},/*横幅(%)*/
    lc_maxlines:     {TYPE: 'int',    DEFAULT: 30  },/*最大行数(文字サイズ連動)*/
    lc_linemargin:   {TYPE: 'float',  DEFAULT: 0.50},/*改行されたコメントの行間(比率)*/
    lc_margin:       {TYPE: 'float',  DEFAULT: 1.65},/*コメント同士の間隔(比率)*/
    lc_transparency: {TYPE: 'float',  DEFAULT: 25.0},/*文字の透明度(%)*/
    lb_transparency: {TYPE: 'float',  DEFAULT: 75.0},/*背景の透明度(%)*/
    /* アベマのナビゲーション */
    n_clickonly:     {TYPE: 'bool',   DEFAULT: 1   },/*画面クリック時のみ表示する*/
    n_delay:         {TYPE: 'float',  DEFAULT: 4.00},/*隠れるまでの時間(秒)*/
    n_transparency:  {TYPE: 'float',  DEFAULT: 50.0},/*透明度(%)*/
  };
  const PANELS = ['configPanel', 'ngList', 'ngHelp'];/*パネルの表示順*/
  const AINTERVAL = 7000;/*AbemaTVのコメント取得間隔の基本値(ms)*/
  const STATSUPDATE = 1000*60;/*視聴数とコメント数を更新する間隔(ms)*/
  const FONT = 'Arial, sans-serif';/*スクロールフォント*/
  const BASELINE = 85/100;/*フォントのbaseline比率*/
  const MARGIN = 2/10;/*通常のフォントサイズを飛び出す xgÅ(永◆∬∫√ ̄ などの文字を確実に収めるための余裕(比率)*/
  /* サイト定義 */
  let retry = 10;/*必要な要素が見つからずあきらめるまでの試行回数*/
  let site = {
    targets: [
      /* 構造 */
      function header(){let header = $('body > div > div > header'); return (header) ? site.use(header) : false;},
      function footer(){let fullscreen = $('button[aria-label^="フルスクリーン"]'); return (fullscreen) ? site.use(fullscreen.parentNode.parentNode) : false;},
      function board(){let board = $('[class*="OnReachTop"]'); return (board) ? site.use(board) : false;},
      function screen(){let loading = $('img[src="/images/misc/feed_loading.gif"]'); return (loading) ? site.use(loading.parentNode.parentNode.parentNode.parentNode) : false;},
      /* ペイン */
      function commentPane(){let form = $('form:not([role="search"])'); return (form) ? site.use(form.parentNode.parentNode) : false;},
      function channelPane(){let container = $('[class*="-tv-VChannelList__container"]'); return (container) ? site.use(container.parentNode) : false;},
      function programPane(){let container = $('[class*="-tv-VChannelList__container"]'); return (container) ? site.use(container.parentNode.nextElementSibling) : false;},
      /* ボタン */
      function channelButtons(){let button = $('button[aria-label="放送中の裏番組"]'); return (button) ? site.use(button.parentNode.parentNode) : false;},
      function channelButton(){let button = $('button[aria-label="放送中の裏番組"]'); return (button) ? site.use(button) : false;},
      function commentButton(){let svg = $('use[*|href^="/images/icons/comment.svg"]'); return (svg) ? site.use(svg.parentNode.parentNode) : false;},
      function programButton(){let button = $('button[aria-label^="フルスクリーン"] + div + div > div'); return (button) ? site.use(button) : false;},
      function fullscreenButton(){let fullscreen = $('button[aria-label^="フルスクリーン"]'); return (fullscreen) ? site.use(fullscreen) : false;},
      function VolumeController(){let mute = $('button[aria-label^="音声"]'); return (mute) ? site.use(mute.parentNode.parentNode) : false;},
      /* 要素 */
      function enquete(){let container = $('[class*="-tv-VChannelList__container"]'); return (container) ? site.use(container.parentNode.nextElementSibling.nextElementSibling) : false;},
      function caution(){let header = $('header'); return (header) ? site.use(header.nextElementSibling) : false;},
      function commentForm(){let form = $('form:not([role="search"])'); return (form) ? site.use(form) : false;},
      function viewCounter(){let viewCounter = $('[class*="tv-ViewCounter__"]'); return (viewCounter) ? site.use(viewCounter) : false;},
      function loading(){let loading = $('img[src="/images/misc/feed_loading.gif"]'); return (loading) ? site.use(loading.parentNode) : false;},
      function programName(){let name = $('[data-selector="programButton"] > div > div > div > div + div > p > span > span:last-child'); return (name) ? site.use(name) : false;},
    ],
    addedNode: {
      newCommentsButton: function(node){let button = node.parentNode.querySelector('[data-selector="commentPane"] > div > button'); return (button) ? site.use(node, 'newCommentsButton') : false;},
      comment: function(node){let commentText = node.querySelector('div:not([data-selector]) > p:first-child'); return (commentText) ? site.use(node, 'comment') : false;},
      progressbar: function(node){let circle = node.querySelector('svg circle'); return (circle) ? site.use(circle.parentNode.parentNode, 'progressbar') : false;},
    },
    get: {
      commentText: function(comment){return comment.firstElementChild.firstElementChild.textContent;},
      commentTime: function(comment){return comment.querySelector('time').dateTime;},
      commentBlock: function(comment){return comment.querySelector('button[title="ブロック"]');},
      commentBlockCancel: function(comment){return comment.querySelector('form button');},
      view: function(viewCounter){return viewCounter.querySelector('[data-selector="viewCounter"] > span');},
      comment: function(commentButton){return commentButton.querySelector('[data-selector="commentButton"] > span');},
      closer: function(){
        /* チャンネル切り替えごとに差し替わるのでつど取得 */
        let button = $('[data-selector="screen"] > div > div > button');
        return button;
      },
      statsApi: function(){
        /* アベマの仕様に依存しまくり */
        if(!window.dataLayer) return;
        const API = 'https://api.abema.io/v1/broadcast/slots/{id}/stats';
        for(let i = window.dataLayer.length - 1; window.dataLayer[i]; i--){
          if(window.dataLayer[i].slotId) return API.replace('{id}', window.dataLayer[i].slotId);
        }
      },
    },
    cmNow: function(){
      return (elements.programName && elements.programName.textContent === '');
    },
    use: function use(target = null, key = use.caller.name){
      if(target) target.dataset.selector = key;
      elements[key] = target;
      return target;
    },
  };
  /* 処理本体 */
  let html, elements = {}, ngwords = [], configs = {};
  let canvas, context, lines = [];/*アニメーション関連は極力浅いオブジェクトに*/
  let core = {
    initialize: function(){
      html = document.documentElement;
      core.config.read();
      core.ng.initialize();
      core.listenUserActions();
      core.checkUrl();
    },
    checkUrl: function(){
      let previousUrl = '';
      setInterval(function(){
        if(location.href === previousUrl) return;/*URLが変わってない*/
        if(location.href.startsWith('https://abema.tv/now-on-air/')){/*テレビ視聴ページ*/
          if(previousUrl.startsWith('https://abema.tv/now-on-air/')){/*チャンネルを変えただけ*/
            html.classList.remove('comment');
            html.classList.remove('ng');
            elements.closer = site.get.closer();
          }else{/*テレビ視聴ページになった*/
            core.ready();
          }
        }else{/*テレビ視聴ページではない*/
          core.gone();
        }
        previousUrl = location.href;
      }, 1000);
    },
    ready: function(){
      /* 必要な要素が出揃うまで粘る */
      for(let i = 0, target; target = site.targets[i]; i++){
        if(target() === false){
          if(!retry) return log(`Not found: ${target.name}, I give up.`);
          log(`Not found: ${target.name}, retrying...`);
          return retry-- && setTimeout(core.ready, 1000);
        }
      }
      elements.closer = site.get.closer();
      log("I'm Ready.");
      /* すべての要素が出揃っていたので */
      core.setupFullscreenButton();
      core.createCanvas();
      core.listenComments();
      core.ng.createButton();
      core.config.createButton();
      core.panel.createPanels();
      core.addStyle();
      html.classList.add(SCRIPTNAME);
      core.observeCommentButton();
    },
    gone: function(){
      if(elements.style && elements.style.isConnected) document.head.removeChild(elements.style);
      html.classList.remove(SCRIPTNAME);
      html.classList.remove('comment');
    },
    setupFullscreenButton: function(){
      let full_screen = elements.fullscreenButton.querySelector('use');
      let mini_screen = createElement(core.html.mini_screen());
      full_screen.parentNode.appendChild(mini_screen);
      full_screen.parentNode.outerHTML = full_screen.parentNode.outerHTML;/*svgバグ回避*/
      elements.fullscreenButton.dataset.icon = 'full_screen';
    },
    observeCommentButton: function(){
      /* コメントを開けるようになったら自動で開く */
      let url = null;
      let observer = observe(elements.commentButton, function(records){
        if(elements.commentPane.attributes['aria-hidden'].value === 'false') return;/*既に表示中*/
        if(getComputedStyle(elements.commentButton).cursor !== 'pointer') return;/*まだクリックできない*/
        if(url !== location.href){/*チャンネル切り替え後の初回*/
          elements.commentButton.click();
          url = location.href;
        }else if(html.classList.contains('comment')){/*コメントを開いた状態で番組開始を迎えたとき*/
          core.closeOpenCommentPane();
        }
      }, {attributes: true});
    },
    closeOpenCommentPane: function(){
      /* コメントが閉じられたと認識されたら即開き直す準備 */
      let observer = observe(elements.commentPane, function(records){
        if(elements.commentPane.attributes['aria-hidden'].value === 'false') return;
        observer.disconnect();/*一度だけ*/
        elements.commentButton.click();
        elements.commentPane.classList.remove('keep');
        canvas.classList.remove('keep');
      }, {attributes: true});
      /* ユーザーには閉じたように見せない */
      canvas.classList.add('keep');
      elements.commentPane.classList.add('keep');
      elements.closer.click();
    },
    updateStats: function(){
      /* mはアベマの仕様に合わせて小文字。しかし小数第1位は0も表示する。 */
      let formatNumber = function(number){
        switch(true){
          case(number < 1e3): return (number);
          case(number < 1e6): return (number/1e3).toFixed(1) + 'k';
          default:            return (number/1e6).toFixed(1) + 'm';
        }
      };
      let api = site.get.statsApi();
      if(!api) return log('Failed: site.get.statsApi.');
      let xhr = new XMLHttpRequest();
      xhr.open('GET', api);
      xhr.responseType = 'json';
      xhr.onreadystatechange = function(){
        if(xhr.readyState !== 4 || xhr.status !== 200) return;
        if(!xhr.response.stats || !xhr.response.stats.view || !xhr.response.stats.comment) return log(`Not found: stats`);
        //log('xhr.response:', xhr.response);
        site.get.view(elements.viewCounter).textContent         = formatNumber(xhr.response.stats.view);
        site.get.comment(elements.commentButton).textContent = formatNumber(xhr.response.stats.comment);
      };
      xhr.send();
    },
    listenUserActions: function(){
      let id, timer = function(e){
        clearTimeout(id), id = setTimeout(function(){
          if(['input', 'textarea'].includes(document.activeElement.loaclName)) return;/*入力中はアクティブのまま*/
          html.classList.remove('active');
          if(!configs.l_overlay && configs.l_hide) core.modify();
        }, configs.n_delay * 1000);
      };
      window.addEventListener('keydown', function(e){
        switch(e.key){
          /*テキスト入力中の上下キーによるチャンネル移動を防ぐ*/
          case('ArrowUp'):
          case('ArrowDown'):
            if(['input', 'textarea'].includes(e.target.localName)){
              e.stopPropagation();
            }
            break;
        }
      }, true);
      window.addEventListener('mousemove', function(e){
        if(configs.n_clickonly) return;
        if(!html.classList.contains('active')){
          html.classList.add('active');
          if(!configs.l_overlay && configs.l_hide) animate(core.modify);
        }
        timer();
      });
      window.addEventListener('click', function(e){/*アベマより先にwindowでキャプチャ*/
        switch(e.target){
          case(elements.channelButton):
            return html.classList.toggle('channel');
          case(elements.programButton):
            return html.classList.toggle('program');
          case(elements.commentButton):
            if(html.classList.contains('comment')){
              animate(function(){elements.closer.click()});/*すぐクリックすると競合してしまうのでanimate()*/
            }else{
              html.classList.add('comment');
              if(!configs.l_overlay) core.modify();
              /* デフォルトのボタン動作が実行される */
            }
            return;
          case(elements.newCommentsButton):
            if(e.isTrusted){/*実クリックのみで処理*/
              elements.newCommentsButton.style.height = '0';
              /* スクロールをなめらかにする */
              let scrollTop = elements.board.parentNode.scrollTop;
              if(scrollTop){
                elements.board.style.transition = '500ms ease';
                elements.board.style.transform = `translateY(${scrollTop}px)`;
                elements.board.addEventListener('transitionend', function(e){
                  elements.board.style.transition = 'none';
                  elements.board.style.transform = 'translateY(0)';
                  elements.newCommentsButton.click();
                }, {once: true});
              }else{
                elements.newCommentsButton.click();
              }
              e.stopPropagation();
            }else{
              /* デフォルトのボタン動作が実行される */
            }
            return;
          case(elements.fullscreenButton):
            if(!document.fullscreen){
              document.body.requestFullscreen();
            }else{
              document.exitFullscreen();
            }
            e.stopPropagation();
            return;
          case(elements.closer):
            if(html.classList.contains('comment')) core.ng.closeForm();/*NGフォームを開いているなら閉じる*/
            if(elements.commentPane.classList.contains('keep')) return html.classList.remove('comment');/*core.closeOpenCommentPaneですぐまた開かれる*/
            switch(true){
              case(html.classList.contains('channel')):
                html.classList.remove('channel');
                return e.stopPropagation();
              case(html.classList.contains('program')):
                html.classList.remove('program');
                return e.stopPropagation();
              default:
                if(e.isTrusted){/*実クリックではコメントは閉じない*/
                  e.stopPropagation();
                  if(elements.commentPane.classList.contains('active')) return;/*コメントフォームからフォーカスを外すだけ*/
                  html.classList.add('click');/*250msのtransition遅延をなくしてからキビキビactivate*/
                  animate(function(){
                    html.classList.toggle('active');
                    elements.header.addEventListener('transitionend', function(e){
                      html.classList.remove('click');
                    }, {once: true});
                    if(!configs.l_overlay) core.modify();
                  });
                  timer();
                }else{/*elements.closer.click()でのみ閉じる*/
                  html.classList.remove('comment');
                  if(!configs.l_overlay) core.modify();
                  /* default and propagateする */
                }
                return;
            }
          default:
            return;/*デフォルトの動作に任せる*/
        }
      }, true);
      /* アベマ公式ブロックを「コメントクリックでトグル」に差し替える */
      window.addEventListener('click', function(e){
        if(!e.isTrusted) return;
        let comment;
        for(let target = e.target; target; target = target.parentNode){
          if(target.localName === 'form') return;/*アベマ公式ブロックフォーム*/
          if(target === elements.board) return;
          if(target === document.body) return;
          if(target.dataset.selector === 'comment'){
            comment = target;
            break;
          }
        }
        if(!comment) return;
        let cancel = site.get.commentBlockCancel(comment);
        if(!cancel){
          /* ブロックフォームを開く */
          site.get.commentBlock(comment).click();
          comment.style.transition = 'background 500ms ease';
          comment.classList.add('blockform');
          let observer = observe(comment, function(records){
            if(site.get.commentBlockCancel(comment)) return;
            comment.classList.remove('blockform');
            observer.disconnect();
          });
        }else{
          /* ブロックフォームを閉じる */
          comment.classList.remove('blockform');
          comment.addEventListener('transitionend', function(e){
            comment.style.transition = 'none';
            cancel.click();
          });
        }
      }, true);
      /* 番組開始のタイミングを挟んだバックグラウンドからの復帰でコメント取得が停止する現象を防ぐ */
      document.addEventListener('visibilitychange', function(e){
        if(document.hidden) return;
        if(site.cmNow()) return;/*CM中はクリックしない*/
        if(html.classList.contains('comment')){
          core.closeOpenCommentPane();
        }
      });
      /* フルスクリーン */
      document.addEventListener('fullscreenchange', function(e){
        if(!document.fullscreen){
          document.fullscreen = true;/*ブラウザサポート待ち*/
          elements.fullscreenButton.dataset.icon = 'mini_screen';
        }else{
          document.fullscreen = false;/*ブラウザサポート待ち*/
          elements.fullscreenButton.dataset.icon = 'full_screen';
        }
        core.modify();
        setTimeout(core.modify, 2500);/*ダメ押し*/
      });
      /* ウィンドウリサイズ */
      window.addEventListener('resize', function(e){
        if(!window.resizing) core.modify();
        clearTimeout(window.resizing), window.resizing = setTimeout(function(){
          core.modify();
          setTimeout(core.modify, 2500);/*ダメ押し*/
          window.resizing = null;
        }, 500);
      });
      /* コメント入力中にcssで表示を制御する */
      window.addEventListener('focusin', function(e){
        if(e.target.form && e.target.form.dataset.selector === 'commentForm') elements.commentPane.classList.add('active');
      });
      window.addEventListener('focusout', function(e){
        if(e.target.form && e.target.form.dataset.selector === 'commentForm') setTimeout(function(){
          elements.commentPane.classList.remove('active')
        }, 250);/*clickイベントより先にactiveが外れてしまうのを防ぐ*/
      });
    },
    createCanvas: function(){
      if(canvas && canvas.isConnected) elements.screen.removeChild(canvas);
      /* コメントcanvasたちを格納する親 */
      canvas = createElement(core.html.canvasDiv());
      /* テキストサイズ計測に使用 */
      elements.preCanvas = createElement(core.html.preCanvas());
      context = elements.preCanvas.getContext('2d', {alpha: false});
      elements.screen.insertBefore(canvas, elements.screen.firstElementChild);
      core.modify();
    },
    modify: function(){
      if(!elements.screen || !canvas) return;
      /* スクリーンサイズを適切に変化させる */
      let fullsize = [
        (configs.l_overlay === 1),
        !html.classList.contains('comment'),
        (configs.l_hide && html.classList.contains('comment') && !html.classList.contains('active')),
      ].includes(true);
      let fonts = (configs.font === '') ? FONT : `${configs.font}, ${FONT}`;
      let width = (fullsize) ? window.innerWidth : Math.round(window.innerWidth * (1 - configs.l_width / 100));
      let height = Math.min(width * (9/16), window.innerHeight);
      elements.screen.style.width = canvas.style.width = width + 'px';
      elements.screen.style.height = canvas.style.height = height + 'px';
      canvas.width = width;
      canvas.height = height;
      canvas.fontsize = Math.round((canvas.height / (configs.maxlines || 1)) / (1 + configs.linemargin));
      context.font = `bold ${canvas.fontsize}px ${fonts}`;
      context.textBaseline = 'alphabetic';
      context.fillStyle = 'white';
      context.ngFillStyle = 'rgb(255,224,32)';/*独自指定*/
      context.strokeStyle = 'black';
      context.lineWidth = Math.round(canvas.fontsize * configs.owidth);
      context.lineJoin = 'round';
      canvas.topDelta = ((canvas.fontsize * configs.linemargin) - context.lineWidth - (canvas.fontsize * MARGIN)) / 2;/*canvasのtop計算に使用する*/
    },
    listenComments: function(){
      if(elements.commentPane.isListening) return;
      elements.commentPane.isListening = true;
      observe(elements.commentPane.firstElementChild, function(records){
        /* 新着コメント表示ボタン */
        if(records[0].addedNodes.length === 1 && site.addedNode.newCommentsButton(records[0].addedNodes[0]) !== false){
          let newCommentsButton = records[0].addedNodes[0];
          if(elements.board.classList.contains('mousedown')){/*テキスト選択を邪魔しないための配慮*/
            window.addEventListener('mouseup', function(){
              animate(function(){newCommentsButton.classList.add('shown')});
            }, {once: true});
          }else{
            animate(function(){newCommentsButton.classList.add('shown')});
          }
        }
      });
      observe(elements.board.firstElementChild, function(records){
        let newComments = [];
        for(let i = 0, record; record = records[i]; i++){
          /* 新着コメント */
          if(record.addedNodes.length === 1 && site.addedNode.comment(record.addedNodes[0]) !== false){
            newComments.push(record.addedNodes[0]);
          }
        }
        if(newComments.length) core.receiveNewComments(newComments);
      });
    },
    receiveNewComments: function(newComments){
      /* コメントの取得間隔を計測する(AINTERVAL仕様の変更に備える) */
      let now = Date.now(), commentInterval = (now - parseInt(elements.board.dataset.received)) || AINTERVAL;
      elements.board.dataset.received = now;
      /* コメント表示中に停止してしまう視聴数とコメント数をこのタイミングで更新する */
      if(!elements.commentButton.statsUpdated && !site.cmNow()/*CM中は更新しない*/){
        elements.commentButton.statsUpdated = true;
        core.updateStats();
        setTimeout(function(){elements.commentButton.statsUpdated = false}, STATSUPDATE);
      }
      /* NGコメントをすぐ判定する */
      core.ng.expire();
      let filteredComments = newComments.filter(core.ng.filter);
      /* コメントの再取得で重複コメントが流れるのを回避する(NG判定をすませた後で) */
      let latest = parseInt(elements.board.dataset.latest);
      if(latest) filteredComments = filteredComments.filter(function(comment){
        return latest < parseInt(site.get.commentTime(comment));
      });
      latest = elements.board.dataset.latest = parseInt(site.get.commentTime(newComments[newComments.length - 1]));
      /* バックグラウンドならここで終了 */
      if(document.hidden) return;
//      /* スライドダウンアニメーションを上書きする */
//      core.slideDownNewComments(newComments);
      /* コメントを流す必要がなければここで終了 */
      if(configs.maxlines === 0) return;
      if(configs.transparency === 100) return;
      if(configs.maxcomments === 0) return;
      if(canvas.children.length >= configs.maxcomments) return;
      /* 配列末尾の古いコメントから順に流す */
      let earliest = Math.max(parseInt(site.get.commentTime(filteredComments[0])), latest - commentInterval);
      for(let i = filteredComments.length - 1, comment; comment = filteredComments[i]; i--){
        setTimeout(function(){
          core.attachComment(site.get.commentText(comment), comment.classList.contains('ng-trial'));
        }, parseInt(site.get.commentTime(comment)) - earliest);
      }
    },
    slideDownNewComments: function(newComments){
      const duration = '500ms', easing = 'cubic-bezier(.215,.61,.355,1)';/*アベマ公式の挙動を尊重する*/
      newComments.style.maxHeight = newComments.style.minHeight = '0px';/*heightの上書き戦争を避けてmaxHeight/minHeightが使えるのは幸運*/
      animate(function(){
        let child = newComments.firstElementChild, naturalHeight = getComputedStyle(child).height;
        newComments.style.transition = 'none';
        child.style.transition = 'none';
        child.style.transform = `translateY(-${naturalHeight})`;
        animate(function(){
          newComments.style.transition = `max-height ${duration} ${easing}, min-height ${duration} ${easing}`;
          child.style.transition = `transform ${duration} ${easing}`;
          animate(function(){
            newComments.style.maxHeight = newComments.style.minHeight = naturalHeight;
            child.style.transform = `translateY(0)`;
          });
        });
      });
    },
    attachComment: function(text, ngTrial = false){
      /* 単一スクロールコメントcanvasを用意する */
      let scrollComment, c, fonts = (configs.font === '') ? FONT : `${configs.font}, ${FONT}`;
      let width = Math.round(context.measureText(text).width + context.lineWidth);
      let height = Math.round(canvas.fontsize * (1 + MARGIN) + context.lineWidth);
      scrollComment = createElement(core.html.scrollComment(width, height));
      c = scrollComment.getContext('2d');
      c.font         = `bold ${canvas.fontsize}px ${fonts}`;/*context.fontを参照したいがSafariでbold指定が文字列として残らないバグ*/
      c.textBaseline = context.textBaseline;
      c.fillStyle    = (ngTrial) ? context.ngFillStyle : context.fillStyle;
      c.strokeStyle  = context.strokeStyle;
      c.lineWidth    = context.lineWidth;
      c.lineJoin     = context.lineJoin;
      let left = Math.round(context.lineWidth/2);
      let top  = Math.round((canvas.fontsize * MARGIN + context.lineWidth)/2 + canvas.fontsize * BASELINE);
      c.strokeText(text, left, top);
      c.fillText(text, left, top);
      /* コメント位置データをまとめる */
      let record = {};
      record.text = text;/*流れる文字列*/
      record.width = width;/*文字列の幅*/
      record.ppms = (canvas.width + record.width) / (configs.duration * 1000);/*ミリ秒あたり移動距離*/
      record.start = Date.now();/*開始時刻*/
      record.reveal = record.start + (record.width / record.ppms);/*文字列が右端から抜ける時刻*/
      record.touch = record.start + (canvas.width / record.ppms);/*文字列が左端に触れる時刻*/
      record.end = record.start + (configs.duration * 1000);/*終了時刻*/
      /* 追加されたコメントをどの行に流すかを決定する */
      for(let i=0; i < configs.maxlines; i++){
        let length = lines[i] ? lines[i].length : 0;/*同じ行に詰め込まれているコメント数*/
        switch(true){
          /* 行がなければ行を追加して流す */
          case(length === 0):
            lines[i] = [];
          /* ひとつ先行するコメントより遅い(短い)文字列なら、現時点で先行コメントがすでに右端から抜けていれば流す */
          case(record.ppms < lines[i][length - 1].ppms && lines[i][length - 1].reveal < record.start):
          /* ひとつ先行するコメントより速い(長い)文字列なら、左端に触れる瞬間までに先行コメントが終了するなら流す */
          case(lines[i][length - 1].ppms < record.ppms && lines[i][length - 1].end < record.touch):
            record.top = Math.round(((canvas.height / configs.maxlines) * i) + canvas.topDelta);
            //if(DEBUG) scrollComment.dataset.former = JSON.stringify(lines[i][length - 1]);
            //if(DEBUG) scrollComment.dataset.self = JSON.stringify(record);
            lines[i].push(record);
            scrollComment.style.top = record.top + 'px';
            canvas.appendChild(scrollComment);
            animate(function(){
              scrollComment.style.transform = `translateX(-${canvas.width + width}px)`;
              scrollComment.addEventListener('transitionend', function(e){
                canvas.removeChild(scrollComment);
                lines[i].shift();
              }, {once: true});
            });
            return;/*行に追加したら終了*/
          default:
            continue;/*条件に当てはまらなければforループを回して次の行に入れられるかの判定へ*/
        }
      }
    },
    ng: {
      initialize: function(){
        core.ng.read();
        core.ng.listenSelection();
      },
      listenSelection: function(){
        /* コメント上でmousedownした状態からのmousemove,mouseupでのみselect() */
        let select = function(e){
          let selection = window.getSelection(), selected = selection.toString();
          let comment = (selection.anchorNode.length) ? selection.anchorNode.parentNode.parentNode.parentNode.parentNode : null;
          /* テキスト選択なしなら登録フォームを閉じる */
          if(selection.isCollapsed && e.type === 'mouseup' && !e.target.dataset.ngword) return core.ng.closeForm();
          /* テキスト選択を邪魔しない場合にのみ登録フォームを表示 */
          if(!elements.ngForm || elements.ngForm.classList.contains('hidden') || e.target.offsetTop < elements.ngForm.offsetTop || e.type === 'mouseup') core.ng.openForm(comment, e);
          /* テキスト選択があれば初期値に */
          if(!selection.isCollapsed) elements.ngForm.querySelector('input[type="text"]').value = selected;
        };
        window.addEventListener('mousedown', function(e){
          for(let target = e.target; target.dataset; target = target.parentNode) if(target.dataset.selector === 'comment'){
            elements.board.classList.add('mousedown');
            window.addEventListener('mousemove', select);
            window.addEventListener('mouseup', function(e){
              animate(function(){select(e)});/*ダブルクリックでのテキスト選択をanimateで確実に補足*/
              window.removeEventListener('mousemove', select);
              elements.board.classList.remove('mousedown');
            }, {once: true});
            return;
          }
        });
      },
      createButton: function(){
        if(elements.ngButton) return;
        /* フルスクリーンボタンを元にNG一覧ボタンを追加する */
        elements.ngButton = createElement(core.html.ngButton());
        elements.ngButton.className = elements.fullscreenButton.className;
        elements.ngButton.addEventListener('click', core.panel.toggle.bind(null, 'ngList', core.ng.createList));
        elements.fullscreenButton.parentNode.insertBefore(elements.ngButton, elements.VolumeController.nextElementSibling);
      },
      createForm: function(comment){
        elements.ngForm = createElement(core.html.ngForm());
        elements.ngForm.querySelector('button.list').addEventListener('click', core.panel.toggle.bind(null, 'ngList', core.ng.createList));
        elements.ngForm.querySelector('button.help').addEventListener('click', core.panel.toggle.bind(null, 'ngHelp', core.ng.createHelp));
        elements.ngForm.querySelector('p.type').addEventListener('click', function(e){
          let word = elements.ngForm.querySelector('p.word input');
          if(word.value === '') return;
          if(e.target.localName !== 'button') return;
          core.ng.add(word, e.target);
          core.ng.closeForm();
          if(elements.ngList) core.ng.buildList();
        });
      },
      openForm: function(comment, e){
        let append = function(comment, ngForm){
          comment.insertBefore(ngForm, comment.firstElementChild.nextElementSibling);/*公式ブロックフォームが最後尾にある*/
        };
        let slideUpDown = function(){
          elements.ngForm.slidingUp = true;
          animate(function(){
            elements.ngForm.classList.add('hidden');
            if(elements.ngForm.isConnected){
              elements.ngForm.addEventListener('transitionend', function(e){
                elements.ngForm.slidingUp = false;
                append(elements.ngForm.targetComment, elements.ngForm);
                slideDown();
              }, {once: true});
            }else{
              elements.ngForm.slidingUp = false;
              append(elements.ngForm.targetComment, elements.ngForm);
              slideDown();
            }
          });
        };
        let slideDown = function(){
          elements.ngForm.slidingDown = true;
          if(elements.ngForm.parentNode !== elements.ngForm.targetComment) append(elements.ngForm.targetComment, elements.ngForm);
          animate(function(){
            elements.ngForm.classList.remove('hidden');
            elements.ngForm.addEventListener('transitionend', function(e){
              elements.ngForm.slidingDown = false;
            }, {once: true});
          });
          let ngword = elements.ngForm.targetComment.dataset.ngword;
          if(ngword && e.type === 'click') elements.ngForm.querySelector('input[type="text"]').value = ngword;
          if(!html.classList.contains('ng')) html.classList.add('ng');/*チャンネル切り替えナビゲーションを隠すなど*/
        };
        if(elements.board.parentNode.scrollTop === 0) elements.board.parentNode.scrollTop = 1;/*新着コメントを停止する*/
        if(elements.ngForm){/*表示位置の移し替え*/
          elements.ngForm.targetComment = comment;/*既にslideDown中の処理も含めてターゲットを差し替える*/
          if(elements.ngForm.classList.contains('hidden')){
            if(elements.ngForm.slidingUp){/*Up中*/
              if(elements.ngForm.parentNode === comment){
                slideDown();/*UpをやめてDownさせる*/
              }else{
                /*予定通りUp後にDownさせる*/
                elements.ngForm.addEventListener('transitionend', function(e){
                  slideDown();
                }, {once: true});
              }
            }else{/*hidden状態*/
              slideDown();
            }
          }else{
            if(elements.ngForm.slidingDown){/*Down中*/
              if(elements.ngForm.parentNode === comment){
                /*なにもしなくてもよい*/
              }else{
                slideUpDown();/*Downをやめて改めてUpDownさせる*/
              }
            }else{/*表示状態*/
              if(elements.ngForm.parentNode === comment){
                /*なにもしなくてもよい*/
              }else{
                slideUpDown();
              }
            }
          }
        }else{/*新規*/
          core.ng.createForm(comment);
          elements.ngForm.classList.add('hidden');
          elements.ngForm.targetComment = comment;
          slideDown();
        }
      },
      closeForm: function(){
        if(!elements.ngForm) return;
        if(elements.ngForm.classList.contains('hidden')) return;
        elements.ngForm.slidingUp = true;
        animate(function(){
          elements.ngForm.classList.add('hidden');
          if(elements.ngForm.isConnected){
            elements.ngForm.addEventListener('transitionend', function(e){
              elements.ngForm.slidingUp = false;
            }, {once: true});
          }else{
            elements.ngForm.slidingUp = false;
          }
        });
        html.classList.remove('ng');/*チャンネル切り替えナビゲーションを隠すなど*/
      },
      toggleForm: function(comment, e){
        if(!elements.ngForm) return core.ng.openForm(comment, e);
        if(elements.ngForm.classList.contains('hidden')) return core.ng.openForm(comment, e);
        if(elements.ngForm.parentNode !== comment) return core.ng.openForm(comment, e);
        core.ng.closeForm();
      },
      createList: function(){
        let ngList = elements.ngList = createElement(core.html.ngList());
        ngList.querySelector('button.help').addEventListener('click', core.panel.toggle.bind(null, 'ngHelp', core.ng.createHelp));
        ngList.querySelector('button.cancel').addEventListener('click', core.panel.close.bind(null, 'ngList'));
        ngList.querySelector('button.save').addEventListener('click', function(e){
          core.ng.save(core.ng.getNewNgwords().filter((ngword) => (ngword.type !== 'remove')));
          core.panel.close('ngList');
        });
        ngList.querySelector('ul > li.add > p.words > textarea').addEventListener('keypress', function(e){
          animate(function(){
            let checked = ngList.querySelector('ul > li.add > p.type input:checked');
            if(e.target.value === '') return checked && (checked.checked = false);
            if(!checked) ngList.querySelector('ul > li.add > p.type input[value="forever"]').checked = true;
          });
        }, true);
        /* 並べ替え */
        configs.ng_sort = configs.ng_sort || {key: 'date', reverse: false};
        ngList.querySelector('p.sort').addEventListener('click', function(e){
          if(e.target.localName !== 'label') return;
          let input = document.getElementById(e.target.htmlFor);
          if(input.checked) input.classList.toggle('reverse');
          configs.ng_sort = {key: input.value, reverse: input.classList.contains('reverse')};
          core.ng.buildList();
        });
        /* リスト構築 */
        core.ng.buildList();
        /* 表示 */
        core.panel.open('ngList');
      },
      getNewNgwords: function(){
        let new_ngwords = Array.from(ngwords);/*clone*/
        /* input */
        let lis = elements.ngList.querySelectorAll('ul > li.edit');
        for(let i = 0, li; li = lis[i]; i++){
          let word = li.querySelector('p.word input');
          let checked = li.querySelector('p.type input:checked');
          let match = word.value.match(/^\/(.+)\/([a-z]+)?$/);
          new_ngwords[i] = {};
          new_ngwords[i].original = word.value;
          new_ngwords[i].value = (match) ? word.value : normalize(word.value).toLowerCase();
          new_ngwords[i].regex = (match) ? new RegExp(match[1], match[2]) : null;
          new_ngwords[i].type = checked.value;
          new_ngwords[i].added = parseInt(li.dataset.added) || null;
          new_ngwords[i].limit = (checked.value === 'for24h') ? parseInt(li.dataset.limit) : null;
        }
        /* textarea */
        let add = elements.ngList.querySelector('ul > li.add');
        let textarea = add.querySelector('p.words textarea');
        let lines = textarea.value.split('\n');
        for(let i = 0; lines[i] !== undefined; i++){
          let checked = add.querySelector('p.type input:checked');
          let match = lines[i].match(/^\/(.+)\/([a-z]+)?$/);
          let index = new_ngwords.length;
          new_ngwords[index] = {};
          new_ngwords[index].original = lines[i];
          new_ngwords[index].value = (match) ? lines[i] : normalize(lines[i]).toLowerCase();
          new_ngwords[index].regex = (match) ? new RegExp(match[1], match[2]) : null;
          new_ngwords[index].type = (checked) ? checked.value : null;
          new_ngwords[index].added = Date.now() + i;/*並べ替え用に同一時刻を避ける*/
          new_ngwords[index].limit = (checked && checked.value === 'for24h') ? new_ngwords[index].added + 1000*60*60*24 : null;
        }
        textarea.value = '';
        return new_ngwords.filter((ngword, index) => {
          if(ngword.value === '') return false;/*空欄除外*/
          for(let i = index + 1; new_ngwords[i]; i++) if(ngword.value === new_ngwords[i].value) return false;/*重複除外*/
          return true;
        });
      },
      buildList: function(){
        /* 編集中の既存のリストがあればそのまま使う */
        let new_ngwords = core.ng.getNewNgwords();
        /* 並べ替え */
        if(new_ngwords.length < 2){
          elements.ngList.querySelector('p.sort').classList.add('disabled');
        }else{
          elements.ngList.querySelector('p.sort').classList.remove('disabled');
          let sort = elements.ngList.querySelector(`p.sort input[value="${configs.ng_sort.key}"]`);
          sort.checked = true;
          if(configs.ng_sort.reverse) sort.classList.add('reverse');
        }
        new_ngwords.sort(function(a, b){
          let types = {trial: 1, for24h: 2, forever: 3, remove: 4};
          switch(configs.ng_sort.key){
            case('date'): return (a.added < b.added);
            case('word'): return (a.original < b.original);
            case('type'): return (a.limit && b.limit) ? (a.limit < b.limit) : (types[a.type] < types[b.type]);
          }
        });
        if(configs.ng_sort.reverse) new_ngwords.reverse();
        /* リスト構築 */
        let ul = elements.ngList.querySelector('ul');
        while(2 < ul.children.length) ul.removeChild(ul.children[1]);/*冒頭のテンプレートと追加登録のみ残す*/
        let template = ul.querySelector('li.template');
        let now = Date.now();
        let formatTime = function(limit){
          let left = limit - now;
          switch(true){
            case(1000*60*60 <= left): return Math.floor(left/(1000*60*60)) + '時間';
            case(0 <= left): return Math.floor(left/(1000*60)) + '分';
            case(left < 0): return '0分';
          }
        };
        for(let i = 0, new_ngword; new_ngword = new_ngwords[i]; i++){
          let li = template.cloneNode(true);
          li.className = 'edit';
          li.innerHTML = li.innerHTML.replace(/\{i\}/g, i);
          li.querySelector('p.word input').value = new_ngword.original || new_ngword.value/*移行用*/;
          if(new_ngword.type) li.querySelector(`p.type input[value="${new_ngword.type}"]`).checked = true;
          li.dataset.added = new_ngword.added || 0;
          li.dataset.limit = new_ngword.limit || 0;
          let for24h = li.querySelector('p.type label.for24h');
          for24h.textContent = (new_ngword.limit) ? formatTime(new_ngword.limit) : '24時間';
          for24h.addEventListener('click', function(e){
            animate(function(){/*checked処理の後に*/
              if(li.querySelector('p.type input[value="for24h"]').checked){
                if(for24h.classList.toggle('extended')){
                  li.dataset.limit = Date.now() + 1000*60*60*24;
                  for24h.textContent = '24時間';
                }else{
                  li.dataset.limit = new_ngword.limit;
                  for24h.textContent = formatTime(new_ngword.limit);
                }
              }
            });
          });
          ul.insertBefore(li, template.nextElementSibling);
        }
      },
      createHelp: function(){
        elements.ngHelp = createElement(core.html.ngHelp());
        elements.ngHelp.querySelector('button.ok').addEventListener('click', core.panel.close.bind(null, 'ngHelp'));
        core.panel.open('ngHelp');
      },
      add: function(word, type){
        let index = ngwords.length;
        for(let i = 0; ngwords[i]; i++) if(ngwords[i].value === word.value) index = i;/*重複させない*/
        let match = word.value.match(/^\/(.+)\/([a-z]+)?$/);
        if(!ngwords[index]) ngwords[index] = {};
        ngwords[index].original = word.value;
        ngwords[index].value = (match) ? word.value : normalize(word.value).toLowerCase();
        ngwords[index].regex = (match) ? new RegExp(match[1], match[2]) : null;
        ngwords[index].type = type.classList[0];
        ngwords[index].added = ngwords[index].added || Date.now();
        switch(true){
          case(type.classList.contains('for24h') && !ngwords[index].limit):
          case(type.classList.contains('for24h') && type.classList.contains('extended')):
            ngwords[index].limit = ngwords[index].added + 1000*60*60*24;
            break;
          case(type.classList.contains('for24h')):
            ngwords[index].limit = ngwords[index].limit;
            break;
          default:
            ngwords[index].limit = null;
            break;
        }
        Storage.save('ngwords', ngwords);
      },
      read: function(){
        /* 保存済みの設定を読む */
        ngwords = Storage.read('ngwords') || [];
        /* 正規表現(word.regex)はJSONに保存されないので復活させる */
        for(let i = 0; ngwords[i]; i++){
          let match = ngwords[i].value.match(/^\/(.+)\/([a-z]+)?$/);
          ngwords[i].regex = (match) ? new RegExp(match[1], match[2]) : null;
        }
      },
      save: function(new_ngwords){
        ngwords = new_ngwords;
        Storage.save('ngwords', ngwords);
      },
      expire: function(){
        let now = Date.now();
        ngwords = ngwords.filter(function(ngword, i, ngwords){
          if(!ngword.limit || now < ngword.limit) return true;
        });
      },
      filter: function(comment){
        const match = function(comment, ngword){
          let commentText = site.get.commentText(comment);
          if(ngword.regex && ngword.regex.test(commentText)) return true;
          if(normalize(commentText).toLowerCase().includes(ngword.value)) return true;
        };
        for(let i = 0, ngword; ngword = ngwords[i]; i++){
          switch(ngword.type){
            case('forever'):
            case('for24h'):
              if(match(comment, ngword)){
                comment.classList.add('ng-deleted');
                return false;
              }
              break;
            case('trial'):
              if(match(comment, ngword)){
                comment.classList.add('ng-trial');
                comment.dataset.ngword = ngword.value;
                comment.addEventListener('click', function(e){
                  if(e.target === comment && window.getSelection().isCollapsed) core.ng.toggleForm(comment, e);
                });
              }
              break;
          }
        }
        return true;
      },
    },
    config: {
      read: function(){
        /* 保存済みの設定を読む */
        configs = Storage.read('configs') || {};
        /* 未定義項目をデフォルト値で上書きしていく */
        Object.keys(CONFIGS).forEach((key) => {if(configs[key] === undefined) configs[key] = CONFIGS[key].DEFAULT});
      },
      save: function(new_config){
        configs = {};/*CONFIGSに含まれた設定値のみ保存する*/
        /* CONFIGSを元に文字列を型評価して値を格納していく */
        Object.keys(CONFIGS).forEach((key) => {
          /* 値がなければデフォルト値 */
          if(new_config[key] === "") return configs[key] = CONFIGS[key].DEFAULT;
          switch(CONFIGS[key].TYPE){
            case 'bool':
              configs[key] = (new_config[key]) ? 1 : 0;
              break;
            case 'int':
              configs[key] = parseInt(new_config[key]);
              break;
            case 'float':
              configs[key] = parseFloat(new_config[key]);
              break;
            default:
              configs[key] = new_config[key];
              break;
          }
        });
        Storage.save('configs', configs);
      },
      createButton: function(){
        if(elements.configButton) return;
        /* フルスクリーンボタンを元に設定ボタンを追加する */
        elements.configButton = createElement(core.html.configButton());
        elements.configButton.className = elements.fullscreenButton.className;
        elements.configButton.addEventListener('click', core.panel.toggle.bind(null, 'configPanel', core.config.createPanel));
        elements.fullscreenButton.parentNode.insertBefore(elements.configButton, elements.ngButton.nextElementSibling);
      },
      createPanel: function(){
        elements.configPanel = createElement(core.html.configPanel());
        elements.configPanel.querySelector('button.cancel').addEventListener('click', core.panel.close.bind(null, 'configPanel'));
        elements.configPanel.querySelector('button.save').addEventListener('click', function(e){
          let inputs = elements.configPanel.querySelectorAll('input'), new_configs = {};
          for(let i = 0, input; input = inputs[i]; i++){
            switch(CONFIGS[input.name].TYPE){
              case('bool'):
                new_configs[input.name] = (input.checked) ? 1 : 0;
                break;
              case('object'):
                if(!new_configs[input.name]) new_configs[input.name] = {};
                new_configs[input.name][input.value] = (input.checked) ? 1 : 0;
                break;
              default:
                new_configs[input.name] = input.value;
                break;
            }
          }
          core.config.save(new_configs);
          core.panel.close('configPanel')
          /* 新しい設定値で再スタイリング */
          core.addStyle();
          core.createCanvas();/*modify含む*/
        }, true);
        core.panel.open('configPanel');
      },
    },
    panel: {
      createPanels: function(){
        if(elements.panels) return;
        elements.panels = createElement(core.html.panels());
        elements.panels.dataset.panels = 0;
        document.body.appendChild(elements.panels);
      },
      open: function(key){
        let target = null;
        for(let i = PANELS.indexOf(key) + 1; PANELS[i] && !target; i++) if(elements[PANELS[i]]) target = elements[PANELS[i]];
        elements[key].classList.add('hidden');
        elements.panels.insertBefore(elements[key], target);
        animate(function(){
          elements.panels.dataset.panels = parseInt(elements.panels.children.length);
          elements[key].classList.remove('hidden');
        });
        elements.panels.listeningKeypress = elements.panels.listeningKeypress || [];
        if(!elements.panels.listeningKeypress[key]){
          elements.panels.listeningKeypress[key] = true;
          window.addEventListener('keypress', function(e){
            if(['input', 'textarea'].includes(document.activeElement.localName)) return;
            if(elements[key] && e.key === 'Escape') core.panel.close(key);
          });
        }
      },
      close: function(key){
        elements[key].classList.add('hidden');
        elements[key].addEventListener('transitionend', function(e){
          if(!elements[key]) return;
          elements.panels.dataset.panels = parseInt(elements.panels.children.length - 1);
          elements.panels.removeChild(elements[key]);
          elements[key] = null;
        }, {once: true});
      },
      toggle: function(key, create){
        (!elements[key]) ? create() : core.panel.close(key);
      },
    },
    addStyle: function(){
      let style = createElement(core.html.style());
      document.head.appendChild(style);
      if(elements.style && elements.style.isConnected) document.head.removeChild(elements.style);
      elements.style = style;
    },
    html: {
      mini_screen: () => `<use xlink:href="/images/icons/mini_screen.svg#svg-body"></use>`,
      canvasDiv: () => `
        <div id="${SCRIPTNAME}-canvas"></div>
      `,
      preCanvas: () => `
        <canvas width="0" height="0"></canvas>
      `,
      scrollComment: (width, height) => `
        <canvas class="comment" width="${width}" height="${height}"></canvas>
      `,
      ngButton: () => `
        <button id="${SCRIPTNAME}-ng-button" title="${SCRIPTNAME} 登録NGワード一覧"><svg width="20" height="20"><use xlink:href="/images/icons/list.svg#svg-body"></use></svg></button>
      `,
      ngForm: () => `
        <div id="${SCRIPTNAME}-ng-form">
          <h1><span>NGワード登録</span><button class="list"><svg width="14" height="16"><use xlink:href="/images/icons/list.svg#svg-body"></use></svg></button></h1>
          <p class="word"><input type="text" value=""><button class="help">?</button></p>
          <p class="type"><button class="trial">お試し</button><button class="for24h">24時間</button><button class="forever">無期限</button></p>
        </div>
      `,
      ngList: () => `
        <div class="panel" id="${SCRIPTNAME}-ng-list">
          <header>
            <h1>登録NGワード一覧</h1>
            <p class="buttons"><button class="help">?</button></p>
          </header>
          <p class="sort">
            <input type="radio" name="sort" id="ngwords-sort-date" value="date"><label for="ngwords-sort-date">登録日時順</label>
            <input type="radio" name="sort" id="ngwords-sort-word" value="word"><label for="ngwords-sort-word">NGワード順</label>
            <input type="radio" name="sort" id="ngwords-sort-type" value="type"><label for="ngwords-sort-type">期限順</label>
          </p>
          <ul>
            <li class="template">
              <p class="word"><input type="text" name="ngwords[{i}][value]" value=""></p>
              <p class="type">
                <input type="radio" name="ngwords[type][{i}]" id="ngwords-type-{i}-trial"   value="trial"  ><label class="trial"   for="ngwords-type-{i}-trial"  >お試し</label>
                <input type="radio" name="ngwords[type][{i}]" id="ngwords-type-{i}-for24h"  value="for24h" ><label class="for24h"  for="ngwords-type-{i}-for24h" >24時間</label>
                <input type="radio" name="ngwords[type][{i}]" id="ngwords-type-{i}-forever" value="forever"><label class="forever" for="ngwords-type-{i}-forever">無期限</label>
                <input type="radio" name="ngwords[type][{i}]" id="ngwords-type-{i}-remove"  value="remove" ><label class="remove"  for="ngwords-type-{i}-remove" >削除</label>
              </p>
            </li>
            <li class="add">
              <p class="words"><textarea name="ngwords[add][value]" placeholder="追加"></textarea></p>
              <p class="type">
                <input type="radio" name="ngwords[type][add]" id="ngwords-type-add-trial"   value="trial"  ><label class="trial"   for="ngwords-type-add-trial"  >お試し</label>
                <input type="radio" name="ngwords[type][add]" id="ngwords-type-add-for24h"  value="for24h" ><label class="for24h"  for="ngwords-type-add-for24h" >24時間</label>
                <input type="radio" name="ngwords[type][add]" id="ngwords-type-add-forever" value="forever"><label class="forever" for="ngwords-type-add-forever">無期限</label>
                <input type="radio" name="ngwords[type][add]" id="ngwords-type-add-remove"  value="remove" ><label class="remove"  for="ngwords-type-add-remove" >削除</label>
              </p>
            </li>
          </ul>
          <p class="buttons"><button class="cancel">キャンセル</button><button class="save primary">保存</button></p>
        </div>
      `,
      ngHelp: () => `
        <div class="panel" id="${SCRIPTNAME}-ng-help">
          <h1>NGワードについて</h1>
          <p>登録したワードを含むコメントを削除します。</p>
          <p>お試しの場合はハイライト表示されるので、NG対象の確認や、NGとは逆の注目したいキーワードとしても活用できます。24時間の場合は登録時からの期限付きなので、ネタバレや時事ネタなど一時的なNGとしてご活用ください。</p>
          <p>コメント一覧のテキスト選択から登録できるほか、NGワード一覧ボタンをクリックして、登録したNGワードを編集したり、複数行での一括登録もできます。</p>
          <p>英数字と記号とカタカナは、全角半角や大文字小文字を区別しません。</p>
          <p>下記のような正規表現も使えます。</p>
          <section>
            <h2>「NGです」を消す登録例:</h2>
            <dl>
              <dt><code>NG</code></dt><dd>通常のNGワード</dd>
              <dt><code>/^NG/</code></dt><dd>前方一致</dd>
              <dt><code>/です$/</code></dt><dd>後方一致</dd>
              <dt><code>/^NGです$/</code></dt><dd>完全一致</dd>
            </dl>
            <h2>そのほかの例:</h2>
            <dl>
              <dt><code>/^.$/</code></dt><dd>1文字だけのコメント</dd>
              <dt><code>/.{30}/</code></dt><dd>30文字以上のコメント</dd>
              <dt><code>/^[a-z]+$/i</code></dt><dd>アルファベットだけのコメント</dd>
              <dt><code>/[0-9]{3}/</code></dt><dd>3桁以上の数字を含むコメント</dd>
            </dl>
          </section>
          <p class="buttons"><button class="ok primary">OK</button></p>
        </div>
      `,
      configButton: () => `
        <button id="${SCRIPTNAME}-config-button" title="${SCRIPTNAME} 設定"><svg width="20" height="20" role="img"><use xlink:href="/images/icons/config.svg#svg-body"></use></svg></button>
      `,
      configPanel: () => `
        <div class="panel" id="${SCRIPTNAME}-config-panel">
          <h1>${SCRIPTNAME}設定</h1>
          <fieldset>
            <legend>スクロールコメント</legend>
            <p><label>最大行数(文字サイズ連動):       <input type="number"   name="maxlines"        value="${configs.maxlines}"        min="0"  max="50"  step="1"></label></p>
            <p><label>行間(比率):                     <input type="number"   name="linemargin"      value="${configs.linemargin}"      min="0"  max="1"   step="0.05"></label></p>
            <p><label>透明度(%):                      <input type="number"   name="transparency"    value="${configs.transparency}"    min="0"  max="100" step="5"></label></p>
            <p><label>縁取りの太さ(比率):             <input type="number"   name="owidth"          value="${configs.owidth}"          min="0"  max="0.5" step="0.01"></label></p>
            <p><label>横断にかける秒数:               <input type="number"   name="duration"        value="${configs.duration}"        min="1"  max="30"  step="1"></label></p>
            <p><label>最大同時表示数:                 <input type="number"   name="maxcomments"     value="${configs.maxcomments}"     min="0"  max="100" step="1"></label></p>
            <p><label>フォント指定<sup>※</sup>:      <input type="text"     name="font"            value="${configs.font.replace(/"/g, '&quot;')}" placeholder="Arial, sans-serif" pattern="[^/*{}:;]+"></label></p>
            <p class="note">※スクロールするコメント内の文字とフォントの組み合わせによっては、文字の上下が切れてしまうこともあります。</p>
          </fieldset>
          <fieldset>
            <legend>一覧コメント</legend>
            <p><label>操作していない時は画面外に隠す: <input type="checkbox" name="l_hide"          value="${configs.l_hide}"          ${configs.l_hide      ? 'checked' : ''}></label></p>
            <p><label>映像に重ねる:                   <input type="checkbox" name="l_overlay"       value="${configs.l_overlay}"       ${configs.l_overlay   ? 'checked' : ''}></label></p>
            <p><label>投稿時刻を表示する:             <input type="checkbox" name="l_showtime"      value="${configs.l_showtime}"      ${configs.l_showtime  ? 'checked' : ''}></label></p>
            <p><label>横幅(%):                        <input type="number"   name="l_width"         value="${configs.l_width}"         min="0"  max="100" step="0.5"></label></p>
            <p><label>最大行数(文字サイズ連動):       <input type="number"   name="lc_maxlines"     value="${configs.lc_maxlines}"     min="10" max="100" step="1"></label></p>
            <p><label>改行されたコメントの行間(比率): <input type="number"   name="lc_linemargin"   value="${configs.lc_linemargin}"   min="0"  max="1"   step="0.05"></label></p>
            <p><label>コメント同士の間隔(比率):       <input type="number"   name="lc_margin"       value="${configs.lc_margin}"       min="0"  max="2"   step="0.05"></label></p>
            <p><label>文字の透明度(%):                <input type="number"   name="lc_transparency" value="${configs.lc_transparency}" min="0"  max="100" step="5"></label></p>
            <p><label>背景の透明度(%):                <input type="number"   name="lb_transparency" value="${configs.lb_transparency}" min="0"  max="100" step="5"></label></p>
          </fieldset>
          <fieldset>
            <legend>アベマのナビゲーション</legend>
            <p><label>画面クリック時のみ表示する:     <input type="checkbox" name="n_clickonly"     value="${configs.n_clickonly}"     ${configs.n_clickonly ? 'checked' : ''}></label></p>
            <p><label>隠れるまでの時間(秒):           <input type="number"   name="n_delay"         value="${configs.n_delay}"         min="1"  max="60"  step="1"></label></p>
            <p><label>透明度(%):                      <input type="number"   name="n_transparency"  value="${configs.n_transparency}"  min="0"  max="100" step="5"></label></p>
          </fieldset>
          <p class="buttons"><button class="cancel">キャンセル</button><button class="save primary">保存</button></p>
        </div>
      `,
      panels: () => `
        <div class="panels" id="${SCRIPTNAME}-panels"></div>
      `,
      style: () => `
        <style type="text/css">
          /* 共通変数 */
          /* opacity:                ${configs.opacity    = 1 - (configs.transparency / 100)} */
          /* lc_opacity:             ${configs.lc_opacity = 1 - (configs.lc_transparency / 100)} */
          /* lb_opacity:             ${configs.lb_opacity = 1 - (configs.lb_transparency / 100)} */
          /* n_opacity:              ${configs.n_opacity  = 1 - (configs.n_transparency / 100)} */
          /* opacityHover:           ${configs.opacityHover    = 1 - (configs.transparency / 200)} */
          /* lc_opacityHover:        ${configs.lc_opacityHover = 1 - (configs.lc_transparency / 200)} */
          /* lb_opacityHover:        ${configs.lb_opacityHover = 1 - (configs.lb_transparency / 200)} */
          /* n_opacityHover:         ${configs.n_opacityHover  = 1 - (configs.n_transparency / 200)} */
          /* fontsize:               ${configs.fontsize = (100 / (configs.maxlines || 1)) / (1 + configs.linemargin)} (設定値の表現をわかりやすくする代償はここで支払う) */
          /* lc_fontsize:            ${configs.lc_fontsize = (100 / (configs.lc_maxlines + 1)) / (1 + configs.lc_margin)} (設定値の表現をわかりやすくする代償はここで支払う) */
          /* header_height:          ${configs.header_height = configs.header_height || elements.header.firstElementChild.clientHeight} */
          /* footer_height:          ${configs.footer_height = configs.footer_height || elements.footer.firstElementChild.clientHeight} */
          /* channelButtons_size:    ${configs.channelButtons_size = configs.channelButtons_size || elements.channelButtons.firstElementChild.clientWidth} */
          /* screen_zIndex:          ${configs.screen_zIndex          =   2} */
          /* canvas_zIndex:          ${configs.canvas_zIndex          =   3} */
          /* header_zIndex:          ${configs.header_zIndex          =   8} */
          /* footer_zIndex:          ${configs.footer_zIndex          =   8} */
          /* commentPane_zIndex:     ${configs.commentPane_zIndex     =   9} */
          /* headerHover_zIndex:     ${configs.headerHover_zIndex     =  10} */
          /* footerHover_zIndex:     ${configs.footerHover_zIndex     =  10} */
          /* channelButtons_zIndex1: ${configs.channelButtons_zIndex1 =  10} */
          /* channelPane_zIndex:     ${configs.channelPane_zIndex     =  11} */
          /* programPane_zIndex:     ${configs.programPane_zIndex     =  11} */
          /* channelButtons_zIndex2: ${configs.channelButtons_zIndex2 =  12} */
          /* panel_zIndex:           ${configs.panel_zIndex           = 100} */
          /* nav_transition:         ${configs.nav_transition         = '500ms cubic-bezier(.17,.84,.44,1)'} (Quartic) */
          /* nav_transitionDelay:    ${configs.nav_transitionDelay    = '500ms cubic-bezier(.17,.84,.44,1) 250ms'} (Quartic) */
          /* アベマ公式の不要要素 */
          /* (レイアウトを崩す謎要素に、とりあえず穏便に表示位置の調整で対応する) */
          .pub_300x250,
          .pub_300x250m,
          .pub_728x90,
          .text-ad,
          .textAd,
          .text_ad,
          .text_ads,
          .text-ads,
          .text-ad-links,
          #announcer,
          dummy{
            position: absolute;
            bottom: 0;
          }
          /* スクロールコメント */
          #${SCRIPTNAME}-canvas{
            z-index: ${configs.canvas_zIndex};
            pointer-events: none;
            position: absolute;
            top: 50%;
            left: 0;
            transform: translateY(-50%);
            overflow: hidden;
            opacity: 0;/*コメント非表示なら速やかに消える*/
            transition: opacity 500ms ease 250ms;
          }
          html.comment #${SCRIPTNAME}-canvas,
          #${SCRIPTNAME}-canvas.keep{
            opacity: ${configs.opacity};
          }
          #${SCRIPTNAME}-canvas > canvas{
            position: absolute;
            left: 100%;
            transition: transform ${configs.duration}s linear;
            will-change: transform;
            pointer-events: none;/*継承されないので*/
          }
          /* 映像 */
          [data-selector="screen"]{
            /* widthはコメントペインに応じて可変 */
            height: 100% !important;
            transition: ${configs.nav_transition};
          }
          [data-selector="screen"] > div,
          [data-selector="screen"] > div > div{
            width: 100% !important;
            height: 100% !important;
            top: 0;
            right: 0;
            transition: ${configs.nav_transition};
          }
          [data-selector="closer"]{
            pointer-events: auto;
          }
          /* コメントペインの表示非表示 */
          [data-selector="commentPane"]{
            width: auto;
            padding-left: ${configs.l_hide ? configs.l_width : configs.l_width / 4}vw;
            transform: translateX(100%);
            z-index: ${configs.commentPane_zIndex};
            transition: ${configs.nav_transitionDelay};
          }
          html.click [data-selector="commentPane"]{
            transition: ${configs.nav_transition};
          }
          html.comment [data-selector="commentPane"],
          [data-selector="commentPane"].keep/*core.closeOpenCommentPane用*/{
            transform: translateX(${configs.l_hide ? 50 : 0}%);
          }
          html.comment [data-selector="commentPane"]:hover,
          html.comment [data-selector="commentPane"].active,
          html.comment.active [data-selector="commentPane"]{
            transform: translateX(0);/*表示*/
            padding-left: ${configs.l_hide ? configs.l_width /4 : configs.l_width / 4}vw;/*隠れているときもマウスオーバー領域を確保する*/
          }
          [data-selector="commentPane"] > div{
            width: ${configs.l_width}vw;
            position: relative;
          }
          [data-selector="commentPane"] [class$="comment-SnackBarTransition"]{
            left: auto;
          }
          [data-selector="commentPane"] [data-selector="board"] > div > div:not([data-selector="comment"]) > div{
            margin: auto;/*全称セレクタ(*)のせいでローディングアニメーションが左に寄ってしまうので*/
          }
          html:not(.comment) [data-selector="commentPane"] [class$="Loading"] *{
            animation: none;/*画面外のローディングアニメーションにCPUを消費するアベマの悲しい仕様を上書き*/
          }
          /* コメントペインの透過 */
          [data-selector="commentPane"] > div{
            background: rgba(0,0,0,${configs.l_overlay ? configs.lb_opacity : 1});
            mask-image: linear-gradient(transparent 0%, black 25%, black 75%, transparent 100%);/*上下ともに透過していくマスクを用意しておいて...*/
            -webkit-mask-image: linear-gradient(transparent 0%, black 25%, black 75%, transparent 100%);/*まだ-webkit取れない*/
            mask-size: 100% 200%;/*マスクの可視範囲を一部分に限定する(トリッキー!!)*/
            mask-position: 0 0;/*上部番組テロップなどが見やすいように上側だけを透過させる*/
            transition: 500ms ease;
          }
          [data-selector="commentPane"]:hover > div{
            background: rgba(0,0,0,${configs.l_overlay ? configs.lb_opacityHover : 1});
            mask-position: 0 50%;/*上側も下側も透過しない*/
          }
          [data-selector="commentPane"]:hover *{
            color: rgba(255,255,255,${configs.l_overlay ? configs.lc_opacityHover : 1});
          }
          [data-selector="footer"]:hover ~ div > [data-selector="commentPane"] > div/*フッタにマウスホバー中*/,
          html.active [data-selector="commentPane"] > div/*フッタを含むナビゲーション表示中*/{
            background: rgba(0,0,0,${configs.l_overlay ? configs.lb_opacity : .5});/*透明度を指定しないと効かない*/
            mask-position: 0 100%;/*フッタが見やすいように下側だけを透過させる*/
          }
          [data-selector="commentPane"],
          [data-selector="commentPane"] *,
          [data-selector="commentPane"] *:hover{
            color: rgba(255,255,255,${configs.l_overlay ? configs.lc_opacity : 1}) !important;
            background: transparent;
          }
          /* コメントペインの統一フォントサイズ */
          [data-selector="commentPane"] *{
            font-size: ${configs.lc_fontsize}vh;
          }
          /* コメント投稿フォーム*/
          [class*="-comment-SnackBar"]/*投稿失敗時などの案内*/{
            pointer-events: none;/*なぜか入力欄を邪魔してしまうので*/
          }
          [data-selector="commentForm"],
          [data-selector="commentForm"] */*リセット*/{
            padding: 0;
            margin: 0;
          }
          [data-selector="commentForm"]{
            width: auto;
            padding: 0 .75vw;
            z-index: 10;/*新着コメントに隠れるバグを回避(公式もおかしい)*/
          }
          [data-selector="commentForm"] [class*="textarea-wrapper"]/*textareaの親*/{
            background: rgba(32,32,32,${configs.l_overlay ? configs.lb_opacity : 1});
            border-radius: .2vw;
            padding: .5vw;
            margin: .75vw 0;
          }
          [data-selector="commentForm"] textarea,
          [data-selector="commentForm"] textarea + div/*textareaの分身*/{
            width: calc(${configs.l_width}vw - 2.5vw);/*公式の指定を上書き*/
          }
          [data-selector="commentForm"] textarea + div > span:nth-child(2)/*文字数制限を超過した文字*/{
            border-bottom: 1px solid red;
          }
          [data-selector="commentForm"] [class*="twitter-wrapper"]/*(Twitter)連携する/連携中(バルーン含む)*/{
            width: 100%;
            padding-bottom: 1vw;
          }
          [data-selector="commentForm"] [class*="twitter-wrapper"] [class*="twitter-button"]/*(Twitter)連携する/連携中ボタン*/{
            width: calc(100% - 1vw);
            border-radius: .2vw;
            padding: 0 .5vw;
            height: ${configs.lc_fontsize * 2}vh;
            line-height: ${configs.lc_fontsize * 2}vh;
            overflow: hidden;
          }
          [data-selector="commentForm"] [class*="twitter-wrapper"] [class*="twitter-button--active"]/*(Twitter)連携中ボタン*/{
            background: rgba(80,163,225,${configs.l_overlay ? configs.lb_opacityHover : 1});
          }
          [data-selector="commentForm"] [class*="twitter-wrapper"] [class*="twitter-icon"]/*(Twitter)鳥アイコン*/{
            width: ${configs.lc_fontsize * (17/13)}vh;
            height: ${configs.lc_fontsize}vh;
            margin-right: 0.2vw;
          }
          [data-selector="commentForm"] [class*="CommentForm__etc-modules"] [class*="CommentForm__count"]/*残り文字数*/{
            padding: .5vw;
          }
          [data-selector="commentForm"] [class*="CommentForm__etc-modules"] [class*="post-button"]/*投稿する*/{
            border-radius: .2vw;
            padding: 0 .5vw;
            height: ${configs.lc_fontsize * 2}vh;
            line-height: ${configs.lc_fontsize * 2}vh;
            overflow: hidden;
            background: rgba(81,195,0,${configs.l_overlay ? configs.lb_opacityHover : 1});
          }
          [data-selector="commentForm"] [class*="twitter-balloon"]/*(Twitterアカウントバルーン)*/{
            border: 1px solid black;
            border-radius: .2vw;
            background: rgba(0,0,0,${configs.lb_opacityHover});
            width: calc(100% - 1.5vw);
            padding: .5em;
            top: .75vw;
            opacity: 0;/*transitionさせたいので*/
            display: block;/*transitionさせたいので*/
            pointer-events: none;/*transitionさせたいので*/
            transition: 500ms ease;
          }
          [data-selector="commentForm"] [class*="twitter-balloon--show"]/*(Twitterアカウントバルーン表示中)*/{
            opacity: 1;
            pointer-events: auto;
          }
          [data-selector="commentForm"] [class*="CommentForm__twitter-account"] > div:first-child > div/*アカウントアイコンの親*/{
            margin: .1vw;
          }
          [data-selector="commentForm"] [class*="CommentForm__twitter-account"] > div:first-child > div/*アカウントアイコンの親*/,
          [data-selector="commentForm"] [class*="CommentForm__twitter-profile-thumbnail"]/*アカウントアイコン*/{
            width: ${configs.lc_fontsize * 3}vh !important;
            height: ${configs.lc_fontsize * 3}vh !important;
            border-radius: .2vw;
          }
          [data-selector="commentForm"] [class*="CommentForm__twitter-account"] > div:last-child/*アカウント情報*/{
            line-height: ${configs.lc_fontsize * 3/2}vh;
            padding: .1vw .5vw;
            bottom: 0;
          }
          [data-selector="commentForm"] [class*="CommentForm__twitter-logout"]/*ログアウト*/{
            padding: .1vw .2vw;
            position: absolute;
            bottom: 0;
            right: 0;
          }
          /* コメント投稿フォームの表示制御 */
          [data-selector="commentPane"] [class*="CommentForm__etc-modules"]/*(Twitter)連携する/連携中・文字数・投稿するボタン*/{
            transition: height 500ms ease;
            height: 0;
            overflow: hidden;
          }
          [data-selector="commentPane"].active [class*="CommentForm__etc-modules"]/*(Twitter)連携する/連携中・文字数・投稿するボタン*/{
            height: calc(${configs.lc_fontsize * 1.5}vh + 1vw);
          }
          /* 新着コメント表示ボタン */
          [data-selector="newCommentsButton"]{
            background: rgba(81,195,0,${configs.l_overlay ? configs.lb_opacityHover : 1}) !important;
            border: none;
            width:80%;
            margin: 0 10%;
            padding: 0;
            line-height: 3em;
            height: 0;/*デフォルトで非表示*/
            overflow: hidden;
            transition: height 500ms ease;
          }
          [data-selector="newCommentsButton"]:hover{
            opacity: .75;
          }
          [data-selector="newCommentsButton"].shown{
            height: 3em;
          }
          /* 新着コメントのスライドダウン */
          [data-selector="newComments"]{/*jsでやる*/
          }
          [data-selector="newComments"] > div{
            will-change: transform;/*アベマが公式に指定すべきでは*/
          }
          html:not(.active) [data-selector="commentPane"]:not(:hover) [data-selector="newComments"] > div
          html:not(.active) [data-selector="commentPane"]:not(:hover) [data-selector="newComments"]{
            ${configs.l_hide ? 'transition: none !important;' : ''}/*画面外に隠れてるときはCPU負荷を下げる*/
          }
          /* コメント一覧 */
          /* (セレクタがNGワード登録フォームと合致しないように気を付ける) */
          [data-selector="board"] */*リセット*/{
            padding: 0;
            margin: 0;
          }
          [data-selector="board"] > div > span/*「まだ投稿がありません」のあやうい判定*/{
            margin-left: 20%;
            display: block;
          }
          [data-selector="comment"]{
            padding: 0 .75vw;
          }
          [data-selector="comment"] > div{
            flex-wrap: wrap;/*NGワード登録フォームの配置用*/
          }
          [data-selector="comment"]:not(.blockform) > div[class*=" "]/*自分が投稿したコメントのあやうい判定*/{
            background: rgba(255,255,255,.125);
            padding: 0 .75vw;/*hack*/
            margin: 0 -.75vw;
          }
          [data-selector="board"] div:not([id]) > p/*コメント*/,
          [data-selector="board"] div:not([id]) > p + div/*経過時間・ブロックボタン*/{
            margin: ${configs.lc_fontsize * (configs.lc_margin - configs.lc_linemargin) / 2}vh 0;
            line-height: ${1 + configs.lc_linemargin};
          }
          [data-selector="board"] div:not([id]) > p/*コメント*/{
            word-wrap: break-word;
          }
          [data-selector="board"] div:not([id]) > p + div/*経過時間・ブロックボタン*/{
            display: ${(configs.l_showtime) ? 'block' : 'none'};
            filter: opacity(75%);
            width: 4em;/*00秒前*/
            white-space: nowrap;
          }
          [data-selector="board"] div:not([id]) > p + div > button/*ブロックボタン*/{
            padding: ${configs.lc_margin / 2}em  ${configs.lc_margin}em;
            margin: -${configs.lc_margin / 2}em -${configs.lc_margin}em;
          }
          [data-selector="board"] div:not([id]) > p + div > button/*ブロックボタン*/ > svg{
            width: ${configs.lc_fontsize}vh;
            max-width: ${configs.lc_fontsize}vh;/*max指定しておけばword-break-allしなくてすむ*/
            height: ${configs.lc_fontsize}vh;
          }
          /* アベマ公式ブロック */
          [data-selector="comment"]{
            cursor: pointer;
          }
          [data-selector="comment"].blockform{
            background: rgba(255,255,255,.25);
            border-bottom: 1px solid transparent;/*マージンの相殺を回避する*/
          }
          [data-selector="comment"] form{
            height: 0;
            overflow: hidden;
            transition: height 500ms ease;
          }
          [data-selector="comment"].blockform form{
            height: calc(${configs.lc_fontsize * 2 + configs.lc_fontsize * (configs.lc_margin - configs.lc_linemargin) * 1.5}vh + .5em + .75vw);/*アニメーションのためにキッチリ計算*/
          }
          [data-selector="comment"] form > div:first-child > *{
            white-space: nowrap;
            overflow: hidden;
            margin: 0 0 ${configs.lc_fontsize * (configs.lc_margin - configs.lc_linemargin) / 2}vh;
          }
          [data-selector="comment"] form > div:first-child > p/*ブロックします*/{
            flex: 1;
          }
          [data-selector="comment"] form > div:first-child > button/*キャンセル*/{
            text-align: right;
            width: 5em;
          }
          [data-selector="comment"] form > div:last-child{
            margin-bottom: .75vw;
          }
          [data-selector="comment"] form > div:last-child > div/*select*/{
            height: auto;
          }
          [data-selector="comment"] form > div:last-child > div > span/*つまみ*/{
            right: .5em;
            top: calc(${configs.lc_fontsize / 2 + configs.lc_fontsize * (configs.lc_margin - configs.lc_linemargin) / 2}vh - .325em);
            border-width: 0.5em;
            border-top-width: .75em;
          }
          [data-selector="comment"] form > div:last-child > div > select{
            border-radius: .2em 0 0 .2em;
            padding: .25em .125em;
            height: auto;
            border: none;
          }
          [data-selector="comment"] form > div:last-child > div > select > option{
            padding: 0;
            margin: ${configs.lc_fontsize * (configs.lc_margin - configs.lc_linemargin) / 2}vh 0;
          }
          [data-selector="comment"] form > div:last-child > button/*ブロック*/{
            background: #f0163a;
            border-radius: 0 .2em .2em 0;
            padding: 0 1em;
          }
          [data-selector="comment"] form > div:last-child > button:hover/*ブロック*/{
            background: #bb122e;
          }
          [data-selector="comment"] form > div:last-child > button > span{
            margin: ${configs.lc_fontsize * (configs.lc_margin - configs.lc_linemargin) / 2}vh 0;
            line-height: 1.5;
          }
          [data-selector="board"] div:not([id]) > form select,
          [data-selector="board"] div:not([id]) > form select > option{
            color: black !important;
            background: white !important;
          }
          /* コメント一覧のスクロールバー */
          [data-selector="commentPane"] > div > div{
            overflow-y: scroll;
            margin-right: -${getScrollbarWidth()}px;/*スクロールバーを隠す*/
            transition: margin-right 0ms;
          }
          /* 上下ナビゲーションの表示非表示 */
          [data-selector="header"]{
            background: transparent;/*hover用paddingを持たせたいのでbackgroundはdivに移譲*/
            height: auto;
            padding: 0 0 ${configs.header_height}px;
            transform: translateY(calc(-100% + ${configs.header_height}px)) !important;/*隠れているときもマウスオーバー領域を確保する*/
            visibility: visible !important;
            z-index: ${configs.header_zIndex};
            transition: ${configs.nav_transitionDelay};
          }
          [data-selector="header"] > *:first-child{
            padding-left: 16px;
          }
          [data-selector="header"] > *:last-child{
            padding-right: 16px;
          }
          html.active [data-selector="commentPane"] > div{
            padding-top: ${configs.header_height}px;/*右コメント一覧を映像に重ねたせいで上部ナビゲーションと重なるのを避ける*/
          }
          [data-selector="footer"]{
            transform: translateY(calc(100% - ${configs.footer_height}px));/*隠れているときもマウスオーバー領域を確保する*/
            padding-top: ${configs.footer_height}px;
            z-index: ${configs.footer_zIndex};
            visibility: visible !important;
            transition: ${configs.nav_transitionDelay};
          }
          html:not(.active) [data-selector="footer"]:not(:hover) > div > *{
            bottom: 0;/*フルスクリーンボタンと音量ボタンが突然消えないようにアベマが指定すべき値*/
          }
          html.click [data-selector="header"],
          html.click [data-selector="footer"]{
            transition: ${configs.nav_transition};
          }
          ${(configs.n_clickonly) ? '' : 'dummy'} [data-selector="header"]:hover,
          html.active [data-selector="header"]{
            padding-bottom: ${configs.header_height * (1/2)}px;
            z-index: 11;
          }
          ${(configs.n_clickonly) ? '' : 'dummy'} [data-selector="footer"]:hover,
          html.active [data-selector="footer"]{
            padding-top: ${configs.footer_height * (1/2)}px;
            z-index: ${configs.footerHover_zIndex};/*コメントペインに勝たなければならない*/
          }
          ${(configs.n_clickonly) ? '' : 'dummy'} [data-selector="header"]:hover,
          html.active [data-selector="header"],
          ${(configs.n_clickonly) ? '' : 'dummy'} [data-selector="footer"]:hover,
          html.active [data-selector="footer"]{
            transform: translateY(0%) !important;
          }
          html.active [data-selector="header"],
          html.active [data-selector="footer"]{
            padding-top: 0;
            padding-bottom: 0;
          }
          [data-selector="footer"] > div > div:last-child > div:first-child:hover{/*ここにだけ追加して背景色を指定してるのはアベマのミスだろう*/
            background: transparent;
          }
          /* 上下ナビゲーション(+裏番組一覧・チャンネル切り替えボタン)の透過 */
          [data-selector="header"] > *,/*上部(hover用padding付き透明ラッパに包みたいのでdivに適用)*/
          [data-selector="header"] button + div,/*メニュードロップダウン*/
          [data-selector="footer"] > div > div:last-child/*下部*/,
          [data-selector="channelButtons"] button/*裏番組一覧・チャンネル切り替えボタン*/{
            background: rgba(0,0,0,${configs.n_opacity}) !important;
            transition: 500ms ease;
          }
          [data-selector="header"]:hover > *,
          [data-selector="header"] button + div:hover,
          [data-selector="footer"] > div > div:last-child:hover,
          [data-selector="channelButtons"] button:hover{
            background: rgba(0,0,0,${configs.n_opacityHover}) !important;
          }
          [data-selector="footer"] > div > div:last-child{
            border-top: none;
          }
          [data-selector="programButton"] div{/*チャンネル画像の背景が透過されていないアベマの仕様に対応*/
            background: transparent !important;
          }
          /* ブラウザ警告の透過 */
          [data-selector="caution"]{
            opacity: ${configs.n_opacity};
          }
          [data-selector="caution"],
          [data-selector="caution"] *{
            color: white;
            background: transparent;
          }
          /* 通知を受け取るボタン・視聴数・ローディングの表示非表示 */
          [class$="-AdReservationButton"]/*デフォルト(隠れているとき)*/{
            transition: ${configs.nav_transition};/*常に遅延なし*/
            right: 100vw;/*左側に変更*/
            bottom: ${configs.footer_height}px !important;
            transform: translate(-100%, 0);
            border-left: none;
            border-right: 1px solid #444;
            border-radius: 0 4px 4px 0;
          }
          [class$="-AdReservationButton"][aria-hidden="false"]/*出てきたとき*/{
            transform: translate(100%, 0);
          }
          [data-selector="viewCounter"]{
            position: absolute;
            top: ${configs.header_height}px;
            right: 0%;
            transform: translate(100%, .75vw);
            visibility: visible;
            transition: ${configs.nav_transition};/*常に遅延なし*/
          }
          [data-selector="viewCounter"]:hover,/*コメント一覧がない場合*/
          html.active [data-selector="viewCounter"]{
            transform: translate(-.75vw, .75vw);
          }
          html.comment [data-selector="viewCounter"]:hover,/*コメント一覧が表示されている場合*/
          html.comment.active [data-selector="viewCounter"]{
            right: ${configs.l_overlay ? configs.l_width : '0'}%;
          }
          [data-selector="loading"]{
            margin-right: 16px;/*デフォルトの位置を再現*/
            transform: translateY(${configs.footer_height}px);
          }
          /* 通知を受け取るボタン・視聴数・ローディングの透過 */
          [class$="-AdReservationButton"],
          [data-selector="viewCounter"]{
            background: rgba(0,0,0,${configs.n_opacity}) !important;
            transition: 500ms ease;
            pointer-events: auto;
          }
          [class$="-AdReservationButton"]:hover,
          [data-selector="viewCounter"]:hover{
            background: rgba(0,0,0,${configs.n_opacityHover}) !important;
          }
          [data-selector="screen"]/*視聴数をマウスオーバーにちゃんと反応させる工夫*/{
            z-index: ${configs.screen_zIndex};
            pointer-events: none;
          }
          [data-selector="screen"] button{/*補完*/
            pointer-events: auto;
          }
          /* 番組アンケートの透過 */
          [data-selector="enquete"] > div{
            background: rgba(255,255,255,${configs.n_opacityHover}) !important;
          }
          [data-selector="enquete"] > div button{
            background: rgba(255,255,255,${configs.n_opacityHover}) !important;
          }
          /* 裏番組一覧の表示非表示 */
          [data-selector="channelPane"]{
            z-index: ${configs.channelPane_zIndex};
            transform: translateX(100%);
          }
          html.channel [data-selector="channelPane"]{
            transform: translateX(0);
          }
          html:not(.channel) [data-selector="channelPane"] [role="progressbar"] *{
            animation: none;/*画面外のローディングアニメーションにCPUを消費するアベマの悲しい仕様を上書き*/
          }
          /* 裏番組一覧の透過 */
          [data-selector="channelPane"] > div{
            background: rgba(0,0,0,${configs.n_opacityHover});
          }
          [data-selector="channelPane"] > div > a{
            background: transparent;
          }
          [data-selector="channelPane"] > div > a:hover{
            background: rgba(34,34,34,${configs.n_opacityHover});
          }
          [data-selector="channelPane"] *{
            color: white;
          }
          /* 番組情報の表示非表示 */
          [data-selector="programPane"]{
            z-index: ${configs.programPane_zIndex};
            transform: translateX(100%);
          }
          html.program [data-selector="programPane"]{
            transform: translateX(0);
          }
          /* 番組情報の透過 */
          [data-selector="programPane"]{
            color: white;
            background: rgba(0,0,0,${configs.n_opacityHover});
            transition: 500ms ease;
          }
          [data-selector="programPane"] svg > use:not([*|href*="_rect.svg"]){/*rectは赤背景*/
            fill: white;
          }
          /* ボタン共通 */
          [data-selector="channelButtons"] button *,
          [data-selector="commentButton"] *,
          [data-selector="programButton"] *,
          [data-selector="fullscreenButton"] *{
            pointer-events: none;/*クリックイベント発生箇所を親のボタン要素に統一する*/
          }
          #${SCRIPTNAME}-ng-button svg,
          #${SCRIPTNAME}-config-button svg{
            fill: white;
            vertical-align: middle;
          }
          [data-selector="footer"] > div > button/*各ボタン*/,
          [data-selector="footer"] > div > [data-selector="VolumeController"] button/*ボリュームボタン*/{
            padding: 15px 15px 15px;/*クリック判定範囲を広くしてあげる*/
            margin: -15px -15px -15px;
            box-sizing: content-box;
            filter: drop-shadow(0 0 2.5px rgba(0,0,0,.75));/*白い背景で見にくいアベマの悲しい仕様を回避*/
          }
          [data-selector="footer"] > div > button/*各ボタン*/,
          [data-selector="footer"] > div > [data-selector="VolumeController"]/*ボリュームボタンセット*/{
            transition: bottom ${configs.nav_transitionDelay}, opacity ${configs.nav_transition};
          }
          html.click [data-selector="footer"] > div > button/*各ボタン*/,
          html.click [data-selector="footer"] > div > [data-selector="VolumeController"]/*ボリュームボタンセット*/{
            transition: ${configs.nav_transition};
          }
          /* 裏番組一覧・チャンネル切り替えボタン */
          [data-selector="channelButtons"]{
            filter: drop-shadow(0 0 2.5px rgba(0,0,0,.75));/*白い背景で見にくいアベマの悲しい仕様を回避*/
            transform: translate(calc(100% - ${configs.channelButtons_size}px), -50%);
            padding: ${configs.channelButtons_size * (1/2)}px 0 ${configs.channelButtons_size * (1/2)}px ${configs.channelButtons_size}px;/*隠れているときもサイズ3/4まではマウスオーバー領域を確保する*/
            transition: ${configs.nav_transitionDelay};/*アベマの指定漏れ?*/
            z-index: ${configs.channelButtons_zIndex1};/*フッタ操作を邪魔しない*/
          }
          html.click [data-selector="channelButtons"]{
            transition: ${configs.nav_transition};
          }
          [data-selector="channelButtons"]:hover,
          html.active [data-selector="channelButtons"]{
            padding: ${configs.channelButtons_size * (1/2)}px 0 ${configs.channelButtons_size * (1/2)}px ${configs.channelButtons_size * (1/2)}px;
            transform: translate(0%, -50%);
          }
          html.ng [data-selector="channelButtons"]/*NGワード登録中は控えて出しゃばらない*/{
            padding: 0;
            transform: translate(100%, -50%);
          }
          html.channel [data-selector="channelButtons"],
          html.program [data-selector="channelButtons"]{
            z-index: ${configs.channelButtons_zIndex2};/*フッタ操作中ではないはずなので*/
          }
          /* フルスクリーンボタン */
          [data-selector="fullscreenButton"]{
            right: 170px;
          }
          [data-selector="fullscreenButton"][data-icon="mini_screen"] use[*|href^="/images/icons/full_screen.svg"],
          [data-selector="fullscreenButton"][data-icon="full_screen"] use[*|href^="/images/icons/mini_screen.svg"]{
            display: none;
          }
          /* ボリュームボタン */
          [data-selector="VolumeController"]{
            right: 120px;
          }
          [data-selector="VolumeController"] button > svg{
            vertical-align: bottom;/*アベマのわずかなズレを修正*/
            transform: translateX(3px);/*アイコンの重心のズレを修正*/
          }
          [data-selector="VolumeController"] [class$="slider-container"] > div::before/*スライダ可動レール*/{
            content: "";
            position: absolute;
            width: calc(15px + 100% + 15px);/*クリック判定範囲を広くしてあげる*/
            height: calc(15px + 100% + 5px);/*クリック判定範囲を広くしてあげる*/
            left: -15px;
            top: -15px;
          }
          /* 登録NGワード一覧ボタン */
          #${SCRIPTNAME}-ng-button{
            right: 70px;
          }
          /* 設定ボタン */
          #${SCRIPTNAME}-config-button{
            right: 20px;
          }
          button[aria-label="フルスクリーン解除"] + div/*フルスクリーン時のボリュームUIセット*/{
            pointer-events: auto;
          }
          /* コメントボタン */
          [data-selector="commentButton"]{
            transition: 500ms ease;/*アベマの指定漏れ?*/
          }
          html.comment.active [data-selector="commentButton"] svg,
          html.comment [data-selector="footer"]:hover [data-selector="commentButton"] svg,
          [data-selector="commentPane"].keep [data-selector="commentButton"] svg{
            animation: spin 1s infinite alternate cubic-bezier(.45,.05,.55,.95)/*sin*/;
          }
          @keyframes spin{/*CPU食うので注意*/
            from{
              transform: scaleX(1);
            }
            to{
              transform: scaleX(-1);
            }
          }
          /* NGワード登録フォーム */
          #${SCRIPTNAME}-ng-form{
            border-radius: .5vw;
            margin-bottom: .75vw;/*お試しNGワードでハイライトされた場合に内包されるように*/
            width: 100%;
            background: rgba(32,32,32,${configs.l_overlay ? configs.lb_opacity : 1});
            height: calc(${configs.lc_fontsize}vh + 2 * ${configs.lc_fontsize * 2}vh + 4 * .5vw);/*アニメーションのためにキッチリ計算*/
            overflow: hidden;
            transition: 500ms ease;
          }
          #${SCRIPTNAME}-ng-form.hidden{
            height: 0;
            margin-bottom: 0;
          }
          #${SCRIPTNAME}-ng-form h1,
          #${SCRIPTNAME}-ng-form p{
            color: white;
            width: auto;
            margin: .5vw;
            display: flex;
          }
          #${SCRIPTNAME}-ng-form h1{
            line-height: ${configs.lc_fontsize}vh;
          }
          #${SCRIPTNAME}-ng-form p{
            line-height: ${configs.lc_fontsize * 2}vh;
          }
          #${SCRIPTNAME}-ng-form h1 span{
            flex-grow: 1;
          }
          #${SCRIPTNAME}-ng-form h1 button.list{
            width: ${configs.lc_fontsize * 2}vh;
            padding: ${configs.lc_fontsize / 2}vh 0;
            margin: -${configs.lc_fontsize / 2}vh 0;
          }
          #${SCRIPTNAME}-ng-form h1 button.list svg{
            vertical-align: top;
            width: ${configs.lc_fontsize}vh;
            height: ${configs.lc_fontsize}vh;
            fill: white;
          }
          #${SCRIPTNAME}-ng-form button.help{
            width: ${configs.lc_fontsize * 2}vh;
            margin-left: .5vw;
            background: rgba(0,0,0,${configs.lb_opacity});
            border-radius: .25vw;
          }
          #${SCRIPTNAME}-ng-form p.word input{
            color: white;
            border: none;
            border-radius: .25vw;
            background: rgba(0,0,0,${configs.lb_opacity});
            height: ${configs.lc_fontsize * 2}vh;
            padding: 0 .5vw;
            width: 50%;
            flex-grow: 1;
          }
          #${SCRIPTNAME}-ng-form p.type{
            border-radius: .25vw;
            overflow: hidden;
            display: flex;
          }
          #${SCRIPTNAME}-ng-form p.type button{
            color: white;
            font-weight: bold;
            width: 100%;
            margin-left: 1px;
            flex-grow: 1;
            height: ${configs.lc_fontsize * 2}vh;
          }
          #${SCRIPTNAME}-ng-form p.type button.trial{
            margin-left: 0;
            background: rgba(255,224,32,${configs.l_overlay ? configs.lb_opacity : .5});
          }
          #${SCRIPTNAME}-ng-form p.type button.for24h,
          #${SCRIPTNAME}-ng-form p.type button.forever{
            background: rgba(255,32,32,${configs.l_overlay ? configs.lb_opacity : .5});
          }
          #${SCRIPTNAME}-ng-form p.type button.trial:hover,
          #${SCRIPTNAME}-ng-form p.type button.trial:focus{
            color: black;
            background: rgba(255,224,32,${configs.l_overlay ? configs.lb_opacityHover : 1});
          }
          #${SCRIPTNAME}-ng-form p.type button.for24h:hover,
          #${SCRIPTNAME}-ng-form p.type button.for24h:focus,
          #${SCRIPTNAME}-ng-form p.type button.forever:hover,
          #${SCRIPTNAME}-ng-form p.type button.forever:focus{
            background: rgba(255,32,32,${configs.l_overlay ? configs.lb_opacityHover : 1});
          }
          #${SCRIPTNAME}-ng-form h1 button.list:hover svg,
          #${SCRIPTNAME}-ng-form h1 button.list:focus svg,
          #${SCRIPTNAME}-ng-form p.word button.help:hover,
          #${SCRIPTNAME}-ng-form p.word button.help:focus{
            filter: brightness(.5);
          }
          /* NGワードコメント */
          [data-selector="comment"].ng-trial{
            background: rgba(255,224,32,${configs.l_overlay ? configs.lb_opacityHover : .75});
            border-bottom: 1px solid transparent;/*マージンの相殺を回避する*/
            cursor: pointer;
          }
          [data-selector="comment"].ng-trial:hover{
            background: rgba(255,224,32,${configs.l_overlay ? configs.lb_opacity : .5});
          }
          [data-selector="comment"].ng-trial > *:first-child/*NGワード登録フォームや公式ブロックには適用しない*/{
            pointer-events: none;/*イベントはcommentで発生させる*/
          }
          [data-selector="comment"].ng-deleted{
            display: none;
          }
          /* パネル共通 */
          #${SCRIPTNAME}-panels{
            position: absolute;
            width: 100%;
            height: 100%;
            top: 0;
            left: 0;
            overflow: hidden;
            pointer-events: none;
          }
          #${SCRIPTNAME}-panels div.panel{
            position: absolute;
            width: 360px;
            max-height: 100%;/*小さなウィンドウに対応*/
            overflow: auto;
            left: 50%;
            bottom: 50%;
            transform: translate(-50%, 50%);
            z-index: ${configs.panel_zIndex};
            background: rgba(0,0,0,.75);
            transition: ${configs.nav_transition};
            padding: 5px 0;
            pointer-events: auto;
          }
          #${SCRIPTNAME}-panels div.panel.hidden{
            bottom: 0;
            transform: translate(-50%, 100%) !important;
          }
          #${SCRIPTNAME}-panels h1,
          #${SCRIPTNAME}-panels h2,
          #${SCRIPTNAME}-panels h3,
          #${SCRIPTNAME}-panels h4,
          #${SCRIPTNAME}-panels legend,
          #${SCRIPTNAME}-panels ul,
          #${SCRIPTNAME}-panels ol,
          #${SCRIPTNAME}-panels dl,
          #${SCRIPTNAME}-panels code,
          #${SCRIPTNAME}-panels p{
            color: rgba(255,255,255,1);
            font-size: 14px;
            padding: 2px 10px;
            line-height: 1.4;
          }
          #${SCRIPTNAME}-panels header{
            display: flex;
          }
          #${SCRIPTNAME}-panels header h1{
            flex: 1;
          }
          #${SCRIPTNAME}-panels div.panel > p.buttons{
            text-align: right;
            padding: 5px 10px;
          }
          #${SCRIPTNAME}-panels div.panel > p.buttons button{
            width: 120px;
            padding: 5px 10px;
            margin-left: 10px;
            border-radius: 5px;
            color: rgba(255,255,255,1);
            background: rgba(64,64,64,1);
            border: 1px solid rgba(255,255,255,1);
          }
          #${SCRIPTNAME}-panels div.panel > p.buttons button.primary{
            font-weight: bold;
            background: rgba(0,0,0,1);
          }
          #${SCRIPTNAME}-panels div.panel > p.buttons button:hover,
          #${SCRIPTNAME}-panels div.panel > p.buttons button:focus{
            background: rgba(128,128,128,.75);
          }
          #${SCRIPTNAME}-panels .template{
            display: none !important;
          }
          #${SCRIPTNAME}-panels[data-panels="2"] div.panel:nth-child(1){
            transform: translate(-100%, 50%);
          }
          #${SCRIPTNAME}-panels[data-panels="2"] div.panel:nth-child(2){
            transform: translate(0%, 50%);
          }
          #${SCRIPTNAME}-panels[data-panels="3"] div.panel:nth-child(1){
            transform: translate(-150%, 50%);
          }
          #${SCRIPTNAME}-panels[data-panels="3"] div.panel:nth-child(3){
            transform: translate(50%, 50%);
          }
          /* NGワード一覧 */
          #${SCRIPTNAME}-ng-list button.help{
            color: white;
            width: 20px;
            background: rgba(0,0,0,.5);
            border-radius: 5px;
          }
          #${SCRIPTNAME}-ng-list button.help:hover,
          #${SCRIPTNAME}-ng-list button.help:focus{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-ng-list p.sort{
            width: 80%;
            height: 20px;
            padding: 0;
            margin: 5px auto;
            border-radius: 5px;
            overflow: hidden;
            display: flex;
          }
          #${SCRIPTNAME}-ng-list p.sort.disabled{
            filter: brightness(.5);
            pointer-events: none;
          }
          #${SCRIPTNAME}-ng-list p.sort input{
            display: none;
          }
          #${SCRIPTNAME}-ng-list p.sort label{
            color: white;
            background: rgba(128,128,128,.25);
            font-size: 10px;
            line-height: 20px;
            text-align: center;
            width: 100%;
            margin-left: 1px;
          }
          #${SCRIPTNAME}-ng-list p.sort label:first-of-type{
            margin-left: 0;
          }
          #${SCRIPTNAME}-ng-list p.sort input + label::after{
            font-size: 75%;
            vertical-align: top;
            content: " ▼";
          }
          #${SCRIPTNAME}-ng-list p.sort input.reverse + label::after{
            content: " ▲";
          }
          #${SCRIPTNAME}-ng-list p.sort input:checked + label,
          #${SCRIPTNAME}-ng-list p.sort label:hover,
          #${SCRIPTNAME}-ng-list p.sort label:focus{
            background: rgba(128,128,128,.75);
          }
          #${SCRIPTNAME}-ng-list ul{
            max-height: calc(${window.innerHeight}px - (5px + 24px + 30px + 42px + 5px) - 20px);
            overflow-y: auto;
          }
          #${SCRIPTNAME}-ng-list ul > li{
            padding: 2px 10px;
            display: flex;
          }
          #${SCRIPTNAME}-ng-list p.word,
          #${SCRIPTNAME}-ng-list p.words{
            padding: 0;
            flex: 1;
          }
          #${SCRIPTNAME}-ng-list p.word input,
          #${SCRIPTNAME}-ng-list p.words textarea{
            font-size: 12px;
            width: 100%;
          }
          #${SCRIPTNAME}-ng-list p.word input{
            height: 20px;
          }
          #${SCRIPTNAME}-ng-list p.words textarea{
            height: 40px;
            resize: vertical;
          }
          #${SCRIPTNAME}-ng-list p.type{
            height: 20px;
            border-radius: 5px;
            overflow: hidden;
            padding: 0;
            margin-left: 10px;
            flex: 1;
            display: flex;
          }
          #${SCRIPTNAME}-ng-list p.type input{
            display: none;
          }
          #${SCRIPTNAME}-ng-list p.type label{
            text-align: center;
            font-size: 10px;
            line-height: 20px;
            font-weight: bold;
            width: 100%;
            margin-left: 1px;
          }
          #${SCRIPTNAME}-ng-list p.type label.trial{
            margin-left: 0;
            background: rgba(255,224,32,.25);
          }
          #${SCRIPTNAME}-ng-list p.type label.for24h,
          #${SCRIPTNAME}-ng-list p.type label.forever{
            background: rgba(255,32,32,.25);
          }
          #${SCRIPTNAME}-ng-list p.type input:checked + label.trial,
          #${SCRIPTNAME}-ng-list p.type label.trial:hover,
          #${SCRIPTNAME}-ng-list p.type label.trial:focus{
            color: black;
            background: rgba(255,224,32,.75);
          }
          #${SCRIPTNAME}-ng-list p.type input:checked + label.for24h,
          #${SCRIPTNAME}-ng-list p.type label.for24h:hover,
          #${SCRIPTNAME}-ng-list p.type label.for24h:focus,
          #${SCRIPTNAME}-ng-list p.type input:checked + label.forever,
          #${SCRIPTNAME}-ng-list p.type label.forever:hover,
          #${SCRIPTNAME}-ng-list p.type label.forever:focus{
            background: rgba(255,32,32,.75);
          }
          #${SCRIPTNAME}-ng-list p.type label.remove{
            background: rgba(128,128,128,.25);
          }
          #${SCRIPTNAME}-ng-list p.type input:checked + label.remove,
          #${SCRIPTNAME}-ng-list p.type label.remove:hover,
          #${SCRIPTNAME}-ng-list p.type label.remove:focus{
            background: rgba(128,128,128,.75);
          }
          #${SCRIPTNAME}-ng-list li.add p.type label.remove{
            visibility: hidden;
          }
          #${SCRIPTNAME}-ng-list input + label{
            cursor: pointer;
          }
          /* NGヘルプパネル */
          #${SCRIPTNAME}-ng-help h2{
            margin-top: 10px;
          }
          #${SCRIPTNAME}-ng-help dl{
            display: flex;
            flex-wrap: wrap;
          }
          #${SCRIPTNAME}-ng-help dl dt{
            width: 100px;
            margin: 2px 0;
            background: rgba(0,0,0,.5);
            border-radius: 5px;
          }
          #${SCRIPTNAME}-ng-help dl dt code{
            padding:0 5px;
          }
          #${SCRIPTNAME}-ng-help dl dd{
            width: 230px;
            margin: 2px 0 2px 10px;
          }
          /* 設定パネル */
          #${SCRIPTNAME}-config-panel fieldset p{
            padding-left: calc(10px + 1em);
          }
          #${SCRIPTNAME}-config-panel fieldset p:hover{
            background: rgba(255,255,255,.25);
          }
          #${SCRIPTNAME}-config-panel label{
            display: block;
          }
          #${SCRIPTNAME}-config-panel input{
            width: 80px;
            height: 20px;
            position: absolute;
            right: 10px;
          }
          #${SCRIPTNAME}-config-panel input[type="text"]{
            width: 160px;
          }
          #${SCRIPTNAME}-config-panel input[type="text"]:invalid{
            border: 1px solid rgba(255, 0, 0, 1);
            background: rgba(255, 0, 0, .5);
          }
          #${SCRIPTNAME}-config-panel p.note{
            color: gray;
            font-size: 75%;
            width: 100%;
          }
        </style>
      `,
    },
  };
  class Storage{
    static key(key){
      return (SCRIPTNAME) ? (SCRIPTNAME + '-' + key) : key;
    }
    static save(key, value, expire = null){
      key = Storage.key(key);
      localStorage[key] = JSON.stringify({
        value: value,
        saved: Date.now(),
        expire: expire,
      });
    }
    static read(key){
      key = Storage.key(key);
      if(localStorage[key] === undefined) return undefined;
      let data = JSON.parse(localStorage[key]);
      if(data.value === undefined) return data;
      if(data.expire === undefined) return data;
      if(data.expire === null) return data.value;
      if(data.expire < Date.now()) return localStorage.removeItem(key);
      return data.value;
    }
    static delete(key){
      key = Storage.key(key);
      delete localStorage[key];
    }
    static saved(key){
      key = Storage.key(key);
      if(localStorage[key] === undefined) return undefined;
      let data = JSON.parse(localStorage[key]);
      if(data.saved) return data.saved;
      else return undefined;
    }
  }
  let $ = function(s){return document.querySelector(s)};
  let $$ = function(s){return document.querySelectorAll(s)};
  let animate = function(callback, ...params){requestAnimationFrame(() => requestAnimationFrame(() => callback(...params)))};
  let observe = function(element, callback, options = {childList: true, attributes: false, characterData: false}){
    let observer = new MutationObserver(callback.bind(element));
    observer.observe(element, options);
    return observer;
  };
  let createElement = function(html){
    let outer = document.createElement('div');
    outer.innerHTML = html;
    return outer.firstElementChild;
  };
  let getScrollbarWidth = function(){
    let div = document.createElement('div');
    div.textContent = 'dummy';
    document.body.appendChild(div);
    div.style.overflowY = 'scroll';
    let clientWidth = div.clientWidth;
    div.style.overflowY = 'hidden';
    let offsetWidth = div.offsetWidth;
    document.body.removeChild(div);
    return offsetWidth - clientWidth;
  };
  let normalize = function(string){
    return string.replace(/[!-~]/g, function(s){
      return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
    }).replace(normalize.RE, function(s){
      return normalize.KANA[s];
    }).replace(/ /g, ' ').replace(/~/g, '〜');
  };
  normalize.KANA = {
    ガ:'ガ', ギ:'ギ', グ:'グ', ゲ:'ゲ', ゴ: 'ゴ',
    ザ:'ザ', ジ:'ジ', ズ:'ズ', ゼ:'ゼ', ゾ: 'ゾ',
    ダ:'ダ', ヂ:'ヂ', ヅ:'ヅ', デ:'デ', ド: 'ド',
    バ:'バ', ビ:'ビ', ブ:'ブ', ベ:'ベ', ボ: 'ボ',
    パ:'パ', ピ:'ピ', プ:'プ', ペ:'ペ', ポ: 'ポ',
    ヷ:'ヷ', ヺ:'ヺ', ヴ:'ヴ',
    ア:'ア', イ:'イ', ウ:'ウ', エ:'エ', オ:'オ',
    カ:'カ', キ:'キ', ク:'ク', ケ:'ケ', コ:'コ',
    サ:'サ', シ:'シ', ス:'ス', セ:'セ', ソ:'ソ',
    タ:'タ', チ:'チ', ツ:'ツ', テ:'テ', ト:'ト',
    ナ:'ナ', ニ:'ニ', ヌ:'ヌ', ネ:'ネ', ノ:'ノ',
    ハ:'ハ', ヒ:'ヒ', フ:'フ', ヘ:'ヘ', ホ:'ホ',
    マ:'マ', ミ:'ミ', ム:'ム', メ:'メ', モ:'モ',
    ヤ:'ヤ', ユ:'ユ', ヨ:'ヨ',
    ラ:'ラ', リ:'リ', ル:'ル', レ:'レ', ロ:'ロ',
    ワ:'ワ', ヲ:'ヲ', ン:'ン',
    ァ:'ァ', ィ:'ィ', ゥ:'ゥ', ェ:'ェ', ォ:'ォ',
    ッ:'ッ', ャ:'ャ', ュ:'ュ', ョ:'ョ',
    "。":'。', "、":'、', "ー":'ー', "「":'「', "」":'」', "・":'・',
  };
  normalize.RE = new RegExp('(' + Object.keys(normalize.KANA).join('|') + ')', 'g');
  let log = function(){
    if(!DEBUG) return;
    let l = log.last = log.now || new Date(), n = log.now = new Date();
    let stack = new Error().stack, callers = stack.match(/^([^/<]+(?=<?@))/gm) || stack.match(/[^. ]+(?= \(<anonymous)/gm) || [];
    console.log(
      SCRIPTNAME + ':',
      /* 00:00:00.000  */ n.toLocaleTimeString() + '.' + n.getTime().toString().slice(-3),
      /* +0.000s       */ '+' + ((n-l)/1000).toFixed(3) + 's',
      /* :00           */ ':' + stack.match(/:[0-9]+:[0-9]+/g)[1].split(':')[1],/*LINE*/
      /* caller.caller */ (callers[2] ? callers[2] + '() => ' : '') +
      /* caller        */ (callers[1] || '')  + '()',
      ...arguments
    );
  };
  core.initialize();
  if(window === top && console.timeEnd) console.timeEnd(SCRIPTNAME);
})();