Greasy Fork

来自缓存

Greasy Fork is available in English.

AbemaTV Timetable Viewer

AbemaTV に見やすく使いやすい番組表と、気軽に登録できる通知機能を提供します。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        AbemaTV Timetable Viewer
// @namespace   knoa.jp
// @description AbemaTV に見やすく使いやすい番組表と、気軽に登録できる通知機能を提供します。
// @include     https://abema.tv/*
// @version     1.1.4
// @grant       none
// ==/UserScript==

// console.log('AbemaTV? => hireMe()');
(function(){
  const SCRIPTNAME = 'TimetableViewer';
  const DEBUG = false;/*
  [update] 1.1.4
  軽微な修正。
  (Storage, main消失即復帰)

  [bug]
  main消失 DOMException NotFoundError
    番組枠変更が複数回ぶん溜まると起きる?背後で一度閉じて開き直すとどうだ? >1枠でもダメだった
      「変更しないまま溜まって」なくても、変更回数が溜まるだけで起きる可能性はどうか
      2+1=3で起きた。1+1+5でも当然起きた。1:39,40,42,50,57(3)で初めて起きた。1+7当然。
      一度に3を乗り越えることもあるな...初回だったから?その後1を経て2で死んだ。
      変更対象のチャンネル位置による?
    全チャンネル表示の時にも起きるのかどうか >起きた
      休眠チャンネルを追加しない、後続番組も追加しないアベマフレンドリな状態でも?
    正式にチャンネル閉じればアベマは中身クリアするんだっけ?そこに賭ける手もあるか。
    アベマ的にはクリックイベントをきっかけにしているのかも?
  
  [to do]
  複数タブで設定保存しても元に戻ったりしちゃうかも。これもlocalStorageイベントか。
  channelsも複数タブで取得するのは無駄だわな
  番組枠変わらなくても時間幅調整したいからmodifyしたいかも
  単独起動時に裏番一覧が消えたまま
  画質変更・・・? CM明けや番組開始の低画質が解消されるならやる価値はあるが。
  
  Windows
    summaryのmarks位置がずれてる
    キャスト・スタッフのキーワード行間つまりすぎ
    Edge: element.animate ポリフィル

  [to research]
  video[src]消失もある Unhandled rejection Error
  文字のにじみ
  Chrome 境界1pxでチラチラチラチラチラチラ
  マウスホバー判定の1pxギャップを上手になくしたい

  [possible]
  通知の隣にマイリストボタン
    番組タイトル
    3日後の1月26日(木)まで
    # 分数、放送日、チャンネル
    # 放送日順で期限は赤黄緑で色分け?
  スマホUI/アプリ提案?(番組表・通知)

  [requests]
  画質変更・・・このスクリプトでやることではない気もする

  [common]
  windowイベントリスナの統一化(userActions = {})
  */
  if(window === top && console.time) console.time(SCRIPTNAME);
  const ID = Math.random().toString(36).substr(2,10);/*スクリプト実行ごとのID([0-9a-z]{10})*/
  const UPDATECHANNELS = false;/*デバッグ用*/
  const CONFIGS = {
    /* 番組表パネル */
    transparency: {TYPE: 'int',    DEFAULT: 25},/*透明度(%)*/
    height:       {TYPE: 'int',    DEFAULT: 50},/*番組表の高さ(%)(文字サイズ連動)*/
    span:         {TYPE: 'int',    DEFAULT:  4},/*番組表の時間幅(時間)*/
    replace:      {TYPE: 'bool',   DEFAULT: 1 },/*アベマ公式の番組表を置き換える*/
    /* 通知(abema.tvを開いているときのみ) */
    n_before:     {TYPE: 'int',    DEFAULT: 10},/*番組開始何秒前に通知するか(秒)*/
    n_change:     {TYPE: 'bool',   DEFAULT: 1 },/*自動でチャンネルも切り替える*/
    n_overlap:    {TYPE: 'bool',   DEFAULT: 1 },/* 時間帯が重なっている時は通知のみ*/
    n_sync:       {TYPE: 'bool',   DEFAULT: 1 },/*アベマ公式の通知と共有する*/
    /* 表示チャンネル */
    c_visibles:   {TYPE: 'object', DEFAULT: {}},/*(チャンネル名)*/
  };
  const PIXELRATIO = window.devicePixelRatio;/*Retina比*/
  const SECOND = 1;/*秒(s)*/
  const MINUTE = 60*SECOND;/*分(s)*/
  const HOUR = 60*MINUTE;/*時間(s)*/
  const DAY = 24*HOUR;/*日(s)*/
  const JST = 9*HOUR;/*JST時差(s)*/
  const JDAYS = ['日', '月', '火', '水', '木', '金', '土'];/*曜日*/
  const EDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];/*曜日(class)*/
  const TERM = 7 + 1;/*番組スケジュールの取得期間(日)*/
  const TERMLABEL = '1週間';/*TERMのユーザー向け表現*/
  const CACHEEXPIRE = 3*HOUR*1000;/*番組スケジュールのキャッシュ期間(ms)*/
  const LOGOINDICATE = 2*SECOND*1000;/*チャンネルロゴの表示時間(ms)*/
  const BOUNCINGPIXEL = 1;/*バウンシングエフェクト用ピクセル*/
  const TIMES = [0,3,6,9,12,15,18,21];/*番組表のスクロール位置(時)*/
  const NAMEWIDTH = 7.5;/*番組表のチャンネル名幅(vw)*/
  const MAXRESULTS = 100;/*番組取得の最大数*/
  const NOTIFICATIONREMAINS = 5;/*番組開始後も通知を残しておく時間(s)*/
  const NOTIFICATIONAFTER = DAY;/*番組終了後にアベマを開いても通知する期間(s)*/
  const ABEMATIMETABLEDURATION = 500;/*アベマ公式番組表を置き換えた際の遷移アニメーション時間(ms)*/
  const STALLEDLIMIT = 6;/*映像が停止してから自動リロードするまでの時間(s)*/
  const EXPIRESTALLED = 1*MINUTE*1000;/*停止疑惑を持ち続ける時間(ms)*/
  const PROGRESS = .75;/*1秒の間に最低限進んでいなければならない映像秒数(s)*/
  const PANELS = ['timetablePanel', 'configPanel'];/*パネルの表示順*/
  /* サイト定義 */
  const APIS = {
    CHANNELS:     'https://api.abema.io/v1/channels',/*全チャンネル取得API*/
    SCHEDULE:     'https://api.abema.io/v1/media?dateFrom={dateFrom}&dateTo={dateTo}',/*番組予定取得API*/
    RESERVATION:  'https://api.abema.io/v1/viewing/reservations/{type}/{id}',/*番組通知API*/
    RESERVATIONS: 'https://api.abema.io/v1/viewing/reservations/slots?limit={limit}',/*番組通知取得API*/
    FAVORITE:     'https://api.abema.io/v1/favorites/slots/{id}?userId={userId}',/*マイビデオAPI*/
    FAVORITES:    'https://api.abema.io/v1/favorites/slots?limit={limit}',/*マイビデオ取得API*/
    SLOT:         'https://api.abema.io/v1/viewing/reservations/slots/{id}',/*通知番組情報取得API*/
  };
  const CHANNELLOGO = 'https://hayabusa.io/abema/channels/logo/{id}.w340.png';/*チャンネルロゴ*/
  const THUMBIMG = 'https://hayabusa.io/abema/programs/{displayProgramId}/{name}.q{q}.w{w}.h{h}.x{x}.jpg';/*番組サムネイル*/
  const NOCONTENTS = [/*コンテンツなし番組タイトル*/
    '番組なし',/*存在しないけどNOCONTENTS[0]はスクリプト内で代替用のラベルとして使う*/
    /^番組告知$/,
    /^《告知》/,
    /^CM$/,
    /^CM 【[^】]+】$/,/*【煽り】付きCM(REPLACE後の文字列にマッチ)*/
    /^$/,/*空欄*/
  ];
  const REPLACES = [/*番組タイトル置換*/
    [/^([^【<\[\\]+(?:配信|放送|公開)中?!)\/?(.+)$/,  '$2 $1'],/*最後に回す(間に SPACE を挟む)*/
    [/^([^【<\[\\]+(?:配信|放送|公開)中?)\/(.+)$/,    '$2 $1'],/*最後に回す(間に SPACE を挟む)*/
    [/^([^【<\[\\]+(?:無料|観れる|見放題)!)\/?(.+)$/, '$2 $1'],/*最後に回す(間に SPACE を挟む)*/
    [/^([^【<\[\\]+(?:無料|観れる|見放題))\/(.+)$/,   '$2 $1'],/*最後に回す(間に SPACE を挟む)*/
    [/^(【.+?】)(.+)$/,              '$2 $1' ],/*【煽り】を最後に回す(間に HAIR SPACE を挟む)*/
    [/^(<.+?>)(.+)$/,                '$2 $1' ],/*<煽り>を最後に回す(間に SPACE を挟む)*/
    [/^(\\.+?\/)(.+)$/,              '$2 $1' ],/*\煽り/を最後に回す(間に SPACE を挟む)*/
    [/^([^/]+一挙)\/(.+)$/,          '$2 /$1'],/*...一挙/を最後に回す(間に SPACE を挟む)*/
    [/^(見逃し)\/(.+)$/,             '$2 /$1'],/*見逃し/を最後に回す(間に SPACE を挟む)*/
    [/^(今夜[0-9:時分]+?)〜(.+)$/,   '$2 〜$1'],/*今夜...時〜を最後に回す(間に 〜 を挟む)*/
    [/^((?:TV|テレビ)?アニメ)\s?(?:「|『)(.+?)(?:」|』)(.*)$/, '$2$3 $1'],/*TVアニメを最後に回しカッコを取り除く(間に SPACE を挟む)*/
    [/^(.+主演)\s?(?:「|『)(.+?)(?:」|』)(.*)$/,               '$2$3 $1'],/*...主演を最後に回しカッコを取り除く(間に SPACE を挟む)*/
    [/♯([0-9]+)/g, '#$1'],/*シャープをナンバーに統一*/
    [/([^ ])((?:\(|\[|<)?#[0-9]+)/g, '$1 $2'],/*直前にスペースがないナンバリングを補完*/
    [/([^ ])(\(|\[|<)/g, '$1 $2'],/*直前にスペースがないカッコ開始を補完*/
  ];
  const NAMEFRAGS = {/*キャストとスタッフの名前の正規化用*/
    NONAMES: new RegExp([
      '^(?:-|ー|未定|なし|coming ?soon)$', /*なし*/
      '^(?:【|《|〈|<|〔|[|ー|-)[^:]+$',  /*【見出し】*/
      '^(?:■|◆|●|▶︎|▼|◼︎)[^:]+$',      /*■見出し*/
      ':.+:', /*コロンを複数含む複数人のベタテキストは判定不能*/
    ].join('|'), 'i'),
    SKIPS: new RegExp([
      '^(?:[^(:]+|[^:]+\\)[^:]*):', /*最初のコロンまでは役職名(カッコ内は無視)*/
      '\\([^)]+\\)(?:・|、|\\/)?',   /*(カッコ)内とそれに続く区切り文字は無視*/
      '\\[[^\\]]+\\](?:・|、|\\/)?', /*[カッコ]内とそれに続く区切り文字は無視*/
      '\\s*(?:…+|その)?(?:ほか|他)(?:多数)?$', /*ほか*/
      '\\s*(?:etc\.?|(?:and|&|&) ?more)$',     /*ほか*/
      '※.+$',
      '順不同',
    ].join('|'), 'i'),
    SEPARATORS: new RegExp([
      '、',  /*名前、名前*/
      '\\/', /*名前/名前*/
    ].join('|')),
  };
  const PADDINGBEFORE = 50*MINUTE;/*休止チャンネルが再開する前に事前映像が流れ始める時間*/
  let retry = 10;/*必要な要素が見つからずあきらめるまでの試行回数*/
  let site = {
    targets: [
      function screen(){let loading = $('img[src="/images/misc/feed_loading.gif"]'); return (loading) ? site.use(loading.parentNode.parentNode.parentNode.parentNode) : false;},
      function channelButton(){let button = $('button[aria-label="放送中の裏番組"]'); return (button) ? site.use(button) : false;},
      function channelUpButton(){let button = $('button[aria-label="前のチャンネルに切り替える"]'); return (button) ? site.use(button) : false;},
      function channelDownButton(){let button = $('button[aria-label="次のチャンネルに切り替える"]'); return (button) ? site.use(button) : false;},
      function channelPane(){let container = $('[class*="-tv-VChannelList__container"]'); return (container) ? site.use(container.parentNode) : false;},
      function progressbar(){let progressbar = $('#main [role="progressbar"]'); return (progressbar) ? site.use(progressbar.parentNode) : false;},
    ],
    get: {
      onairChannels: function(channelPane){return channelPane.querySelectorAll('a[href^="/now-on-air/"]');},
      onairChannel: function(channelPane, id){return channelPane.querySelector(`a[href="/now-on-air/${id}"]`);},
      thumbnail: function(a){return a.querySelector('a > div > div:nth-child(1) > div > div:nth-child(1) > img');},
      channelLogo: function(a){return a.querySelector('a > div > div:nth-child(1) > div > img');},
      nowonairSlot: function(a){return a.querySelector('a > div > div:nth-child(2)');},
      title: function(slot){return slot.querySelector('div:nth-child(1) > span > span:last-of-type');},
      duration: function(slot){return slot.querySelector('div:nth-child(2) > span');},
      timetableLinkOnNOA: function(){return $('[class*="__timetable-link"]');},
      token: function(){return localStorage.getItem('abm_token');},
      userId: function(){return localStorage.getItem('abm_userId');},
      closer: function(){
        /* チャンネル切り替えごとに差し替わるのでつど取得 */
        let button = $('[data-selector="screen"] > div > div > button');
        return button;
      },
      abemaTimetableButton: function(){
        let a = $('header a[href="/timetable"]');
        return (a) ? site.use(a, 'abemaTimetableButton') : null;
      },
      abemaTimetableSlotButton: function(channelId, programId){
        /* アベマの仕様に依存しまくり */
        let index = Array.from($$('div > a[href^="/timetable/channels/"]')).findIndex((a) => a.href.endsWith('/' + channelId));
        if(index === -1) return log(`Not found: "${channelId}" anchor.`);
        let buttons = $$(`div:nth-child(${index + 1}) > div > article > button`);/*index該当チャンネルに絞って効率化*/
        if(buttons.length === 0) return log(`Not found: "${channelId}" buttons.`);
        if(DAY/2 < MinuteStamp.past()) buttons = Array.from(buttons).reverse();/*正午を過ぎていたら逆順に探す*/
        for(let i = 0, button; button = buttons[i]; i++){
          let div = button.parentNode.parentNode;
          if(Object.keys(div).some((key) => key.includes('reactInternalInstance') && (div[key].key === programId))) return button;
        }
        return log(`Not found: "${programId}" button.`);
      },
      abemaTimetableNowOnAirLink: function(channelId){
        let a = $(`a[href="/now-on-air/${channelId}"]`);
        return (a) ? a : log(`Not found: "${channelId}" link.`);
      },
      abemaNotifyButton: function(target){
        switch(true){
          case(target.classList.contains('notify')):
            return false;
          /* textContentでしか判定できない */
          case(target.textContent === 'この番組の通知を受け取る'):/*放送視聴中のボタン*/
          case(target.textContent === '通知を受け取る'):
          case(target.textContent === '今回のみ通知を受け取る'):
          case(target.textContent === '毎回通知を受け取る'):
          case(target.textContent === '解除する'):/*マイビデオの可能性もあるが仕方ない*/
            return true;
          default:
            return false;
        }
      },
      abemaMyVideoButton: function(target){
        switch(true){
          case(target.classList.contains('myvideo')):
            return false;
          /* 番組表の埋め込みボタンのあやうい判定 */
          case(target.attributes['role'] && target.attributes['role'].value === 'checkbox'):
          /* textContentでしか判定できない */
          case(target.textContent === 'マイビデオに追加'):
          case(target.textContent === '解除する'):/*通知の可能性もあるが仕方ない*/
            return true;
          default:
            return false;
        }
      },
      subscriptionType: function(){
        /* アベマの仕様に依存しまくり */
        if(!window.dataLayer) return log('Not found: window.dataLayer');
        for(let i = 0; window.dataLayer[i]; i++){
          if(window.dataLayer[i].subscriptionType) return window.dataLayer[i].subscriptionType;
        }
      },
      screenCommentScroller: function(){return html.classList.contains('ScreenCommentScroller')},
      apis: {
        channels: function(){return APIS.CHANNELS},
        timetable: function(){
          let toDigits = (date) => date.toLocaleDateString('ja-JP', {year: 'numeric', month: '2-digit', day: '2-digit'}).replace(/[^0-9]/g, '');
          let from = new Date(), to = new Date(from.getTime() + TERM*DAY*1000);
          return APIS.SCHEDULE.replace('{dateFrom}', toDigits(from)).replace('{dateTo}', toDigits(to));
        },
        reservation: function(id, type){
          const types = {repeat: 'slotGroups', once: 'slots'};
          return APIS.RESERVATION.replace('{type}', types[type]).replace('{id}', id);
        },
        reservations: function(){return APIS.RESERVATIONS.replace('{limit}', MAXRESULTS)},
        favorite: function(id){return APIS.FAVORITE.replace('{id}', id).replace('{userId}', site.get.userId())},
        favorites: function(){return APIS.FAVORITES.replace('{limit}', MAXRESULTS)},
        slot: function(id){return APIS.SLOT.replace('{id}', id)},
      },
    },
    use: function use(target = null, key = use.caller.name){
      if(target) target.dataset.selector = key;
      elements[key] = target;
      return target;
    },
    /* [live(生), newcomer(新), first(初), last(終), bingeWatching(一挙), recommendation(注目), none(なし)] の順番 */
    marks: ['live', 'newcomer', 'first', 'last', 'bingeWatching', 'recommendation', 'none'],
  };
  class Channel{
    constructor(channel = {}){
      Object.keys(channel).forEach((key) => {
        switch(key){
          case('programs'): return this.programs = channel.programs.map((program) => new Program(program));
          default: return this[key] = channel[key];
        }
      });
    }
    fromChannelSlots(channel, slots){
      this.id = channel.id;
      this.name = channel.name.replace(/^Abema/, '').replace(/チャンネル$/, '');
      this.fullName = channel.name;
      this.order = channel.order;
      this.programs = slots.map((slot) => new Program().fromSlot(slot, {id: this.id, name: this.fullName}));
      /* 空き時間を埋める */
      let now = MinuteStamp.now(), justToday = MinuteStamp.justToday(), createPadding = (id, startAt, endAt) => new Program({
        id: id,
        title: NOCONTENTS[0],
        padding: true,
        noContent: true,
        channel: {id: this.id, name: this.fullName},
        startAt: startAt,
        endAt: endAt,
      });
      if(now < this.programs[0].startAt) this.programs.unshift(createPadding(channel.id + '-' + now, now, this.programs[0].startAt));
      for(let i = 0; this.programs[i]; i++){
        if(this.programs[i + 1] && this.programs[i].endAt !== this.programs[i + 1].startAt){
          this.programs.splice(i + 1, 0, createPadding(channel.id + '-' + this.programs[i].endAt, this.programs[i].endAt, this.programs[i + 1].startAt));
        }else if(!this.programs[i + 1] && this.programs[i].endAt < justToday + (TERM+1)*DAY){
          this.programs.push(createPadding(channel.id + '-' + this.programs[i].endAt, this.programs[i].endAt, justToday + (TERM+1)*DAY));
          break;/*抜けないと無限ループになる*/
        }
      }
      return this;
    }
  }
  class Program{
    constructor(program = {}){
      Object.keys(program).forEach((key) => {
        this[key] = program[key];
      });
    }
    fromSlot(slot, channel){
      /* ID */
      this.id = slot.id;
      this.displayProgramId = slot.displayProgramId;
      this.series = (slot.programs[0].series) ? slot.programs[0].series.id : slot.programs[0].seriesId;
      //this.sequence = slot.programs[0].episode.sequence;/*次回*/
      this.slotGroup = slot.slotGroup;/*{id, lastSlotId, fixed, name}*/
      /* 概要 */
      /* {live(生), newcomer(新), first(初), last(終), bingeWatching(一挙), recommendation(注目), drm(マークなし)} からマークなしを取り除く */
      Object.keys(slot.mark).forEach((key) => {
        if(core.html.marks[key] === undefined){
          delete slot.mark[key];
          if(DEBUG && key !== 'drm') log('Unknown mark:', key);
        }
      });
      this.marks = slot.mark || {};
      this.title = Program.modifyTitle(normalize(slot.title));
      this.links = slot.links;/*[{title, type(2のみ), value(url)}]*/
      //this.highlight = slot.highlight;/*短い*/
      this.detailHighlight = slot.detailHighlight/*長い*/ || slot.highlight/*短い*/;
      this.content = slot.content;/*詳細*/
      this.padding = false;/*空き時間の枠埋めではない*/
      this.noContent = this.hasNoContent(this.title);
      this.channel = channel;/*{id, name}*/
      /* サムネイル */
      this.thumbImg = slot.programs[0].providedInfo.thumbImg;
      this.sceneThumbImgs = slot.programs[0].providedInfo.sceneThumbImgs || [];
      /* クレジット */
      this.casts = (slot.programs[0].credit.casts || []).map(normalize);
      this.crews = (slot.programs[0].credit.crews || []).map(normalize);
      this.copyrights = slot.programs[0].credit.copyrights;
      /* 時間 */
      this.startAt = slot.startAt;
      this.endAt = slot.endAt;
      this.timeshiftEndAt = slot.timeshiftEndAt;
      this.timeshiftFreeEndAt = slot.timeshiftFreeEndAt;
      /* シェア */
      //this.hashtag = slot.hashtag;
      //this.sharedLink = slot.sharedLink;
      return this;
    }
    static modifyTitle(title){
      for(let i = 0, replace; replace = REPLACES[i]; i++){
        title = title.replace(replace[0], replace[1]);
      }
      return title;
    }
    static appendMarks(title, marks){
      const latters = ['last'];/*タイトルの後に付くマーク*/
      if(marks) Object.keys(marks).forEach((mark) => {
        if(!core.html.marks[mark]) return;/*htmlが用意されていない*/
        if(latters.includes(mark)) return title.parentNode.appendChild(createElement(core.html.marks[mark]()));
        return title.parentNode.insertBefore(createElement(core.html.marks[mark]()), title);
      });
    }
    static getRepeatTitle(a, b){
      let getCommon = (a, b) => {
        for(let i = 0, parts = a.split(/(?=\s)/), common = ''; parts[i]; i++){
          if(b.includes(parts[i].trim())) common += parts[i];
          else if(common) return common;/*共通部分が途切れたら終了*/
        }
        return b;/*共通部分がなければ後続を優先する*/
      }
      return [getCommon(a, b), getCommon(b, a)].sort((a, b) => a.length - b.length)[0].trim();
    }
    static linkifyNames(node, click){
      if(node.textContent.match(NAMEFRAGS.NONAMES) !== null) return;
      for(let i = 0, n; n = node.childNodes[i]; i++){/*回しながらchildNodesは増えていく*/
        if(n.data === '') continue;
        let pos = n.data.search(NAMEFRAGS.SKIPS);
        switch(true){
          case(pos === -1):
            if(split(n)) i++;/*セパレータの分を1つ飛ばす*/
            append(n);
            break;
          case(pos === 0):
            n.splitText(RegExp.lastMatch.length);
            break;
          case(0 < pos):
            n.splitText(pos);/*nをpos直前で分割*/
            if(split(n)) i++;/*セパレータの分を1つ飛ばす*/
            append(n);
            break;
        }
      }
      function split(n){
        let pos = n.data.search(NAMEFRAGS.SEPARATORS);
        if(1 <= pos){
          n.splitText(pos);
          n.nextSibling.splitText(RegExp.lastMatch.length);
          return true;
        }
      }
      function append(n){
        n.data = n.data.trim();
        if(n.data === '') return;
        let span = document.createElement('span');
        span.className = 'name';
        node.insertBefore(span, n.nextSibling);
        span.appendChild(n);
        span.addEventListener('click', click);
      }
    }
    get group(){
      return (this.slotGroup) ? this.slotGroup.id : undefined;
    }
    get repeat(){
      return this.group;
    }
    get once(){
      return this.id;
    }
    get duration(){
      return this.endAt - this.startAt;
    }
    get dateString(){
      let long = {month: 'short', day: 'numeric', weekday: 'short', hour: 'numeric', minute: '2-digit'}, short = {hour: 'numeric', minute: '2-digit'};
      let start = new Date(this.startAt*1000), end = new Date(this.endAt*1000);
      let startString = start.toLocaleString('ja-JP', long);
      let endString = end.toLocaleString('ja-JP', (start.getDate() === end.getDate()) ? short : long);
      return `${startString} 〜 ${endString}`;
    }
    get justifiedDateString(){
      return this.justifiedDateToString(this.startAt) + ' 〜 ' + this.justifiedTimeToString(this.endAt);
    }
    get justifiedStartAtShortDateString(){
      return this.justifiedShortDateToString(this.startAt);
    }
    get startAtString(){
      return this.timeToString(this.startAt);
    }
    get endAtString(){
      return this.timeToString(this.endAt);
    }
    get timeString(){
      return this.startAtString + ' 〜 ' + this.endAtString;
    }
    get timeshiftString(){
      let today = MinuteStamp.justToday(), endAt = MyVideo.isPremiumUser() ? this.timeshiftEndAt : this.timeshiftFreeEndAt;
      if(!endAt) return '';
      let remain = endAt - today, term = endAt - MinuteStamp.justToday(this.endAt);
      switch(true){
        case(remain < 0 || term < 0):
          return '';
        case(remain < DAY):
          return `きょうの ${this.timeToString(endAt)} まで見逃し視聴`;
        case(remain < DAY*2):
          return `あす ${this.dateToString(endAt)} の ${this.timeToString(endAt)} まで見逃し視聴`;
        case(term < DAY):
          return `放送当日 ${this.dateToString(endAt)} の ${this.timeToString(endAt)} まで見逃し視聴`;
        case(term < DAY*2):
          return `放送翌日 ${this.dateToString(endAt)} の ${this.timeToString(endAt)} まで見逃し視聴`;
        case(this.startAt <= MinuteStamp.now()):/*放送中なら「放送...日後」ではなく単に「...日後」とする*/
          return `${Math.floor(term/DAY)}日後の ${this.dateToString(endAt)} まで見逃し視聴`;
        default:
          return `放送${Math.floor(term/DAY)}日後の ${this.dateToString(endAt)} まで見逃し視聴`;
      }
    }
    hasNoContent(title){
      return NOCONTENTS.some((frag) => title.match(frag));
    }
    dateToString(timestamp){
      return new Date(timestamp * 1000).toLocaleDateString('ja-JP', {month: 'short', day: 'numeric', weekday: 'short'});
    }
    timeToString(timestamp){
      return new Date(timestamp * 1000).toLocaleTimeString('ja-JP', {hour: 'numeric', minute: '2-digit'});
    }
    justifiedShortDateToString(timestamp){
      /* toLocaleString('ja-JP')の2-digitが効かないバグがあるので */
      let date = new Date(timestamp * 1000),  d = {
        date:    ('00' + date.getDate()).slice(-2),
        day:     JDAYS[date.getDay()],
        hours:   ('00' + date.getHours()).slice(-2),
        minutes: ('00' + date.getMinutes()).slice(-2),
      };
      return `${d.date}(${d.day}) ${d.hours}:${d.minutes}`;
    }
    justifiedDateToString(timestamp){
      /* toLocaleString('ja-JP')の2-digitが効かないバグがあるので */
      let date = new Date(timestamp * 1000),  d = {
        month:   date.getMonth() + 1,
        date:    ('00' + date.getDate()).slice(-2),
        day:     JDAYS[date.getDay()],
        hours:   ('00' + date.getHours()).slice(-2),
        minutes: ('00' + date.getMinutes()).slice(-2),
      };
      return `${d.month}月${d.date}日(${d.day}) ${d.hours}:${d.minutes}`;
    }
    justifiedTimeToString(timestamp){
      let date = new Date(timestamp * 1000),  d = {
        hours:   ('00' + date.getHours()).slice(-2),
        minutes: ('00' + date.getMinutes()).slice(-2),
      };
      return `${d.hours}:${d.minutes}`;
    }
  }
  class Thumbnail{
    constructor(displayProgramId, name, size = 'small'){
      const x = (window.innerWidth * PIXELRATIO < 960) ? 1 : 2;
      const sizes = {/*解像度確保のためx2を指定させていただく*/
        large: {q: 95, w: 256, h: 144, x: x},
        small: {q: 95, w: 135, h:  76, x: x},
      };
      this.displayProgramId = displayProgramId;
      this.name = name;
      this.params = sizes[size];
    }
    get node(){
      let img = document.createElement('img');
      img.classList.add('loading');
      img.addEventListener('load', function(){
        img.classList.remove('loading');
      });
      img.src = THUMBIMG.replace(
        '{displayProgramId}', this.displayProgramId
      ).replace(
        '{name}', this.name
      ).replace(
        '{q}', this.params.q
      ).replace(
        '{w}', this.params.w
      ).replace(
        '{h}', this.params.h
      ).replace(
        '{x}', this.params.x
      );
      return img;
    }
  }
  class MinuteStamp{
    static now(){
      let now = new Date(), minutes = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes());
      return minutes.getTime() / 1000;
    }
    static past(){
      let now = new Date(), minutes = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), now.getMinutes());
      return ((minutes.getTime() / 1000) + JST) % DAY;
    }
    static justToday(timestamp){
      let now = timestamp ? new Date(timestamp*1000) : new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      return today.getTime() / 1000;
    }
    static timeToString(timestamp){
      return new Date(timestamp * 1000).toLocaleTimeString('ja-JP', {hour: 'numeric', minute: '2-digit'});
    }
    static timeToClock(timestamp){
      let time = new Date(timestamp * 1000);
      return createElement(core.html.clock(time.getHours(), ('00' + time.getMinutes()).slice(-2)));
    }
    static shortestDateToString(timestamp){
      let d = new Date(timestamp * 1000);
      return `${d.getDate()}${JDAYS[d.getDay()]}`;
    }
  }
  class Button{
    static getOnceButtons(id){
      return document.querySelectorAll(`button.notify[data-once="${id}"]`);
    }
    static getRepeatButtons(id){
      return document.querySelectorAll(`button.notify[data-once][data-repeat="${id}"]`);
    }
    static getButtonTitle(button){
      if(button.classList.contains('active')) return button.dataset.titleActive;
      if(button.classList.contains('search')) return button.dataset.titleSearch;
      if(button.classList.contains('repeat')) return button.dataset.titleRepeat;
      if(button.classList.contains('once')) return button.dataset.titleOnce;
      return button.dataset.titleDefault;
    }
    static addActive(button){
      Button.reverse(button, 'add', 'active');
    }
    static removeActive(button){
      Button.reverse(button, 'remove', 'active');
    }
    static addOnce(id){
      Button.getOnceButtons(id).forEach((b) => Button.reverse(b, 'add', 'once'));
      Slot.getOnceSlots(id).forEach((s) => Slot.highlight(s, 'add', 'active'));
    }
    static removeOnce(id){
      Button.getOnceButtons(id).forEach((b) => Button.reverse(b, 'remove', 'once'));
      Slot.getOnceSlots(id).forEach((s) => {if(!Notifier.match(s.dataset.once)) Slot.highlight(s, 'remove', 'active')});
    }
    static addRepeat(id){
      Button.getRepeatButtons(id).forEach((b) => Button.reverse(b, 'add', 'repeat'));
      Slot.getRepeatSlots(id).forEach((s) => Slot.highlight(s, 'add', 'active'));
    }
    static removeRepeat(id){
      Button.getRepeatButtons(id).forEach((b) => Button.reverse(b, 'remove', 'repeat'));
      Slot.getRepeatSlots(id).forEach((s) => {if(!Notifier.match(s.dataset.once)) Slot.highlight(s, 'remove', 'active')});
    }
    static addSearch(id){
      Button.getOnceButtons(id).forEach((b) => Button.reverse(b, 'add', 'search'));
      Slot.getOnceSlots(id).forEach((s) => Slot.highlight(s, 'add', 'active'));
    }
    static removeSearch(id){
      Button.getOnceButtons(id).forEach((b) => Button.reverse(b, 'remove', 'search'));
      Slot.getOnceSlots(id).forEach((s) => {if(!Notifier.match(s.dataset.once)) Slot.highlight(s, 'remove', 'active')});
    }
    static reverse(button, action, name){
      button.classList.add('reversing');
      button.addEventListener('transitionend', function(e){
        button.classList[action](name);
        button.classList.remove('reversing');
        button.title = Button.getButtonTitle(button);
      }, {once: true});
    }
    static shake(button){
      button.animate([
        {transform: 'translateX(-10%)'},
        {transform: 'translateX(+10%)'},
      ], {
        duration: 50,
        iterations: 5,
      });
    }
    static pop(button){
      button.animate([/*放物線*/
        {transform: 'translateY( +7%)'},
        {transform: 'translateY( +6%)'},
        {transform: 'translateY( +4%)'},
        {transform: 'translateY(  0%)'},
        {transform: 'translateY(-32%)'},
        {transform: 'translateY(-48%)'},
        {transform: 'translateY(-56%)'},
        {transform: 'translateY(-60%)'},
        {transform: 'translateY(-62%)'},
        {transform: 'translateY(-63%)'},
        {transform: 'translateY(-63%)'},
        {transform: 'translateY(-62%)'},
        {transform: 'translateY(-60%)'},
        {transform: 'translateY(-56%)'},
        {transform: 'translateY(-48%)'},
        {transform: 'translateY(-32%)'},
        {transform: 'translateY(  0%)'},
        {transform: 'translateY(-16%)'},
        {transform: 'translateY(-24%)'},
        {transform: 'translateY(-28%)'},
        {transform: 'translateY(-30%)'},
        {transform: 'translateY(-31%)'},
        {transform: 'translateY(-31%)'},
        {transform: 'translateY(-30%)'},
        {transform: 'translateY(-28%)'},
        {transform: 'translateY(-24%)'},
        {transform: 'translateY(-16%)'},
        {transform: 'translateY(  0%)'},
        {transform: 'translateY( +3%)'},
        {transform: 'translateY( +2%)'},
        {transform: 'translateY(  0%)'},
      ], {
        duration: 750,
      });
    }
  }
  class Slot{
    static getOnceSlots(id){
      return document.querySelectorAll(`.slot[data-once="${id}"]`);
    }
    static getRepeatSlots(id){
      return document.querySelectorAll(`.slot[data-repeat="${id}"]`);
    }
    static highlight(slot, action, name){
      slot.classList.add('transition');
      animate(function(){
        slot.classList[action](name);
        slot.addEventListener('transitionend', function(e){
          slot.classList.remove('transition');
        }, {once: true});
      });
    }
  }
  class Notifier{
    static sync(){
      if(!configs.n_sync) return;
      let add = (type, id) => {
        let updateLocal = (type, program) => {
          switch(type){
            case('once'):
              notifications['once'][program.once] = Program.modifyTitle(normalize(program.title));
              Notifier.updateOnceProgram(program);
              break;
            case('repeat'):
              notifications['repeat'][program.repeat] = Program.modifyTitle(normalize(program.title));
              Notifier.updateRepeatPrograms(program);
              break;
          }
          Notifier.save();
        };
        let program = core.getProgramById(id);
        if(program) return updateLocal(type, program);
        /* 臨時チャンネル番組などでprogramが見つからないときはアベマに問い合わせる */
        /* (最初からprogramデータ付きで取得するAPIオプション(&withDataSet=true)もあるけど無駄が多いので採用しない) */
        let xhr = new XMLHttpRequest();
        xhr.open('GET', site.get.apis.slot(id));
        xhr.setRequestHeader('Authorization', 'bearer ' + site.get.token());
        xhr.responseType = 'json';
        xhr.onreadystatechange = function(){
          if(xhr.readyState !== 4 || xhr.status !== 200) return;
          if(!xhr.response.dataSet || !xhr.response.dataSet.slots) return log(`Not found: reservation data ${type} "${id}"`);
          log('xhr.response:', xhr.response);
          let slot = xhr.response.dataSet.slots[0], channel = xhr.response.dataSet.channels[0];/*xhr.responseをそのまま使うとパフォーマンス悪い*/
          slot.programs = xhr.response.dataSet.programs;
          let program = new Program().fromSlot(slot, {id: channel.id, name: channel.name});
          updateLocal(type, program);
        };
        xhr.send();
      };
      /* こっからsync処理 */
      let xhr = new XMLHttpRequest();
      xhr.open('GET', site.get.apis.reservations());
      xhr.setRequestHeader('Authorization', 'bearer ' + site.get.token());
      xhr.responseType = 'json';
      xhr.onreadystatechange = function(){
        if(xhr.readyState !== 4 || xhr.status !== 200) return;
        if(!xhr.response.slots) return log(`Not found: reservations data`);
        //log('xhr.response:', xhr.response);
        let slots = xhr.response.slots;/*xhr.responseをそのまま使うとパフォーマンス悪い*/
        /* あちらにしかないものはあちらで能動的に登録したとみなしてこちらにも登録 */
        for(let i = 0; slots[i]; i++){
          if(!slots[i].repetition && !Notifier.matchOnce(slots[i].slotId)){
            if(Notifier.match(slots[i].slotId)) continue;/*こちらでは検索通知として登録済み*/
            add('once', slots[i].slotId);
          }else if(slots[i].repetition && !Notifier.matchRepeat(slots[i].slotGroupId)){
            add('repeat', slots[i].slotId);
          }
        }
        /* こちらにしかないものはあちらで能動的に削除したとみなしてこちらでも削除 */
        let now = MinuteStamp.now();/*放送終了までは残す*/
        Object.keys(notifications.once).forEach((key) => {
          if(slots.some((slot) => slot.slotId === key)) return;/*1回か毎回かは問わずあちらにもある*/
          let program = notifications.programs.find((p) => p.once === key);
          if(program && (now < program.endAt)) return;/*あちらにないけどまだ放送中*/
          delete notifications.once[key];
        });
        Object.keys(notifications.repeat).forEach((key) => {
          if(slots.some((slot) => slot.slotGroupId === key)) return;/*あちらにもある*/
          let program = notifications.programs.find((p) => p.repeat === key);
          if(program && (now < program.endAt)) return;/*あちらにないけどまだ放送中*/
          delete notifications.repeat[key];
        });
        notifications.programs = notifications.programs.filter((program) => {
          if(slots.some((slot) => slot.slotId === program.id)) return true;/*あちらにもある*/
          if(Notifier.matchSearch(program)) return true;/*検索通知として登録済み*/
          if(now < program.endAt) return true;/*まだ放送中*/
        });
        Notifier.save();
      };
      xhr.send();
    }
    static addOnce(program){
      if(Notifier.matchOnce(program.once)) return;
      Notifier.add(program, 'once');
      Notifier.updateOnceProgram(program);
      Notifier.save();
    }
    static removeOnce(program){
      Notifier.remove(program, 'once');
      notifications.programs = notifications.programs.filter((p) => {
        if(Notifier.matchOnce(p.once)) return true;
        if(Notifier.matchRepeat(p.repeat)) return true;
        if(Notifier.matchSearch(p)) return true;
      });
      Notifier.save();
    }
    static addRepeat(program){
      if(Notifier.matchRepeat(program.repeat)) return;
      Notifier.add(program, 'repeat');
      Notifier.updateRepeatPrograms(program);
      Notifier.save();
    }
    static removeRepeat(program){
      Notifier.remove(program, 'repeat');
      notifications.programs = notifications.programs.filter((p) => {
        if(Notifier.matchOnce(p.once)) return true;
        if(Notifier.matchRepeat(p.repeat)) return true;
        if(Notifier.matchSearch(p)) return true;
      });
      Notifier.save();
    }
    static updateRepeatTitle(program){
      notifications.repeat[program.repeat] = Program.getRepeatTitle(notifications.repeat[program.repeat], program.title);
    }
    static add(program, type){
      Notification.requestPermission();
      notifications[type][program[type]] = program.title;
      if(configs.n_sync) Notifier.reserve(program[type], type);
    }
    static remove(program, type){
      delete notifications[type][program[type]];
      if(configs.n_sync) Notifier.unreserve(program[type], type);
    }
    static addSearch(key, marks){
      Notification.requestPermission();
      notifications.search[key] = marks;
      let matchIds = Notifier.updateSearchPrograms(key, marks);
      Notifier.save();
      return matchIds;/*通知ボタンくるりんぱ用*/
    }
    static removeSearch(key, marks){
      delete notifications.search[key];
      let unmatchIds = [];
      notifications.programs = notifications.programs.filter((p) => {
        if(Notifier.matchSearch(p)) return true;
        unmatchIds.push(p.id);/*今回searchの対象から外れたid*/
        if(Notifier.matchRepeat(p.repeat)) return true;
        if(Notifier.matchOnce(p.once)) return true;
        if(configs.n_sync) Notifier.unreserve(p.id, 'once');/*公式に検索通知がないので1回通知として削除する*/
      });
      Notifier.save();
      return unmatchIds;/*通知ボタンくるりんぱ用*/
    }
    static reserve(id, type){
      notifications.requests = notifications.requests.filter((r) => r.id !== id);/*既に予定済みなら上書きするのでいったん削除*/
      notifications.requests.push({action: 'PUT', id: id, type: type});
    }
    static unreserve(id, type){
      notifications.requests = notifications.requests.filter((r) => r.id !== id);/*既に予定済みなら上書きするのでいったん削除*/
      notifications.requests.push({action: 'DELETE', id: id, type: type});
    }
    static request(){
      if(!configs.n_sync || !notifications.requests[0]) return;/*リクエスト予定なし*/
      let request = notifications.requests[0], action = request.action, id = request.id, type = request.type;/*1つずつしか処理しない*/
      /* APIから通知を予約する */
      let xhr = new XMLHttpRequest();
      xhr.open(action, site.get.apis.reservation(id, type));
      xhr.setRequestHeader('Authorization', 'bearer ' + site.get.token());
      if(DEBUG) xhr.onreadystatechange = function(){
        if(xhr.readyState !== 4 || xhr.status !== 200) return;
        log('xhr.response:', xhr.response);
      };
      xhr.send();
      /* リクエストキューを削除 */
      notifications.requests.shift();
      Notifier.save();
    }
    static updateOnceProgram(program){
      let now = MinuteStamp.now();
      if(program.startAt < now) return;/*放送中・終了した番組は登録しない*/
      if(Notifier.match(program.id)) return;/*既に通知予定済み*/
      notifications.programs.push(program);
      notifications.programs.sort((a, b) => a.startAt - b.startAt);
    }
    static updateRepeatPrograms(program){
      /* channelsに含まれない臨時チャンネルの番組もあるので先に登録を済ませておく */
      if(!Notifier.match(program.id)){
        notifications.programs.push(program);
        Notifier.updateRepeatTitle(program);
      }
      /* channelsから該当する番組を登録する */
      for(let c = 0, now = MinuteStamp.now(); channels[c]; c++){
        for(let p = 0, target; target = channels[c].programs[p]; p++){
          if(target.startAt < now) continue;/*放送中・終了した番組は登録しない*/
          if(!target.repeat || target.repeat !== program.repeat) continue;/*検証対象のidではない*/
          if(Notifier.match(target.id)) continue;/*既に通知予定済み*/
          notifications.programs.push(target);
          Notifier.updateRepeatTitle(target);
        }
      }
      notifications.programs.sort((a, b) => a.startAt - b.startAt);
    }
    static updateSearchPrograms(key, marks){
      let matchIds = [], now = MinuteStamp.now();
      for(let c = 0; channels[c]; c++){
        for(let p = 0, program; program = channels[c].programs[p]; p++){
          if(program.startAt < now) continue;/*放送中・終了した番組は登録しない*/
          if(!core.matchProgram(program, key, marks)) continue;/*key,marksに該当しない番組はもちろん登録しない*/
          matchIds.push(program.id);/*このkey,marksに該当するid*/
          if(Notifier.match(program.id)) continue;/*programsに重複登録はしない(onceやrepeat,または既存searchによって登録済み)*/
          notifications.programs.push(program);
          if(configs.n_sync) Notifier.reserve(program.id, 'once');/*公式に検索通知がないので1回通知として登録する*/
        }
      }
      notifications.programs.sort((a, b) => a.startAt - b.startAt);
      return matchIds;
    }
    static updateAllPrograms(){
      Object.keys(notifications.repeat).forEach((repeat) => Notifier.updateRepeatPrograms(notifications.programs.find((p) => p.repeat === repeat)));
      Object.keys(notifications.search).forEach((key) => Notifier.updateSearchPrograms(key, notifications.search[key]));
      Notifier.save();
    }
    static matchOnce(once){
      return notifications.once[once];
    }
    static matchRepeat(repeat){
      return notifications.repeat[repeat];
    }
    static matchSearch(program){
      return Object.keys(notifications.search).find((key) => core.matchProgram(program, key, notifications.search[key]));
    }
    static match(id){
      if(notifications.programs.some((p) => p.id === id)) return true;
    }
    static createPlayButton(program){
      let button = createElement(core.html.playButton());
      button.classList.add('channel-' + program.channel.id);
      if(core.getCurrentChannelId() === program.channel.id) button.classList.add('current');
      button.addEventListener('click', Notifier.playButtonListener.bind(program));
      return button;
    }
    static playButtonListener(e){
      let program = this, button = e.target/*playButtonListener.bind(program)*/;
      core.goChannel(program.channel.id);
      e.stopPropagation();
    }
    static createRepeatAllButton(program){
      let button = createElement(core.html.repeatAllButton());
      if(Notifier.matchRepeat(program.repeat)){
        button.classList.add('active');
        button.title = button.dataset.titleActive;
      }else{
        button.title = button.dataset.titleDefault;
      }
      button.dataset.repeat = program.repeat;
      button.addEventListener('click', Notifier.repeatAllButtonListener.bind(program));
      return button;
    }
    static repeatAllButtonListener(e){
      let program = this, button = e.target/*repeatAllButtonListener.bind(program)*/;
      switch(true){
        case(button.classList.contains('active')):
          Notifier.removeRepeat(program);
          Button.removeActive(button);
          Button.removeRepeat(program.repeat);
          break;
        default:
          Notifier.addRepeat(program);
          Button.addActive(button);
          Button.addRepeat(program.repeat);
          break;
      }
    }
    static createNotifyButton(program){
      let button = createElement(core.html.notifyButton());
      if(Notifier.matchOnce(program.once)) button.classList.add('once');
      if(Notifier.matchRepeat(program.repeat)) button.classList.add('repeat');
      let key = Notifier.matchSearch(program);
      if(key){
        button.classList.add('search');
        button.dataset.key = key;
      }
      button.title = Button.getButtonTitle(button);
      button.dataset.once = program.once;
      if(program.repeat) button.dataset.repeat = program.repeat;
      button.addEventListener('click', Notifier.notifyButtonListener.bind(program));
      return button;
    }
    static notifyButtonListener(e){
      let program = this, button = e.target/*notifyButtonListener.bind(program)*/, searchKey = button.dataset.key;
      let updateSearchPane = () => {
        if(!elements.searchPane || !elements.searchPane.isConnected) return;
        if(elements.searchPane.dataset.mode !== 'notifications') return;
        for(let target = button.parentNode; target; target = target.parentNode){
          if(target === elements.searchPane) return;/*searchPaneでのクリック時はなにもしない*/
        }
        core.timetable.searchPane.buildNotificationsHeader();
        core.timetable.searchPane.listAllNotifications();
      };
      switch(true){
        case(searchKey !== undefined):
          if(!elements.timetablePanel.isConnected) core.timetable.createPanel();
          core.timetable.searchPane.search(searchKey, notifications.search[searchKey]);
          Button.shake(button);
          break;
        case(Notifier.matchRepeat(program.repeat) !== undefined):
          Button.shake(button);
          break;
        case(Notifier.matchOnce(program.once) !== undefined):
          Notifier.removeOnce(program);
          Button.removeOnce(program.once);
          updateSearchPane();
          break;
        default:
          Notifier.addOnce(program);
          Button.addOnce(program.once);
          updateSearchPane();
          break;
      }
      e.preventDefault();
      e.stopPropagation();
    }
    static createSearchButton(key){
      let button = createElement(core.html.notifyButton());
      button.classList.add('search');
      button.dataset.key = key;
      button.title = Button.getButtonTitle(button);
      button.addEventListener('click', Notifier.searchButtonListener);
      return button;
    }
    static searchButtonListener(e){
      let button = e.target/*notifyButtonListener.bind(key)*/, searchKey = button.dataset.key;
      if(!elements.timetablePanel.isConnected) core.timetable.createPanel();
      core.timetable.searchPane.search(searchKey, notifications.search[searchKey]);
      Button.shake(button);
      e.preventDefault();
      e.stopPropagation();
    }
    static createButton(program){
      let now = MinuteStamp.now();
      if(program.startAt <= now) return Notifier.createPlayButton(program);
      if(now < program.startAt) return Notifier.createNotifyButton(program);
    }
    static createSearchAllButton(key, marks){
      let button = createElement(core.html.searchAllButton(key, marks.map((name) => core.html.marks[name]()).join('')));
      if(notifications.search[key] && notifications.search[key].join() === marks.join()) button.classList.add('active');
      button.addEventListener('click', Notifier.searchAllButtonListener.bind({key: key, marks: marks}));
      return button;
    }
    static searchAllButtonListener(e){
      let key = this.key, marks = this.marks, button = e.target/*searchAllButtonListener.bind({key: key, marks: marks})*/;
      switch(true){
        case(notifications.search[key] && notifications.search[key].join() === marks.join()):
          Notifier.removeSearch(key, marks).forEach((id) => Button.removeSearch(id));
          Button.removeActive(button);
          break;
        default:
          Notifier.addSearch(key, marks).forEach((id) => Button.addSearch(id));
          Button.addActive(button);
          break;
      }
      core.timetable.searchPane.updateSearchFillters(key, marks);
      Notifier.save();
    }
    static notify(){
      /* アベマを開いている複数のタブがある場合、自分が通知担当のタブでなければなにもしない */
      if(!notifications.tabs[ID].primary) return;
      /* 通知すべき番組を notifications.programs から探して通知を表示する。番組が終了するまでprogramは保持しておく */
      let now = Date.now() / 1000, closeMe;
      for(let p = 0, programs = notifications.programs, program; program = programs[p]; p++){
        if(now < program.startAt - configs.n_before) return;/*まだ通知時刻じゃない(後続のprogramも同様なのでreturn)*/
        /* 複数タブで通知させないための保険 */
        let ns = Storage.read('notifications');/*先にprogramを通知していないか確認する*/
        switch(true){
          case(ns.programs.length !== programs.length):/*ほかで終了番組を削除済み*/
          case(ns.programs[p].notification && !program.notification):/*ほかで通知済み*/
            notifications = ns;
            for(let i = 0; notifications.programs[i]; i++) notifications.programs[i] = new Program(notifications.programs[i]);
            return;
        }
        /* 通知時刻になっている */
        if(!program.notification){/*未通知*/
          switch(true){
            case(!configs.c_visibles[program.channel.id]):/*非表示チャンネル*/
              if(program.endAt <= now) programs = programs.filter((p) => p.id !== program.id), p--;
              break;
            case(now < program.startAt):/*通知時刻*/
            case(now < program.endAt):/*放送中*/
              program.notification = new Notification(program.title, {
                body: `${program.timeString}  ${program.channel.name}`,
              });
              closeMe = program.notification.close.bind(program.notification);
              program.notification.addEventListener('click', function(e){
                core.goChannel(program.channel.id);
                closeMe();
              });
              if(configs.n_change && (!configs.n_overlap || p === 0)){
                window.addEventListener('beforeunload', closeMe);/*通知が開いたままになるのを防ぐ*/
                core.goChannel(program.channel.id);/*ページ遷移が発生した場合に即閉じられるのはやむを得ない*/
                setTimeout(function(){
                  window.removeEventListener('beforeunload', closeMe);
                  closeMe();
                }, (Math.max(program.startAt - now, 0) + NOTIFICATIONREMAINS)*1000);/*番組開始時刻+REMAINSまで*/
              }else{
                setTimeout(function(){
                  closeMe();
                }, (Math.max(program.endAt - now, 0))*1000);/*番組終了時刻まで*/
              }
              break;
            case(program.endAt <= now):/*手遅れ*/
              program.notification = new Notification(program.title, {
                body: `[放送終了]  ${program.channel.name}`,
              });
              /*勝手に閉じない*/
              break;
          }
        }else{/*通知済み*/
          if(now < program.endAt) return;/*まだ番組は続いている*/
          /* 番組が終了したようなので */
          programs = programs.filter((p) => p.id !== program.id), p--;
          if(programs[0] && programs[0].notification){/*別の通知番組がまだ放送中である*/
            if(configs.n_change){
              /* 「時間帯が重なっている時は通知のみ」オンなら、優先されるべき最初に開始した番組へ移動 */
              if(configs.n_overlap){
                core.goChannel(programs[0].channel.id);
                if(programs[0].notification.close){
                  setTimeout(function(){programs[0].notification.close()}, NOTIFICATIONREMAINS*1000);
                }
              /* 「時間帯が重なっている時は通知のみ」オフなら、優先されるべき最後に開始した番組へ移動 */
              }else{
                for(let n = 0; programs[n]; n++){
                  if(programs[n + 1] && programs[n + 1].notification) continue;/*まだ最後じゃない*/
                  core.goChannel(programs[n].channel.id);
                  if(programs[n].notification.close){
                    setTimeout(function(){programs[n].notification.close()}, NOTIFICATIONREMAINS*1000);
                  }
                }
              }
            }else{
              /* 開いたままの通知があるならその後を託す */
              for(let n = 0; programs[n]; n++){
                if(!programs[n].notification) break;/*開いたままの通知はひとつもなさそう*/
                if(programs[n].notification.close) return;/*開いたままの通知にその後を託す*/
              }
              /* 放送中の通知番組が複数ある */
              if(programs[1] && programs[1].notification){
                let notification = new Notification(SCRIPTNAME, {
                  body: `通知予約した番組がまだほかにも放送中です`,
                });
                setTimeout(function(){notification.close()}, 2 * NOTIFICATIONREMAINS*1000);
                if(elements.channelPane.getAttribute('aria-hidden') === 'true') elements.channelButton.click();
              /* 放送中の通知番組がひとつだけある */
              }else{
                programs[0].notification = new Notification(programs[0].title, {
                  body: `${programs[0].timeString}  ${programs[0].channel.name}`,
                });
                closeMe = programs[0].notification.close.bind(programs[0].notification);
                programs[0].notification.addEventListener('click', function(e){
                  core.goChannel(programs[0].channel.id);
                  closeMe();
                });
                setTimeout(function(){
                  closeMe();
                }, (Math.max(programs[0].endAt - now, 0))*1000);/*番組終了時刻まで*/
              }
            }
          }
        }
        notifications.programs = programs;/*参照じゃなかったの?という気もするけどこうしないと保存されない*/
        Notifier.save();
      }
    }
    static updateCount(){
      let button = elements.notificationsButton;
      if(!button) return;
      if(parseInt(button.dataset.count) === notifications.programs.length) return;
      button.dataset.count = button.querySelector('.count').textContent = notifications.programs.length;
      Button.pop(button);
    }
    static read(){
      notifications = Storage.read('notifications') || notifications;
      for(let i = 0; notifications.programs[i]; i++){
        notifications.programs[i] = new Program(notifications.programs[i]);
      }
    }
    static save(){
      Notifier.updateCount();
      Storage.save('notifications', notifications);
    }
    static setPrimaryTab(){
      if(!notifications.tabs) notifications.tabs = {};
      let tabs = notifications.tabs, now = Date.now(), url = location.href;
      let nowonair = url.includes('/now-on-air/'), active = !document.hidden, timetable = url.endsWith('/timetable');
      tabs[ID] = {/*まず自分のタブを明らかにする*/
        score: (nowonair ? 100 : 0) + (active ? 10 : 0) + (timetable ? 1 : 0),
        nowonair: nowonair,
        active: active,
        url: url,
        updated: now,
        primary: undefined,
      };
      let ids = Object.keys(tabs);
      ids.forEach((id, i) => {if(tabs[id].updated < now - 2*MINUTE*1000) delete tabs[id], delete ids[i]});/*古いタブは削除する*/
      ids.forEach((id) => tabs[id].primary = false);/*いったんリセットして*/
      /* 優先順に並び替えて先頭に通知担当タブの栄誉を与える */
      ids.sort((a, b) => tabs[b].score - tabs[a].score);/*スコアの大きい順*/
      tabs[ids[0]].primary = true;/*おまえが担当だ!!*/
      notifications.tabs = tabs;/*参照じゃなかったの?という気もするけどこうしないと保存されない(と思う)*/
      Notifier.save();
    }
  }
  class MyVideo{
    static sync(){
      /* 視聴期限切れもあちらで消えるので自動的に反映される */
      let xhr = new XMLHttpRequest();
      xhr.open('GET', site.get.apis.favorites());
      xhr.setRequestHeader('Authorization', 'bearer ' + site.get.token());
      xhr.responseType = 'json';
      xhr.onreadystatechange = function(){
        if(xhr.readyState !== 4 || xhr.status !== 200) return;
        if(!xhr.response.slots) return log(`Not found: MyVideo data`);
        //log('xhr.response:', xhr.response);
        let slots = xhr.response.dataSet.slots;/*xhr.responseをそのまま使うとパフォーマンス悪い*/
        /* あちらにしかないものはあちらで能動的に登録したとみなしてこちらにも登録 */
        for(let i = 0; slots[i]; i++){
          if(!myvideos.some((p) => p.id === slots[i].id)){
            let program = core.getProgramById(slots[i].id);
            if(program) myvideos.push(program);
          }
        }
        /* こちらにしかないものはあちらで能動的に削除したとみなしてこちらでも削除 */
        myvideos = myvideos.filter((myvideo) => (slots.some((slot) => slot.id === myvideo.id)));
        /* 更新 */
        myvideos.sort((a, b) => a.startAt - b.startAt);
        Storage.save('myvideos', myvideos);
      };
      xhr.send();
    }
    static add(program){
      if(MyVideo.match(program.id)) return;
      myvideos.push(program);
      myvideos.sort((a, b) => a.startAt - b.startAt);
      Storage.save('myvideos', myvideos);
      MyVideo.request('PUT', program.id);
    }
    static remove(program){
      myvideos = myvideos.filter((p) => (p.id !== program.id));
      Storage.save('myvideos', myvideos);
      MyVideo.request('DELETE', program.id);
    }
    static request(action, id){
      /* APIから通知を予約する */
      let xhr = new XMLHttpRequest();
      xhr.open(action, site.get.apis.favorite(id));
      xhr.setRequestHeader('Authorization', 'bearer ' + site.get.token());
      if(DEBUG) xhr.onreadystatechange = function(){
        if(xhr.readyState !== 4 || xhr.status !== 200) return;
        log('xhr.response:', xhr.response);
      };
      xhr.send();
    }
    static match(id){
      if(myvideos.some((p) => p.id === id)) return true;
    }
    static createMyvideoButton(program){
      let button = createElement(core.html.myvideoButton());
      if(MyVideo.match(program.id)){
        button.classList.add('active');
        button.title = button.dataset.titleActive;
      }else{
        button.title = button.dataset.titleDefault;
      }
      button.addEventListener('click', MyVideo.buttonListener.bind(program));
      return button;
    }
    static buttonListener(e){
      let program = this, button = e.target;/*buttonListener.bind(program)*/
      switch(true){
        case(MyVideo.match(program.id)):
          MyVideo.remove(program);
          Button.removeActive(button);
          break;
        default:
          MyVideo.add(program);
          Button.addActive(button);
          break;
      }
      e.stopPropagation();
    }
    static isPremiumUser(){
      return (site.get.subscriptionType() !== 'freeUser');
    }
    static read(){
      myvideos = Storage.read('myvideos') || myvideos;
      for(let i = 0; myvideos[i]; i++) myvideos[i] = new Program(myvideos[i]);
    }
  }
  let html, elements = {}, configs = {};
  let channels = [], myvideos = [], notifications = {
    once: {},/*1回通知{id: title}*/
    repeat: {},/*毎回通知{id: title}*/
    search: {},/*検索通知{key: marks}*/
    programs: [],/*通知予定{program}*/
    requests: [],/*リクエスト予定{action, id, type}*/
    tabs: {},/*開いているタブ{id: {score, nowonair, active, updated, primary}}*/
  };
  let core = {
    initialize: function(){
      /* 一度だけ */
      html = document.documentElement;
      html.classList.add(SCRIPTNAME);
      core.config.read();
      core.read();
      core.addStyle();
      core.panel.createPanels();
      core.listenUserActions();
      core.abemaTimetable.initialize();
      Notifier.updateAllPrograms();
      core.ticktock();
    },
    ticktock: function(){
      let last = new Date(), now = new Date(), random = 1000*Math.random();
      setInterval(function(){
        last = now, now = new Date();
        switch(true){
          /* 毎日処理 */
          case (now.getDate() !== last.getDate()):
            core.timetable.buildTimes();
            core.timetable.buildDays();/*先に作ったtimesのdisable判定を含む*/
            setTimeout(function(){/*アベマへの負荷分散*/
              core.updateChannels();
            }, MINUTE*random/*ささやかだけど最大1分遅延*/);
          /* 毎時処理 */
          case (now.getHours() !== last.getHours()):
            setTimeout(function(){/*アベマへの負荷分散*/
              Notifier.sync();
              MyVideo.sync();
              if(now.getDate() !== last.getDate()) return;/*毎日処理と重複させない*/
              if(Storage.saved('channels') < Date.now() - CACHEEXPIRE) core.updateChannels();/*キャッシュ期間が終わっていれば更新*/
              else core.checkChannels();/*キャッシュ期間でも臨時チャンネルの有無は確認する*/
            }, HOUR*random/*最大1時間遅延*/);
          /* 毎分処理 */
          case (now.getMinutes() !== last.getMinutes()):
            core.timetable.shiftTimetable();
            if(now.getHours() !== last.getHours()) return;/*毎時処理と重複させない*/
            Notifier.read();/*複数タブの同期用*/
            Notifier.setPrimaryTab();/*タブの生存確認*/
//elements.channelButton.click();
//setTimeout(function(){elements.channelButton.click()}, 5000);
//log();
          /* 毎秒処理 */
          default:
            core.checkUrl();
            Notifier.notify();
            Notifier.request();
            core.checkStalled();
        }
      }, 1000);
    },
    checkUrl: function(){
      location.previousUrl = location.previousUrl || '';
      if(location.href === location.previousUrl) return;/*URLが変わってない*/
      Notifier.setPrimaryTab();/*URLが変わったので通知プライマリタブを確認*/
      if(location.href.startsWith('https://abema.tv/now-on-air/')){/*テレビ視聴ページ*/
        if(location.previousUrl.startsWith('https://abema.tv/now-on-air/')){/*チャンネルを変えただけ*/
          elements.closer = site.get.closer();
        }else{/*テレビ視聴ページになった*/
          core.ready();
        }
      }else if(location.href.startsWith('https://abema.tv/timetable')){/*番組表ページ*/
        if(location.previousUrl === '') core.abemaTimetable.openOnAbemaTimetable();/*初回のみ*/
      }else{
        /*nothing*/
      }
      location.previousUrl = location.href;
    },
    read: function(){
      /* ストレージデータの取得とクラスオブジェクト化 */
      channels = Storage.read('channels') || channels;
      for(let i = 0; channels[i]; i++) channels[i] = new Channel(channels[i]);
      if(!channels.length) core.updateChannels();
      else if(Storage.saved('channels') < Date.now() - CACHEEXPIRE) core.updateChannels();
      else if(DEBUG && UPDATECHANNELS) core.updateChannels();
      Notifier.read();
      Notifier.sync();
      MyVideo.read();
      MyVideo.sync();
    },
    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.createChannelIndicator();
      core.timetable.createButton();
      core.channelPane.observe();
      /* ScreenCommentScrollerとの連携(clickイベントを統括するScreenCommentScrollerの準備完了を待つ必要がある) */
      setTimeout(function(){
        if(!site.get.screenCommentScroller()) return;
        /* チャンネル切り替えイベントをいつでも流用するための準備 */
        if(elements.channelPane.getAttribute('aria-hidden') === 'false') return;/*既に開かれている*/
        /* 裏番組一覧が開かれたら即閉じる準備 */
        let observer = observe(elements.channelPane.firstElementChild, function(records){
          if(elements.channelPane.getAttribute('aria-hidden') === 'true') return;
          observer.disconnect();/*一度だけ*/
          elements.closer.click();
          setTimeout(function(){html.classList.remove('channelPaneHidden')}, 1000);/*チラ見せさせない*/
        });
        core.channelPane.openHide();
      }, 1000);
    },
    createChannelIndicator: function(){
      if(elements.indicator && elements.indicator.isConnected) elements.indicator.parentNode.removeChild(elements.indicator);
      let indicator = elements.indicator = createElement(core.html.channelIndicator());
      let ul = indicator.querySelector('ul'), template = indicator.querySelector('li.template');
      for(let c = 0, channel; channel = channels[c]; c++){
        if(!configs.c_visibles[channel.id]) continue;
        let li = template.cloneNode(true), img = li.querySelector('img');
        li.classList.remove('template');
        li.dataset.channel = channel.id;
        img.src = CHANNELLOGO.replace('{id}', channel.id);
        img.alt = channel.id;
        ul.appendChild(li);
      }
      document.body.appendChild(indicator);
      core.scrollChannelIndicator(core.getCurrentChannelId());
    },
    scrollChannelIndicator: function(id){
      if(!id) return;
      let indicator = elements.indicator, ul = indicator.querySelector('ul.channels');
      clearTimeout(indicator.timer);
      for(let i = 0, lis = ul.querySelectorAll('li.channel:not(.template)'), li; li = lis[i]; i++){
        if(li.dataset.channel === id){
          li.classList.add('current');
          ul.style.transform = `translateY(-${i * 25}vh)`;
          indicator.classList.add('active');
          indicator.timer = setTimeout(function(){indicator.classList.remove('active')}, LOGOINDICATE);
        }else{
          if(li.classList.contains('current')) li.classList.remove('current');
          let padding = core.getProgramNowOnAir(li.dataset.channel).padding;
          if(padding && li.classList.contains('onair'))  li.classList.remove('onair');
          else if(!padding && !li.classList.contains('onair')) li.classList.add('onair');
        }
      }
    },
    checkChannels: function(){
      let xhr = new XMLHttpRequest();
      xhr.open('GET', site.get.apis.channels());
      xhr.responseType = 'json';
      xhr.onreadystatechange = function(){
        if(xhr.readyState !== 4 || xhr.status !== 200) return;
        if(!xhr.response.channels) return log(`Not found: data`);
        log('xhr.response:', xhr.response);
        let cs = xhr.response.channels;/*xhr.responseをそのまま使うとパフォーマンス悪い*/
        if(!cs.every((c) => channels.some((channel) => channel.id === c.id))) core.updateChannels();
      };
      xhr.send();
    },
    updateChannels: function(callback){
      let xhr = new XMLHttpRequest();
      xhr.open('GET', site.get.apis.timetable());
      xhr.setRequestHeader('Authorization', 'bearer ' + site.get.token());
      xhr.responseType = 'json';
      xhr.onreadystatechange = function(){
        if(xhr.readyState !== 4 || xhr.status !== 200) return;
        if(!xhr.response.channels || !xhr.response.channelSchedules) return log(`Not found: data`);
        log('xhr.response:', xhr.response);
        let ss = xhr.response.channelSchedules, cs = xhr.response.channels, slots = {};/*xhr.responseをそのまま使うとパフォーマンス悪い*/
        /* channels更新 */
        let oldChannels = channels;
        channels = [];/* いったんクリア */
        for(let i = 0, s; s = ss[i]; i++){
          slots[s.channelId] = (slots[s.channelId]) ? slots[s.channelId].concat(s.slots) : s.slots;
        }
        for(let i = 0, c; c = cs[i]; i++){
          channels[i] = new Channel().fromChannelSlots(c, slots[c.id]);
        }
        /* 増減するチャンネルは番組予定がある限り保持し続ける */
        for(let i = 0, now = MinuteStamp.now(), o; o = oldChannels[i]; i++){
          if(channels.some((c) => c.id === o.id)) continue;/*引き続き存在する*/
          if(now < o.programs[o.programs.length - 1].endAt){/*番組予定がある*/
            o.programs = o.programs.filter((p) => now < p.endAt);/*過去の番組を削除*/
            channels.push(o);
          }
        }
        channels.sort((a, b) => a.order - b.order);
        /* configs.c_visibles更新 */
        if(Object.keys(configs.c_visibles).length === 0){
          for(let i = 0, c; c = channels[i]; i++) configs.c_visibles[c.id] = 1;
        }else{
          for(let i = 0, c; c = channels[i]; i++){
            if(configs.c_visibles[c.id] === undefined) configs.c_visibles[c.id] = 1;/*新規チャンネル*/
          }
          Object.keys(configs.c_visibles).forEach((key) => {
            if(configs.c_visibles[key] === 0) return;/*非表示にした情報は残す*/
            if(!channels.some((c) => c.id === key)) delete configs.c_visibles[key];/*非表示でなければ将来復活しても表示されるだけなので廃止チャンネルとみなしてかまわない*/
          });
        }
        /* 反映 */
        Storage.save('channels', channels);
        Storage.save('configs', configs);
        core.addStyle();/*チャンネル数によってフォントサイズを変えるので*/
        core.createChannelIndicator();
        core.timetable.rebuildTimetable();
        Notifier.updateAllPrograms();
        if(callback) callback();
      };
      xhr.send();
    },
    goChannel: function(id){
      if(core.getCurrentChannelId() === id) return;/*すでに目的のチャンネルにいる*/
      switch(true){
        /* 番組視聴ページにいる */
        case(elements.channelPane && elements.channelPane.isConnected):
          /* 裏番組一覧から正規のチャンネル切り替えイベントを流用する */
          let a = elements.channelPane.querySelector(`a[data-channel="${id}"]`);
          if(a === null) return location.assign('/now-on-air/' + id);
          a.click();
          core.updateCurrentChannel(id);
          return true;
        /* 置き換えた公式番組表ページにいる */
        case(configs.replace && location.href.endsWith('/timetable')):
          core.abemaTimetable.goChannel(id);
          return true;
        default:
          return location.assign('/now-on-air/' + id);
      }
    },
    skipChannel: function(direction = +1){
      if(!location.href.includes('/now-on-air/')) return;
      let loop = (i) => {/*ループありでiをインクリメント*/
        switch(true){
          case(direction === +1):
            if(i === channels.length - 1) return 0;
            else return i + 1;
          case(direction === -1):
            if(i === 0) return channels.length - 1;
            else return i - 1;
        }
      };
      for(let c = 0; channels[c]; c++){
        if(core.getCurrentChannelId() !== channels[c].id) continue;
        /* 現在視聴中のチャンネルからチェックスタート */
        for(let i = loop(c), target; target = channels[i]; i = loop(i)){
          if(i === c) return false;/*一周してしまった*/
          if(!configs.c_visibles[target.id]) continue;/*非表示チャンネルは飛ばす*/
          if(core.getProgramNowOnAir(target.id).padding) continue;/*放送中じゃない休止チャンネルは飛ばす*/
          if(i === loop(c)){/*隣のチャンネル*/
            core.updateCurrentChannel(target.id);
            return false;/*スキップ不要*/
          }else{
            core.goChannel(target.id);/*updateCurrentChannelも含む*/
            return true;/*スキップした*/
          }
        }
      }
    },
    updateCurrentChannel(id){
      core.scrollChannelIndicator(id);
      /* playButtonのcurrentを付け替える */
      $$('button.play.current').forEach((b) => b.classList.remove('current'));
      $$('button.play.channel-' + id).forEach((b) => b.classList.add('current'));
      /* channelPaneのcurrentを付け替える */
      if(elements.channelPane){
        let previous = elements.channelPane.querySelector('a[data-current="true"]');
        if(previous) delete previous.dataset.current;
        let current = elements.channelPane.querySelector(`a[data-channel="${id}"]`);
        if(current) current.dataset.current = 'true';/*classは公式にclassNameで上書きされてしまうので*/
        core.channelPane.scrollToChannel(current);
      }
      /* channelsUlのcurrentを付け替える */
      if(elements.channelsUl){
        let previous = elements.channelsUl.querySelector('.channel.current');
        if(previous) previous.classList.remove('current');
        let current = elements.channelsUl.querySelector('.channel#channel-' + id);
        if(current) current.classList.add('current');
      }
    },
    listenUserActions: function(){
      window.addEventListener('click', function(e){
        switch(true){
          /* チャンネル切り替えボタン */
          case(e.target === elements.channelUpButton):
          case(e.target === elements.channelDownButton):
            if(core.skipChannel((e.target === elements.channelDownButton) ? +1 : -1)){
              e.stopPropagation();
              e.preventDefault();
            }
            /*skip不要ならデフォルトのチャンネル切り替えに任せる*/
            return;
          /* アベマ公式の通知・マイビデオボタンが押されたら同期する */
          case(e.isTrusted && configs.n_sync && site.get.abemaNotifyButton(e.target)):
            return setTimeout(Notifier.sync, 1000);
          case(e.isTrusted && site.get.abemaMyVideoButton(e.target)):
            return setTimeout(MyVideo.sync, 1000);
        }
      }, true);
      window.addEventListener('keydown', function(e){
        if(!location.href.includes('/now-on-air/')) return;
        switch(true){
          /* テキスト入力中は反応しない */
          case(['input', 'textarea'].includes(document.activeElement.localName)):
            return;
          /* Alt/Shift/Ctrl/Metaキーが押されていたら反応しない */
          case(e.altKey || e.shiftKey || e.ctrlKey || e.metaKey):
            return;
          /* 上下キーによるチャンネル切り替え */
          case(e.key === 'ArrowUp'):
          case(e.key === 'ArrowDown'):
            document.activeElement.blur();/*上下キーによるスクロールを防止*/
            if(core.skipChannel((e.key === 'ArrowDown') ? +1 : -1)){
              e.stopPropagation();
              e.preventDefault();
            }
            /*skip不要ならデフォルトのチャンネル切り替えに任せる*/
            return;
        }
      }, true);
      let resizing, resize = function(){
        core.channelPane.modify();
        core.timetable.fitWidth();
      };
      window.addEventListener('resize', function(e){
        if(!resizing) resize();
        clearTimeout(resizing), resizing = setTimeout(function(){
          resize();
          resizing = null;
        }, 500);
      });
      document.addEventListener('visibilitychange', function(e){
        Notifier.setPrimaryTab();
      });
      window.addEventListener('storage', function(e){
        if(e.key === SCRIPTNAME + '-notifications') return Notifier.read();
      });
      window.addEventListener('unload', function(e){
        delete notifications.tabs[ID];
        Notifier.save();
      });
    },
    checkStalled: function(){
      if(document.hidden || !location.href.includes('/now-on-air/')) return;
      /* main消失バグに対応 */
      let main = $('#main');/*ついでに main.playedOnce としてカウンタに使う*/
      if(!main) return;
      if(main.children.length === 0){
        if(!main.playedOnce) return;/*ページ読み込み直後は猶予する*/
        log('Vanished.');
        return location.reload(true);
      }
      /* 映像の停止を検知する(連続で再読込した時はもう少し粘る) */
      if(!location.href.startsWith('https://abema.tv/now-on-air/')) return;/*放送ページのみで判定*/
      let videos = $$('video[src]'), limit = Storage.read('stalledLimit') || STALLEDLIMIT;
      if(!videos.length) return;
      videos.forEach((v) => {
        v.progressTime = v.currentTime - (v.previousTime || 0);
        v.previousTime = v.currentTime;
        v.srcChanged   = (v.src !== v.previousSrc);
        v.previousSrc  = v.src;
      });
      switch(true){
        /*判定しないケース*/
        case(Array.from(videos).some((v) => v.srcChanged)):/*src差し替え*/
        case(Array.from(videos).some((v) => v.progressTime < 0)):/*巻き戻し*/
          return;
        /* 映像の停止判定 */
        case(Array.from(videos).every((v) => v.paused)):
        case(Array.from(videos).every((v) => v.currentTime === 0)):
        case(Array.from(videos).some((v) => !v.paused && v.progressTime < PROGRESS)):
          if(!main.playedOnce) return;/*ページ読み込み直後は猶予する*/
          videos[0].pausedCount = (videos[0].pausedCount || 0) + 1;
          videos.forEach((v) => log(
            `${videos[0].pausedCount}/${limit}`,
            'Paused?',
            (v.progressTime < 0 ? '' : '+') + v.progressTime.toFixed(3),
            v.src,
          ));
          if(limit <= videos[0].pausedCount){
            Storage.save('stalledLimit', limit + 1, Date.now() + EXPIRESTALLED);
            return location.reload(true);
          }
          break;
        /* 映像が正常に流れていたので */
        default:
          if(!main.playedOnce) main.playedOnce = true;/*一度正常に流れて初めて停止判定を開始する*/
          if(1 <= videos[0].pausedCount){
            videos[0].pausedCount--;
            log(`${videos[0].pausedCount}/${limit}`, 'Recovering...');
          }
          break;
      }
    },
    channelPane: {
      observe: function(){
        /* 裏番組一覧を常に改変する */
        if(elements.channelPane.modifying === undefined) observe(elements.channelPane, function(records){
          if(elements.channelPane.modifying) return;
          elements.channelPane.modifying = true;
          core.channelPane.modify();/*アベマによる更新を上書きする*/
          animate(function(){elements.channelPane.modifying = false});/*DOM処理の完了後に*/
        }, {childList: true, characterData: true, subtree: true});
      },
      openHide: function(){
        /* ユーザーには開閉したように見せない */
        html.classList.add('channelPaneHidden');/*開いても隠しておく*/
        elements.channelButton.click();
      },
      scrollToChannel: function(a, transition = true){
        let channelPane = elements.channelPane, child = channelPane.firstElementChild;
        let pHeight = child.offsetHeight, aTop = a.offsetTop, aHeight = a.offsetHeight, innerHeight = window.innerHeight;
        if(pHeight <= innerHeight) return;/*スクロールの必要なし*/
        let scrollTop = Math.min(Math.max(aTop - (innerHeight / 2) + (aHeight / 2), 0), pHeight - innerHeight -1/*端数対応*/);
        if(!transition) return channelPane.scrollTop = scrollTop;/*アニメーションの必要なし*/
        child.style.transition = 'none';
        child.style.transform = `translateY(${scrollTop - channelPane.scrollTop}px)`;
        channelPane.scrollTop = scrollTop;
        animate(function(){
          child.style.transition = 'transform 500ms ease';
          child.style.transform = '';
        });
      },
      modify: function(){
        if(!elements.channelPane) return;
        let channelPane = elements.channelPane, as = site.get.onairChannels(channelPane), now = MinuteStamp.now(), nowonairSlots = {}/*現在放送中の番組枠ハッシュテーブル*/;
        if(!as.length) return;/*再挑戦のタイミングはobserverに任せる*/
        for(let i = 0, a; a = as[i]; i++){
          /* 各aのhrefがchannelsにあるかを確認して、臨時チャンネルaが増えているようならchannelsを更新する */
          if(!channels.some((c) => c.id === a.href.match(/\/([^/]+)$/)[1])) return core.updateChannels(core.channelPane.modify);
          /* アベマが増減させるチャンネルに対応するため、独自に挿入した休止チャンネルは一旦取り除く */
          if(a.dataset.onair === 'false') a.parentNode.removeChild(a);
        }
        as = site.get.onairChannels(channelPane);/*Live Elements じゃないので再取得*/
        /* 各チャンネルの準備 */
        /* (classは公式にclassNameで上書きされてしまうのでdatasetを使う) */
        let currentA;/*現在視聴中のチャンネルのa要素*/
        for(let c = 0, channel; channel = channels[c]; c++){
          let a = site.get.onairChannel(channelPane, channel.id);
          /* 放送中じゃない休止チャンネルも表示させる */
          if(!a){
            a = as[0].cloneNode(true);
            a.dataset.onair = 'false';
            if(core.getProgramNowOnAir(channel.id).endAt < now + PADDINGBEFORE) a.dataset.comming = 'true';/*もうすぐ再開する*/
            a.href = `/now-on-air/${channel.id}`;
            let logo = site.get.channelLogo(a);
            logo.src = logo.src.replace(/\/logo\/(.+?)\.(w[0-9]+)\.([^.]+)/, `/logo/${channel.id}.$2.$3`);
            logo.alt = channel.id;
            as[0].parentNode.insertBefore(a, as[c] || site.get.timetableLinkOnNOA());
            as = site.get.onairChannels(channelPane);/*Live Elements じゃないので再取得*/
          }
          /* 放送中かどうかにかかわらずチャンネルa要素をを用意できたので */
          nowonairSlots[channel.id] = site.get.nowonairSlot(a);
          a.dataset.channel = channel.id;
          a.dataset.hidden = (configs.c_visibles[channel.id]) ? 'false' : 'true';
          /* クリックでのチャンネル切り替えを core.goChannel が引き受ける */
          if(!a.listening){
            a.listening = true;
            a.addEventListener('click', function(e){
              if(!e.isTrusted) return;/*実クリックのみ*/
              e.preventDefault();
              e.stopPropagation();
              if(a.dataset.onair === 'false' && a.dataset.comming !== 'true') return;/*休止チャンネルで再開もまだならなにもしない*/
              core.goChannel(channel.id);/*その後の処理もすべておまかせ*/
            });
          }
          /* 現在のチャンネルをハイライト */
          if(location.href.endsWith(a.href)){
            a.dataset.current = 'true';
            currentA = a;
          }else if(a.dataset.current){
            delete a.dataset.current;
          }
          /* サムネイルサイズの固定値(vw)を求めておく */
          let thumbnail = site.get.thumbnail(a);
          channelPane.thumbWidth = thumbnail.clientWidth || channelPane.thumbWidth;/*非表示チャンネルの場合もあるので*/
          channelPane.thumbOffsetWidth = thumbnail.parentNode.parentNode.parentNode.offsetWidth || channelPane.thumbOffsetWidth;
        }
        /* チャンネルa要素が出揃ったのでしかるべきスクロール位置へ */
        core.channelPane.scrollToChannel(currentA, false/*アニメーション不要*/);
        /* 後続番組を重ねる */
        let end = now + HOUR, ratio = (channelPane.offsetWidth - channelPane.thumbWidth) / HOUR, vw = 100 / window.innerWidth;
        for(let c = 0, channel; channel = channels[c]; c++){
          let nowonair = nowonairSlots[channel.id];
          if(!nowonair) continue;/*臨時チャンネルはnowonairSlotsに入ってない場合がある*/
          while(nowonair.nextElementSibling) nowonair.parentNode.removeChild(nowonair.nextElementSibling);/*いったん後続をクリアする*/
          for(let p = 0, program; program = channel.programs[p]; p++){
            /* 現在からendまでの番組のみ表示させる */
            if(program.endAt < now) continue;/*過去*/
            if(end < program.startAt) break;/*未来*/
            let slot;/*番組枠*/
            if(program.startAt <= now){/*現在放送中*/
              slot = nowonair;
              slot.previousElementSibling.title = program.title;/*サムネイルのツールチップ*/
              /* 要素幅 */
              slot.style.left = channelPane.thumbOffsetWidth * vw + 'vw';
              slot.style.width = (program.duration - (now - program.startAt)) * ratio * vw + 'vw';
            }else{/* 後続番組 */
              slot = nowonair.cloneNode(true);
              nowonair.parentNode.appendChild(slot);
              /* 要素幅 */
              slot.style.left = (channelPane.thumbOffsetWidth + ((program.startAt - now) * ratio)) * vw + 'vw';
              slot.style.width = (program.duration) * ratio * vw + 'vw';
            }
            slot.classList.add('slot');
            slot.classList[(program.noContent ? 'add' : 'remove')]('nocontent');/*コンテンツなし*/
            /* タイトル */
            let title = site.get.title(slot);
            title.textContent = slot.title = program.title || NOCONTENTS[0];
            Array.from(title.parentNode.children).forEach((node) => {
              if(node !== title) title.parentNode.removeChild(node);/*マークをいったん取り除く*/
            });
            Program.appendMarks(title, program.marks);
            /* 放送時間と通知 */
            let duration = site.get.duration(slot);
            duration.textContent = program.timeString;
            slot.dataset.once = program.id;
            if(program.repeat) slot.dataset.repeat = program.repeat;
            else delete slot.dataset.repeat;
            if(Notifier.match(program.id)) slot.classList.add('active');
            else slot.classList.remove('active');
            if(now < program.startAt && !program.noContent) duration.parentNode.insertBefore(Notifier.createButton(program), duration);
          }
        }
        /* 公式番組表リンク */
        let timetableLink = site.get.timetableLinkOnNOA();
        if(!timetableLink) return log('Not found: timetableLink');
        core.abemaTimetable.link(timetableLink);
      },
    },
    abemaTimetable: {
      initialize: function(){
        let button = site.get.abemaTimetableButton();
        if(!button) return setTimeout(core.abemaTimetable.initialize, 1000);
        core.abemaTimetable.link(button);
      },
      link: function(element){
        if(configs.replace){
          if(element.dataset.replaced === 'true') return;
          element.addEventListener('click', core.abemaTimetable.buttonListener);
          element.dataset.replaced = 'true';
        }else{
          if(element.dataset.replaced === 'false') return;
          element.removeEventListener('click', core.abemaTimetable.buttonListener);
          element.dataset.replaced = 'false';
        }
      },
      openOnAbemaTimetable: function(){
        html.classList.add('abemaTimetable');
        $('#splash > div').animate([{opacity: '0'}, {opacity: '1'}], {duration: ABEMATIMETABLEDURATION, fill: 'forwards'});
        core.panel.toggle('timetablePanel', core.timetable.createPanel);
        core.timetable.addCloseListener('closeOnAbemaTimetable', core.abemaTimetable.closeOnAbemaTimetable);
      },
      closeOnAbemaTimetable: function(e){
        sequence(function(){
          $('#splash > div').animate([{opacity: '1'}, {opacity: '0'}], {duration: ABEMATIMETABLEDURATION, fill: 'forwards'});
        }, ABEMATIMETABLEDURATION, function(){
          html.classList.remove('abemaTimetable');
        });
      },
      buttonListener: function(e){
        /* 既に番組表ページにいるならすぐ開く */
        if(location.href.startsWith('https://abema.tv/timetable')){
          e.preventDefault();
          core.abemaTimetable.openOnAbemaTimetable();
          return;
        }
        /* ふんわり遷移させる */
        if(e.isTrusted){/*実クリック時のみ*/
          e.preventDefault();
          core.abemaTimetable.volumeDown();
          if(elements.closer) elements.closer.click();/*コメントを閉じることでmain消失バグを防ぐ*/
          sequence(function(){
            html.classList.add('abemaTimetable');
            $('#splash > div').animate([{opacity: '0'}, {opacity: '1'}], {duration: ABEMATIMETABLEDURATION, fill: 'forwards'});
          }, ABEMATIMETABLEDURATION/*重たい処理に邪魔されず音量をなめらかに下げる猶予*/, function(){
            core.panel.toggle('timetablePanel', core.timetable.createPanel);
            core.timetable.addCloseListener('closeOnAbemaTimetable', core.abemaTimetable.closeOnAbemaTimetable);
          }, 2500/*映像も隠し音量も下げたので、重たい公式番組表ページに移動するのは落ち着いたあとでよい*/, function(){
            elements.abemaTimetableButton.click()
          });
        }
      },
      volumeDown: function(){
        /* 音量ダウンの耳心地ベストを検証した末のeaseout */
        let media = Array.from([...$$('video[src]'), ...$$('audio[src]')]), step = 10, begin = Date.now();
        let easeoutDown = (now, original) => original * Math.pow(1 - Math.min(((now - begin) / ABEMATIMETABLEDURATION), 1), 2);/* (1-X)^2 */
        if(!media.length) return;
        /* 元音量 */
        for(let i = 0; media[i]; i++){
          media[i].originalVolume = media[i].volume;
        }
        /* 音量ダウンタイマーを一気に設置(intervalに比べてタイミングが乱れにくい) */
        for(let s = 1; s <= step; s++){
          setTimeout(function(){
            for(let i = 0; media[i]; i++){
              if(s === step) media[i].volume = 0;
              else media[i].volume = easeoutDown(Date.now(), media[i].originalVolume);
            }
          }, ABEMATIMETABLEDURATION * (s/step));
        }
      },
      goChannel: function(id){
        /* 目的チャンネルで現在放送中の番組を番組表の中から探す */
        let button = site.get.abemaTimetableSlotButton(id, core.getProgramNowOnAir(id).id);
        if(!button) return location.assign('/now-on-air/' + id);
        /* クリックして放送ページへのリンクを出現させる */
        button.click();
        animate(function(){
          let a = site.get.abemaTimetableNowOnAirLink(id);
          if(!a) return location.assign('/now-on-air/' + id);
          /* ついに念願のチャンネル切り替えイベントを流用できるa要素を手に入れた */
          a.click();
          /* 放送中のチャンネルに移動するときは番組表を閉じる */
          sequence(1000/*ページ遷移に時間がかかるので慌てて番組表を閉じずに*/, function(){
            $('#splash > div').animate([{opacity: '1'}, {opacity: '0'}], {duration: ABEMATIMETABLEDURATION, fill: 'forwards'});
            core.panel.toggle('timetablePanel', core.timetable.createPanel);
          }, ABEMATIMETABLEDURATION, function(){
            html.classList.remove('abemaTimetable');
          });
        });
      },
    },
    timetable: {
      createButton: function(){
        let button = elements.channelButton.cloneNode(true);
        button.dataset.selector = SCRIPTNAME + '-button';
        button.title = SCRIPTNAME + ' 番組表';
        button.setAttribute('aria-label', SCRIPTNAME);
        button.appendChild(button.firstElementChild.cloneNode(true));
        button.addEventListener('click', function(e){
          core.panel.toggle('timetablePanel', core.timetable.createPanel);
        }, true);
        elements.channelButton.parentNode.insertBefore(button, elements.channelButton.nextElementSibling)
      },
      createPanel: function(){
        let timetablePanel = elements.timetablePanel = createElement(core.html.timetablePanel());
        timetablePanel.querySelector('button.ok').addEventListener('click', core.panel.close.bind(null, 'timetablePanel'));
        core.timetable.buildTimes();
        core.timetable.buildDays();/*先に作ったtimesのdisable判定を含む*/
        core.timetable.buildTimetable();
        core.timetable.searchPane.build();
        core.timetable.searchPane.buildNotifications();
        core.config.createButton();
        core.panel.open('timetablePanel');
        core.timetable.listenSelection();
        core.timetable.listenMousewheel();
        core.timetable.shiftTimetable();
        core.timetable.useChannelPane();
        setTimeout(core.timetable.setupScrolls, 1000);
      },
      buildDays: function(){
        if(!elements.timetablePanel) return;
        let now = new Date(), starts = [], today = MinuteStamp.justToday();
        let getDay = (d) => EDAYS[d.getDay()];
        let formatDate = (d) => `${d.getMonth() + 1}月${d.getDate()}日(${JDAYS[d.getDay()]})`;
        let formatDay = (d) => `${d.getDate()}${JDAYS[d.getDay()]}`;
        let disableTimes = function(){
          let past = MinuteStamp.past();
          let inputs = elements.timetablePanel.querySelectorAll('nav .times input:not(.template)');
          for(let i = 0; inputs[i]; i++){
            if(parseInt(inputs[i].value*HOUR) < past) inputs[i].disabled = true;
          }
        };
        for(let t = 0, y = now.getFullYear(), m = now.getMonth(), d = now.getDate(); t <= TERM; t++) starts.push(new Date(y, m, d + t));
        let days = elements.days = elements.timetablePanel.querySelector('nav .days');
        let templates = {input: days.querySelector('input.template'), label: days.querySelector('label.template')};
        while(days.children.length > 2/*template*2*/) days.removeChild(days.children[0]);
        for(let i = 0; starts[i]; i++){
          let time = parseInt(starts[i].getTime() / 1000);
          let input = templates.input.cloneNode(true);
          let label = templates.label.cloneNode(true);
          input.classList.remove('template');
          label.classList.remove('template');
          input.id = 'day-' + time;
          input.value = time;
          input.checked = (i === 0);
          input.addEventListener('click', function(e){
            let past = MinuteStamp.past();
            let checked = elements.timetablePanel.querySelector('nav .times input:checked');
            let start = time;
            let delta = (!checked) ? past : (checked.value*HOUR);
            core.timetable.buildTimetable(start + delta);
            core.timetable.scrollTo(start + delta);
          });
          input.addEventListener('change', function(e){
            if(i === 0) return disableTimes();
            elements.timetablePanel.querySelectorAll('nav .times input:disabled').forEach((input) => input.disabled = false);
          });
          label.setAttribute('for', input.id);
          label.classList.add(getDay(starts[i]));
          label.firstElementChild.textContent = (i === 0) ? formatDate(starts[i]) : formatDay(starts[i]);
          days.insertBefore(input, templates.input);
          days.insertBefore(label, templates.input);
        }
        disableTimes();/*初期化*/
      },
      buildTimes: function(){
        if(!elements.timetablePanel) return;
        let deltas = TIMES, past = MinuteStamp.past(), today = MinuteStamp.justToday();
        let times = elements.times = elements.timetablePanel.querySelector('nav .times');
        let templates = {input: times.querySelector('input.template'), label: times.querySelector('label.template')};
        while(times.children.length > 2/*template*2*/) times.removeChild(times.children[0]);
        for(let i = 0; deltas[i] !== undefined/*0も入ってるので*/; i++){
          let input = templates.input.cloneNode(true);
          let label = templates.label.cloneNode(true);
          input.classList.remove('template');
          label.classList.remove('template');
          input.id = 'time-' + deltas[i];
          input.value = deltas[i];
          input.addEventListener('click', function(e){
            let checked = elements.timetablePanel.querySelector('nav .days input:checked');
            let start = (checked.value === 'now') ? today : parseInt(checked.value);
            let delta = deltas[i]*HOUR;
            core.timetable.buildTimetable(start + delta);
            core.timetable.scrollTo(start + delta);
          });
          label.setAttribute('for', input.id);
          label.firstElementChild.textContent = deltas[i] + ':00';
          times.insertBefore(input, templates.input);
          times.insertBefore(label, templates.input);
        }
      },
      setupScrolls: function(){
        if(!elements.timetablePanel) return;
        let channelsUl = elements.channelsUl, scrollers = elements.scrollers = elements.timetablePanel.querySelector('.scrollers');
        let nowButton = elements.timetablePanel.querySelector('button.now');
        /* スクロールボタン */
        let left = elements.scrollerLeft = scrollers.querySelector('.left'), right = elements.scrollerRight = scrollers.querySelector('.right');
        let searchPane = elements.searchPane;
        right.addEventListener('click', function(e){
          if(searchPane.classList.contains('active')) return searchPane.classList.remove('active');/*検索ペインを閉じるボタンとして機能させる*/
          core.timetable.scrollTo(channelsUl.scrollTime + HOUR);
        });
        left.addEventListener('click', function(e){
          core.timetable.scrollTo(channelsUl.scrollTime - HOUR);
        });
        right.classList.remove('disabled');
        /* スクロール先の時間帯で番組表示 */
        channelsUl.addEventListener('scroll', function(e){
          if(channelsUl.scrolling) return;
          channelsUl.scrolling = true;
          setTimeout(function(){
            let now = MinuteStamp.now(), past = MinuteStamp.past(), range = (TERM + 1)*DAY - past;
            let start = now + ((channelsUl.scrollLeft / channelsUl.scrollWidth) * range);
            core.timetable.buildTimetable(start);
            channelsUl.scrolling = false;
            /* バウンシングエフェクト */
            let scrollLeftMax = channelsUl.scrollWidth - channelsUl.clientWidth;
            if(channelsUl.scrollLeft === 0) channelsUl.scrollLeft = BOUNCINGPIXEL;
            else if(channelsUl.scrollLeft === scrollLeftMax) channelsUl.scrollLeft = scrollLeftMax - BOUNCINGPIXEL;
            /* Days/Timesの切り替え */
            let days = elements.timetablePanel.querySelectorAll('nav .days input:not(.template)');
            for(let i = 1; days[i]; i++){
              if(start < parseInt(days[i].value)){
                days[i - 1].checked = true;
                days[i - 1].dispatchEvent(new Event('change'));
                break;
              }else if(i === days.length - 1){
                days[i].checked = true;
                days[i].dispatchEvent(new Event('change'));
              }
            }
            let times = elements.timetablePanel.querySelectorAll('nav .times input:not(.template)');
            for(let i = 1; times[i]; i++){
              if(((start + MINUTE/*1分タイマーシフトのズレをカバー*/ + JST) % DAY) / HOUR < parseInt(times[i].value)){
                times[i - 1].checked = true;
                break;
              }else if(i === times.length - 1){
                times[i].checked = true;
              }
            }
            /* 現在時刻に戻るボタン */
            if(channelsUl.scrollLeft <= BOUNCINGPIXEL) nowButton.classList.add('disabled');
            else if(nowButton.classList.contains('disabled')) nowButton.classList.remove('disabled');
            /* スクロールボタンの切り替え */
            if(channelsUl.scrollLeft <= BOUNCINGPIXEL) left.classList.add('disabled');
            else if(left.classList.contains('disabled')) left.classList.remove('disabled');
            if(channelsUl.scrollLeft === scrollLeftMax - BOUNCINGPIXEL) right.classList.add('disabled');
            else if(right.classList.contains('disabled')) right.classList.remove('disabled');
          }, 100);
        }, {passive: true});/*Passive Event Listener*/
      },
      buildTimetable: function(start = MinuteStamp.now()){
        let now = MinuteStamp.now(), past = MinuteStamp.past(), range = (configs.span*HOUR), ratio = (100 - NAMEWIDTH) / (configs.span*HOUR);
        let fullwidth = (((TERM + 1)*DAY - past) / range) * (100 - NAMEWIDTH) + 'vw';
        let timetablePanel = elements.timetablePanel, channelsUl = elements.channelsUl = timetablePanel.querySelector('.channels');
        let show = function(element){
          element.classList.remove('hidden');
          element.addEventListener('transitionend', function(e){
            element.classList.remove('animate');
          }, {once: true});
        };
        channelsUl.scrollTime = start;/*スクロール用に保持しておく*/
        /* 時間帯(目盛りになるので一括して全部作る) */
        let timeLi = channelsUl.querySelector('.channels > .time'), timesUl = timeLi.querySelector('.times');
        timeLi.style.width = fullwidth;
        if(timesUl.children.length === 2/*templates*/){
          /* 現在時刻に戻るボタン */
          let button = timeLi.querySelector('button.now');
          button.addEventListener('click', function(e){
            core.timetable.scrollTo(MinuteStamp.now());
          });
          /* 時と日を生成 */
          let ht = timesUl.querySelector('.hour.template'), dt = timesUl.querySelector('.day.template');
          for(let hour = now - (start - range)%HOUR; hour < now - past + (TERM+1)*DAY; hour += HOUR){
            /* 時を作成 */
            let hourLi = ht.cloneNode(true);
            hourLi.classList.remove('template');
            hourLi.startAt  = hour;
            hourLi.endAt    = hour + HOUR;
            hourLi.duration = HOUR;
            hourLi.style.left = Math.max((hour - now) * ratio, 0) + 'vw';
            hourLi.style.width = ((hour < now) ? HOUR - (now%HOUR) : HOUR) * ratio + 'vw';
            hourLi.addEventListener('click', function(e){
              core.timetable.scrollTo(hour);
            });
            let oclock = (((hour+JST)%DAY)/HOUR);
            if(hour < now){
              hourLi.classList.add('nowonair');
              hourLi.querySelector('.time').appendChild(MinuteStamp.timeToClock(now));
            }else{
              hourLi.querySelector('.time').textContent = oclock + ':00';
            }
            timesUl.insertBefore(hourLi, ht);
            /* 日を作成 */
            if(hour < now || oclock === 0){
              let dayLi = dt.cloneNode(true);
              dayLi.classList.remove('template');
              dayLi.startAt  = (hour < now) ? (now - past) : hour;
              dayLi.endAt    = (hour < now) ? (now - past) + DAY : (hour + DAY);
              dayLi.duration = DAY;
              dayLi.style.left = Math.max((hour - now) * ratio, 0) + 'vw';
              dayLi.style.width = (((hour < now) ? (DAY - past) : DAY) * ratio) + 'vw';
              dayLi.querySelector('.date').textContent = MinuteStamp.shortestDateToString(hour);
              timesUl.insertBefore(dayLi, hourLi);
            }
          }
          animate(show.bind(null, timeLi));
        }
        /* スワイプによるブラウザバックを防ぐためにバウンシングエフェクトを作る */
        if(start === now) animate(() => channelsUl.scrollLeft = BOUNCINGPIXEL);
        /* 各チャンネル */
        let final;/*最後のチャンネルの判定に使う*/
        for(let c = channels.length - 1, channel; channel = channels[c]; c--){
          if(!configs.c_visibles[channel.id]) continue;
          final = c;
          break;
        }
        for(let c = 0, delay = 0, channel; channel = channels[c]; c++){
          if(!configs.c_visibles[channel.id]) continue;
          let channelLi = document.getElementById('channel-' + channel.id), current = (core.getCurrentChannelId() === channel.id);
          if(!channelLi){
            channelLi = channelsUl.querySelector('.channel.template').cloneNode(true);
            channelLi.classList.remove('template');
            channelLi.id = 'channel-' + channel.id;
            if(current) channelLi.classList.add('current');
            channelLi.querySelector('.name').textContent = channel.name;
            channelLi.querySelector('header').addEventListener('click', function(e){
              /* 間もなく再開する休止チャンネルをクリッカブルにすると番組表が閉じてしまうのでここではやらない */
              core.timetable.showProgramData(core.getProgramNowOnAir(channel.id));/*移り変わるのでつど取得*/
              animate(function(){core.goChannel(channel.id)});
            });
            channelsUl.insertBefore(channelLi, channelsUl.lastElementChild);
          }
          channelLi.style.width = fullwidth;
          let programsUl = channelLi.querySelector('.programs');
          clearTimeout(channelLi.timer), channelLi.timer = setTimeout(function(){/*非同期処理にする*/
            /* 表示済みの番組要素の再利用と削除 */
            let programLis = programsUl.querySelectorAll('.program:not(.template)');/*nowonairや最初の一画面だけ残す手もるけどshiftTimetableが複雑化するので保留*/
            for(let i = 0, li; li = programLis[i]; i++){
              if(li.endAt <= start - range/2 || start + range + range < li.startAt) programsUl.removeChild(li);
            }
            /* 各番組 */
            for(let p = 0, program; program = channel.programs[p]; p++){
              if(document.getElementById('program-' + program.id)) continue;/*表示済み*/
              if(program.endAt <= now) continue;/*現在より過去*/
              if(program.endAt <= start - range/2) continue;/*表示範囲より過去*/
              if(start + range + range <= program.startAt) break;/*表示範囲より未来(以降は処理不要)*/
              /* programLiを作成 */
              let programLi = programsUl.querySelector('.program.template').cloneNode(true);
              programLi.classList.remove('template');
              programLi.id = 'program-' + program.id;
              programLi.dataset.once = program.id;
              if(program.repeat) programLi.dataset.repeat = program.repeat;
              if(program.noContent) programLi.classList.add('nocontent');
              let time = programLi.querySelector('.time'), title = programLi.querySelector('.title');
              /* 時刻と通知ボタン */
              time.textContent = program.startAtString;
              programLi.insertBefore(Notifier.createButton(program), time);
              /* タイトル */
              title.textContent = program.title || NOCONTENTS[0];
              if(program.padding){/*空き枠*/
                programLi.classList.add('padding');
              }else{
                if(Notifier.match(program.id)) programLi.classList.add('active');
                Program.appendMarks(title, program.marks);
                programLi.addEventListener('click', function(e){
                  /* 2度目のクリック時のみ番組開始時刻にスクロールさせる */
                  if(elements.programDiv && elements.programDiv.programData.id === program.id){/*shownクラスがなぜか判定に使えないので*/
                    core.timetable.scrollTo(program.startAt);
                  }
                  core.timetable.showProgramData(program);
                });
              }
              /* 番組の幅を決める */
              programLi.startAt  = program.startAt;
              programLi.endAt    = program.endAt;
              programLi.duration = program.duration;
              if(program.startAt <= now){/*現在放送中*/
                programLi.classList.add('nowonair');
                programLi.style.left = '0vw';
                programLi.style.width = (program.duration - (now - program.startAt)) * ratio + 'vw';
                if(program.padding) channelLi.classList.add('notonair');
                /* 番組情報が空欄なら現在視聴中の番組情報を表示 */
                if(current && timetablePanel.isConnected && timetablePanel.querySelector('.panel > .program.nocontent')) core.timetable.showProgramData(program);
              }else{/*後続番組*/
                programLi.style.left = (program.startAt - now) * ratio + 'vw';
                programLi.style.width = (program.duration) * ratio + 'vw';
              }
              programsUl.insertBefore(programLi, programsUl.lastElementChild);
              animate(function(){programLi.classList.remove('hidden')});
            }
            /* 最後に1度だけ */
            if(c === final && timetablePanel){
              for(let i = 0, channelLis = channelsUl.querySelectorAll('.channel:not(.template)'); channelLis[i]; i++){
                if(channelLis[i].classList.contains('hidden')) show(channelLis[i]);
              }
              animate(core.timetable.fitWidth);
              let program = timetablePanel.querySelector('.panel > .program').programData;
              if(program) core.timetable.highlightProgram(program);
            }
          }, (delay++) * (1000/60));
        }
      },
      rebuildTimetable: function(){
        if(!elements.timetablePanel || !elements.timetablePanel.isConnected) return;
        let channelsUl = elements.channelsUl = elements.timetablePanel.querySelector('.channels');
        for(let i = 0, channelLis = channelsUl.querySelectorAll('.channel:not(.template)'); channelLis[i]; i++){
          channelLis[i].parentNode.removeChild(channelLis[i]);
        }
        core.timetable.buildTimetable(channelsUl.scrollTime);
      },
      scrollTo: function(start){
        let past = MinuteStamp.past(), range = (TERM + 1)*DAY - past, today = MinuteStamp.justToday(), ratio = (start - (today + past)) / range;
        let channelsUl = elements.channelsUl, scrollLeftMax = channelsUl.scrollWidth - channelsUl.clientWidth;
        let to = Math.max(0, Math.min(channelsUl.scrollWidth * ratio, scrollLeftMax)), gap = to - channelsUl.scrollLeft;
        if(gap === 0) return;
        channelsUl.scrollTime = start;
        let streams = channelsUl.querySelectorAll('li:not(.template) > .stream'), count = 0;
        for(let i = 0; streams[i]; i++){
          streams[i].style.willChange = 'transform';
          streams[i].style.transition = 'transform 1s ease';
        }
        animate(function(){
          for(let i = 0; streams[i]; i++){
            streams[i].style.transform = `translateX(${-gap}px)`;
          }
        });
        streams[streams.length - 1].addEventListener('transitionend', function(e){/*疑似スクロールを破綻させないようにタイミングを一致させる*/
          for(let i = 0; streams[i]; i++){
            streams[i].style.willChange = '';
            streams[i].style.transition = 'none';/*scrollLeftを即反映させる*/
            streams[i].style.transform = '';
          }
          channelsUl.scrollLeft = Math.ceil(to)/*borderズレを常に回避する*/;
        }, {once: true});
      },
      shiftTimetable: function(){
        // animateをひとつにするべきなのかもしれないけど
        let channelsUl = elements.channelsUl;
        if(!channelsUl || !channelsUl.isConnected) return;
        if(document.hidden){
          if(document.shiftTimetable) return;/*重複防止*/
          document.shiftTimetable = true;
          document.addEventListener('visibilitychange', function(){
            document.shiftTimetable = false;
            core.timetable.shiftTimetable();
          }, {once: true});
          return;
        }
        const change = function(element, left, width, callback){
          if(channelsUl.scrollLeft <= BOUNCINGPIXEL && left < 100){
            element.style.willChange = 'left';
            element.style.transition = 'left 1000ms ease, width 1000ms ease, background 1000ms ease, filter 1000ms ease, padding-left 500ms ease 1000ms';
            animate(function(){
              element.style.left = left + 'vw';
              if(left === 0) element.style.width = width + 'vw';
              element.addEventListener('transitionend', function(e){
                element.style.willChange = '';
                element.style.transition = '';
                if(callback) callback();
              }, {once: true});
            });
          }else{
            element.style.left = left + 'vw';
            if(left === 0) element.style.width = width + 'vw';
            if(callback) callback();
          }
        };
        let now = MinuteStamp.now(), past = MinuteStamp.past(), end = now + (configs.span*HOUR), ratio = (100 - NAMEWIDTH) / (configs.span*HOUR);
        /* 各チャンネル */
        let channelLis = channelsUl.querySelectorAll('.channels > li:not(.template)');
        let oldWidth = channelLis[0].scrollWidth, newlWidthVW = (((TERM + 1)*DAY - past) / (configs.span*HOUR)) * (100 - NAMEWIDTH);
        for(let c = 0, channelLi; channelLi = channelLis[c]; c++){
          channelLi.style.width = newlWidthVW + 'vw';/*チャンネル自体の幅を狭める*/
          /* 各番組 */
          let slots = channelLi.querySelectorAll('.slot:not(.template)');
          for(let s = 0, slotLi; slotLi = slots[s]; s++){
            let startAt = slotLi.startAt, endAt = slotLi.endAt, duration = slotLi.duration;
            switch(true){
              case(endAt <= now):/*放送終了*/
                change(slotLi, 0, 0, function(e){
                  if(slots[s + 1]){/*必ずしも隣じゃないけどレアケースなので*/
                    Slot.highlight(slots[s + 1], 'add', 'nowonair');
                    if(slotLi.classList.contains('shown')) slots[s + 1].click();
                  }
                  if(slotLi.isConnected) Slot.highlight(slotLi, 'remove', 'nowonair'), slotLi.parentNode.removeChild(slotLi);
                  if(channelLi.classList.contains('notonair')) channelLi.classList.remove('notonair');
                });
                break;
              case(startAt <= now):/*現在放送中*/
                change(slotLi, 0, (duration - (now - startAt)) * ratio);
                /* 現在時刻更新 */
                if(slotLi.classList.contains('hour')){
                  let time = slotLi.querySelector('.time');
                  time.replaceChild(MinuteStamp.timeToClock(now), time.firstChild);
                }
                break;
              case(startAt < end):/*後続番組*/
              default:/*画面外*/
                change(slotLi, (startAt - now) * ratio, duration * ratio);
                break;
            }
          }
        }
        /* 短くなったぶんだけスクロールする */
        if(oldWidth < channelLis[0].scrollWidth) oldWidth += ((DAY * ratio) * window.innerWidth) /100;/*日付が変わったときだけは1日分長くなるので*/
        channelsUl.scrollLeft = Math.max(channelsUl.scrollLeft - (oldWidth - channelLis[0].scrollWidth), BOUNCINGPIXEL);
        /* 現在時刻にいるときは長時間放置で後続番組がなくならないように再構築させる */
        if(channelsUl.scrollLeft === BOUNCINGPIXEL) setTimeout(function(){core.timetable.buildTimetable(channelsUl.scrollTime = now)}, 1000);
      },
      showProgramData: function(program){
        /* timetable */
        let shown = elements.timetablePanel.querySelector('.channels .shown'), show = document.getElementById('program-' + program.id);
        if(shown) shown.classList.remove('shown');
        if(show)  show.classList.add('shown');
        /* programDiv */
        let programDiv = elements.programDiv = elements.timetablePanel.querySelector('.panel > .program');
        programDiv.scrollTop = 0;
        if(programDiv.classList.contains('nocontent')) programDiv.classList.remove('nocontent');
        else programDiv.animate([{opacity: 0}, {opacity: 1}], {duration: 250, easing: 'ease-out'});
        programDiv.programData = program;/*番組表をハイライトするタイミングで活用*/
        /* title */
        let title = programDiv.querySelector('.title');
        title.textContent = program.title;
        Array.from(title.parentNode.children).forEach((node) => {
          if(node !== title) title.parentNode.removeChild(node);/*マークをいったん取り除く*/
        });
        Program.appendMarks(title, program.marks);
        /* thumbnails */
        let thumbnailsDiv = programDiv.querySelector('.thumbnails');
        while(thumbnailsDiv.children.length) thumbnailsDiv.removeChild(thumbnailsDiv.children[0]);
        if(program.thumbImg){
          thumbnailsDiv.appendChild(new Thumbnail(program.displayProgramId, program.thumbImg, 'large').node);
        }
        for(let i = 0; program.sceneThumbImgs[i]; i++){
          thumbnailsDiv.appendChild(new Thumbnail(program.displayProgramId, program.sceneThumbImgs[i]).node);
        }
        /* summary */
        let summaryDiv = programDiv.querySelector('.summary');
        summaryDiv.querySelector('.channel').textContent = program.channel.name;
        let dateP = summaryDiv.querySelector('.date');
        dateP.querySelector('span').textContent = program.dateString;
        summaryDiv.querySelector('.highlight').textContent = program.detailHighlight;
        /* links */
        let linksUl = summaryDiv.querySelector('.links');
        while(linksUl.children.length > 1/*template*/) linksUl.removeChild(linksUl.firstElementChild);
        if(program.links){
          linksUl.classList.remove('inactive');
          let templateLi = linksUl.querySelector('.template');
          for(let i = 0; program.links[i]; i++){
            let li = templateLi.cloneNode(true), a = li.querySelector('a');
            li.classList.remove('template');
            a.href = program.links[i].value;
            a.textContent = program.links[i].title;
            linksUl.insertBefore(li, templateLi);
          }
        }else{
          linksUl.classList.add('inactive');
        }
        /* myvideo */
        let timeshiftP = summaryDiv.querySelector('.timeshift');
        if(program.timeshiftString !== ''){
          timeshiftP.classList.remove('inactive');
          timeshiftP.querySelector('span').textContent = program.timeshiftString;
          while(timeshiftP.children.length > 1/*template*/) timeshiftP.removeChild(timeshiftP.firstElementChild);
          let myvideoButton = MyVideo.createMyvideoButton(program);
          timeshiftP.insertBefore(myvideoButton, timeshiftP.firstElementChild);
        }else{
          timeshiftP.classList.add('inactive');
        }
        /* group and series */
        let now = MinuteStamp.now(), results = [], count = {};
        ['group', 'series'].forEach((key, i) => {
          /* 一致program取得 */
          for(let c = 0; channels[c]; c++){
            for(let p = 0; channels[c].programs[p]; p++){
              if(channels[c].programs[p].endAt < now) continue;/*終了した番組は表示しない*/
              if(channels[c].programs[p][key] && channels[c].programs[p][key] === program[key]){
                if(1 <= i && results.some((result) => result.id === channels[c].programs[p].id)) continue;/*重複させない*/
                results.push(channels[c].programs[p]);
                count[key] = count[key] + 1 || 1;
              }
            }
          }
          if(results.length === 1) results.pop(results[0]);/*自分自身の番組しかなければ取り除く*/
          else if(1 <= i && !count[key]) while(results.length) results.pop(results[0]);/*同じ内容なら繰り返さない*/
          results.sort((a, b) => a.startAt - b.startAt);/*日付順*/
          /* タイトルの重複文字列を省略する準備 */
          let shorten;
          if(results.every((r) => r.title === program.title)){
            shorten = () => '同';
          }else{
            /* 区切り文字は/(?=\s)/とし、全タイトル共通文字列と、半数以上のタイトルに共通する文字列を削除する */
            let parts = program.title.split(/(?=\s)/), former = {all: '', majority: ''}, latter = {all: '', majority: ''}, n = '\n';
            /* 前方一致部分文字列 */
            for(let i = 0; parts[i]; i++) if(results.every((r) => r.title.startsWith(former.all + parts[i]))) former.all += parts[i];
            for(let i = 0; parts[i]; i++) if(results.filter((r) => r.title.startsWith(former.majority + parts[i])).length >= results.length/2) former.majority += parts[i];
            /* 後方一致部分文字列 */
            for(let i = parts.length - 1; parts[i]; i--) if(results.every((r) => r.title.endsWith(parts[i] + latter.all))) latter.all = parts[i] + latter.all;
            for(let i = parts.length - 1; parts[i]; i--) if(results.filter((r) => r.title.endsWith(parts[i] + latter.majority)).length >= results.length/2) latter.majority = parts[i] + latter.majority;
            /* 削りすぎを回避する */
            if((former.majority + latter.majority).length >= program.title.length) former.majority = '';
            if((former.majority + latter.majority).length >= program.title.length) latter.majority = '';
            shorten = (title) => (title + n).replace(former.majority, '').replace(former.all, '').replace(latter.majority + n, n).replace(latter.all + n, n).trim();
          }
          /* 放送予定リストDOM構築 */
          let div = summaryDiv.querySelector(`.${key}`), ul = div.querySelector(`.${key} ul`), templateLi = ul.querySelector('.template');
          if(1 <= i && !results.length) div.classList.add('inactive');
          else div.classList.remove('inactive');
          while(ul.children.length > 1/*template*/) ul.removeChild(ul.children[0]);
          if(results.length === 0){
            let li = templateLi.cloneNode(true);
            li.classList.remove('template');
            li.textContent = '-';
            ul.insertBefore(li, templateLi);
          }else{
            for(let p = 0, result; result = results[p]; p++){
              let li = templateLi.cloneNode(true), header = li.querySelector('header');
              li.classList.remove('template');
              if(program.id === result.id) li.classList.add('current');
              else header.addEventListener('click', function(e){
                core.timetable.showProgramData(result);
                core.timetable.scrollTo(result.startAt);
              });
              header.insertBefore(Notifier.createButton(result), header.firstElementChild);
              li.querySelector('.date').textContent = result.justifiedStartAtShortDateString;
              let title = li.querySelector('.title');
              title.textContent = shorten(result.title);
              Program.appendMarks(title, result.marks);
              ul.insertBefore(li, templateLi);
            }
          }
        });
        /* 1回通知  */
        while(dateP.children.length > 1) dateP.removeChild(dateP.firstElementChild);
        let button = Notifier.createButton(program);
        dateP.insertBefore(button, dateP.firstElementChild);
        /* 毎回通知 */
        let h3 = summaryDiv.querySelector('h3');
        while(h3.children.length > 1) h3.removeChild(h3.firstElementChild);
        if(program.repeat){
          let repeatButton = Notifier.createRepeatAllButton(program);
          h3.insertBefore(repeatButton, h3.firstElementChild);
        }
        /* content */
        let content = programDiv.querySelector('.content div'), paragraphs = program.content.split(/\n+/);
        while(content.children.length) content.removeChild(content.children[0]);
        for(let i = 0; paragraphs[i]; i++){
          let p = document.createElement('p');
          p.textContent = paragraphs[i];
          linkify(p);
          content.appendChild(p);
        }
        /* casts and crews */
        let searchInput = elements.timetablePanel.querySelector('nav > .search input');
        ['casts', 'crews'].forEach((key) => {
          let ul = programDiv.querySelector(`.${key} ul`);
          while(ul.children.length) ul.removeChild(ul.children[0]);
          for(let i = 0; program[key][i]; i++){
            let li = document.createElement('li');
            li.textContent = program[key][i];
            Program.linkifyNames(li, function(e){
              core.timetable.searchPane.search(e.target.textContent);
            });
            ul.appendChild(li);
          }
          if(ul.children.length === 0){
            let li = document.createElement('li');
            li.textContent = '-';
            ul.appendChild(li);
          }
        });
        /* copyrights */
        programDiv.querySelector('.copyrights').textContent = program.copyrights.join(', ');
        /* highlight */
        core.timetable.highlightProgram(program);
      },
      highlightProgram: function(program){
        let oldShown = elements.channelsUl.querySelector('.program.shown');
        if(oldShown) Slot.highlight(oldShown, 'remove', 'shown');
        let newShown = document.getElementById('program-' + program.id);
        if(newShown) Slot.highlight(newShown, 'add', 'shown');
      },
      listenSelection: function(){
        let programDiv = elements.timetablePanel.querySelector('.panel > .program');
        let select = function(e){
          let selection = window.getSelection(), selected = selection.toString();
          if(selection.isCollapsed) return;
          if(0 <= selected.indexOf('\n')) return;
          let value = selected.trim();
          if(value === '') return;
          core.timetable.searchPane.search(value);
        };
        programDiv.addEventListener('mousedown', function(e){
          programDiv.addEventListener('mouseup', function(e){
            animate(function(){select(e)});/*ダブルクリックでのテキスト選択をanimateで確実に補足*/
          }, {once: true});
        });
      },
      listenMousewheel: function(){
        let channelsUl = elements.channelsUl;
        channelsUl.addEventListener('wheel', function(e){
          if(0 < Math.abs(e.deltaX)) return;/*通常の左右ホイール・スワイプ*/
          if(Math.abs(e.deltaY) < 1) return;/*ホイール量が小さすぎる*/
          if(e.target.localName === 'h2') return;
          channelsUl.scrollLeft += e.deltaY;
        }, {passive: true});
      },
      useChannelPane: function(){
        /* ChannelPaneのチャンネル切り替えイベントを流用できるようにしておく */
        if(location.href.includes('/now-on-air/')){
          if(site.get.screenCommentScroller()) return;/*既に開いてくれているはず*/
          core.channelPane.openHide();
          core.timetable.addCloseListener('channelPaneHidden', function(){
            elements.closer.click();
            html.classList.remove('channelPaneHidden');
          });
        }
      },
      addCloseListener: function(name, listener){
        elements.timetablePanel.querySelector('button.ok').addEventListener('click', listener);
        if(!elements.panels['listening-' + name]){
          elements.panels['listening-' + name] = true;
          window.addEventListener('keypress', function(e){
            if(['input', 'textarea'].includes(document.activeElement.localName)) return;
            if(elements.timetablePanel && e.key === 'Escape') return listener();
          });
        }
      },
      fitWidth: function(){
        if(!elements.timetablePanel || !elements.timetablePanel.isConnected) return;
        let timetablePanel = elements.timetablePanel, fits = timetablePanel.querySelectorAll('.fit');
        for(let i = 0; fits[i]; i++){
          if(fits[i].scrollWidth < fits[i].clientWidth) fits[i].style.transform = '';
          else fits[i].style.transform = `scaleX(${fits[i].clientWidth / fits[i].scrollWidth})`;
        }
      },
      searchPane: {
        build: function(){
          let searchInput = elements.searchInput = elements.timetablePanel.querySelector('nav > .search input[type="search"]');
          let searchButton = elements.searchButton = elements.timetablePanel.querySelector('nav > .search button.search');
          let searchPane = elements.searchPane = elements.timetablePanel.querySelector('.programs > .search');
          /* 検索 */
          searchInput.addEventListener('keypress', function(e){
            if(e.key === 'Escape') return searchPane.classList.remove('active');
            if(e.key !== 'Enter') return;
            let value = searchInput.value.trim();
            if(value === ''){
              searchPane.classList.remove('active');
              searchPane.dataset.mode = '';
              return;
            }
            core.timetable.searchPane.search(value);/*marks絞りなしで一括取得*/
          });
          searchButton.addEventListener('click', function(e){
            let value = searchInput.value.trim();
            if(value === ''){
              searchPane.classList.remove('active');
              searchPane.dataset.mode = '';
              return;
            }
            core.timetable.searchPane.search(value);/*marks絞りなしで一括取得*/
          });
        },
        search: function(value, marks = site.marks){
          let searchInput = elements.searchInput, searchPane = elements.searchPane, ul = searchPane.querySelector('ul');
          value = value.trim();
          searchInput.value = value;
          ul.results = core.searchPrograms(value);/*全件取得してDOMプロパティに渡しておく(絞り込みはlistSearchResultsでinputを判定して行う)*/
          core.timetable.searchPane.buildSearchHeader();
          core.timetable.searchPane.updateSearchFillters(value, marks);
          core.timetable.searchPane.listSearchResults(value);
          searchPane.classList.add('active');
          searchPane.dataset.mode = 'search';
        },
        buildSearchHeader: function(){
          let searchInput = elements.searchInput, searchPane = elements.searchPane;
          while(searchPane.children.length > 1/*ul*/) searchPane.removeChild(searchPane.children[0]);
          searchPane.insertBefore(createElement(core.html.searchHeader()), searchPane.firstElementChild);
          /* 絞り込みDOM生成 */
          let filtersP = searchPane.querySelector('.filters');
          let it = filtersP.querySelector('input.template'), lt = filtersP.querySelector('label.template');
          site.marks.forEach(function(key){
            let input = it.cloneNode(true);
            let label = lt.cloneNode(true);
            input.classList.remove('template');
            label.classList.remove('template');
            input.id = 'mark-' + key;
            input.value = key;
            label.setAttribute('for', 'mark-' + key);
            label.appendChild(createElement(core.html.marks[key]()));
            filtersP.insertBefore(input, it);
            filtersP.insertBefore(label, it);
          });
          /* eventListener付与 */
          let labels = filtersP.querySelectorAll('label:not(.template)');
          labels.forEach((label) => {
            /* 連続で次々クリックするとマウスポインタのズレによるclick判定漏れが起きやすいので、mousedownで処理する */
            label.addEventListener('mousedown', function(e){
              let input = label.previousElementSibling;
              input.checked = !input.checked;
              core.timetable.searchPane.listSearchResults(searchInput.value);
            });
            label.addEventListener('click', function(e){
              e.preventDefault();
            });
          });
        },
        updateSearchFillters: function(value, marks){
          let searchPane = elements.searchPane, labels = searchPane.querySelectorAll('.filters label:not(.template)');
          labels.forEach((label) => {
            let input = label.previousElementSibling;
            input.checked = (marks.some((mark) => mark === input.value));
            if(notifications.search[value] && notifications.search[value].includes(input.value)) label.classList.add('notify');
            else label.classList.remove('notify');
          });
        },
        listSearchResults: function(value){
          let searchPane = elements.searchPane, summary = searchPane.querySelector('.summary');
          let ul = searchPane.querySelector('ul');
          /* 絞り込み */
          let marks = [], filters = searchPane.querySelectorAll('.filters input:not(.template)');
          filters.forEach((filter) => {if(filter.checked === true) marks.push(filter.value)});
          let filteredResults = ul.results.filter((program) => {
            if(marks.some((mark) => program.marks[mark] !== undefined)) return true;
            if(marks.includes('none') && Object.keys(program.marks).length === 0) return true;
          });
          /* 検索結果リスト */
          core.timetable.searchPane.listPrograms(ul, filteredResults);
          core.timetable.searchPane.updateResultCount(filteredResults.length);
          /* 検索結果を常に通知する */
          while(summary.children.length > 1/*count*/) summary.removeChild(summary.lastElementChild);
          summary.appendChild(Notifier.createSearchAllButton(value, marks));
        },
        buildNotifications: function(){
          let searchInput = elements.searchInput, searchPane = elements.searchPane, ul = searchPane.querySelector('ul');
          let button = elements.notificationsButton = elements.timetablePanel.querySelector('nav button.notifications');
          Notifier.updateCount();
          button.addEventListener('click', function(e){
            if(searchPane.dataset.mode === 'notifications'){
              searchPane.classList.remove('active');
              searchPane.dataset.mode = '';
              return;
            }
            searchPane.classList.add('active');
            searchPane.dataset.mode = 'notifications';
            searchInput.value = '';
            ul.results = notifications.programs;/*DOMプロパティとして検索結果を渡す約束*/
            core.timetable.searchPane.buildNotificationsHeader();
            core.timetable.searchPane.listAllNotifications();
          });
        },
        buildNotificationsHeader: function(){
          let searchPane = elements.searchPane, header = searchPane.querySelector('header');
          if(header) searchPane.removeChild(header);
          searchPane.insertBefore(createElement(core.html.notificationsHeader()), searchPane.firstElementChild);
          /* eventListener付与 */
          let labels = searchPane.querySelectorAll('nav.tabs > label');
          let actions = {
            all: core.timetable.searchPane.listAllNotifications,
            once: core.timetable.searchPane.listOnceNotifications,
            repeat: core.timetable.searchPane.listRepeatNotifications,
            search: core.timetable.searchPane.listSearchNotifications,
          }
          labels.forEach((label) => {
            /* 連続で次々クリックするとマウスポインタのズレによるclick判定漏れが起きやすいので、mousedownで処理する */
            label.addEventListener('mousedown', function(e){
              let input = label.previousElementSibling;
              input.checked = true;
              actions[input.value]();
            });
            label.addEventListener('click', function(e){
              e.preventDefault();
            });
          });
        },
        listAllNotifications: function(){
          core.timetable.searchPane.listDaysNotifications(notifications.programs);
          core.timetable.searchPane.updateResultCount(notifications.programs.length);
        },
        listOnceNotifications: function(){
          let filteredResults = notifications.programs.filter((p) => {
            if(!Notifier.matchOnce(p.once)) return false;
            if(Notifier.matchRepeat(p.repeat)) return false;/*毎回通知は含めない*/
            return true;
          });
          core.timetable.searchPane.listDaysNotifications(filteredResults);
          core.timetable.searchPane.updateResultCount(filteredResults.length);
        },
        listDaysNotifications: function(programs){
          let searchPane = elements.searchPane, ul = searchPane.querySelector('ul');
          let labels = ['きょう', 'あした', 'あさって以降'], days = {}, today = MinuteStamp.justToday();
          labels.forEach((label) => days[label] = []);
          programs.forEach((p) => {
            switch(true){
              case(p.startAt < today + DAY*1): return days[labels[0]].push(p);
              case(p.startAt < today + DAY*2): return days[labels[1]].push(p);
              default: return days[labels[2]].push(p);
            }
          });
          while(ul.children.length > 0) ul.removeChild(ul.children[0]);
          labels.forEach((key) => {
            let li = createElement(core.html.dayListItem()), h2 = li.querySelector('h2');
            h2.textContent = key;
            core.timetable.searchPane.listPrograms(li.querySelector('ul'), days[key]);
            ul.appendChild(li);
          });
        },
        listRepeatNotifications: function(){
          let searchPane = elements.searchPane, ul = searchPane.querySelector('ul');
          let repeats = {}, count = 0;
          notifications.programs.forEach((p) => {
            if(!Notifier.matchRepeat(p.repeat)) return;
            if(MAXRESULTS <= count) return count++;
            if(!repeats[p.repeat]) repeats[p.repeat] = [];
            repeats[p.repeat].push(p);
            count++;
          });
          Object.keys(notifications.repeat).forEach((key) => {
            if(!repeats[key]) repeats[key] = [];
          });
          while(ul.children.length > 0) ul.removeChild(ul.children[0]);
          Object.keys(repeats).forEach((key) => {
            let li = createElement(core.html.repeatListItem()), h2 = li.querySelector('h2');
            h2.querySelector('.title').textContent = notifications.repeat[key];
            h2.insertBefore(Notifier.createRepeatAllButton(repeats[key][0] || {
              /* 期間内に番組がなくても repeat, title さえあればボタン生成できる */
              repeat: key,
              title: notifications.repeat[key],
            }), h2.firstChild);
            core.timetable.searchPane.listPrograms(li.querySelector('ul'), repeats[key]);
            ul.appendChild(li);
          });
          core.timetable.searchPane.updateResultCount(count);
        },
        listSearchNotifications: function(){
          let searchPane = elements.searchPane, ul = searchPane.querySelector('ul');
          let searches = {}, programs = [], limit = Infinity;
          Object.keys(notifications.search).forEach((key) => {
            searches[key] = core.searchPrograms(key, notifications.search[key]);
            programs = programs.concat(searches[key]);
          });
          if(MAXRESULTS < programs.length) limit = programs[MAXRESULTS - 1].startAt;
          while(ul.children.length > 0) ul.removeChild(ul.children[0]);
          Object.keys(searches).sort((a, b) => {
            if(searches[a][0] && searches[b][0]) return searches[a][0].startAt - searches[b][0].startAt;/*放送開始の早い順*/
            else if(searches[a].length) return -1;/*検索に該当する番組がなければあとまわし*/
            else if(searches[b].length) return +1;
            else return b < a;/*該当する番組がないもの同士では辞書順*/
          }).forEach((key) => {
            let marks = notifications.search[key].map((name) => core.html.marks[name]());
            let li = createElement(core.html.searchListItem(key, marks.join(''))), h2 = li.querySelector('h2');
            h2.insertBefore(Notifier.createSearchButton(key), h2.firstChild);
            core.timetable.searchPane.listPrograms(li.querySelector('ul'), searches[key].filter((p) => p.startAt <= limit));
            ul.appendChild(li);
          });
          core.timetable.searchPane.updateResultCount(programs.length);
        },
        listPrograms: function(ul, programs){
          let searchPane = elements.searchPane, summary = searchPane.querySelector('.summary');
          /* 前準備 */
          searchPane.scrollTop = 0;
          [summary, ul].forEach((e) => e.animate([{opacity: 0}, {opacity: 1}], {duration: 250, easing: 'ease-out'}));
          while(ul.children.length > 0) ul.removeChild(ul.children[0]);
          if(programs.length === 0){
            let li = createElement(core.html.noProgramListItem());
            ul.appendChild(li);
          }
          for(let p = 0; programs[p] && p < MAXRESULTS; p++){
            let li = createElement(core.html.programListItem());
            let title = li.querySelector('.title');
            title.textContent = programs[p].title;
            Program.appendMarks(title, programs[p].marks);
            let data = li.querySelector('.data');
            data.insertBefore(Notifier.createButton(programs[p]), data.firstElementChild);
            li.querySelector('.date').textContent = programs[p].justifiedDateString;
            li.querySelector('.channel').textContent = programs[p].channel.name;
            let thumbnail = li.querySelector('.thumbnail');
            /* 遅延読み込み */
            let observer = new IntersectionObserver(function(entries){
              if(!entries[0].isIntersecting) return;
              observer.disconnect();
              thumbnail.appendChild(new Thumbnail(programs[p].displayProgramId, programs[p].thumbImg, 'large').node);
            }, {root: searchPane, rootMargin: '50%'});
            observer.observe(thumbnail);
            li.addEventListener('click', function(e){
              core.timetable.showProgramData(programs[p]);
              core.timetable.scrollTo(programs[p].startAt);
            });
            ul.appendChild(li);
          }
        },
        updateResultCount: function(length){
          let searchPane = elements.searchPane, count = searchPane.querySelector('.count');
          switch(true){
            case(length === 0):
              count.textContent = `見つかりませんでした`;
              break;
            case(length <= MAXRESULTS):
              count.textContent = `${length}件見つかりました`;
              break;
            case(MAXRESULTS < length):
              count.textContent = `${MAXRESULTS}件以上見つかりました`;
              break;
          }
        },
      },
    },
    getCurrentChannelId: function(){
      let match = location.href.match(/\/now-on-air\/([a-z0-9-]+)/);
      if(!match) return false;
      else return match[1];
    },
    getProgramById: function(id){
      for(let c = 0, channel; channel = channels[c]; c++){
        for(let p = 0, program; program = channel.programs[p]; p++){
          if(program.id === id) return program;
        }
      }
    },
    matchProgram: function(program, value, marks = []){
      if(program.noContent) return false;
      let words = normalize(value.toLowerCase()).split(/\s+/);
      if(!words.every((word) => {
        return [
          program.channel.name,
          program.title,
          ...program.casts || [],
          ...program.crews || [],
        ].some((p) => (0 <= p.toLowerCase().indexOf(word)));
      })) return false;
      if(marks.length === 0) return true;
      if(marks.some((mark) => program.marks[mark] !== undefined)) return true;
      if(marks.includes('none') && Object.keys(program.marks).length === 0) return true;
    },
    searchPrograms: function(value, marks = []){
      let now = MinuteStamp.now(), results = [];
      for(let c = 0, channel; channel = channels[c]; c++){
        for(let p = 0, program; program = channel.programs[p]; p++){
          if(!configs.c_visibles[program.channel.id]) continue;
          if(program.endAt <= now) continue;
          if(program.noContent) continue;
          if(core.matchProgram(program, value, marks)) results.push(program);
        }
      }
      results.sort((a, b) => a.startAt - b.startAt);/*日付順*/
      return results;
    },
    getProgramNowOnAir: function(channelId){
      for(let now = MinuteStamp.now(), c = 0, channel; channel = channels[c]; c++){
        if(channel.id !== channelId) continue;
        for(let p = 0, program; program = channel.programs[p]; p++){
          if(program.endAt < now) continue;
          if(now < program.startAt) break;/*念のため*/
          return program;
        }
      }
    },
    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(){
        elements.configButton = elements.timetablePanel.querySelector('button.config');
        elements.configButton.addEventListener('click', core.panel.toggle.bind(null, 'configPanel', core.config.createPanel));
      },
      createPanel: function(){
        elements.configPanel = createElement(core.html.configPanel());
        let channelsUl = elements.configPanel.querySelector('.channels'), templateLi = channelsUl.querySelector('li.template');
        for(let i = 0; channels[i]; i++){
          let li = templateLi.cloneNode(true);
          li.classList.remove('template');
          let input = li.querySelector('input');
          input.value = channels[i].id;
          input.checked = configs.c_visibles[channels[i].id];
          li.querySelector('label > span').textContent = channels[i].name;
          channelsUl.insertBefore(li, templateLi);
        }
        channelsUl.removeChild(templateLi);
        elements.configPanel.querySelector('button.cancel').addEventListener('click', core.panel.close.bind(null, 'configPanel'));
        elements.configPanel.querySelector('button.save').addEventListener('click', function(){
          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.createChannelIndicator();
          core.channelPane.modify();
          core.abemaTimetable.initialize();
          core.timetable.rebuildTimetable();
          Notifier.sync();
        }, true);
        elements.configPanel.querySelector('input[name="n_change"]').addEventListener('click', function(e){
          let n_overlap = elements.configPanel.querySelector('input[name="n_overlap"]');
          n_overlap.disabled = !n_overlap.disabled;
          n_overlap.parentNode.parentNode.classList.toggle('disabled');
        }, 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: {
      marks: {/*live(生), newcomer(新), first(初), last(終), bingeWatching(一挙), recommendation(注目), none(なし)*/
        live:           () => `<span class="mark live"    ><svg height="14" width="14"><use xlink:href="/images/icons/text_live_rect.svg#svg-body"></use></svg><svg height="14" width="14"><use xlink:href="/images/icons/text_live_path.svg#svg-body"></use></svg></span>`,
        newcomer:       () => `<span class="mark newcomer"><svg height="14" width="14"><use xlink:href="/images/icons/text_newcomer_rect.svg#svg-body"></use></svg><svg height="14" width="14"><use xlink:href="/images/icons/text_newcomer_path.svg#svg-body"></use></svg></span>`,
        first:          () => `<svg  class="mark first"          height="14" width="14"><use xlink:href="/images/icons/text_new.svg#svg-body"></use></svg>`,
        last:           () => `<svg  class="mark last"           height="14" width="14"><use xlink:href="/images/icons/text_end.svg#svg-body"></use></svg>`,
        bingeWatching:  () => `<svg  class="mark bingeWatching"  height="14" width="23.333333333333336"><use xlink:href="/images/icons/text_binge_watching.svg#svg-body"></use></svg>`,
        recommendation: () => `<svg  class="mark recommendation" height="14" width="23.333333333333336"><use xlink:href="/images/icons/text_recommendation.svg#svg-body"></use></svg>`,
        none:           () => `<span class="mark none">なし</span>`,
      },
      channelIndicator: () => `
        <div id="${SCRIPTNAME}-channelIndicator">
          <ul class="channels">
            <li class="channel template"><img class="logo" width="340" height="128"></li>
          </ul>
        </div>
      `,
      myvideoButton: () => `
        <button class="myvideo" data-title-default="マイビデオに追加する" data-title-active="マイビデオを解除する">
          <svg width="16" height="16">
            <use class="plus"    xlink:href="/images/icons/plus.svg#svg-body"></use>
            <use class="checked" xlink:href="/images/icons/checkmark.svg#svg-body"></use>
          </svg>
        </button>
      `,
      repeatAllButton: () => `
        <button class="repeat_all" data-title-default="毎回通知を受け取る" data-title-active="毎回通知を解除する"><svg width="20" height="12"><use xlink:href="/images/icons/repeat.svg#svg-body"></use></svg></button>
      `,
      playButton: () => `
        <button class="play" title="現在放送中"><svg width="17" height="17"><use xlink:href="/images/icons/play.svg#svg-body"></use></svg></button>
      `,
      notifyButton: () => `
        <button class="notify" data-title-default="通知を受け取る" data-title-once="通知を解除する" data-title-repeat="毎回通知に登録済み" data-title-search="登録済みの検索通知を確認する">
          <svg width="17" height="17">
            <use class="plus"    xlink:href="/images/icons/alarm_clock_plus.svg#svg-body"></use>
            <use class="checked" xlink:href="/images/icons/alarm_clock_checkmark.svg#svg-body"></use>
            <use class="repeat"  xlink:href="/images/icons/repeat.svg#svg-body"></use>
            <use class="search"  xlink:href="/images/icons/search.svg#svg-body"></use>
          </svg>
        </button>
      `,
      searchAllButton: (value, marks) => `
        <button class="search_all" data-title-default="検索結果を常に通知する" data-title-active="検索結果の通知を解除する"><svg width="17" height="17"><use xlink:href="/images/icons/search.svg#svg-body"></use></svg>${value} (${marks}) を常に通知</button>
      `,
      clock: (hours, minutes) => `<span class="clock">${hours}<span class="blink">:</span>${minutes}</span>`,
      searchHeader: () => `
        <header>
          <p class="filters">
            <input class="template" type="checkbox" name="filter" checked><label class="template"></label>
          </p>
          <p class="summary"><span class="count"></span></p>
        </header>
      `,
      notificationsHeader: () => `
        <header>
          <nav class="tabs">
            <input type="radio" name="notifications" value="all"    id="notifications-all" checked><label for="notifications-all"   >すべて</label>
            <input type="radio" name="notifications" value="once"   id="notifications-once"       ><label for="notifications-once"  ><svg width="17" height="17"><use xlink:href="/images/icons/alarm_clock_checkmark.svg#svg-body"></use></svg>1回通知</label>
            <input type="radio" name="notifications" value="repeat" id="notifications-repeat"     ><label for="notifications-repeat"><svg width="20" height="12"><use xlink:href="/images/icons/repeat.svg#svg-body"></use></svg>毎回通知</label>
            <input type="radio" name="notifications" value="search" id="notifications-search"     ><label for="notifications-search"><svg width="17" height="17"><use xlink:href="/images/icons/search.svg#svg-body"></use></svg>検索通知</label>
          </nav>
          <p class="summary"><span class="count"></span></p>
        </header>
      `,
      programListItem: () => `
        <li class="program">
          <p class="thumbnail"></p>
          <h2><span class="title"></span></h2>
          <p class="data"><span class="date"></span><span class="channel"></span></p>
        </li>
      `,
      noProgramListItem: () => `
        <li class="noprogram">該当する番組はありません</li>
      `,
      dayListItem: () => `
        <li class="day">
          <h2></h2>
          <ul></ul>
        </li>
      `,
      repeatListItem: () => `
        <li class="repeat">
          <h2><span class="title"></span></h2>
          <ul></ul>
        </li>
      `,
      searchListItem: (value, marks) => `
        <li class="search">
          <h2><span class="key">${value} (${marks})</span></h2>
          <ul></ul>
        </li>
      `,
      timetablePanel: () => `
        <div class="panel" id="${SCRIPTNAME}-timetable-panel">
          <header>
            <h1>番組表</h1>
            <p class="buttons"><button class="config" title="${SCRIPTNAME} 設定"><svg width="20" height="20"><use xlink:href="/images/icons/config.svg#svg-body"></use></svg></button></p>
          </header>
          <div class="program nocontent">
            <h2><span class="title">番組タイトル</span></h2>
            <div class="thumbnails"></div>
            <div class="summary">
              <p class="channel">チャンネル</p>
              <p class="date"><span>放送日時</span></p>
              <p class="timeshift"><span>見逃し視聴</span></p>
              <p class="highlight"></p>
              <ul class="links">
                <li class="template"><a></a></li>
              </ul>
              <div class="group">
                <h3><span>今後${TERMLABEL}の放送予定</span></h3>
                <ul>
                  <li class="template"><header><span class="date"></span><span class="title"></span></header></li>
                </ul>
              </div>
              <div class="series">
                <h3>(再放送などを含む)</h3>
                <ul>
                  <li class="template"><header><span class="date"></span><span class="title"></span></header></li>
                </ul>
              </div>
            </div>
            <div class="content">
              <h3>番組概要</h3>
              <div></div>
            </div>
            <div class="casts">
              <h3>キャスト</h3>
              <ul></ul>
            </div>
            <div class="crews">
              <h3>スタッフ</h3>
              <ul></ul>
            </div>
            <p class="copyrights"></p>
          </div>
          <nav>
            <div class="timeshift">
              <p class="days"><input class="template" type="radio" name="day"><label class="template"><span class="fit"></span></label></p>
              <p class="times"><input class="template" type="radio" name="time"><label class="template"><span class="fit"></span></label></p>
            </div>
            <p class="search">
              <input type="search" name="q" placeholder="検索 (番組名、チャンネル名、キャスト、スタッフ)"><button class="search"><svg width="17" height="17"><use xlink:href="/images/icons/search.svg#svg-body"></use></svg></button>
              <button class="notifications"><svg width="17" height="17"><use class="checked" xlink:href="/images/icons/alarm_clock_checkmark.svg#svg-body"></use></svg><span class="count"></span></button>
            </p>
          </nav>
          <div class="programs">
            <div class="search">
              <ul>
              </ul>
            </div>
            <ul class="channels">
              <li class="time hidden animate">
                <header><button class="now disabled" title="現在時刻に戻る"><span class="arrows">‹‹</span></button></header>
                <ul class="stream times">
                  <li class="slot hour template"><span class="time"></span></li>
                  <li class="slot day template"><span class="date"></span></li>
                </ul>
              </li>
              <li class="channel template hidden animate">
                <header><h2 class="name fit"></h2></header>
                <ul class="stream programs">
                  <li class="slot program template hidden"><span class="time"></span><span class="title"></span></li>
                </ul>
              </li>
            </ul>
            <p class="scrollers">
              <button class="left  disabled" aria-label="表示を左に移動"><svg height="20" width="12"><use xlink:href="/images/icons/chevron_left.svg#svg-body"></use></svg></button>
              <button class="right disabled" aria-label="表示を右に移動"><svg height="20" width="12"><use xlink:href="/images/icons/chevron_right.svg#svg-body"></use></svg></button>
            </p>
          </div>
          <p class="buttons"><button class="ok primary">OK</button></p>
        </div>
      `,
      configPanel: () => `
        <div class="panel" id="${SCRIPTNAME}-config-panel">
          <h1>${SCRIPTNAME}設定</h1>
          <fieldset>
            <legend>番組表パネル</legend>
            <p><label>透明度(%):                          <input type="number"   name="transparency" value="${configs.transparency}" min="0"  max="100" step="5"></label></p>
            <p><label>番組表の高さ(%)(文字サイズ連動):    <input type="number"   name="height"       value="${configs.height}"       min="5"  max="95"  step="5"></label></p>
            <p><label>番組表の時間幅(時間):               <input type="number"   name="span"         value="${configs.span}"         min="1"  max="24"  step="1"></label></p>
            <p><label>アベマ公式の番組表を置き換える:     <input type="checkbox" name="replace"      value="${configs.replace}"      ${configs.replace  ? 'checked' : ''}></label></p>
          </fieldset>
          <fieldset>
            <legend>通知(abema.tvを開いているときのみ)</legend>
            <p><label>番組開始何秒前に通知するか(秒):     <input type="number"   name="n_before"     value="${configs.n_before}"     min="0"  max="600" step="1"></label></p>
            <p><label>自動でチャンネルも切り替える:       <input type="checkbox" name="n_change"     value="${configs.n_change}"     ${configs.n_change ? 'checked' : ''}></label></p>
            <p class="sub ${configs.n_change ? '' : 'disabled'}"><label>時間帯が重なっている時は通知のみ: <input type="checkbox" name="n_overlap" value="${configs.n_overlap}" ${configs.n_overlap ? 'checked' : ''} ${configs.n_change ? '' : 'disabled'}></label></p>
            <p><label>アベマ公式の通知と共有する:         <input type="checkbox" name="n_sync"       value="${configs.n_sync}"       ${configs.n_sync   ? 'checked' : ''}></label></p>
          </fieldset>
          <fieldset>
            <legend>表示するチャンネル</legend>
            <ul class="channels">
              <li class="template"><label><input type="checkbox" name="c_visibles" value="id"><span>チャンネル名</span></label></li>
            </ul>
          </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">
          /* 共通変数 */
          /* visible_channels:        ${configs.visible_channels        = Object.keys(configs.c_visibles).filter((id) => configs.c_visibles[id] === 1).length || 25} */
          /* channelPane_width:       ${configs.channelPane_width       = 25} */
          /* channelPane_rowheight:   ${configs.channelPane_rowheight   = 100 / 16.75} (端数が望ましい) */
          /* channelPane_thumbheight: ${configs.channelPane_thumbheight = configs.channelPane_rowheight *1.000} */
          /* channelPane_padding:     ${configs.channelPane_padding     = configs.channelPane_rowheight * .125} */
          /* channelPane_lineheight:  ${configs.channelPane_lineheight  = configs.channelPane_rowheight * .375} */
          /* channelPane_fontsize:    ${configs.channelPane_fontsize    = configs.channelPane_rowheight * .250} */
          /* channelPane_timetable:   ${configs.channelPane_timetable   = configs.channelPane_rowheight * .500} */
          /* opacity:                 ${configs.opacity                 = 1 - (configs.transparency / 100)} */
          /* scrollbarWidth:          ${configs.scrollbarWidth          = getScrollbarWidth()} */
          /* rowheight:               ${configs.rowheight               = configs.height / (configs.visible_channels + 1)} */
          /* rowfontsize:             ${configs.rowfontsize             = Math.min(2.0, configs.rowheight * .6)} */
          /* lineheight:              ${configs.lineheight              = 3.0} */
          /* fontsize:                ${configs.fontsize                = 1.8} */
          /* search_lineheight:       ${configs.search_lineheight       = Math.max(1.8, configs.rowfontsize * Math.min(configs.height/50, 1) * 1.5)} */
          /* search_fontsize:         ${configs.search_fontsize         = Math.max(1.2, configs.rowfontsize * Math.min(configs.height/50, 1))} */
          /* transparentGray:         ${configs.transparentGray         = `rgba(255,255,255,.5)`} */
          /* link_color:              ${configs.link_color              = `rgba( 81,195,  0,1)`} */
          /* listed_background:       ${configs.listed_background       = `rgba(112,112,112,${configs.opacity})`} */
          /* listed_backgroundHover:  ${configs.listed_backgroundHover  = `rgba(112,112,112,${configs.opacity / 2})`} */
          /* times_background:        ${configs.times_background        = `rgba( 64, 64, 64,${configs.opacity / 2})`} */
          /* nowOnAir_background:     ${configs.nowOnAir_background     = `rgba(112,112,112,${configs.opacity})`} */
          /* comming_background:      ${configs.comming_background      = `rgba( 64, 64, 64,${configs.opacity})`} */
          /* noContent_background:    ${configs.noContent_background    = `rgba(  0,  0,  0,${configs.opacity})`} */
          /* hover_background:        ${configs.hover_background        = `rgba( 61,146,  0,${configs.opacity})`} */
          /* current_background:      ${configs.current_background      = `rgba( 81,195,  0,${configs.opacity})`} */
          /* scroller_background:     ${configs.scroller_background     = `rgba(255,255,255,${configs.opacity})`} */
          /* search_background:       ${configs.search_background       = `rgba(  0,  0,  0,${configs.opacity / 2})`} */
          /* border_color:            ${configs.border_color            = `rgba(  0,  0,  0,${configs.opacity})`} */
          /* activeButton_color:      ${configs.activeButton_color      = `rgba( 81,195,  0,1)`} */
          /* progressbar_zIndex:      ${configs.progressbar_zIndex      = 110} */
          /* panel_zIndex:            ${configs.panel_zIndex            = 100} */
          /* channelPane_zIndex:      ${configs.channelPane_zIndex      =  11} */
          /* scrollers_zIndex:        ${configs.scrollers_zIndex        =  10} */
          /* button_zIndex:           ${configs.button_zIndex           =  10} */
          /* search_zIndex:           ${configs.search_zIndex           =  10} */
          /* channelIndicator_zIndex: ${configs.channelIndicator_zIndex =   2} */
          /* program_zIndex:          ${configs.program_zIndex          =   1} */
          /* nav_transition:          ${configs.nav_transition          = '500ms cubic-bezier(.17,.84,.44,1)'} (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;
          }
          /* マーク共通 */
          .mark,
          .mark > *{
            width: 1em !important;
          }
          .mark.bingeWatching,
          .mark.recommendation{
            width: ${5/3}em !important;
          }
          .mark.none{
            width: 2em !important;
            height: auto !important;
          }
          .mark{
            fill: white;
            margin: 0 .2em 0 0;
          }
          span.mark{
            vertical-align: middle;
            position: relative;
            display: inline-block;
          }
          .mark > svg{
            vertical-align: middle;
            position: absolute;
            left: 0;
            top: 0;
          }
          .mark.newcomer > svg:nth-child(1),
          .mark.live > svg:nth-child(1){
            fill: #f0163a;
          }
          .mark.last{
            margin-right: 0;
            margin-left: .2em;
          }
          /* 通知その他ボタン共通 */
          button.myvideo,
          button.repeat_all,
          button.play,
          button.notify{
            padding: 5px;/*クリッカブル領域を広げる*/
            margin: -5px;
            box-sizing: content-box;
            position: relative;/*個別調整用*/
            z-index: ${configs.button_zIndex};
            pointer-events: auto;
            transition: transform 250ms ease-in;
          }
          button.myvideo > *,
          button.repeat_all > *,
          button.play > *,
          button.notify > *{
            fill: white;
            width: auto;
            border-radius: 50vmax;
            overflow: visible;/*目覚まし時計アイコンのベル部分*/
            transform: scaleX(1);
            pointer-events: none;
          }
          button.repeat_all > *{
            position: relative;
            bottom: .1em !important;/*微調整*/
          }
          button.notify.once > *{
            fill: ${configs.activeButton_color};
            background: white;
            padding: 0;
            border: .05em solid white;
            box-sizing: border-box;
            overflow: hidden;/*目覚まし時計アイコンのベル部分*/
          }
          button.notify.repeat > *{
            fill: white;
            background: ${configs.activeButton_color};
            padding: .05em 0 0 .05em;
            border: .1em solid white;
            box-sizing: border-box;
            overflow: visible;
          }
          button.notify.search > *{
            fill: white;
            background: ${configs.activeButton_color};
            padding: .1em;
            border: .1em solid white;
            box-sizing: border-box;
            overflow: visible;
          }
          button.myvideo:hover,
          button.repeat_all:hover,
          button.play:hover,
          button.notify:hover{
            filter: brightness(.5);
          }
          button.myvideo.reversing,
          button.repeat_all.reversing,
          button.play.reversing,
          button.notify.reversing{
            transform: scaleX(0);
            transition: transform 250ms ease-out;
          }
          button.repeat_all:not(.active) > *{
            transform: scaleX(-1);
          }
          button.myvideo.active > *,
          button.repeat_all.active > *,
          button.play.current > *{
            fill: ${configs.activeButton_color};
            filter: brightness(1.25);/*視認性を高める*/
          }
          button.myvideo use,
          button.notify use{
            display: none;
          }
          button.myvideo:not(.active) use.plus,
          button.myvideo.active use.checked,
          button.notify:not(.once):not(.repeat):not(.search) use.plus,
          button.notify.once:not(.repeat):not(.search) use.checked,
          button.notify.repeat:not(.search) use.repeat,
          button.notify.search use.search{
            display: inline;
          }
          /* アベマ公式 裏番組一覧の表示非表示 */
          html.channelPaneHidden [data-selector="channelPane"]{
            opacity: 0;/*translateXでは読み込みが発生しない*/
            z-index: -1;
            transition: opacity 500ms ease 500ms;/*チラ見えさせない努力*/
          }
          html.channelPaneHidden [data-selector="screen"] > div[style]{
            width: 100% !important;
            height: 100% !important;
          }
          /* アベマ公式 裏番組一覧の改変 */
          /* (アベマ公式に上書きされがちなので独自セレクタは使いにくい) */
          [data-selector="channelPane"]{
            width: 25vw;
            min-width: 40vh;
          }
          [data-selector="channelPane"] > div{
            min-height: 100%;
            display: flex;
            flex-direction: column;
          }
          [data-selector="channelPane"] > div > a{
            height: ${configs.channelPane_rowheight}vh;
            border-left: none !important;/*現在チャンネルに付く公式のボーダーをなくす*/
            padding: 0;
            width: 100%;
            overflow: hidden;
          }
          [data-selector="channelPane"] > div > a[data-onair="false"]:not([data-comming="true"]){
            cursor: auto;
          }
          [data-selector="channelPane"] > div > a ~ a{
            border-top: 1px solid ${configs.border_color} !important;/*公式のボーダーを置き換える*/
          }
          [data-selector="channelPane"] > div > a[data-hidden="true"]{
            display: none;
          }
          [data-selector="channelPane"] > div > a > div{
            position: relative;
          }
          [data-selector="channelPane"] > div > a > div > div:nth-child(1)/*サムネイル枠*/{
            border-left: 1px solid ${configs.border_color};
            border-right: 1px solid ${configs.border_color};
            background: ${configs.noContent_background};
            box-sizing: content-box;
          }
          [data-selector="channelPane"] > div > a > div > div:nth-child(1)/*サムネイル枠*/ > div > div{
            height: ${configs.channelPane_thumbheight}vh !important;
            width: ${configs.channelPane_thumbheight * (16/9)}vh !important;
            background: transparent;/*公式を上書き*/
            opacity: 1;/*公式を上書き*/
          }
          [data-selector="channelPane"] > div > a > div > div:nth-child(1)/*サムネイル枠*/ > div > div > img/*サムネイル*/{
            min-height: 100%;
            min-width: 100%;
            opacity: .5;
          }
          [data-selector="channelPane"] > div > a:hover > div > div:nth-child(1)/*サムネイル枠*/ > div > div > img/*サムネイル*/{
            opacity: 1;
          }
          [data-selector="channelPane"] > div > a > div > div:nth-child(1)/*サムネイル枠*/ > div > div + img/*チャンネルロゴ*/{
            width: ${configs.channelPane_thumbheight * (16/9) * .8}vh;
            height: auto;
            display: block;
            transition: none !important;/*公式を上書き*/
          }
          [data-selector="channelPane"] > div > a > div > div ~ div/*番組共通*/,
          [data-selector="channelPane"] > div > a:last-child/*番組表リンク*/{
            border-right: 1px solid ${configs.border_color};
            background: ${configs.comming_background};
            padding: ${configs.channelPane_padding}vh 0 !important;/*公式の左パディングを打ち消す*/
            overflow: hidden;
            min-width: 0;/*中身が長くても伸ばさずすぐあふれさせる*/
            position: absolute;
          }
          [data-selector="channelPane"] > div > a > div > div ~ div/*番組共通*/{
            clip-path: inset(0 -100vw 0 0);/*overflowさせるのは右方向だけ*/
            background-clip: padding-box !important;/*border-leftの色を保証*/
            height: 100%;
          }
          [data-selector="channelPane"] > div > a > div > div ~ div/*番組共通*/.transition/*ハイライトを付与する際のトランジション*/{
            transition: background 500ms ease;
          }
          [data-selector="channelPane"] > div > a > div > div:nth-child(2)/*放送中の番組*/{
            background: ${configs.nowOnAir_background};
          }
          [data-selector="channelPane"] > div > a > div > div ~ div > div/*タイトルと放送時間*/{
            margin-left: .5em;
          }
          [data-selector="channelPane"] > div > a > div > div ~ div > div/*タイトルと放送時間*/,
          [data-selector="channelPane"] > div > a > div > div ~ div > div/*タイトルと放送時間*/ *{
            font-weight: normal !important;;/*公式の現在チャンネルの太字を打ち消す*/
            font-size: ${configs.channelPane_fontsize}vh;
            line-height: ${configs.channelPane_lineheight}vh;
            margin-bottom: 0;/*公式の下マージンを打ち消す*/
            white-space: nowrap;
          }
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(1)/*タイトル*/ > span,
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(2)/*放送時間*/{
            display: flex;
            align-items: center;
          }
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(1)/*タイトル*/ > span > span[style],
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(1)/*タイトル*/ > span > span[style] > *,
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(1)/*タイトル*/ > span > svg[width],
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(1)/*タイトル*/ > span > .mark,
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(1)/*タイトル*/ > span > .mark *{
            font-size: ${configs.channelPane_fontsize}vh;
            height: ${configs.channelPane_fontsize}vh !important;
            width: 1em !important;
            min-width: 1em !important;/*なぜかこれを指定しないとレイアウトが崩れる要素が発生する*/
          }
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(1)/*タイトル*/ > span > svg[width^="23"]{
            width: ${5/3}em !important;/*ちょっとトリッキーだが…*/
            min-width: ${5/3}em !important;
          }
          [data-selector="channelPane"] > div > a > div > div ~ div:not(:hover) > div:nth-child(2)/*放送時間*/ *{
            color: ${configs.transparentGray};
          }
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(2)/*放送時間*/ > button/*通知ボタン*/,
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(2)/*放送時間*/ > button/*通知ボタン*/ *{
            height: ${configs.channelPane_lineheight}vh;
          }
          [data-selector="channelPane"] > div > a > div > div ~ div > div:nth-child(2)/*放送時間*/ > button/*通知ボタン*/{
            padding: 0;
            margin: 0 ${configs.channelPane_padding}vh 0 calc(-${configs.channelPane_padding}vh - ${configs.channelPane_lineheight}vh - 1px/*端数対応*/);
            transition: margin 500ms ease, transform 250ms ease-in;;
            pointer-events: auto !important;
          }
          [data-selector="channelPane"] > div > a > div > div ~ div:hover > div:nth-child(2)/*放送時間*/ > button/*通知ボタン*/{
            margin: 0 .2em 0 0;
          }
          [data-selector="channelPane"] > div > a[data-current="true"] > div > div:nth-child(1)/*サムネイル枠*/,
          [data-selector="channelPane"] > div > a[data-current="true"] > div > div:nth-child(1)/*サムネイル枠*/ > div > div,
          [data-selector="channelPane"] > div > a[data-current="true"] > div > div:nth-child(1) + div/*放送中の番組*/,
          [data-selector="channelPane"] > div > a > div > div:nth-child(1) ~ div.active/*通知番組*/{
            background: ${configs.current_background} !important;
          }
          [data-selector="channelPane"] > div > a[data-current="true"]:not(:hover) > div > div:nth-child(1)/*サムネイル枠*/ > div > div{
            opacity: .25;
          }
          [data-selector="channelPane"] > div > a[data-current="true"] > div > div:nth-child(1) + div > */*放送中の番組*/,
          [data-selector="channelPane"] > div > a > div > div:nth-child(1) ~ div.active > */*通知番組*/{
            filter: brightness(100%) !important;/*nocontentの指定を上書き*/
          }
          [data-selector="channelPane"] > div > a[data-current="true"] > div > div:nth-child(1) + div */*放送中の番組の中身*/,
          [data-selector="channelPane"] > div > a > div > div:nth-child(1) ~ div.active */*通知番組の中身*/{
            color: white !important;
          }
          [data-selector="channelPane"] > div > a[data-onair="false"] > div > div:nth-child(1)/*サムネイル枠*/ > div > div > img/*サムネイル*/{
            visibility: hidden;
          }
          [data-selector="channelPane"] > div > a[data-onair="false"] > div > div:nth-child(1)/*サムネイル枠*/ > div > div + img/*チャンネルロゴ*/{
            visibility: visible;/*本来hover時にはロゴが消えて番組サムネイルが残る仕様だが、番組なしの時はロゴしか頼れる情報が無いので消さない*/
          }
          [data-selector="channelPane"] > div > a[data-onair="false"]:hover > div > div:nth-child(1)/*サムネイル枠*/ > div > div + img/*チャンネルロゴ*/{
            opacity: .25;/*とはいえhoverに対する反応はさせたい*/
          }
          [data-selector="channelPane"] > div > a:hover{
            opacity: 1;/*公式の不透明度をなくす*/
          }
          [data-selector="channelPane"] > div > a:hover > div > div:nth-child(1)/*サムネイル枠*/,
          [data-selector="channelPane"] > div > a:hover > div > div:nth-child(1)/*サムネイル枠*/ ~ div/*番組*/{
            background: ${configs.hover_background};
          }
          [data-selector="channelPane"] > div > a > div > div:nth-child(1):hover/*サムネイル枠*/ + div/*放送中の番組*/,
          [data-selector="channelPane"] > div > a > div > div:nth-child(1)/*サムネイル枠*/ ~ div:hover/*番組*/{
            overflow: visible;
            z-index: ${configs.program_zIndex};
          }
          [data-selector="channelPane"] > div > a > div > div:nth-child(1):hover/*サムネイル枠*/ + div */*放送中の番組の中身*/,
          [data-selector="channelPane"] > div > a > div > div:nth-child(1)/*サムネイル枠*/ ~ div:hover */*番組の中身*/{
            color: white !important;
            pointer-events: none;/*overflowしていても後続番組にhoverをゆずる*/
          }
          [data-selector="channelPane"] > div > a:hover > div > div:nth-child(1):not(:hover)/*サムネイル枠*/ + div:not(:hover)/*放送中の番組*/,
          [data-selector="channelPane"] > div > a:hover > div > div:nth-child(1)/*サムネイル枠*/ + div ~ div:not(:hover)/*後続番組*/{
            filter: brightness(.5);
          }
          [data-selector="channelPane"] > div > a > div > div:nth-child(1):hover/*サムネイル枠*/ + div ~ div > */*後続番組の中身*/,
          [data-selector="channelPane"] > div > a > div > div:nth-child(1)/*サムネイル枠*/ ~ div:hover/*番組*/ ~ div > */*後続番組の中身*/{
            opacity: .25;
          }
          [data-selector="channelPane"] > div > a:not(:hover) > div > div:nth-child(1)/*サムネイル枠*/ ~ div.nocontent{
            background: ${configs.noContent_background};
          }
          [data-selector="channelPane"] > div > a:not(:hover) > div > div:nth-child(1)/*サムネイル枠*/ ~ div.nocontent > *{
            filter: brightness(.25);
          }
          [data-selector="channelPane"] > div > a:last-child/*番組表リンク*/{
            font-size: ${configs.channelPane_fontsize}vh;
            line-height: ${configs.channelPane_timetable}vh;
            padding: 0 !important;
            position: relative;
            flex: 1;
          }
          [data-selector="channelPane"] > div > a:last-child:hover/*番組表リンク*/{
            background: rgba(128,128,128,.5);
          }
          [data-selector="channelPane"] > div > a:last-child/*番組表リンク*/ > svg{
            height: ${configs.channelPane_fontsize}vh;
          }
          /* アベマ公式 番組表の置き換え */
          html.abemaTimetable #splash > div{
            display: block;/*公式のnoneを上書き*/
          }
          html.abemaTimetable main *{/*負荷の低減を試みる*/
            transition: none !important;
            animation: none !important;
            pointer-events: none !important;
          }
          html.abemaTimetable [data-selector="progressbar"]{
            display: none;
          }
          /* アベマ公式 プログレスバー */
          [data-selector="progressbar"]{
            z-index: ${configs.progressbar_zIndex};
          }
          /* チャンネルロゴ表示 */
          [data-selector="screen"] img[src*="/logo/"]{
            visibility: hidden;/*公式のロゴは使わない*/
          }
          #${SCRIPTNAME}-channelIndicator{
            position: absolute;
            top: 0;
            left: 50%;
            transform: translate(-50%, 0);
            height: 100%;
            overflow: hidden;
            opacity: 0;
            filter: drop-shadow(0 0 2.5px rgba(0,0,0,.75));
            z-index: ${configs.channelIndicator_zIndex};
            pointer-events: none;
            transition: opacity 1000ms ease-out;/*消えるとき*/
          }
          #${SCRIPTNAME}-channelIndicator.active{
            opacity: 1;
            transition: opacity 500ms ease-out;/*現れるとき*/
          }
          #${SCRIPTNAME}-channelIndicator ul.channels{
            position: relative;
            top: 50%;
            transition: transform 500ms ease-out;
          }
          #${SCRIPTNAME}-channelIndicator li.channel{
            transform: translateY(-12.2vh);
            opacity: .100;/*ギリギリのコントラスト最適解*/
          }
          #${SCRIPTNAME}-channelIndicator li.channel.onair{
            opacity: .250;/*ギリギリのコントラスト最適解*/
          }
          #${SCRIPTNAME}-channelIndicator li.channel.current{
            opacity: 1;
          }
          #${SCRIPTNAME}-channelIndicator img.logo{
            width: 34.0vh;
            height: 12.8vh;
            margin: 6.1vh 0;
          }
          #${SCRIPTNAME}-channelIndicator .template{
            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;
            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%);
          }
          #${SCRIPTNAME}-panels h1,
          #${SCRIPTNAME}-panels h2,
          #${SCRIPTNAME}-panels h3,
          #${SCRIPTNAME}-panels h4,
          #${SCRIPTNAME}-panels legend,
          #${SCRIPTNAME}-panels li,
          #${SCRIPTNAME}-panels dt,
          #${SCRIPTNAME}-panels dd,
          #${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}-timetable-panel{
            background: rgba(0,0,0,${configs.opacity}) !important;
            width: 100% !important;
            height: 100% !important;
            padding: 0 !important;
            overflow: hidden !important;
            /*display: grid;*//*Chrome Gridバグ回避*/
            grid-template-columns: auto;
            grid-template-rows: 1fr auto ${configs.height}vh;
          }
          #${SCRIPTNAME}-timetable-panel > header > h1{
            display: none;
          }
          #${SCRIPTNAME}-timetable-panel > header > p.buttons{
            padding: 5px 10px;
            position: fixed;
            top: 0;
            right: 0;
            z-index: ${configs.panel_zIndex};
          }
          #${SCRIPTNAME}-timetable-panel button.config{
            fill: white;
            filter: drop-shadow(0 0 2.5px rgba(0,0,0,.75));
            padding: 5px;
            margin: -5px;
            height: 30px;
          }
          #${SCRIPTNAME}-timetable-panel button.config:hover{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-timetable-panel > .program{
            grid-column: 1;
            grid-row: 1;
            height: calc(100vh - ${configs.rowheight * 1.4}vh - ${configs.height}vh);/*Chrome Gridバグ回避*/
          }
          #${SCRIPTNAME}-timetable-panel > nav{
            grid-column: 1;
            grid-row: 2;
            position: absolute;/*Chrome Gridバグ回避*/
            bottom: ${configs.height}vh;/*Chrome Gridバグ回避*/
          }
          #${SCRIPTNAME}-timetable-panel > .programs{
            grid-column: 1;
            grid-row: 3;
            overflow: hidden;
            position: absolute !important;/*Chrome Gridバグ回避*/
            bottom: -${configs.scrollbarWidth}px;/*Chrome Gridバグ回避*/
            width: 100vw;/*Chrome Gridバグ回避*/
          }
          #${SCRIPTNAME}-timetable-panel > p.buttons:last-of-type{
            position: absolute;
            bottom: 0;
            right: 0;
            z-index: ${configs.panel_zIndex};
          }
          #${SCRIPTNAME}-timetable-panel h1,
          #${SCRIPTNAME}-timetable-panel h2,
          #${SCRIPTNAME}-timetable-panel h3,
          #${SCRIPTNAME}-timetable-panel h4,
          #${SCRIPTNAME}-timetable-panel legend,
          #${SCRIPTNAME}-timetable-panel li,
          #${SCRIPTNAME}-timetable-panel dt,
          #${SCRIPTNAME}-timetable-panel dd,
          #${SCRIPTNAME}-timetable-panel code,
          #${SCRIPTNAME}-timetable-panel p{
            padding: .1em .5em;
          }
          /* 番組情報 */
          #${SCRIPTNAME}-timetable-panel > .program{
            word-wrap: break-word;
            margin-right: -${configs.scrollbarWidth}px;/*スクロールバーを隠す*/
            -webkit-mask-image: linear-gradient(black 90%, rgba(0,0,0,.5));/*まだ-webkit取れない*/
            mask-image: linear-gradient(black 90%, rgba(0,0,0,.5));
            position: relative;
            overflow-x: hidden;
            overflow-y: scroll;
            display: grid;
            grid-template-columns: 2fr 2fr 2fr 1fr 1fr;
            grid-template-rows: ${configs.lineheight * 1.5}vh auto ${configs.lineheight * .825}vh;
            transition: opacity 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .thumbnails,
          #${SCRIPTNAME}-timetable-panel > .program > .summary,
          #${SCRIPTNAME}-timetable-panel > .program > .content{
            max-width: 25vw;/*max指定しておけばword-break-allしなくてすむ*/
          }
          #${SCRIPTNAME}-timetable-panel > .program > .casts,
          #${SCRIPTNAME}-timetable-panel > .program > .crews{
            max-width: 12.5vw;
          }
          #${SCRIPTNAME}-timetable-panel > .program *{
            font-size: ${configs.fontsize}vh;
            line-height: ${configs.lineheight}vh;
          }
          #${SCRIPTNAME}-timetable-panel > .program a{
            color: ${configs.link_color};
            text-decoration: none;
            filter: brightness(1.25);/*視認性を高める*/
          }
          #${SCRIPTNAME}-timetable-panel > .program a:hover{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-timetable-panel > .program.nocontent{
            opacity: .25;
          }
          /* 番組情報 タイトル */
          #${SCRIPTNAME}-timetable-panel > .program > h2{
            white-space: nowrap;
            grid-column: 2 / 6;
            grid-row: 1;
          }
          #${SCRIPTNAME}-timetable-panel > .program > h2 *{
            font-size: ${configs.fontsize * 1.5}vh;
            line-height: ${configs.lineheight * 1.5}vh;
            vertical-align: middle;
          }
          #${SCRIPTNAME}-timetable-panel > .program > h2 > .mark,
          #${SCRIPTNAME}-timetable-panel > .program > h2 > .mark *{
            height: ${configs.fontsize * 1.5}vh !important;
          }
          /* 番組情報 サムネイル */
          #${SCRIPTNAME}-timetable-panel > .program > .thumbnails{
            grid-column: 1;
            grid-row: 1 / 4;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .thumbnails img:first-child{
            width: calc(100% - 2vh);
          }
          #${SCRIPTNAME}-timetable-panel > .program > .thumbnails img{
            width: calc(50% - 1.5vh);
            margin: 1vh 0 0 1vh;
            transition: opacity 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .thumbnails img.loading{
            opacity: 0;
          }
          /* 番組情報 サマリ */
          #${SCRIPTNAME}-timetable-panel > .program > .summary{
            grid-column: 2;
            grid-row: 2 / 4;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary .inactive{
            display: none;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary h3 > *{
            vertical-align: middle;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary button,
          #${SCRIPTNAME}-timetable-panel > .program > .summary button > *{
            width: calc(${configs.fontsize}vh + .2em);/*限界まで膨れさせる*/
            height: calc(${configs.fontsize}vh + .2em);
            bottom: -.2em;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary button.repeat_all,
          #${SCRIPTNAME}-timetable-panel > .program > .summary button.repeat_all > *{
            width: calc(${configs.fontsize}vh + .4em);/*限界まで膨れさせる*/
            height: calc(${configs.fontsize}vh + .4em);
            bottom: 0em;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary button + *{
            margin-left: .25em;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary button + .date + *{
            margin-left: 1em;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary li header{
            background: ${configs.listed_background};
            border-radius: calc(${configs.lineheight / 2}vh);
            padding: .1em calc(${configs.fontsize / 2}vh - .1em) .1em 0;
            display: inline;
            cursor: pointer;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary li header .mark,
          #${SCRIPTNAME}-timetable-panel > .program > .summary li header .mark *{
            height: ${configs.fontsize}vh;
            vertical-align: text-bottom;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary li header:hover,
          #${SCRIPTNAME}-timetable-panel > .program > .summary li.current header{
            color: ${configs.transparentGray};
            background: ${configs.listed_backgroundHover};
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary li header:hover .mark,
          #${SCRIPTNAME}-timetable-panel > .program > .summary li.current header .mark{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary li.current header{
            cursor: auto;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .summary li header *{
            vertical-align: baseline;/*headerが既にinline要素なので*/
          }
          /* 番組情報 番組概要 */
          #${SCRIPTNAME}-timetable-panel > .program > .content{
            grid-column: 3;
            grid-row: 2 / 4;
          }
          /* 番組情報 キャスト・スタッフ */
          #${SCRIPTNAME}-timetable-panel > .program > .casts{
            grid-column: 4;
            grid-row: 2 / 3;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .crews{
            grid-column: 5;
            grid-row: 2 / 3;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .casts .name,
          #${SCRIPTNAME}-timetable-panel > .program > .crews .name{
            color: black;
            background: white;
            padding: 1px calc(${configs.fontsize / 2}vh - .2em);
            border-radius: calc(${configs.lineheight / 2}vh);
            margin: 0 1px;
            cursor: pointer;
          }
          #${SCRIPTNAME}-timetable-panel > .program > .casts .name:hover,
          #${SCRIPTNAME}-timetable-panel > .program > .crews .name:hover{
            filter: brightness(.5);
          }
          /* 番組情報 コピーライト */
          #${SCRIPTNAME}-timetable-panel > .program > .copyrights{
            font-size: ${configs.fontsize * .825}vh;
            line-height: ${configs.lineheight * .825}vh;
            text-align: right;
            padding: 0 1vh;
            grid-column: 3 / 6;
            grid-row: 3;
          }
          /* 番組表ナビゲーション */
          #${SCRIPTNAME}-timetable-panel > nav{
            display: flex;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift{
            flex: 1;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search{
            width: ${100*(1/3)}vw;
          }
          /* 番組表ナビゲーション 日付・時間 */
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift{
            display: flex;
            align-items: center;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift .days{
            width: calc((${100*(2/3)}vw - ${NAMEWIDTH}vw)/2 + ${NAMEWIDTH}vw - 2vh);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift .times{
            width: calc((${100*(2/3)}vw - ${NAMEWIDTH}vw)/2);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times{
            font-size: ${configs.rowfontsize}vh;
            line-height: ${configs.rowheight}vh;
            height: ${configs.rowheight}vh;
            padding: 0;
            margin: .2em 0 .2em 1vh;
            border-radius: .5em;
            overflow: hidden;
            display: flex;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days input,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times input{
            display: none;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times label{
            color: white;
            background: rgba(112,112,112,.25);
            text-align: center;
            white-space: nowrap;
            width: 100%;
            margin-left: 1px;
            padding: 0 1px;
            min-width: 1em;
            overflow: hidden;
            flex: 1;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label:first-of-type{
            min-width: calc(${NAMEWIDTH}vw - 1vh - 1px);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label:first-of-type,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times label:first-of-type{
            margin-left: 0;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days input:not(:checked) + label,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times input:not(:checked) + label{
            cursor: pointer;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days input:checked + label,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label:hover,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label:focus,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times input:checked + label,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times label:hover,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times label:focus{
            background: rgba(112,112,112,.75);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label.sat{
            background: rgba(112,160,192,.25);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label.sun{
            background: rgba(192,112,112,.25);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days input:checked + label.sat,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label.sat:hover,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label.sat:focus{
            background: rgba(112,160,192,.75);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days input:checked + label.sun,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label.sun:hover,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label.sun:focus{
            background: rgba(192,112,112,.75);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times input:disabled + label{
            color: black;
            opacity: .25;
            cursor: auto;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .days label > .fit,
          #${SCRIPTNAME}-timetable-panel > nav > .timeshift > .times label > .fit{
            display: block;/*以下、チャンネル名幅調整用*/
            transform-origin: left;
            transition: transform 500ms ease;
          }
          /* 番組表ナビゲーション 検索 */
          #${SCRIPTNAME}-timetable-panel > nav > .search{
            font-size: ${configs.rowfontsize}vh;
            line-height: ${configs.rowheight}vh;
            padding: 0;
            display: flex;
            align-items: center;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search > input[type="search"]{
            border: 1px solid transparent;/*ブラウザデフォルトスタイルの解消も兼ねる*/
            border-radius: .2em 0 0 .2em;
            height: ${configs.rowheight}vh;
            box-sizing: content-box;
            width: 100%;
            min-width: 0;/*幅が足りなければ素直に縮ませる*/
            padding: 0 0 0 .5em;
            margin: .2em 0 .2em 1vh;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search > button.search{
            background: rgb(192,192,192);
            border: 1px solid transparent;
            border-radius: 0 .2em .2em 0;
            flex-shrink: 0;
            height: ${configs.rowheight}vh;
            box-sizing: content-box;
            padding: 0 .5em;
            margin: .2em 1vh .2em 0;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search > button.search:hover{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search > button.search > svg{
            width: ${configs.rowfontsize}vh;
            height: ${configs.rowfontsize}vh;
            fill: white;
            vertical-align: middle;
          }
          /* 番組表ナビゲーション 通知 */
          #${SCRIPTNAME}-timetable-panel > nav > .search > button.notifications{
            font-size: ${configs.rowfontsize}vh;
            white-space: nowrap;
            margin-right: ${(configs.height < 90) ? '1vh' : 'calc(1vh + 30px)'};/*設定ボタンにかぶらないように*/
            display: flex;
            align-items: center;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search > button.notifications:hover{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search > button.notifications > *{
            flex-shrink: 0;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search > button.notifications svg{
            fill: ${configs.activeButton_color};
            filter: brightness(1.25);/*視認性を高める*/
            width: ${configs.rowheight}vh;
            height: ${configs.rowheight}vh;
            margin-right: .2em;
          }
          #${SCRIPTNAME}-timetable-panel > nav > .search > button.notifications[data-count="0"] svg{
            fill: ${configs.transparentGray};
            filter: brightness(1);
          }
          /* 番組表 */
          #${SCRIPTNAME}-timetable-panel > .programs{
            position: relative;
          }
          #${SCRIPTNAME}-timetable-panel .channels{
            position: relative;
            left: ${NAMEWIDTH}vw;
            width: ${100 - NAMEWIDTH}vw;
            padding: 0;
            overflow-y: hidden;
            transition: width 500ms ease;/*searchPaneの開閉用*/
          }
          #${SCRIPTNAME}-timetable-panel .search.active ~ .channels{
            width: calc(${100*(2/3) - NAMEWIDTH}vw - 1px);
          }
          #${SCRIPTNAME}-timetable-panel .channels *{
            color: white;
            font-size: ${configs.rowfontsize}vh;
            line-height: calc(${configs.rowheight}vh - 1px);/*borderぶん*/
            white-space: nowrap;
          }
          #${SCRIPTNAME}-timetable-panel .channels > li{
            height: ${configs.rowheight}vh;
            padding: 0;
            position: relative;
          }
          #${SCRIPTNAME}-timetable-panel .channels > li.channel{
            overflow: hidden;
          }
          #${SCRIPTNAME}-timetable-panel .channels > li > header{
            padding: 0;
            border-top: 1px solid ${configs.border_color};
            border-right: 1px solid ${configs.border_color};
            text-align: center;
            position: fixed;
            left: 0;
            width: ${NAMEWIDTH}vw;
            overflow: hidden;/*これを指定しないとFirefoxでposition:fixedが狂うバグあり*/
            cursor: pointer;
            transition: transform 500ms ease;/*初回左右からの登場用*/
          }
          #${SCRIPTNAME}-timetable-panel .channels > li.channel > header{
            background: ${configs.nowOnAir_background};
            background-clip: padding-box !important;
          }
          #${SCRIPTNAME}-timetable-panel .channels > li.channel.notonair > header{
            pointer-events: none;/*放送してないチャンネルはクリッカブルにしない*/
          }
          #${SCRIPTNAME}-timetable-panel .channels > li > header > .name{
            width: 100%;
            padding: 0;
            text-align: center;
            transform-origin: left;/*以下、チャンネル名幅調整用*/
            transition: transform 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel .channels > li > .stream{
            width: 100%;
            border-top: 1px solid ${configs.border_color};
            position: relative;
            transition: transform 500ms ease;/*初回左右からの登場用(疑似スクロール処理時はjsで上書きする)*/
          }
          #${SCRIPTNAME}-timetable-panel .channels > li.animate,
          #${SCRIPTNAME}-timetable-panel .channels > li.animate *{
            pointer-events: none !important;/*position:fixedバグの回避*/
          }
          #${SCRIPTNAME}-timetable-panel .channels > li.animate > header,
          #${SCRIPTNAME}-timetable-panel .channels > li.animate > .stream{
            will-change: transform;/*position:fixedが狂うバグが発生するのでGrid時はコメントアウト*/
          }
          #${SCRIPTNAME}-timetable-panel .channels > li.hidden > header{
            transform: translateX(-${NAMEWIDTH}vw);/*これもposition:fixedが狂うバグに関与している*/
          }
          #${SCRIPTNAME}-timetable-panel .channels > li.hidden > .stream{
            transform: translateX(${100 - NAMEWIDTH}vw);
          }
          /* 番組表 時刻・個別番組共通 */
          #${SCRIPTNAME}-timetable-panel .channels .slot{
            padding: 0;
            border-right: 1px solid ${configs.border_color};
            position: absolute;
            overflow: hidden;
            display: flex;
            align-items: center;/*.markを中央揃え*/
            cursor: pointer;
            transition: opacity 250ms ease;/*出現時(1分シフト時はjsで上書きする)*/
          }
          #${SCRIPTNAME}-timetable-panel .channels .slot.transition/*ハイライトを付与する際専用のトランジション*/{
            transition: background 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel .channels .slot > *{
            flex-shrink: 0;
            pointer-events: none;/*e.targetをli.programに統一 & overflowしていても後続番組にhoverをゆずる*/
          }
          #${SCRIPTNAME}-timetable-panel .channels .hour > *:nth-child(1)/*時刻*/,
          #${SCRIPTNAME}-timetable-panel .channels .program > *:nth-child(2)/*(通知ボタンの次の)時刻*/,
          #${SCRIPTNAME}-timetable-panel .channels .program > *:nth-child(3)/*タイトルまたはmark*/{
            margin-left: .25em;
          }
          #${SCRIPTNAME}-timetable-panel .channels > .time .hour:not(.nowonair) .time,
          #${SCRIPTNAME}-timetable-panel .channels > .channel:not(:hover) .program:not(.active):not(.shown) .time{
            color: ${configs.transparentGray};/*区切りとしての役割も果たす*/
          }
          /* 番組表 時刻列 */
          #${SCRIPTNAME}-timetable-panel .channels > .time > header > button.now{
            background: ${configs.nowOnAir_background};
            text-align: right !important;
            width: 100%;
            padding-right: .25em;
            overflow: hidden;
            display: block;
            transition: opacity 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel .channels > .time > header > button.now:hover,
          #${SCRIPTNAME}-timetable-panel .channels .hour:hover{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-timetable-panel .channels > .time > header > button.now .arrows{
            display: inline-block;
            transition: transform 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel .channels > .time > header > button.now.disabled{
            opacity: .5;
          }
          #${SCRIPTNAME}-timetable-panel .channels > .time > header > button.now.disabled .arrows{
            transform: translateX(1.25em);
            pointer-events: none;
          }
          #${SCRIPTNAME}-timetable-panel .channels .times .hour:nth-child(2)/*clock*/{
            padding-left: 1px;
          }
          #${SCRIPTNAME}-timetable-panel .channels .times .hour .clock .blink{
            animation: blink 2s step-end infinite;
          }
          @keyframes blink{
            50%{opacity: .5}
          }
          #${SCRIPTNAME}-timetable-panel .channels .day{
            border-right: 1px solid ${configs.border_color};
            height: ${configs.height}vh;
            position: absolute;
            overflow: hidden;
            cursor: auto;
          }
          #${SCRIPTNAME}-timetable-panel .channels .day .date{
            color: rgba(255,255,255,.125);
            font-weight: bold;
            font-size: ${configs.height}vh;
            line-height: ${configs.height}vh;
          }
          #${SCRIPTNAME}-timetable-panel .channels .hour{
            background: ${configs.times_background};
          }
          /* 番組表 個別番組 */
          #${SCRIPTNAME}-timetable-panel .channels .program.hidden{
            opacity: 0;
          }
          #${SCRIPTNAME}-timetable-panel .channels .program{
            background: ${configs.comming_background};
          }
          #${SCRIPTNAME}-timetable-panel .channels .program > button,
          #${SCRIPTNAME}-timetable-panel .channels .program > button *{
            height: calc(${configs.rowheight}vh - 1px);/*borderぶん*/
          }
          #${SCRIPTNAME}-timetable-panel .channels .program > button{
            padding: 0;
            margin: 0 0 0 calc(-${configs.rowheight}vh + 1px);
            opacity: 0;/*端数ピクセルがチラ見えしてしまうことがあるので*/
            transition: margin 500ms ease, transform 250ms ease-in;
            pointer-events: auto;
          }
          #${SCRIPTNAME}-timetable-panel .channels .program.shown > button{
            margin-left: .25em;
            opacity: 1;
          }
          #${SCRIPTNAME}-timetable-panel .channels .program > .mark,
          #${SCRIPTNAME}-timetable-panel .channels .program > .mark *{
            height: ${configs.fontsize}vh;
          }
          #${SCRIPTNAME}-timetable-panel .channels .program.nowonair{
            background: ${configs.nowOnAir_background};
            padding-left: ${BOUNCINGPIXEL}px;
          }
          #${SCRIPTNAME}-timetable-panel .channels .program.nowonair > button{
            display: none;
          }
          #${SCRIPTNAME}-timetable-panel .channels .program > .time{
            transition: max-width 1000ms ease, margin-left 1000ms ease;
            max-width: 3em;
            overflow: hidden;
          }
          #${SCRIPTNAME}-timetable-panel .channels .program.nowonair > .time{
            max-width: 0;
            margin-left: 0;
          }
          #${SCRIPTNAME}-timetable-panel .channels .program.shown/*番組情報表示中の番組*/,
          #${SCRIPTNAME}-timetable-panel .channels .channel:hover header,
          #${SCRIPTNAME}-timetable-panel .channels .channel:hover .program{
            background: ${configs.hover_background} !important;
          }
          #${SCRIPTNAME}-timetable-panel .channels header:hover + .stream .program.nowonair,
          #${SCRIPTNAME}-timetable-panel .channels .program:hover{
            overflow: visible;
            clip-path: inset(0 -100vw 0 0);/*overflowさせるのは右方向だけ*/
            z-index: ${configs.program_zIndex};
          }
          #${SCRIPTNAME}-timetable-panel .channels .channel:hover header:not(:hover),
          #${SCRIPTNAME}-timetable-panel .channels .channel:hover header:not(:hover) + .stream .program.nowonair:not(:hover),
          #${SCRIPTNAME}-timetable-panel .channels .channel:hover .program:not(:hover):not(.nowonair)/*後続番組*/{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-timetable-panel .channels header:hover + .stream .program:not(.nowonair) > */*後続番組の中身*/,
          #${SCRIPTNAME}-timetable-panel .channels .channel:hover .program:not(:hover):not(.nowonair) > */*後続番組の中身*/{
            opacity: .25;
          }
          html:not(.abemaTimetable) #${SCRIPTNAME}-timetable-panel .channels .channel.current header,
          html:not(.abemaTimetable) #${SCRIPTNAME}-timetable-panel .channels .channel.current .program.nowonair,
          #${SCRIPTNAME}-timetable-panel .channels .program.active{
            background: ${configs.current_background} !important;
          }
          #${SCRIPTNAME}-timetable-panel .channels .channel:not(:hover) .program.nocontent{
            background: ${configs.noContent_background};
          }
          #${SCRIPTNAME}-timetable-panel .channels .channel:not(:hover) .program.nocontent:not(.shown) .title{
            opacity: .25;
          }
          #${SCRIPTNAME}-timetable-panel .channels .channel .program.padding/*空き枠*/{
            cursor: auto;
          }
          /* 番組表スクローラ */
          #${SCRIPTNAME}-timetable-panel .scrollers{
            font-size: ${configs.fontsize}vh;/*emサイズ指定用*/
            position: absolute;
            bottom: 0;
            left: ${NAMEWIDTH}vw;
            width: ${100 - NAMEWIDTH}vw;
            height: 100%;
            z-index: ${configs.scrollers_zIndex};
            pointer-events: none;
          }
          #${SCRIPTNAME}-timetable-panel .scrollers button.disabled{
            pointer-events: none;
            opacity: 0;
          }
          #${SCRIPTNAME}-timetable-panel .scrollers button{
            background: ${configs.scroller_background};
            border-radius: 5em;
            width: 5em;
            height: 5em;
            transform: translateY(50%);
            position: absolute;
            bottom: 50%;
            opacity: .25;
            transition: opacity 500ms ease, right 500ms ease;
            pointer-events: auto;
          }
          #${SCRIPTNAME}-timetable-panel .scrollers button:hover{
            opacity: 1;
          }
          #${SCRIPTNAME}-timetable-panel .scrollers button.left{
            left: .5em;
          }
          #${SCRIPTNAME}-timetable-panel .scrollers button.right{
            right: .5em;
          }
          #${SCRIPTNAME}-timetable-panel .search.active ~ .scrollers button.right{
            right: calc(${100*(1/3)}vw + .5em);
            opacity: 1;
          }
          #${SCRIPTNAME}-timetable-panel .scrollers button > *{
            width: 2em;
            height: 2em;
            vertical-align: middle;
            opacity: .5;
          }
          /* 番組検索結果 */
          #${SCRIPTNAME}-timetable-panel > .programs > .search{
            background: ${configs.search_background};
            border-top: 1px solid ${configs.border_color};
            border-left: 1px solid ${configs.border_color};
            width: calc(${100*(1/3)}vw + 1px + ${configs.scrollbarWidth}px);
            height: 100%;
            box-sizing: border-box;
            margin-right: -${configs.scrollbarWidth}px;/*スクロールバーを隠す*/
            -webkit-mask-image: linear-gradient(black 90%, rgba(0,0,0,.5));/*まだ-webkit取れない*/
            mask-image: linear-gradient(black 90%, rgba(0,0,0,.5));
            overflow-x: hidden;
            overflow-y: scroll;
            position: absolute;
            top: 0;
            right: 0;
            z-index: ${configs.search_zIndex};
            transform: translateX(100%);
            transition: transform 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search.active,
          #${SCRIPTNAME}-timetable-panel > .programs > .search:hover{
            transform: translateX(0);
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search *{
            font-size: ${configs.search_fontsize}vh;
            line-height: ${configs.search_lineheight}vh;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .mark,
          #${SCRIPTNAME}-timetable-panel > .programs > .search .mark *{
            height: ${configs.search_fontsize}vh;
            line-height: ${configs.search_fontsize}vh;
          }
          /* 番組検索結果 ヘッダ */
          #${SCRIPTNAME}-timetable-panel > .programs > .search header{
            display: block;
          }
          /* 番組検索結果 フィルタ */
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters{
            width: calc(100% - 2vh);
            padding: 0;
            margin: .25em 1vh;
            border: 1px solid transparent;/*検索欄とツラ合わせしやすく*/
            height: calc(1.92vh + .5em);
            border-radius: .5em;
            overflow: hidden;
            display: flex;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input{
            display: none;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters label::before{
            content: '✓';
            color: black;
            font-size: ${configs.search_lineheight}vh;
            left: .1em;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input:checked + label::before{
            color: white;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input + label{
            color: ${configs.transparentGray};/*:not:checked*/
            filter: brightness(.25);/*:not:checked*/
            background: rgba(96,96,96,.75);
            white-space: nowrap;
            position: relative;
            margin-left: 1px;
            overflow: hidden;
            flex: 1;
            display: flex;
            align-items: center;/*.markを中央揃え*/
            cursor: pointer;
            transition: background 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input:checked + label{
            color: white;
            filter: brightness(1);
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input + label:hover{
            filter: brightness(.5);
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input:checked + label:hover{
            filter: brightness(.75);
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input + label.notify{
            background: ${configs.activeButton_color};
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input + label > *{
            flex-shrink: 0;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input + label > .mark,
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input + label > .mark *{
            font-size: ${(configs.search_fontsize + configs.search_lineheight) / 2}vh;
            height: ${(configs.search_fontsize + configs.search_lineheight) / 2}vh;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .filters input + label:first-of-type{
            margin-left: 0;
          }
          /* 番組検索結果 通知種別 */
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs{
            margin: .25em 0;
            display: flex;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input{
            display: none;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input + label{
            color: rgb(128,128,128);
            padding: .25em 1vh;
            border-bottom: 1px solid rgb(64,64,64);
            flex: 1;
            display: flex;
            align-items: center;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input:not(:checked) + label:hover{
            color: rgb(192,192,192);
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input:checked + label{
            color: rgb(256,256,256);
            border: 1px solid rgb(64,64,64);
            border-bottom: none;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input:checked + label:first-of-type{
            border-left: none;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input:checked + label:last-of-type{
            border-right: none;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input + label > svg{
            fill: rgb(128,128,128);
            height: ${(configs.search_fontsize + configs.search_lineheight) / 2}vh;
            margin-right: .2em;
            flex-shrink: 0;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input:not(:checked) + label:hover > svg{
            fill: rgb(192,192,192);
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .tabs > input:checked + label > svg{
            fill: rgb(256,256,256);
          }
          /* 番組検索結果 サマリ(件数・検索通知) */
          #${SCRIPTNAME}-timetable-panel > .programs > .search .summary{
            padding: 0;
            margin: .5em 1vh;
            display: flex;
            align-items: center;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .summary > *{
            white-space: nowrap;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search .summary .count{
            flex: 1;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search button.search_all{
            text-align: left;
            background: rgba(96,96,96,.75);
            border: .1em solid transparent;
            border-radius: 50vmax;
            padding: .15em calc(${configs.search_fontsize / 2}vh - 1px);
            margin: 0;
            flex-shrink: 0;
            display: flex;
            align-items: center;/*.markを中央揃え*/
            transition: transform 250ms ease-in;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search button.search_all:hover{
            background: rgba(96,96,96,.25);
            filter: brightness(1);/*打ち消し*/
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search button.search_all.reversing{
            transform: scaleY(0);
            transition: transform 250ms ease-out;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search button.search_all.active{
            background: ${configs.activeButton_color};
            border: .1em solid white;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search button.search_all > *{
            margin-right: .25em;
            transform: scaleX(1);
            flex-shrink: 0;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search button.search_all > svg:first-child/*検索アイコン*/{
            fill: white;
            width: calc(${configs.search_fontsize}vh + .25em);/*膨れさせる*/
            height: calc(${configs.search_fontsize}vh + .25em);/*膨れさせる*/
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search button.search_all .mark{
            margin-left: .125em;
            margin-right: .125em;
          }
          /* 番組検索結果 番組リスト */
          #${SCRIPTNAME}-timetable-panel > .programs > .search ul{
            padding: 0;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program{
            height: calc(${configs.search_fontsize + configs.search_lineheight}vh);/*この高さがh2,間隙,.dataの高さになる*/
            padding: 0;
            margin: .5vh 1vh 1vh 1vh;
            display: grid;
            grid-template-columns: calc(${configs.search_lineheight * 2 * (16/9)}vh) 1fr;
            grid-template-rows: ${configs.search_lineheight}vh ${configs.search_lineheight}vh;
            height: ${configs.search_lineheight * 2}vh;
            cursor: pointer;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program:hover{
            filter: brightness(.75);
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .thumbnail{
            width:  calc(${configs.search_lineheight * 2 * (16/9)}vh);
            height: calc(${configs.search_lineheight * 2}vh);
            padding: 0;
            grid-column: 1;
            grid-row: 1 / 2;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .thumbnail img{
            display: block;
            width: auto;/*4:3もある*/
            max-width:  calc(${configs.search_lineheight * 2 * (16/9)}vh - 1px);/*端数処理ではみ出すのを防ぐ*/
            height: calc(${configs.search_lineheight * 2}vh);
            margin: 0 auto;
            transition: opacity 500ms ease;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .thumbnail img.loading{
            opacity: 0;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > h2,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .data{
            white-space: nowrap;
            padding: 0 1vh;
            display: flex;
            align-items: center;/*.markを中央揃え*/
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > h2 > *,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .data > *{
            flex-shrink: 0;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > h2{
            height: ${(configs.search_lineheight + configs.search_fontsize) / 2}vh;/*ほどよく切り詰める*/
            grid-column: 2;
            grid-row: 1;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > h2 .mark,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > h2 .mark *{
            height: ${configs.search_fontsize}vh;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > h2 .title{
            vertical-align: middle;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .data{
            color: ${configs.transparentGray};
            vertical-align: middle;
            grid-column: 2;
            grid-row: 2;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .data button,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .data button > *{
            font-size: ${configs.fontsize}vh;/*emサイズを合わせる*/
            height: ${configs.search_lineheight}vh;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .data button + *{
            margin-left: .25em;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.program > .data button + * ~ *{
            margin-left: 1em;
          }
          /* 番組検索結果 番組リスト(日付別・毎回通知・検索通知共通) */
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.day,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.repeat,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.search{
            padding: 0;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.day > h2,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.repeat > h2,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.search > h2{
            padding-left: 1vh;
            white-space: nowrap;
            display: flex;
            align-items: center;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.repeat > h2 button + *,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.search > h2 button + *{
            margin-left: .25em;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.noprogram{
            color: gray;
            padding-left: 1vh;
          }
          /* 番組検索結果 番組リスト(毎回通知) */
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.repeat > h2 button,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.repeat > h2 button > *{
            height: ${configs.search_fontsize}vh;
          }
          /* 番組検索結果 番組リスト(検索通知) */
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.search > h2 button,
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.search > h2 button > *{
            height: ${configs.search_lineheight}vh;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.search > h2 button > *{
            fill: ${configs.activeButton_color};
            background: transparent;
            border: none;
            filter: brightness(1.25);
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.search > h2 .key{
            display: flex;
            align-items: center;
          }
          #${SCRIPTNAME}-timetable-panel > .programs > .search li.search > h2 .key .mark{
            margin-left: .125em;
            margin-right: .125em;
          }
          /* 設定パネル */
          #${SCRIPTNAME}-config-panel{
            width: 360px;
          }
          #${SCRIPTNAME}-config-panel fieldset p,
          #${SCRIPTNAME}-config-panel fieldset li{
            padding-left: calc(10px + 1em);
          }
          #${SCRIPTNAME}-config-panel fieldset .sub{
            padding-left: calc(10px + 2em);
          }
          #${SCRIPTNAME}-config-panel fieldset p:hover,
          #${SCRIPTNAME}-config-panel fieldset li:hover{
            background: rgba(255,255,255,.25);
          }
          #${SCRIPTNAME}-config-panel fieldset p.disabled,
          #${SCRIPTNAME}-config-panel fieldset li.disabled{
            opacity: .5;
          }
          #${SCRIPTNAME}-config-panel label{
            display: block;
          }
          #${SCRIPTNAME}-config-panel input{
            width: 80px;
            height: 20px;
            position: absolute;
            right: 10px;
          }
          #${SCRIPTNAME}-config-panel fieldset ul.channels{
            columns: 2;
            column-gap: 0;
          }
          #${SCRIPTNAME}-config-panel fieldset ul.channels li input{
            width: 20px;
            vertical-align: bottom;
            margin-right: .25em;
            position: static;
          }
        </style>
      `,
    },
  };
  if(!('animate' in HTMLElement.prototype)) HTMLElement.prototype.animate = function(){};
  if(!('isConnected' in Node.prototype)) Node.prototype.isConnected = true;
  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 sequence = function(){
    let chain = [], defer = function(callback, delay, ...params){(delay) ? setTimeout(callback, delay, ...params) : animate(callback, ...params)};
    for(let i = arguments.length - 1, delay = 0; 0 <= i; i--, delay = 0){
      if(typeof arguments[i] === 'function'){
        for(let j = i - 1; typeof arguments[j] === 'number'; j--) delay += arguments[j];
        let f = arguments[i], d = delay, callback = chain[chain.length - 1];
        chain.push(function(pass){defer(function(ch){ch ? ch(f(pass)) : f(pass);}, d, callback)});/*nearly black magic*/
      }
    }
    chain[chain.length - 1]();
  };
  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.trim().replace(/[!-}]/g, function(s){
      return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
    }).replace(/ /g, ' ').replace(/~/g, '〜');
  };
  let linkify = function(node){
    split(node);
    function split(n){
      if(['style', 'script', 'a'].includes(n.localName)) return;
      if(n.nodeType === Node.TEXT_NODE){
        let pos = n.data.search(linkify.RE);
        if(0 <= pos){
          let target = n.splitText(pos);/*pos直前までのnとpos以降のtargetに分割*/
          let rest = target.splitText(RegExp.lastMatch.length);/*targetと続くrestに分割*/
          /* この時点でn(処理済み),target(リンクテキスト),rest(次に処理)の3つに分割されている */
          let a = document.createElement('a');
          let match = target.data.match(linkify.RE);
          switch(true){
            case(match[1] !== undefined): a.href = (match[1][0] == 'h') ? match[1] : 'h' + match[1]; break;
            case(match[2] !== undefined): a.href = 'http://' + match[2]; break;
            case(match[3] !== undefined): a.href = 'mailto:' + match[4] + '@' + match[5]; break;
          }
          a.appendChild(target);/*textContent*/
          rest.parentNode.insertBefore(a, rest);
        }
      }else{
        for(let i = 0; n.childNodes[i]; i++) split(n.childNodes[i]);/*回しながらchildNodesは増えていく*/
      }
    }
  };
  linkify.RE = new RegExp([
    '(h?ttps?://[-\\w_./~*%$@:;,!?&=+#]+[-\\w_/~*%$@:;&=+#])',/*通常のURL*/
    '((?:\\w+\\.)+\\w+/[-\\w_./~*%$@:;,!?&=+#]*)',/*http://の省略形*/
    '((\\w[-\\w_.]+)(?:@|@)(\\w[-\\w_.]+\\w))',/*メールアドレス*/
  ].join('|'));
  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);
})();