text: // ==ClosureCompiler==
  // @compilation_level ADVANCED_OPTIMIZATIONS
  // ==/ClosureCompiler==
  
  var lichess = window.lichess = window.lichess || {};
  
  lichess.getParameterByName = function(name) {
    var match = RegExp('[?&]' + name + '=([^&]*)').exec(location.search);
    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
  };
  
  lichess.StrongSocket = function(url, version, settings) {
    var self = this;
    self.settings = $.extend(true, {}, lichess.StrongSocket.defaults, settings);
    self.url = url;
    self.version = version;
    self.options = self.settings.options;
    self.ws = null;
    self.pingSchedule = null;
    self.connectSchedule = null;
    self.ackableMessages = [];
    self.lastPingTime = self.now();
    self.currentLag = 0;
    self.averageLag = 0;
    self.tryOtherUrl = false;
    self.autoReconnect = true;
    self.debug('Debug is enabled');
    if (self.options.resetUrl || self.options.prodPipe) {
      lichess.storage.remove(self.options.baseUrlKey);
    }
    if (self.options.prodPipe) {
      self.options.baseUrls = ['socket.en.lichess.org:9021'];
    }
    self.connect();
    window.addEventListener('unload', function() {
      self.destroy();
    });
  };
  lichess.StrongSocket.available = window.WebSocket || window.MozWebSocket;
  lichess.StrongSocket.sri = Math.random().toString(36).substring(2);
  lichess.StrongSocket.defaults = {
    events: {
      fen: function(e) {
        $('.live_' + e.id).each(function() {
          lichess.parseFen($(this).data("fen", e.fen).data("lastmove", e.lm));
        });
      }
    },
    params: {
      sri: lichess.StrongSocket.sri
    },
    options: {
      name: "unnamed",
      pingMaxLag: 8000, // time to wait for pong before reseting the connection
      pingDelay: 1000, // time between pong and ping
      autoReconnectDelay: 2000,
      lagTag: false, // jQuery object showing ping lag
      ignoreUnknownMessages: true,
      baseUrls: ['socket.' + document.domain].concat(
        ($('body').data('ports') + '').split(',').map(function(port) {
          return 'socket.' + document.domain + ':' + port;
        })),
      onFirstConnect: $.noop,
      baseUrlKey: 'surl3'
    }
  };
  lichess.StrongSocket.prototype = {
    connect: function() {
      var self = this;
      self.destroy();
      self.autoReconnect = true;
      var fullUrl = "ws://" + self.baseUrl() + self.url + "?" + $.param($.extend(self.settings.params, {
        version: self.version
      }));
      self.debug("connection attempt to " + fullUrl, true);
      try {
        if (window.MozWebSocket) self.ws = new MozWebSocket(fullUrl);
        else if (window.WebSocket) self.ws = new WebSocket(fullUrl);
        else throw "[lila] no websockets found on this browser!";
  
        self.ws.onerror = function(e) {
          self.onError(e);
        };
        self.ws.onclose = function(e) {
          if (self.autoReconnect) {
            self.debug('Will autoreconnect in ' + self.options.autoReconnectDelay);
            self.scheduleConnect(self.options.autoReconnectDelay);
          }
        };
        self.ws.onopen = function() {
          self.debug("connected to " + fullUrl, true);
          self.onSuccess();
          $('body').removeClass('offline');
          self.pingNow();
          $('body').trigger('socket.open');
          if ($('#user_tag').length) setTimeout(function() {
            self.send("following_onlines");
          }, 500);
          var resend = self.ackableMessages;
          self.ackableMessages = [];
          resend.forEach(function(x) {
            self.send(x.t, x.d);
          });
        };
        self.ws.onmessage = function(e) {
          var m = JSON.parse(e.data);
          if (m.t == "n") {
            self.pong();
          } else self.debug(e.data);
          if (m.t == "b") {
            m.d.forEach(function(mm) {
              self.handle(mm);
            });
          } else self.handle(m);
        };
      } catch (e) {
        self.onError(e);
      }
      self.scheduleConnect(self.options.pingMaxLag);
    },
    send: function(t, d, o, again) {
      var self = this;
      var data = d || {},
        options = o || {};
      if (options && options.ackable)
        self.ackableMessages.push({
          t: t,
          d: d
        });
      var message = JSON.stringify({
        t: t,
        d: data
      });
      self.debug("send " + message);
      try {
        self.ws.send(message);
      } catch (e) {
        // maybe sent before socket opens,
        // try again a second later,once.
        if (!again) setTimeout(function() {
          this.send(t, d, o, true);
        }.bind(this), 1000);
        else console.log(e, 'again');
      }
    },
    sendAckable: function(t, d) {
      this.send(t, d, {
        ackable: true
      });
    },
    scheduleConnect: function(delay) {
      var self = this;
      // self.debug('schedule connect ' + delay);
      clearTimeout(self.pingSchedule);
      clearTimeout(self.connectSchedule);
      self.connectSchedule = setTimeout(function() {
        $('body').addClass('offline');
        self.tryOtherUrl = true;
        self.connect();
      }, delay);
    },
    schedulePing: function(delay) {
      var self = this;
      clearTimeout(self.pingSchedule);
      self.pingSchedule = setTimeout(function() {
        self.pingNow();
      }, delay);
    },
    pingNow: function() {
      var self = this;
      clearTimeout(self.pingSchedule);
      clearTimeout(self.connectSchedule);
      try {
        self.ws.send(self.pingData());
        self.lastPingTime = self.now();
      } catch (e) {
        self.debug(e, true);
      }
      self.scheduleConnect(self.options.pingMaxLag);
    },
    pong: function() {
      var self = this;
      clearTimeout(self.connectSchedule);
      self.schedulePing(self.options.pingDelay);
      self.currentLag = self.now() - self.lastPingTime;
      if (!self.averageLag) self.averageLag = self.currentLag;
      else self.averageLag = 0.2 * (self.currentLag - self.averageLag) + self.averageLag;
      if (self.options.lagTag) {
        self.options.lagTag.html(Math.round(self.averageLag));
      }
    },
    pingData: function() {
      return JSON.stringify({
        t: "p",
        v: this.version
      });
    },
    handle: function(m) {
      var self = this;
      if (m.v) {
        if (m.v <= self.version) {
          self.debug("already has event " + m.v);
          return;
        }
        if (m.v > self.version + 1) {
          self.debug("event gap detected from " + self.version + " to " + m.v);
          if (!self.options.prodPipe) return;
        }
        self.version = m.v;
      }
      switch (m.t || false) {
        case false:
          break;
        case 'resync':
          if (!self.options.prodPipe) lichess.reload();
          break;
        case 'ack':
          self.ackableMessages = [];
          break;
        default:
          if (self.settings.receive) self.settings.receive(m.t, m.d);
          var h = self.settings.events[m.t];
          if (h) h(m.d || null);
          else if (!self.options.ignoreUnknownMessages) {
            self.debug('Message not supported ' + JSON.stringify(m));
          }
      }
    },
    now: function() {
      return new Date().getTime();
    },
    debug: function(msg, always) {
      if ((always || this.options.debug) && window.console && console.debug) {
        console.debug("[" + this.options.name + " " + this.settings.params.sri + "]", msg);
      }
    },
    destroy: function() {
      clearTimeout(this.pingSchedule);
      clearTimeout(this.connectSchedule);
      this.disconnect();
      this.ws = null;
    },
    disconnect: function() {
      if (this.ws) {
        this.debug("Disconnect", true);
        this.autoReconnect = false;
        this.ws.onerror = $.noop;
        this.ws.onclose = $.noop;
        this.ws.onopen = $.noop;
        this.ws.onmessage = $.noop;
        this.ws.close();
      }
    },
    onError: function(e) {
      var self = this;
      self.options.debug = true;
      self.debug('error: ' + JSON.stringify(e));
      self.tryOtherUrl = true;
      setTimeout(function() {
        if (!$('#network_error').length) {
          var msg = "Your browser supports websockets, but cannot get a connection. Maybe you are behind a proxy that does not support websockets. Ask your system administrator to fix it!";
          $('#top').append('<span class="fright link text" id="network_error" title="' + msg + '" data-icon="j">Network error</span>');
        }
      }, 1000);
      clearTimeout(self.pingSchedule);
    },
    onSuccess: function() {
      $('#network_error').remove();
      this.nbConnects = (this.nbConnects || 0) + 1;
      if (this.nbConnects === 1) this.options.onFirstConnect();
    },
    baseUrl: function() {
      var key = this.options.baseUrlKey;
      var urls = this.options.baseUrls;
      var url = lichess.storage.get(key);
      if (!url) {
        url = urls[0];
        lichess.storage.set(key, url);
      } else if (this.tryOtherUrl) {
        this.tryOtherUrl = false;
        url = urls[(urls.indexOf(url) + 1) % urls.length];
        lichess.storage.set(key, url);
      }
      return url;
    },
    pingInterval: function() {
      return this.options.pingDelay + this.averageLag;
    }
  };
  
  // declare now, populate later in a distinct script.
  var lichess_translations = lichess_translations || [];
  
  function withStorage(f) {
    // can throw an exception when storage is full
    try {
      return !!window.localStorage ? f(window.localStorage) : null;
    } catch (e) {}
  }
  lichess.storage = {
    get: function(k) {
      return withStorage(function(s) {
        return s.getItem(k);
      });
    },
    remove: function(k) {
      withStorage(function(s) {
        s.removeItem(k);
      });
    },
    set: function(k, v) {
      // removing first may help http://stackoverflow.com/questions/2603682/is-anyone-else-receiving-a-quota-exceeded-err-on-their-ipad-when-accessing-local
      withStorage(function(s) {
        s.removeItem(k);
        s.setItem(k, v);
      });
    }
  };
  
  (function() {
  
    /////////////
    // ctrl.js //
    /////////////
  
    $.userLink = function(u) {
      return $.userLinkLimit(u, false);
    };
    $.userLinkLimit = function(u, limit, klass) {
      var split = u.split(' ');
      var id = split.length == 1 ? split[0] : split[1];
      return (u || false) ? '<a class="user_link ulpt ' + (klass || '') + '" href="/@/' + id + '">' + ((limit || false) ? u.substring(0, limit) : u) + '</a>' : 'Anonymous';
    };
    $.redirect = function(obj) {
      var url;
      if (typeof obj == "string") url = obj;
      else {
        url = obj.url;
        if (obj.cookie) {
          var domain = document.domain.replace(/^.+(\.[^\.]+\.[^\.]+)$/, '$1');
          var cookie = [
            encodeURIComponent(obj.cookie.name) + '=' + obj.cookie.value,
            '; max-age=' + obj.cookie.maxAge,
            '; path=/',
            '; domain=' + domain
          ].join('');
          document.cookie = cookie;
        }
      }
      location.href = 'http://' + location.hostname + '/' + url.replace(/^\//, '');
    };
    $.fp = {};
    $.fp.range = function(to) {
      return Array.apply(null, Array(to)).map(function(_, i) {
        return i;
      });
    };
    $.fp.contains = function(list, needle) {
      return list.indexOf(needle) !== -1;
    };
    $.fp.find = function(list, pred) {
      for (var i = 0, len = list.length; i < len; i++) {
        if (pred(list[i])) return list[i];
      }
      return undefined;
    };
    $.fp.debounce = function(func, wait, immediate) {
      var timeout;
      return function() {
        var context = this,
          args = arguments;
        var later = function() {
          timeout = null;
          if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
      };
    };
    $.spreadNumber = function(el, nbSteps, getDuration) {
      var previous, displayed;
      var display = function(prev, cur, it) {
        var val = Math.round(((prev * (nbSteps - 1 - it)) + (cur * (it + 1))) / nbSteps);
        if (val !== displayed) {
          el.textContent = val;
          displayed = val;
        }
      };
      return function(nb) {
        if (!el || !nb) return;
        var prev = previous || nb;
        previous = nb;
        var interv = getDuration() / nbSteps;
        for (var i = 0; i < nbSteps; i++) {
          setTimeout(display.bind(null, prev, nb, i), Math.round(i * interv));
        }
      };
    };
  
    var nbUserSpread = $.spreadNumber(
      document.querySelector('#nb_connected_players > strong'),
      5,
      function() {
        return lichess.socket.pingInterval();
      });
    lichess.socket = null;
    lichess.idleTime = 20 * 60 * 1000;
    $.extend(true, lichess.StrongSocket.defaults, {
      events: {
        following_onlines: function(us) {
          $('#friend_box').friends("set", us);
        },
        following_enters: function(name) {
          $('#friend_box').friends('enters', name);
        },
        following_leaves: function(name) {
          $('#friend_box').friends('leaves', name);
        },
        n: nbUserSpread,
        message: function(msg) {
          $('#chat').chat("append", msg);
        },
        nbm: function(e) {
          $('#nb_messages').text(e || "0").parent().parent().toggle(e > 0);
        },
        redirect: function(o) {
          setTimeout(function() {
            lichess.hasToReload = true;
            $.redirect(o);
          }, 300);
        },
        tournamentReminder: function(data) {
          if (!$('#tournament_reminder').length && $('body').data("tournament-id") != data.id) {
            $('#notifications').append(data.html).find("a.withdraw").click(function() {
              $.post($(this).attr("href"));
              $('#tournament_reminder').remove();
              return false;
            });
            $('body').trigger('lichess.content_loaded');
          }
        },
        challengeReminder: function(data) {
          if (!lichess.storage.get('challenge-refused-' + data.id)) {
            var refreshButton = function() {
              var nb = $('#challenge_notifications > div').length;
              $('#nb_challenges').text(nb);
              $('#challenge_notifications_tag').toggleClass('none', !nb);
            };
            var htmlId = 'challenge_reminder_' + data.id;
            var $notif = $('#' + htmlId);
            var declineListener = function($a, callback) {
              return $a.click(function() {
                $.post($(this).attr("href"));
                lichess.storage.set('challenge-refused-' + data.id, 1);
                $('#' + htmlId).remove();
                if ($.isFunction(callback)) callback();
                refreshButton();
                return false;
              });
            };
            if ($notif.length) clearTimeout($notif.data('timeout'));
            else {
              $('#challenge_notifications').append(data.html);
              $notif = $('#' + htmlId);
              $notif.find('> a').click(function() {
                lichess.hasToReload = true; // allow quit by accept challenge (simul)
              });
              declineListener($notif.find('a.decline'));
              $('body').trigger('lichess.content_loaded');
              if (!lichess.storage.get('challenge-' + data.id)) {
                if (!lichess.quietMode) {
                  $('#top .challenge_notifications').addClass('shown');
                  $.sound.dong();
                }
                lichess.storage.set('challenge-' + data.id, 1);
              }
              refreshButton();
            }
            $('.lichess_overboard.joining.' + data.id).each(function() {
              if (!$(this).find('a.decline').length) $(this).find('form').append(
                declineListener($(data.html).find('a.decline').text($.trans('decline')), function() {
                  location.href = "/";
                })
              );
            });
            $notif.data('timeout', setTimeout(function() {
              $notif.remove();
              refreshButton();
            }, 3000));
          }
        },
        deployPre: function(html) {
          $('#notifications').append(html);
          setTimeout(function() {
            $('#deploy_pre').fadeOut(1000).remove();
          }, 10000);
        },
        deployPost: function(html) {
          $('#notifications').append(html);
          setTimeout(function() {
            $('#deploy_post').fadeOut(1000).remove();
          }, 10000);
          lichess.socket.disconnect();
        },
        simulEnd: function(simul) {
          $.modal($(
            '<p>Simul complete!</p><br /><br />' +
            '<a class="button" href="/simul/' + simul.id + '">Back to ' + simul.name + ' simul</a>'
          ));
        }
      },
      params: {},
      options: {
        name: "site",
        lagTag: $('#top .ping strong'),
        debug: location.search.indexOf('debug-ws') != -1,
        prodPipe: location.search.indexOf('prod-ws') != -1,
        resetUrl: location.search.indexOf('reset-ws') != -1
      }
    });
  
    lichess.hasToReload = false;
    lichess.reload = function() {
      lichess.hasToReload = true;
      location.reload();
    };
  
    lichess.readServerFen = function(t) {
      return atob(t.split("").reverse().join(""));
    };
  
    lichess.openInMobileApp = function(gameId) {
      if (!/android.+mobile|ipad|iphone|ipod/i.test(navigator.userAgent || navigator.vendor)) return false;
      var storageKey = 'open-game-in-mobile';
      var open = function(v) {
        if (v > 0) {
          lichess.storage.set(storageKey, v - 1);
          location.href = 'lichess://' + gameId;
          return true;
        }
        lichess.storage.set(storageKey, v + 1);
        return false;
      };
      var stored = parseInt(lichess.storage.get(storageKey));
      if (stored) return open(stored);
      return open(confirm('Open in lichess mobile app?') ? 10 : -10);
    };
  
    lichess.parseFen = function($elem) {
      if (!$elem || !$elem.jquery) {
        $elem = $('.parse_fen');
      }
      $elem.each(function() {
        var $this = $(this).removeClass('parse_fen');
        var lm = $this.data('lastmove');
        var lastMove = lm ? [lm[0] + lm[1], lm[2] + lm[3]] : [];
        var color = $this.data('color') || lichess.readServerFen($(this).data('y'));
        var ground = $this.data('chessground');
        var playable = $this.data('playable');
        var config = {
          coordinates: false,
          viewOnly: !playable,
          minimalDom: !playable,
          fen: $this.data('fen') || lichess.readServerFen($this.data('z')),
          lastMove: lastMove
        };
        if (color) config.orientation = color;
        if (ground) ground.set(config);
        else $this.data('chessground', Chessground($this[0], config));
      });
    }
  
    $(function() {
  
      if (!lichess.StrongSocket.available) {
        $('#lichess').on('mouseover', function() {
          $('#lichess').off('mouseover');
          var inUrFaceUrl = window.opera ? '/assets/opera-websocket.html' : '/assets/browser.html';
          $.ajax(inUrFaceUrl, {
            success: function(html) {
              $('body').prepend(html);
            }
          });
        });
      }
  
      if (lichess.round) startRound(document.getElementById('lichess'), lichess.round);
      else if (lichess.prelude) startPrelude(document.querySelector('.lichess_game'), lichess.prelude);
      else if (lichess.analyse) startAnalyse(document.getElementById('lichess'), lichess.analyse);
      else if (lichess.user_analysis) startUserAnalysis(document.getElementById('lichess'), lichess.user_analysis);
      else if (lichess.lobby) startLobby(document.getElementById('hooks_wrap'), lichess.lobby);
      else if (lichess.tournament) startTournament(document.getElementById('tournament'), lichess.tournament);
      else if (lichess.simul) startSimul(document.getElementById('simul'), lichess.simul);
      else if (lichess.relay) startRelay(document.getElementById('relay'), lichess.relay);
  
      document.body.classList.remove('preload');
  
      $('#lichess').on('click', '.socket-link:not(.disabled)', function() {
        lichess.socket.send($(this).data('msg'), $(this).data('data'));
      });
  
      $('#friend_box').friends();
  
      $('#lichess').on('click', '.copyable', function() {
        $(this).select();
      });
  
      $('body').on('click', '.relation_actions a.relation', function() {
        var $a = $(this).addClass('processing');
        $.ajax({
          url: $a.attr('href'),
          type: 'post',
          success: function(html) {
            $a.parent().html(html);
          }
        });
        return false;
      });
  
      function applyPowertip($els, placement) {
        $els.removeClass('ulpt').powerTip({
          fadeInTime: 100,
          fadeOutTime: 100,
          placement: placement,
          mouseOnToPopup: true,
          closeDelay: 200
        }).on({
          powerTipPreRender: function() {
            $.ajax({
              url: ($(this).attr('href') || $(this).data('href')).replace(/\?.+$/, '') + '/mini',
              success: function(html) {
                $('#powerTip').html(html);
                $('body').trigger('lichess.content_loaded');
              }
            });
          }
        }).data('powertip', ' ');
      }
  
      function userPowertips() {
        applyPowertip($('#site_header .ulpt'), 'e');
        applyPowertip($('#friend_box .ulpt'), 'nw');
        applyPowertip($('.ulpt'), 'w');
      }
      setTimeout(userPowertips, 600);
      $('body').on('lichess.content_loaded', userPowertips);
  
      $('#message_notifications_tag').on('click', function() {
        $.ajax({
          url: $(this).data('href'),
          cache: false,
          success: function(html) {
            $('#message_notifications_display').html(html)
              .find('a.mark_as_read').click(function() {
                $.ajax({
                  url: $(this).attr('href'),
                  method: 'post'
                });
                $(this).parents('.notification').remove();
                if ($('#message_notifications_display').children().length === 0)
                  $('#message_notifications_tag').click();
                return false;
              });
            $('body').trigger('lichess.content_loaded');
          }
        });
      });
  
      function setMoment() {
        $("time.moment").removeClass('moment').each(function() {
          var parsed = moment(this.getAttribute('datetime'));
          var format = this.getAttribute('data-format');
          this.textContent = format == 'calendar' ? parsed.calendar() : parsed.format(format);
        });
      }
      setMoment();
      $('body').on('lichess.content_loaded', setMoment);
  
      function setMomentFromNow() {
        $("time.moment-from-now").each(function() {
          this.textContent = moment(this.getAttribute('datetime')).fromNow();
        });
      }
      setMomentFromNow();
      $('body').on('lichess.content_loaded', setMomentFromNow);
      setInterval(setMomentFromNow, 2000);
  
      if ($('body').hasClass('blind_mode')) {
        var setBlindMode = function() {
          $('[data-hint]').each(function() {
            $(this).attr('aria-label', $(this).data('hint'));
          });
        };
        setBlindMode();
        $('body').on('lichess.content_loaded', setBlindMode);
      }
  
      setTimeout(function() {
        if (lichess.socket === null) {
          lichess.socket = new lichess.StrongSocket("/socket", 0);
        }
        $.idleTimer(lichess.idleTime, lichess.socket.destroy.bind(lichess.socket), lichess.socket.connect.bind(lichess.socket));
      }, 200);
  
      // themepicker
      $('#themepicker_toggle').one('mouseover', function() {
        var $themepicker = $('#themepicker');
        $.ajax({
          url: $(this).data('url'),
          cache: false,
          success: function(html) {
            $themepicker.append(html);
            var $body = $('body');
            var $content = $body.children('.content');
            var $dropdown = $themepicker.find('.dropdown');
            var $pieceSprite = $('#piece-sprite');
            var themes = $dropdown.data('themes').split(' ');
            var theme = $.fp.find(document.body.classList, function(a) {
              return $.fp.contains(themes, a);
            });
            var set = $body.data('piece-set');
            var theme3ds = $dropdown.data('theme3ds').split(' ');
            var theme3d = $.fp.find(document.body.classList, function(a) {
              return $.fp.contains(theme3ds, a);
            });
            var set3ds = $dropdown.data('set3ds').split(' ');
            var set3d = $.fp.find(document.body.classList, function(a) {
              return $.fp.contains(set3ds, a);
            });
            var background = $body.hasClass('dark') ? 'dark' : 'light';
            var is3d = $content.hasClass('is3d');
            $themepicker.find('.is2d div.theme').hover(function() {
              $body.removeClass(themes.join(' ')).addClass($(this).data("theme"));
            }, function() {
              $body.removeClass(themes.join(' ')).addClass(theme);
            }).click(function() {
              theme = $(this).data("theme");
              $.post($(this).parent().data("href"), {
                theme: theme
              });
              $themepicker.removeClass("shown");
            });
            $themepicker.find('.is2d div.no-square').hover(function() {
              var s = $(this).data("set");
              $pieceSprite.attr('href', $pieceSprite.attr('href').replace(/\w+\.css/, s + '.css'));
            }, function() {
              $pieceSprite.attr('href', $pieceSprite.attr('href').replace(/\w+\.css/, set + '.css'));
            }).click(function() {
              set = $(this).data("set");
              $.post($(this).parent().data("href"), {
                set: set
              });
              $themepicker.removeClass("shown");
            });
            $themepicker.find('.is3d div.theme').hover(function() {
              $body.removeClass(theme3ds.join(' ')).addClass($(this).data("theme"));
            }, function() {
              $body.removeClass(theme3ds.join(' ')).addClass(theme3d);
            }).click(function() {
              theme3d = $(this).data("theme");
              $.post($(this).parent().data("href"), {
                theme: theme3d
              });
              $themepicker.removeClass("shown");
            });
            $themepicker.find('.is3d div.no-square').hover(function() {
              $body.removeClass(set3ds.join(' ')).addClass($(this).data("set"));
            }, function() {
              $body.removeClass(set3ds.join(' ')).addClass(set3d);
            }).click(function() {
              set3d = $(this).data("set");
              $.post($(this).parent().data("href"), {
                set: set3d
              });
              $themepicker.removeClass("shown");
            });
            var showBg = function(bg) {
              $body.removeClass('light dark').addClass(bg);
              if (bg == 'dark' && $('link[href*="dark.css"]').length === 0) {
                $('link[href*="common.css"]').clone().each(function() {
                  $(this).attr('href', $(this).attr('href').replace(/common\.css/, 'dark.css')).appendTo('head');
                });
              }
            };
            var showDimensions = function(is3d) {
              $content.add('#top').removeClass('is2d is3d').addClass(is3d ? 'is3d' : 'is2d');
              setZoom(getZoom());
            };
            $themepicker.find('.background a').click(function() {
              background = $(this).data('bg');
              $.post($(this).parent().data('href'), {
                bg: background
              });
              $(this).addClass('active').siblings().removeClass('active');
              $themepicker.removeClass("shown");
              return false;
            }).hover(function() {
              showBg($(this).data('bg'));
            }, function() {
              showBg(background);
            }).filter('.' + background).addClass('active');
            $themepicker.find('.dimensions a').click(function() {
              is3d = $(this).data('is3d');
              $.post($(this).parent().data('href'), {
                is3d: is3d
              });
              $(this).addClass('active').siblings().removeClass('active');
              $themepicker.removeClass("shown");
              return false;
            }).hover(function() {
              showDimensions($(this).data('is3d'));
            }, function() {
              showDimensions(is3d);
            }).filter('.' + (is3d ? 'd3' : 'd2')).addClass('active');
            $themepicker.find('.slider').slider({
              orientation: "horizontal",
              min: 1,
              max: 2,
              range: 'min',
              step: 0.01,
              value: getZoom(),
              slide: function(e, ui) {
                manuallySetZoom(ui.value);
              }
            });
          }
        });
      });
  
      // Zoom
      var getZoom = function() {
        return lichess.storage.get('zoom') || 1;
      };
      var setZoom = function(v) {
        lichess.storage.set('zoom', v);
  
        var $lichessGame = $('.lichess_game, .board_and_ground');
        var $boardWrap = $lichessGame.find('.cg-board-wrap');
        var $coordinateProgress = $('.progress_bar_container');
        var px = function(i) {
          return Math.round(i) + 'px';
        };
  
        $boardWrap.css("width", px(512 * getZoom()));
        $coordinateProgress.css("width", px(512 * getZoom()));
        $('.underboard').css("margin-left", px((getZoom() - 1) * 250));
        $lichessGame.find('.lichess_overboard').css("left", px(56 + (getZoom() - 1) * 254));
  
        if ($('body > .content').hasClass('is3d')) {
          $boardWrap.css("height", px(479.08572 * getZoom()));
          $lichessGame.css({
            height: px(479.08572 * getZoom()),
            paddingTop: px(50 * (getZoom() - 1))
          });
          $('.chat_panels').css("height", px(290 + 529 * (getZoom() - 1)));
        } else {
          $boardWrap.css("height", px(512 * getZoom()));
          $lichessGame.css({
            height: px(512 * getZoom()),
            paddingTop: px(0)
          });
          $('.chat_panels').css("height", px(325 + 510 * (getZoom() - 1)));
        }
  
        $('#trainer .overlay_container').css({
          top: px((getZoom() - 1) * 250),
          left: px((getZoom() - 1) * 250)
        });
        // doesn't vertical center score at the end, close enough
        $('#trainer .score_container').css("top", px((getZoom() - 1) * 250));
  
  
        if ($lichessGame.length) {
          // if on a board with a game
          $('body > .content').css("margin-left", 'calc(50% - ' + px(246.5 + 256 * getZoom()) + ')');
        }
      };
  
      var manuallySetZoom = $.fp.debounce(setZoom, 10);
      if (getZoom() > 1) setZoom(getZoom()); // Instantiate the page's zoom
      $('body').on('lichess.coordinate_trainer_loaded', function() {
        setZoom(getZoom());
      });
  
      function translateTexts() {
        $('.trans_me').each(function() {
          $(this).removeClass('trans_me');
          if ($(this).val()) $(this).val($.trans($(this).val()));
          else $(this).text($.trans($(this).text()));
        });
      }
      translateTexts();
      $('body').on('lichess.content_loaded', translateTexts);
  
      $('input.autocomplete').each(function() {
        var $a = $(this);
        $a.autocomplete({
          source: $a.data('provider'),
          minLength: 2,
          delay: 100
        });
      });
  
      $('.infinitescroll:has(.pager a)').each(function() {
        $(this).infinitescroll({
          navSelector: ".pager",
          nextSelector: ".pager a:last",
          itemSelector: ".infinitescroll .paginated_element",
          errorCallback: function() {
            $("#infscr-loading").remove();
          }
        }, function() {
          $("#infscr-loading").remove();
          $('body').trigger('lichess.content_loaded');
        }).find('div.pager').hide();
      });
  
      $('#top a.toggle').each(function() {
        var $this = $(this);
        var $p = $this.parent();
        $this.click(function() {
          $p.toggleClass('shown');
          $p.siblings('.shown').removeClass('shown');
          setTimeout(function() {
            $p.click(function(e) {
              e.stopPropagation();
            });
            $('html').one('click', function(e) {
              $p.removeClass('shown').off('click');
            });
          }, 10);
          return false;
        });
      });
  
      var acceptLanguages = $('body').data('accept-languages');
      if (acceptLanguages) {
        $('#top .lichess_language').one('mouseover', function() {
          var $links = $(this).find('.language_links'),
            langs = acceptLanguages.split(',');
          $.ajax({
            url: $links.data('url'),
            cache: false,
            success: function(list) {
              $links.prepend(list.map(function(lang) {
                var klass = $.fp.contains(langs, lang[0]) ? 'class="accepted"' : '';
                return '<li><button type="submit" ' + klass + '" name="lang" value="' + lang[0] + '">' + lang[1] + '</button></li>';
              }).join(''));
            }
          });
        });
      }
  
      $('#incomplete_translation a.close').one('click', function() {
        $(this).parent().remove();
      });
  
      $('#translation_call .close').click(function() {
        $.post($(this).data("href"));
        $(this).parent().fadeOut(500);
        return false;
      });
  
      $('a.delete, input.delete').click(function() {
        return confirm('Delete?');
      });
      $('input.confirm, button.confirm').click(function() {
        return confirm('Confirm this action?');
      });
  
      $('div.content').on('click', 'a.bookmark', function() {
        var t = $(this).toggleClass("bookmarked");
        $.post(t.attr("href"));
        var count = (parseInt(t.text(), 10) || 0) + (t.hasClass("bookmarked") ? 1 : -1);
        t.find('span').html(count > 0 ? ' ' + count : "");
        return false;
      });
  
      $("#import_game form").submit(function() {
        var pgn = $(this).find('textarea').val();
        var nbMoves = parseInt(pgn.replace(/\n/g, ' ').replace(/^.+\s(\d+)\..+$/, '$1'), 10);
        var delay = 50;
        var duration = nbMoves * delay * 2.1 + 1000;
        $(this).find('button').hide().end()
          .find('.error').hide().end()
          .find('.progression').show().animate({
            width: '100%'
          }, duration);
        return true;
      });
  
      // minimal touchscreen support for topmenu
      if ("ontouchstart" in window)
        $('#topmenu').on('click', '> section > a', function() {
          return false;
        });
  
      $('#ham-plate').click(function() {
        document.body.classList.toggle('fpmenu');
      });
      $('#top button.signin').click(function() {
        $('#ham-plate').click();
        $('#fpmenu input.username').focus();
      });
      if (location.hash === '#fpmenu') $('#ham-plate').click();
      Mousetrap.bind('esc', function() {
        $('#ham-plate').click();
        return false;
      });
      Mousetrap.bind('g h', function() {
        location.href = '/';
      });
      // konami code!
      Mousetrap.bind('up up down down left right left right b a', function() {
        if (!document.getElementById('konami')) {
          $('body').prepend($('<div id="konami"></div>'));
        }
        $('#konami').show(800);
        setTimeout(function() {
          $('#konami').hide(800);
        }, 3000);
      });
    });
  
    $.lazy = function(factory) {
      var loaded = {};
      return function(key) {
        if (!loaded[key]) loaded[key] = factory(key);
        return loaded[key];
      };
    };
  
    $.sound = (function() {
      var baseUrl = $('body').data('sound-dir') + '/';
      Howler.volume(lichess.storage.get('sound-volume') || 0.7);
      var names = {
        dong: 'dong2',
        move: 'move3',
        take: 'take2',
        explode: 'explosion',
        lowtime: 'lowtime'
      };
      var volumes = {
        lowtime: 0.5,
        explode: 0.35
      };
      var get = new $.lazy(function(k) {
        return new Howl({
          src: ['ogg', 'mp3'].map(function(ext) {
            return baseUrl + names[k] + '.' + ext;
          }),
          volume: volumes[k] || 1
        });
      });
      var $control = $('#sound_control');
      var $toggle = $('#sound_state');
      var enabled = function() {
        return lichess.storage.get('sound') !== 'no';
      };
      $control.add($toggle).toggleClass('sound_state_on', enabled());
      var player = function(s) {
        return function() {
          if (enabled()) get(s).play();
        };
      }
      var play = {};
      Object.keys(names).forEach(function(name) {
        play[name] = function() {
          if (enabled()) get(name).play();
        }
      });
      var setVolume = function(v) {
        lichess.storage.set('sound-volume', v);
        Howler.volume(v);
      };
      var manuallySetVolume = $.fp.debounce(function(v) {
        setVolume(v);
        play.move(true);
      }, 50);
      $toggle.click(function() {
        var enab = !enabled();
        lichess.storage.set('sound', enab ? 'yes' : 'no');
        $control.add($toggle).toggleClass('sound_state_on', enab);
        play.dong();
        return false;
      });
      $toggle.one('mouseover', function() {
        $toggle.parent().find('.slider').slider({
          orientation: "vertical",
          min: 0,
          max: 1,
          range: 'min',
          step: 0.01,
          value: Howler.volume(),
          slide: function(e, ui) {
            manuallySetVolume(ui.value);
          }
        });
      });
  
      return play;
    })();
  
    $.fn.orNot = function() {
      return this.length === 0 ? false : this;
    };
  
    $.trans = function() {
      var str = lichess_translations[arguments[0]];
      if (!str) return arguments[0];
      Array.prototype.slice.call(arguments, 1).forEach(function(arg) {
        str = str.replace('%s', arg);
      });
      return str;
    };
  
    function urlToLink(text) {
      var exp = /\bhttp:\/\/(?:[a-z]{0,3}\.)?(lichess\.org[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
      return text.replace(exp, "<a href='http://$1'>$1</a>");
    }
  
    var startTournamentClock = function() {
      $("div.game_tournament div.clock").each(function() {
        $(this).clock({
          time: parseFloat($(this).data("time"))
        });
      });
    };
  
    /////////////
    // game.js //
    /////////////
  
    function startRound(element, cfg) {
      var data = cfg.data;
      if (data.player.spectator && lichess.openInMobileApp(data.game.id)) return;
      var round;
      if (data.tournament) $('body').data('tournament-id', data.tournament.id);
      lichess.socket = new lichess.StrongSocket(
        data.url.socket,
        data.player.version, {
          options: {
            name: "round"
          },
          params: {
            ran: "--ranph--",
            userTv: $('.user_tv').data('user-tv')
          },
          receive: function(t, d) {
            round.socketReceive(t, d);
          },
          events: {
            crowd: function(e) {
              $watchers.watchers("set", e.watchers);
            },
            tvSelect: function(o) {
              if (data.tv && data.tv.channel == o.channel) lichess.reload();
              else $('#tv_channels a.' + o.channel + ' span').text(
                o.player ? o.player.name + ' (' + o.player.rating + ')' : 'Anonymous');
            },
            end: function() {
              var url = data.tv ? ['/tv', data.tv.channel, data.game.id, data.player.color, 'sides'].join('/') : ((
                data.player.spectator ? cfg.routes.Round.sidesWatcher : cfg.routes.Round.sidesPlayer
              )(data.game.id, data.player.color).url);
              $.ajax({
                url: url,
                cache: false,
                success: function(html) {
                  var $html = $(html);
                  $('#site_header div.side').replaceWith($html.find('>.side'));
                  $('#lichess div.crosstable').replaceWith($html.find('>.crosstable'));
                  $('body').trigger('lichess.content_loaded');
                  startTournamentClock();
                }
              });
            },
            checkCount: function(e) {
              $('div.check_count')
                .find('.white').text(e.black).end()
                .find('.black').text(e.white);
            },
            tournamentStanding: function(id) {
              $.ajax({
                url: '/tournament/' + id + '/game-standing',
                cache: false,
                success: function(html) {
                  $('#site_header div.game_tournament').replaceWith(html);
                  startTournamentClock();
                }
              });
            }
          }
        });
      cfg.element = element.querySelector('.round');
      cfg.socketSend = lichess.socket.send.bind(lichess.socket);
      round = LichessRound(cfg);
      $('.crosstable', element).prependTo($('.underboard .center', element)).show();
      $('#chat').chat({
        messages: data.chat,
        initialNote: data.note,
        gameId: data.game.id
      });
      var $watchers = $('#site_header div.watchers').watchers();
      var $nowPlaying = $('#now_playing');
      startTournamentClock();
      var loadPlaying = function() {
        var $moveOn = $nowPlaying.find('.move_on').click(function() {
          setMoveOn(round.moveOn.toggle());
        });
        var setMoveOn = function(value) {
          $moveOn.toggleClass('enabled', value);
        };
        setMoveOn(round.moveOn.get());
      };
      loadPlaying();
      $nowPlaying.on('click', '>a', function() {
        lichess.hasToReload = true;
        return true;
      });
      if (location.pathname.lastIndexOf('/round-next/', 0) === 0)
        window.history.replaceState(null, null, '/' + data.game.id);
    }
  
    function startPrelude(element, cfg) {
      var data = cfg.data;
      if (data.player.spectator && lichess.openInMobileApp(data.game.id)) return;
      lichess.socket = new lichess.StrongSocket(
        data.url.socket,
        data.player.version, {
          options: {
            name: "prelude"
          },
          params: {
            ran: "--ranph--"
          },
          events: {
            declined: function() {
              $('#challenge_await').remove();
              $('#challenge_declined').show();
            }
          }
        });
  
      Chessground(element.querySelector('.lichess_board'), {
        viewOnly: true,
        fen: data.game.fen,
        orientation: data.player.color,
        check: data.game.check,
        coordinates: data.pref.coords !== 0,
        highlight: {
          check: data.pref.highlight
        },
        disableContextMenu: true
      });
      setTimeout(function() {
        $('.lichess_overboard_wrap', element).addClass('visible');
      }, 100);
      $('#challenge_await').each(function() {
        setInterval(function() {
          $('#challenge_await').each(function() {
            lichess.socket.send('challenge', $(this).data('user'));
          });
        }, 1500);
      });
    }
  
    $.widget("lichess.watchers", {
      _create: function() {
        this.list = this.element.find("span.list");
        this.number = this.element.find("span.number");
      },
      set: function(users) {
        var self = this;
        if (users.length > 0) {
          self.list.html(users.map(function(u) {
            return u.indexOf('(') === -1 ? $.userLink(u) : u.replace(/\s\(1\)/, '');
          }).join(", "));
          var nb = 0;
          users.forEach(function(u) {
            nb += (u.indexOf('(') === -1 ? 1 : parseInt(u.replace(/^.+\((\d+)\)$/, '$1')));
          });
          self.number.html(nb);
          self.element.show();
        } else {
          self.element.hide();
        }
      }
    });
  
    $.widget("lichess.friends", {
      _create: function() {
        var self = this;
        self.$list = self.element.find("div.list");
        self.$title = self.element.find('.title').click(function() {
          self.element.find('.content_wrap').toggle(100, function() {
            lichess.storage.set('friends-hide', $(this).is(':visible') ? 0 : 1);
          });
        });
        if (lichess.storage.get('friends-hide') == 1) self.$title.click();
        self.$nbOnline = self.$title.find('.online');
        self.$nobody = self.element.find("div.nobody");
        self.set(self.element.data('preload').split(','));
      },
      repaint: function() {
        this.users = $.unique(this.users).filter(function(u) {
          return u !== '';
        });
        this.$nbOnline.text(this.users.length);
        this.$nobody.toggle(this.users.length === 0);
        this.$list.html(this.users.map(this._renderUser).join(""));
        $('body').trigger('lichess.content_loaded');
      },
      set: function(us) {
        this.users = us || [];
        this.repaint();
      },
      enters: function(user) {
        this.users.push(user);
        this.repaint();
      },
      leaves: function(user) {
        this.users = this.users.filter(function(u) {
          return u != user
        });
        this.repaint();
      },
      _renderUser: function(user) {
        var id = $.fp.contains(user, ' ') ? user.split(' ')[1] : user;
        return '<a class="ulpt" href="/@/' + id + '">' + user + '</a>';
      }
    });
  
    $.widget("lichess.chat", {
      _create: function() {
        this.options = $.extend({
          messages: [],
          initialNote: '',
          gameId: null
        }, this.options);
        var self = this;
        var $parent = self.element.parent();
        self.$msgs = self.element.find('.messages');
        self.withMsgs = !!self.$msgs.length;
        if (self.withMsgs) {
          self.$msgs.on('click', 'a', function() {
            $(this).attr('target', '_blank');
          });
          var $form = self.element.find('form');
          var $input = self.element.find('input.lichess_say');
  
          // send a message
          $form.submit(function() {
            var text = $.trim($input.val());
            if (!text) return false;
            if (text.length > 140) {
              alert('Max length: 140 chars. ' + text.length + ' chars used.');
              return false;
            }
            $input.val('');
            lichess.socket.send('talk', text);
            return false;
          });
  
          self.element.find('a.send').click(function() {
            $input.trigger('click');
            $form.submit();
          });
  
          // toggle the chat
          var $toggle = $parent.find('input.toggle_chat');
          $toggle.change(function() {
            var enabled = $toggle.is(':checked');
            self.element.toggleClass('hidden', !enabled);
            if (!enabled) lichess.storage.set('nochat', 1);
            else lichess.storage.remove('nochat');
          });
          $toggle[0].checked = lichess.storage.get('nochat') != 1;
          if (!$toggle[0].checked) {
            self.element.addClass('hidden');
          }
          if (self.options.messages.length > 0) self._appendMany(self.options.messages);
        }
  
        $panels = self.element.find('div.chat_panels > div');
        $menu = $parent.find('.chat_menu');
        $menu.on('click', 'a', function() {
          var panel = $(this).data('panel');
          $(this).siblings('.active').removeClass('active').end().addClass('active');
          $panels.removeClass('active').filter('.' + panel).addClass('active');
        }).find('a[data-panel=preferences]').one('click', function() {
          self.element.find('.preferences form').each(function() {
            var $form = $(this);
            $form.find('input').change(function() {
              $.ajax({
                url: $form.attr('action'),
                method: $form.attr('method'),
                data: $form.serialize()
              });
            });
          });
        });
        if (lichess.storage.get('game.settings.seen')) $menu.find('a:first').click();
        else {
          lichess.storage.set('game.settings.seen', 1);
          $menu.find('a[data-panel=preferences]').click();
        }
  
        $notes = self.element.find('.notes textarea');
        if (self.options.gameId && $notes.length) {
          $notes.on('change keyup paste', $.fp.debounce(function() {
            $.post('/' + self.options.gameId + '/note', {
              text: $notes.val()
            });
          }, 1000));
          $notes.val(self.options.initialNote || '');
        }
      },
      append: function(msg) {
        this._appendHtml(this._render(msg));
      },
      _appendMany: function(objs) {
        var self = this,
          html = "";
        $.each(objs, function() {
          html += self._render(this);
        });
        self._appendHtml(html);
      },
      _render: function(msg) {
        var user, sys = false;
        if (msg.c) {
          user = '<span class="color">[' + msg.c + ']</span>';
        } else if (msg.u == 'lichess') {
          sys = true;
          user = '<span class="system"></span>';
        } else {
          user = '<span class="user">' + $.userLinkLimit(msg.u, 14) + '</span>';
        }
        return '<li class="' + (sys ? 'system trans_me' : '') + (msg.r ? ' troll' : '') + '">' + user + urlToLink(msg.t) + '</li>';
      },
      _appendHtml: function(html) {
        if (!html) return;
        this.$msgs.each(function(i, el) {
          var autoScroll = (el.scrollTop == 0 || (el.scrollTop > (el.scrollHeight - el.clientHeight - 50)));
          $(el).append(html);
          if (autoScroll) el.scrollTop = 999999;
        });
        $('body').trigger('lichess.content_loaded');
      }
    });
  
    $.widget("lichess.clock", {
      _create: function() {
        var self = this;
        this.options.time = this.options.time * 1000;
        this.timeEl = this.element.find('>.time')[0];
        var end_time = new Date().getTime() + self.options.time;
        var tick = function() {
          var current_time = Math.round(end_time - new Date().getTime());
          if (current_time <= 0) {
            clearInterval(self.options.interval);
            current_time = 0;
          }
          self.options.time = current_time;
          self._show();
        };
        self.options.interval = setInterval(tick, 1000);
        tick();
      },
      destroy: function() {
        this.stop();
        $.Widget.prototype.destroy.apply(this);
      },
      _show: function() {
        this.timeEl.innerHTML = this._formatDate(new Date(this.options.time));
      },
      _formatDate: function(date) {
        var minutes = this._prefixInteger(date.getUTCMinutes(), 2);
        var seconds = this._prefixInteger(date.getSeconds(), 2);
        var b = function(x) {
          return '<b>' + x + '</b>';
        };
        if (this.options.time >= 3600000) {
          var hours = this._prefixInteger(date.getUTCHours(), 2);
          return b(hours) + ':' + b(minutes) + ':' + b(seconds);
        } else {
          return b(minutes) + ':' + b(seconds);
        }
      },
      _prefixInteger: function(num, length) {
        return (num / Math.pow(10, length)).toFixed(length).substr(2);
      }
    });
  
    /////////////////
    // gamelist.js //
    /////////////////
  
    $(function() {
      $('body').on('lichess.content_loaded', lichess.parseFen);
  
      var socketOpened = false;
  
      function startWatching() {
        if (!socketOpened) return;
        var ids = [];
        $('.mini_board.live').removeClass("live").each(function() {
          ids.push(this.getAttribute("data-live"));
        });
        if (ids.length) {
          lichess.socket.send("startWatching", ids.join(" "));
        }
      }
      $('body').on('lichess.content_loaded', startWatching);
      $('body').on('socket.open', function() {
        socketOpened = true;
        startWatching();
      });
  
      setTimeout(function() {
        lichess.parseFen();
        $('div.checkmateCaptcha').each(function() {
          var $captcha = $(this);
          var $board = $captcha.find('.mini_board');
          var $input = $captcha.find('input').val('');
          var cg = $board.data('chessground');
          var dests = JSON.parse(lichess.readServerFen($board.data('x')));
  
          cg.set({
            turnColor: cg.getOrientation(),
            movable: {
              free: false,
              dests: dests,
              color: cg.getOrientation(),
              coordinates: false,
              events: {
                after: function(orig, dest) {
                  $captcha.removeClass("success failure");
                  submit(orig + ' ' + dest);
                }
              }
            },
            disableContextMenu: true
          });
  
          var submit = function(solution) {
            $input.val(solution);
            $.ajax({
              url: $captcha.data('check-url'),
              data: {
                solution: solution
              },
              success: function(data) {
                $captcha.toggleClass('success', data == 1);
                $captcha.toggleClass('failure', data != 1);
                if (data == 1) $board.data('chessground').stop();
                else setTimeout(function() {
                  lichess.parseFen($board);
                  $board.data('chessground').set({
                    turnColor: cg.getOrientation(),
                    movable: {
                      dests: dests
                    }
                  });
                }, 300);
              }
            });
          };
        });
      }, 200);
    });
  
    function startLobby(element, cfg) {
      var lobby;
      var nbRoundSpread = $.spreadNumber(
        document.querySelector('#site_baseline span'),
        4,
        function() {
          return lichess.socket.pingInterval();
        });
      var onFirstConnect = function() {
        var gameId = lichess.getParameterByName('hook_like');
        if (!gameId) return;
        $.post('/setup/hook/' + lichess.StrongSocket.sri + '/like/' + gameId);
        lobby.setTab('real_time');
        window.history.replaceState(null, null, '/');
      };
      lichess.socket = new lichess.StrongSocket(
        '/lobby/socket/v1',
        cfg.data.version, {
          receive: function(t, d) {
            lobby.socketReceive(t, d);
          },
          events: {
            reload_timeline: function() {
              $.ajax({
                url: $("#timeline").data('href'),
                cache: false,
                success: function(html) {
                  $('#timeline').html(html);
                  $('body').trigger('lichess.content_loaded');
                }
              });
            },
            streams: function(html) {
              $('#streams_on_air').html(html);
            },
            featured: function(o) {
              $('#featured_game').html(o.html);
              $('body').trigger('lichess.content_loaded');
            },
            redirect: function(e) {
              lobby.setRedirecting();
              $.redirect(e);
            },
            tournaments: function(data) {
              $("#enterable_tournaments").html(data);
              $('body').trigger('lichess.content_loaded');
            },
            simuls: function(data) {
              $("#enterable_simuls").html(data).parent().toggle($('#enterable_simuls tr').length > 0);
              $('body').trigger('lichess.content_loaded');
            },
            reload_forum: function() {
              var $newposts = $("div.new_posts");
              setTimeout(function() {
                $.ajax({
                  url: $newposts.data('url'),
                  cache: false,
                  success: function(data) {
                    $newposts.find('ol').html(data).end().scrollTop(0);
                    $('body').trigger('lichess.content_loaded');
                  }
                });
              }, Math.round(Math.random() * 5000));
            },
            nbr: nbRoundSpread,
            fen: function(e) {
              lichess.StrongSocket.defaults.events.fen(e);
              lobby.gameActivity(e.id);
            }
          },
          options: {
            name: 'lobby',
            onFirstConnect: onFirstConnect
          }
        });
  
      cfg.socketSend = lichess.socket.send.bind(lichess.socket);
      lobby = LichessLobby(element, cfg);
  
      var $startButtons = $('#start_buttons');
  
      function sliderTime(v) {
        if (v <= 20) return v;
        switch (v) {
          case 21:
            return 25;
          case 22:
            return 30;
          case 23:
            return 35;
          case 24:
            return 40;
          case 25:
            return 45;
          case 26:
            return 60;
          case 27:
            return 90;
          case 28:
            return 120;
          case 29:
            return 150;
          default:
            return 180;
        }
      }
  
      function sliderDays(v) {
        if (v <= 3) return v;
        switch (v) {
          case 4:
            return 5;
          case 5:
            return 7;
          case 6:
            return 10;
          default:
            return 14;
        }
      }
  
      function sliderInitVal(v, f, max) {
        for (var i = 0; i < max; i++) {
          if (f(i) === v) return i;
        }
      }
  
      function prepareForm() {
        var $form = $('.lichess_overboard');
        var $timeModeSelect = $form.find('#timeMode');
        var $modeChoicesWrap = $form.find('.mode_choice');
        var $modeChoices = $modeChoicesWrap.find('input');
        var $casual = $modeChoices.eq(0),
          $rated = $modeChoices.eq(1);
        var $variantSelect = $form.find('#variant');
        var $fenPosition = $form.find(".fen_position");
        var $timeInput = $form.find('.time_choice input');
        var $incrementInput = $form.find('.increment_choice input');
        var $daysInput = $form.find('.days_choice input');
        var isHook = $form.hasClass('game_config_hook');
        var $ratings = $form.find('.ratings > div');
        var randomColorVariants = $form.data('random-color-variants').split(',');
        var toggleButtons = function() {
          var timeMode = $timeModeSelect.val();
          var rated = $rated.prop('checked');
          var timeOk = timeMode != '1' || $timeInput.val() > 0 || $incrementInput.val() > 0;
          var ratedOk = !isHook || !rated || timeMode != '0';
          if (timeOk && ratedOk) {
            $form.find('.color_submits button').show();
            $form.find('.color_submits button:not(.random)').toggle(!rated || randomColorVariants.indexOf($variantSelect.val()) === -1);
          } else
            $form.find('.color_submits button').hide();
        };
        var showRating = function() {
          var timeMode = $timeModeSelect.val();
          var key;
          switch ($variantSelect.val()) {
            case '1':
              if (timeMode == '1') {
                var time = $timeInput.val() * 60 + $incrementInput.val() * 40;
                if (time < 180) key = 'bullet';
                else if (time < 480) key = 'blitz';
                else key = 'classical';
              } else key = 'correspondence';
              break;
            case '2':
              key = 'chess960';
              break;
            case '4':
              key = 'kingOfTheHill';
              break;
            case '5':
              key = 'threeCheck';
              break;
            case '6':
              key = 'antichess'
              break;
            case '7':
              key = 'atomic'
              break;
            case '8':
              key = "horde"
              break;
          }
          $ratings.hide().filter('.' + key).show();
        };
        if (isHook) {
          var $formTag = $form.find('form');
          if ($form.data('anon')) {
            $timeModeSelect.val(1)
              .children('.timeMode_2, .timeMode_0')
              .prop('disabled', true)
              .attr('title', $.trans('You need an account to do that'));
          }
          var ajaxSubmit = function(color) {
            $.ajax({
              url: $formTag.attr('action').replace(/uid-placeholder/, lichess.StrongSocket.sri),
              data: $formTag.serialize() + "&color=" + color,
              type: 'post'
            });
            $form.find('a.close').click();
            lobby.setTab($timeModeSelect.val() === '1' ? 'real_time' : 'seeks');
            return false;
          };
          $formTag.find('.color_submits button').click(function() {
            return ajaxSubmit($(this).val());
          }).attr('disabled', false);
          $formTag.submit(function() {
            return ajaxSubmit('random');
          });
        }
        $form.find('div.buttons').buttonset().disableSelection();
        $form.find('button.submit').button().disableSelection();
        $timeInput.add($incrementInput).each(function() {
          var $input = $(this),
            $value = $input.siblings('span');
          $input.hide().after($('<div>').slider({
            value: sliderInitVal(parseInt($input.val()), sliderTime, 100),
            min: 0,
            max: 30,
            range: 'min',
            step: 1,
            slide: function(event, ui) {
              var time = sliderTime(ui.value);
              $value.text(time);
              $input.attr('value', time);
              showRating();
              toggleButtons();
            }
          }));
        });
        $daysInput.each(function() {
          var $input = $(this),
            $value = $input.siblings('span');
          $input.hide().after($('<div>').slider({
            value: sliderInitVal(parseInt($input.val()), sliderDays, 20),
            min: 1,
            max: 7,
            range: 'min',
            step: 1,
            slide: function(event, ui) {
              var days = sliderDays(ui.value);
              $value.text(days);
              $input.attr('value', days);
            }
          }));
        });
        $form.find('.rating_range').each(function() {
          var $this = $(this);
          var $input = $this.find("input");
          var $span = $this.siblings("span.range");
          var min = $input.data("min");
          var max = $input.data("max");
          var values = $input.val() ? $input.val().split("-") : [min, max];
  
          $span.text(values.join(' - '));
          $this.slider({
            range: true,
            min: min,
            max: max,
            values: values,
            step: 50,
            slide: function(event, ui) {
              $input.val(ui.values[0] + "-" + ui.values[1]);
              $span.text(ui.values[0] + " - " + ui.values[1]);
            }
          });
          var $ratingRangeConfig = $this.parent();
          $modeChoices.add($form.find('.members_only input')).on('change', function() {
            var rated = $rated.prop('checked');
            var membersOnly = $form.find('.members_only input').prop('checked');
            $ratingRangeConfig.toggle(rated || membersOnly);
            $form.find('.members_only').toggle(!rated);
            toggleButtons();
          }).trigger('change');
        });
        $timeModeSelect.on('change', function() {
          var timeMode = $(this).val();
          $form.find('.time_choice, .increment_choice').toggle(timeMode == '1');
          $form.find('.days_choice').toggle(timeMode == '2');
          toggleButtons();
          showRating();
        }).trigger('change');
        var $ratingRangeConfig = $form.find('.rating_range_config');
        var $fenInput = $fenPosition.find('input');
  
        var validateFen = $.fp.debounce(function() {
          $fenInput.removeClass("success failure");
          var fen = $fenInput.val();
          if (fen) {
            $.ajax({
              url: $fenInput.parent().data('validate-url'),
              data: {
                fen: fen
              },
              success: function(data) {
                $fenInput.addClass("success");
                $fenPosition.find('.preview').html(data);
                $fenPosition.find('a.board_editor').each(function() {
                  $(this).attr('href', $(this).attr('href').replace(/editor\/.+$/, "editor/" + fen));
                });
                $('body').trigger('lichess.content_loaded');
              },
              error: function() {
                $fenInput.addClass("failure");
                $fenPosition.find('.preview').html("");
              }
            });
          }
        }, 200);
        $fenInput.on('keyup', validateFen);
  
        $variantSelect.on('change', function() {
          var fen = $(this).val() == '3';
          $fenPosition.toggle(fen);
          $modeChoicesWrap.toggle(!fen);
          if (fen) $casual.click();
          showRating();
          toggleButtons();
        }).trigger('change');
  
        $form.find('div.level').each(function() {
          var $infos = $(this).find('.ai_info > div');
          $(this).find('label').mouseenter(function() {
            $infos.hide().filter('.' + $(this).attr('for')).show();
          });
          $(this).find('#config_level').mouseleave(function() {
            var level = $(this).find('input:checked').val();
            $infos.hide().filter('.level_' + level).show();
          }).trigger('mouseout');
        });
  
        $form.find('a.close.icon').click(function() {
          $form.remove();
          $startButtons.find('a.active').removeClass('active');
          return false;
        });
      }
  
      $startButtons.find('a').not('.disabled').click(function() {
        $(this).addClass('active').siblings().removeClass('active');
        $('.lichess_overboard').remove();
        $.ajax({
          url: $(this).attr('href'),
          cache: false,
          success: function(html) {
            $('.lichess_overboard').remove();
            $('#hooks_wrap').prepend(html);
            prepareForm();
            $('body').trigger('lichess.content_loaded');
          },
          error: function() {
            location.reload();
          }
        });
        return false;
      });
  
      if (['#ai', '#friend', '#hook'].indexOf(location.hash) !== -1) {
        $startButtons
          .find('a.config_' + location.hash.replace('#', ''))
          .each(function() {
            $(this).attr("href", $(this).attr("href") + location.search);
          }).click();
  
        if (location.hash === '#hook') {
          if (/time=realTime/.test(location.search))
            lobby.setTab('real_time');
          else if (/time=correspondence/.test(location.search))
            lobby.setTab('seeks');
        }
  
        window.history.replaceState(null, null, '/');
      }
    };
  
    ///////////////////
    // tournament.js //
    ///////////////////
  
    $(function() {
      if (lichess.tournamentSchedule) {
        lichess.StrongSocket.defaults.params.flag = "tournament";
        lichess.StrongSocket.defaults.events.reload = lichess.tournamentSchedule.update;
      }
    });
  
    function startTournament(element, cfg) {
      $('body').data('tournament-id', cfg.data.id);
      var $watchers = $("div.watchers").watchers();
      if (typeof lichess_chat !== 'undefined') $('#chat').chat({
        messages: lichess_chat
      });
      var tournament;
      lichess.socket = new lichess.StrongSocket(
        '/tournament/' + cfg.data.id + '/socket/v1', cfg.socketVersion, {
          receive: function(t, d) {
            tournament.socketReceive(t, d)
          },
          events: {
            crowd: function(data) {
              $watchers.watchers("set", data);
            }
          },
          options: {
            name: "tournament"
          }
        });
      cfg.socketSend = lichess.socket.send.bind(lichess.socket);
      tournament = LichessTournament(element, cfg);
    };
  
    ///////////////////
    // simul.js //
    ///////////////////
  
    $(function() {
  
      var $simulList = $('#simul_list');
      if ($simulList.length) {
        // handle simul list
        lichess.StrongSocket.defaults.params.flag = "simul";
        lichess.StrongSocket.defaults.events.reload = function() {
          $simulList.load($simulList.data("href"), function() {
            $('body').trigger('lichess.content_loaded');
          });
        };
        $('#site_header .help a.more').click(function() {
          $.modal($(this).parent().find('div.more').clone()).addClass('card');
        });
        return;
      }
    });
  
    function startSimul(element, cfg) {
      $('body').data('simul-id', cfg.data.id);
      var $watchers = $("div.watchers").watchers();
      if (typeof lichess_chat !== 'undefined') $('#chat').chat({
        messages: lichess_chat
      });
      var simul;
      lichess.socket = new lichess.StrongSocket(
        '/simul/' + cfg.data.id + '/socket/v1', cfg.socketVersion, {
          receive: function(t, d) {
            simul.socketReceive(t, d)
          },
          events: {
            crowd: function(data) {
              $watchers.watchers("set", data);
            }
          },
          options: {
            name: "simul"
          }
        });
      cfg.socketSend = lichess.socket.send.bind(lichess.socket);
      simul = LichessSimul(element, cfg);
    };
  
    function startRelay(element, cfg) {
      var $watchers = $("div.watchers").watchers();
      if (typeof lichess_chat !== 'undefined') $('#chat').chat({
        messages: lichess_chat
      });
      var relay;
      lichess.socket = new lichess.StrongSocket(
        '/watch/' + cfg.data.id + '/socket/v1', cfg.socketVersion, {
          receive: function(t, d) {
            relay.socketReceive(t, d)
          },
          events: {
            crowd: function(data) {
              $watchers.watchers("set", data);
            }
          },
          options: {
            name: "relay"
          }
        });
      cfg.socketSend = lichess.socket.send.bind(lichess.socket);
      relay = LichessRelay(element, cfg);
    };
  
    ////////////////
    // analyse.js //
    ////////////////
  
    function startAnalyse(element, cfg) {
      var data = cfg.data;
      if (lichess.openInMobileApp(data.game.id)) return;
      if (data.chat) $('#chat').chat({
        messages: data.chat,
        initialNote: data.note,
        gameId: data.game.id
      });
      var $watchers = $('#site_header div.watchers').watchers();
      var analyse, $panels;
      lichess.socket = new lichess.StrongSocket(
        data.url.socket,
        data.player.version, {
          options: {
            name: "analyse"
          },
          params: {
            ran: "--ranph--",
            userTv: $('.user_tv').data('user-tv')
          },
          receive: function(t, d) {
            analyse.socketReceive(t, d);
          },
          events: {
            analysisAvailable: function() {
              $.sound.dong();
              location.reload();
            },
            crowd: function(event) {
              $watchers.watchers("set", event.watchers);
            }
          }
        });
  
      var $advChart = $("#adv_chart");
      var $timeChart = $("#movetimes_chart");
      var $inputFen = $('input.fen', element);
      var unselect = function(chart) {
        chart.getSelectedPoints().forEach(function(point) {
          point.select(false);
        });
      };
      var lastFen, lastPath;
      cfg.onChange = function(fen, path) {
        if (fen === lastFen) return;
        lastFen = fen = fen || lastFen;
        lastPath = path = path || lastPath;
        var chart, point;
        $inputFen.val(fen);
        if ($advChart.length) {
          chart = $advChart.highcharts();
          if (chart) {
            if (path.length > 1) unselect(chart);
            else {
              point = chart.series[0].data[path[0].ply - 1 - cfg.data.game.startedAtTurn];
              if (typeof point != "undefined") point.select();
              else unselect(chart);
            }
          }
        }
        if ($timeChart.length) {
          chart = $timeChart.highcharts();
          if (chart) {
            if (path.length > 1) unselect(chart);
            else {
              var white = path[0].ply % 2 !== 0;
              var serie = white ? 0 : 1;
              var turn = Math.floor((path[0].ply - 1 - cfg.data.game.startedAtTurn) / 2);
              point = chart.series[serie].data[turn];
              if (typeof point != "undefined") point.select();
              else unselect(chart);
            }
          }
        }
      };
      cfg.path = location.hash ? location.hash.replace(/#/, '') : '';
      cfg.element = element.querySelector('.analyse');
      cfg.socketSend = lichess.socket.send.bind(lichess.socket);
      analyse = LichessAnalyse(cfg);
      cfg.jumpToIndex = analyse.jumpToIndex;
  
      $('.underboard_content', element).appendTo($('.underboard .center', element)).show();
      $('.advice_summary', element).appendTo($('.underboard .right', element)).show();
  
      $panels = $('div.analysis_panels > div');
      var $menu = $('div.analysis_menu');
      var storageKey = 'lichess.analysis.panel';
      var setPanel = function(panel) {
        $menu.children('.active').removeClass('active').end().find('.' + panel).addClass('active');
        $panels.removeClass('active').filter('.' + panel).addClass('active');
        if (panel == 'move_times') try {
          $.renderMoveTimesChart();
        } catch (e) {}
      };
      $menu.on('click', 'a', function() {
        var panel = $(this).data('panel');
        lichess.storage.set(storageKey, panel);
        setPanel(panel);
      });
      if (cfg.data.analysis) setPanel('computer_analysis');
      else {
        var stored = lichess.storage.get(storageKey);
        if (stored && $menu.children('.' + stored).length) setPanel(stored);
        else if ($menu.children('.crosstable').length) $menu.children('.crosstable').click();
        else $menu.children(':first').click();
      }
  
      $panels.find('form.future_game_analysis').submit(function() {
        if ($(this).hasClass('must_login')) {
          if (confirm($.trans('You need an account to do that'))) {
            location.href = '/signup';
          }
          return false;
        }
        $.ajax({
          method: 'post',
          url: $(this).attr('action'),
          success: function(html) {
            $panels.filter('.panel.computer_analysis').html(html);
          }
        });
        return false;
      });
      $panels.find('div.pgn').click(function() {
        var range, selection;
        if (document.body.createTextRange) {
          range = document.body.createTextRange();
          range.moveToElementText($(this)[0]);
          range.select();
        } else if (window.getSelection) {
          selection = window.getSelection();
          range = document.createRange();
          range.selectNodeContents($(this)[0]);
          selection.removeAllRanges();
          selection.addRange(range);
        }
      });
    }
  
    ////////////////
    // user_analysis.js //
    ////////////////
  
    function startUserAnalysis(element, cfg) {
      var analyse;
      cfg.path = location.hash ? location.hash.replace(/#/, '') : '';
      cfg.element = element.querySelector('.analyse');
      lichess.socket = new lichess.StrongSocket('/socket', 0, {
        options: {
          name: "analyse"
        },
        params: {
          ran: "--ranph--"
        },
        receive: function(t, d) {
          analyse.socketReceive(t, d);
        }
      });
      cfg.socketSend = lichess.socket.send.bind(lichess.socket);
      analyse = LichessAnalyse(cfg);
    }
  
    /////////////// forum.js ////////////////////
  
    $('#lichess_forum').on('click', 'a.delete', function() {
      $.post($(this).attr("href"));
      $(this).closest(".post").slideUp(100);
      return false;
    });
  
    $('#lichess_forum').on('click', 'form.unsub button', function() {
      var $form = $(this).parent().toggleClass('on off');
      $.post($form.attr("action") + '?unsub=' + $(this).data('unsub'));
      return false;
    });
  
    $.idleTimer = function(delay, onIdle, onWakeUp) {
      var eventType = 'mousemove';
      var listening = false;
      var active = true;
      var lastSeenActive = new Date();
      var onActivity = function() {
        if (!active) onWakeUp();
        active = true;
        lastSeenActive = new Date();
        stopListening();
      };
      var startListening = function() {
        if (!listening) {
          document.addEventListener(eventType, onActivity);
          listening = true;
        }
      };
      var stopListening = function() {
        if (listening) {
          document.removeEventListener(eventType, onActivity);
          listening = false;
        }
      };
      setInterval(function() {
        if (active && new Date() - lastSeenActive > delay) {
          onIdle();
          active = false;
        }
        startListening();
      }, 5000);
    };
  
    $.modal = function(html) {
      var $wrap = $('<div id="modal-wrap">').html(html.clone().show());
      var $overlay = $('<div id="modal-overlay">').html($wrap);
      $overlay.one('click', $.modal.close);
      $wrap.click(function(e) {
        e.stopPropagation();
      });
      $('body').prepend($overlay);
      return $wrap;
    };
    $.modal.close = function() {
      $('#modal-overlay').remove();
    };
  })();
  