Welcome to Wargaming.net Wiki!
Variants

MediaWiki:Common.js

Jump to: navigation, search
Add theme class hook (html.wows-theme-dark/-light) for CSS-based theming
ShipLineNav: bump cacheVersion to 15.7.0 (data updated: Pan-America DD line + Affondatore)
 
(12 intermediate revisions by the same user not shown)
Line 136: Line 136:
   }, true);
   }, true);
})();
})();
/* ============================================================
  Ship pages: contextual Nation + Class links in the sidebar nav
  On Ship: namespace pages (ns 10000), read the page's categories
  to find its nation ("Ships of X") and class, then add links to
  the matching overview pages into the navigation menu.
  ============================================================ */
( function () {
'use strict';
if ( !window.mw || mw.config.get( 'wgNamespaceNumber' ) !== 10000 ) { return; }
var cats = mw.config.get( 'wgCategories' ) || [];
var CLASSES = [ 'Aircraft Carriers', 'Battleships', 'Cruisers', 'Destroyers', 'Submarines' ];
var nation = null, klass = null;
cats.forEach( function ( c ) {
if ( /^Ships of /.test( c ) ) { nation = c; }
if ( CLASSES.indexOf( c ) !== -1 ) { klass = c; }
} );
if ( !nation && !klass ) { return; }
var artPath = mw.config.get( 'wgArticlePath' );
function url( t ) { return artPath.replace( '$1', t.replace( / /g, '_' ) ); }
function addItem( menu, id, label, target ) {
if ( document.getElementById( id ) ) { return; }
var li = document.createElement( 'li' );
li.id = id;
li.className = 'b-left-menu_item mw-list-item';
var a = document.createElement( 'a' );
a.className = 'b-left-menu_link b-darklink';
a.href = url( target );
var point = document.createElement( 'span' );
point.className = 'b-left-menu_point';
point.title = label;
point.textContent = label;
a.appendChild( point );
li.appendChild( a );
menu.appendChild( li );
}
function inject() {
var menu = document.querySelector( '#p-navigation .b-left-menu' ) ||
document.querySelector( '#mw-panel .b-left-menu' );
if ( !menu ) { return; }
if ( nation ) { addItem( menu, 'n-ship-nation', nation, 'Ship:' + nation ); }
if ( klass ) { addItem( menu, 'n-ship-class', klass, 'Ship:' + klass ); }
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', inject );
} else {
inject();
}
}() );
/* ============================================================
  Article pages: move the table of contents into the sidebar
  On any page with a TOC (#toc), relocate it into #mw-panel so it
  scrolls with the reader (the sidebar is sticky). Pages without a
  TOC (e.g. ship stat pages using __NOTOC__) are unaffected.
  ============================================================ */
( function () {
'use strict';
var toc = document.getElementById( 'toc' );
var panel = document.getElementById( 'mw-panel' );
if ( !toc || !panel ) { return; }
var sec = document.createElement( 'div' );
sec.id = 'p-toc';
sec.className = 'b-sidebar_item';
sec.appendChild( toc );
panel.appendChild( sec );
}() );
/* ===================================================================
  Sidebar "Request Edit" link - open in new tab
  =================================================================== */
( function () {
'use strict';
function fixLink() {
var link = document.querySelector( '#n-Request-Edit a' );
if ( !link ) { return; }
link.setAttribute( 'target', '_blank' );
var relParts = ( link.getAttribute( 'rel' ) || '' ).split( /\s+/ ).filter( Boolean );
if ( relParts.indexOf( 'noopener' ) === -1 ) { relParts.push( 'noopener' ); }
link.setAttribute( 'rel', relParts.join( ' ' ) );
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', fixLink );
} else {
fixLink();
}
// Re-apply shortly after load in case other scripts modify the link's attributes afterward.
setTimeout( fixLink, 500 );
} )();
/**
* ================================================================
*  Tech-Tree Line Navigation  (added via Cowork, 2026-07-21)
* ================================================================
*  Adds a "Tech Tree Line" panel to the sidebar on Ship: pages,
*  reusing the skin's b-left-menu markup so it matches Navigation.
*  Data: MediaWiki:ShipLines.json (WG API encyclopedia/ships next_ships).
*  Ship: namespace only (ns 10000); premium/non-tree ships skipped.
*  Regenerate the JSON each patch and bump CONFIG.cacheVersion.
* ================================================================
*/
( function () {
'use strict';
var CONFIG = {
shipNamespace: 10000,
dataPage: 'MediaWiki:ShipLines.json',
cacheKey: 'wowsShipLines',
cacheVersionKey: 'wowsShipLinesVer',
cacheVersion: '15.7.0'
};
if ( mw.config.get( 'wgNamespaceNumber' ) !== CONFIG.shipNamespace ) { return; }
var shipName = mw.config.get( 'wgTitle' );
if ( !shipName ) { return; }
mw.loader.using( [ 'mediawiki.util' ] ).then( function () {
loadData().then( function ( data ) {
$( function () { setTimeout( function () { try { render( data ); } catch ( e ) { mw.log.warn( '[ShipLineNav] render failed:', e ); } }, 0 ); } );
} ).catch( function ( e ) { mw.log.warn( '[ShipLineNav] data load failed:', e ); } );
} );
function loadData() {
try {
if ( window.sessionStorage && sessionStorage.getItem( CONFIG.cacheVersionKey ) === CONFIG.cacheVersion ) {
var cached = sessionStorage.getItem( CONFIG.cacheKey );
if ( cached ) { return Promise.resolve( JSON.parse( cached ) ); }
}
} catch ( e ) {}
var url = mw.util.getUrl( CONFIG.dataPage, { action: 'raw' } );
return fetch( url, { credentials: 'same-origin' } ).then( function ( r ) { return r.json(); } ).then( function ( data ) {
try { sessionStorage.setItem( CONFIG.cacheKey, JSON.stringify( data ) ); sessionStorage.setItem( CONFIG.cacheVersionKey, CONFIG.cacheVersion ); } catch ( e ) {}
return data;
} );
}
function norm( s ) { return ( s || '' ).normalize( 'NFD' ).replace( /[̀-ͯ]/g, '' ).replace( /[^a-z0-9]/gi, '' ).toLowerCase(); }
function findShip( data ) {
var ships = data.ships || {}, id, exact = null, loose = null, want = norm( shipName );
for ( id in ships ) {
if ( ships[ id ].name === shipName ) { exact = ships[ id ]; break; }
if ( !loose && norm( ships[ id ].name ) === want ) { loose = ships[ id ]; }
}
return exact || loose;
}
function roman( t ) { var r = [ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI' ]; return r[ t ] || String( t ); }
function render( data ) {
var ship = findShip( data );
if ( !ship ) { return; }
var lineIds = ship.lines || [];
if ( !lineIds.length ) { return; }
var wrap = document.createElement( 'div' );
wrap.id = 'p-wows-line';
wrap.className = 'b-sidebar_item wows-line-portlet';
var h = document.createElement( 'h5' );
h.className = 'b-sidebar_title b-sidebar_title__opened';
h.textContent = 'Tech Tree Line';
wrap.appendChild( h );
var ul = document.createElement( 'ul' );
ul.className = 'b-left-menu b-left-menu__opened';
wrap.appendChild( ul );
var multi = lineIds.length > 1;
lineIds.forEach( function ( lid ) {
var line = ( data.lines || [] )[ lid ];
if ( !line ) { return; }
if ( multi ) {
var sub = document.createElement( 'li' );
sub.className = 'b-left-menu_item mw-list-item wows-line-subhead';
sub.textContent = line.line_name + ' line';
sub.style.cssText = 'opacity:.6;font-size:.85em;padding:4px 0 2px 20px;';
ul.appendChild( sub );
}
( line.ships || [] ).forEach( function ( s ) {
var li = document.createElement( 'li' );
li.className = 'b-left-menu_item mw-list-item';
var isCurrent = ( s.ship_id === ship.ship_id );
var a = document.createElement( 'a' );
a.className = 'b-left-menu_link b-darklink' + ( isCurrent ? ' wows-line-current' : '' );
if ( !isCurrent ) { a.href = mw.util.getUrl( 'Ship:' + s.name ); }
var pt = document.createElement( 'span' );
pt.className = 'b-left-menu_point';
var tier = document.createElement( 'span' );
tier.className = 'wows-line-tier';
tier.textContent = roman( s.tier );
tier.style.cssText = 'position:absolute;left:3px;top:50%;transform:translateY(-50%);width:22px;text-align:right;color:#AD7A07;font-weight:bold;';
pt.appendChild( tier );
pt.appendChild( document.createTextNode( s.name ) );
if ( isCurrent ) { a.style.fontWeight = 'bold'; pt.style.color = '#d4af37'; tier.style.color = '#d4af37'; }
a.appendChild( pt );
li.appendChild( a );
ul.appendChild( li );
} );
} );
var nav = document.querySelector( '#p-navigation, .mw-portlet-navigation, #p-Navigation' );
if ( nav && nav.parentNode ) { nav.parentNode.insertBefore( wrap, nav.nextSibling ); return; }
var sidebar = document.querySelector( '#mw-panel, .mw-panel, #column-one, .sidebar' );
if ( sidebar ) { sidebar.appendChild( wrap ); return; }
var content = document.querySelector( '#mw-content-text' );
if ( content && content.parentNode ) { content.parentNode.insertBefore( wrap, content ); }
}
}() );
/**
* ================================================================
*  Event Progress Bar  (added 2026-08-06)
* ================================================================
*  Template:Event progress renders a static, server-side bar. This
*  script replaces it with a live version: per-visitor local times,
*  an EU/NA/ASIA server switcher and a 60-second refresh. If this
*  script fails to run, the static bar remains and stays correct.
*
*  Attributes on div.wows-event-progress (ISO 8601, UTC, trailing Z):
*    data-name                      event title
*    data-eu-start / data-eu-end    per-server outer window (spend deadline)
*    data-na-start / data-na-end
*    data-asia-start / data-asia-end
*    data-start / data-end          single window if no server pairs
*    data-earn-days                  days currency can be earned; the
*                                    remainder becomes a spend-only tail
*    data-token-low / -high / -cap  pacing (300 / 1200 / 207600)
* ================================================================
*/
( function () {
'use strict';
var SELECTOR = '.wows-event-progress';
var TICK_MS  = 60000;
var HOUR    = 3600000;
var DAY      = 86400000;
var STORE    = 'wows-event-region';
var REGIONS  = [ 'eu', 'na', 'asia' ];
var LABELS  = { eu: 'EU', na: 'NA', asia: 'ASIA' };
var TOKEN_LOW  = 300;
var TOKEN_HIGH = 1200;
var TOKEN_CAP  = 207600;
var TOKEN_NOTE = 'Ceilings from daily combat missions only, assuming every mission is ' +
                'completed on every remaining day. Excludes Coal, Steel and Research Point ' +
                'conversion.';
var bars = [];
function detectRegion() {
var tz = '';
try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch ( e ) {}
if ( /^(America|US|Canada|Mexico|Brazil|Chile|Cuba|Jamaica|Atlantic)\b/.test( tz ) ) { return 'na'; }
if ( /^(Europe|Africa|Arctic|GB|Eire|Poland|Portugal|Turkey|Israel|Egypt|Libya|CET|EET|WET|MET)\b/.test( tz ) ) { return 'eu'; }
if ( /^(Asia|Australia|Pacific|Indian|NZ|Japan|Singapore|Hongkong|ROK|PRC|Iran)\b/.test( tz ) ) { return 'asia'; }
var off = -new Date().getTimezoneOffset();
if ( off <= -180 ) { return 'na'; }
if ( off >= 300 )  { return 'asia'; }
return 'eu';
}
function currentRegion() {
var v = null;
try { v = localStorage.getItem( STORE ); } catch ( e ) {}
return REGIONS.indexOf( v ) !== -1 ? v : detectRegion();
}
function setRegion( r ) {
try { localStorage.setItem( STORE, r ); } catch ( e ) {}
bars.forEach( render );
}
function fmtDate( ms ) {
return new Date( ms ).toLocaleDateString( undefined, { year: 'numeric', month: 'short', day: 'numeric' } );
}
function fmtFull( ms ) {
return new Date( ms ).toLocaleString( undefined, {
year: 'numeric', month: 'long', day: 'numeric',
hour: '2-digit', minute: '2-digit', timeZoneName: 'short'
} );
}
function plural( v, word ) { return v + ' ' + word + ( v === 1 ? '' : 's' ); }
function span( ms ) {
if ( ms >= DAY )  { return plural( Math.floor( ms / DAY ), 'day' ); }
if ( ms >= HOUR ) { return plural( Math.floor( ms / HOUR ), 'hour' ); }
return plural( Math.max( 1, Math.round( ms / 60000 ) ), 'minute' );
}
function n( v ) { return v.toLocaleString(); }
function readWindows( el ) {
var out = {}, any = false;
REGIONS.forEach( function ( r ) {
var s = Date.parse( el.getAttribute( 'data-' + r + '-start' ) );
var e = Date.parse( el.getAttribute( 'data-' + r + '-end' ) );
if ( !isNaN( s ) && !isNaN( e ) && e > s ) { out[ r ] = { s: s, e: e }; any = true; }
} );
if ( any ) { return out; }
var s1 = Date.parse( el.getAttribute( 'data-start' ) );
var e1 = Date.parse( el.getAttribute( 'data-end' ) );
if ( !isNaN( s1 ) && !isNaN( e1 ) && e1 > s1 ) { return { all: { s: s1, e: e1 } }; }
return null;
}
function build( el ) {
var win = readWindows( el );
if ( !win ) { return null; }  // bad dates: leave the static bar alone
el.innerHTML =
'<div class="wows-ep-head">' +
'<span class="wows-ep-name"></span>' +
'<span class="wows-ep-remain"></span>' +
'</div>' +
'<div class="wows-ep-track" role="progressbar" aria-valuemin="0" aria-valuemax="100">' +
'<div class="wows-ep-fill"></div>' +
'<div class="wows-ep-tail"></div>' +
'<div class="wows-ep-mark"></div>' +
'</div>' +
'<div class="wows-ep-foot">' +
'<span class="wows-ep-start"></span>' +
'<span class="wows-ep-pct"></span>' +
'<span class="wows-ep-end"></span>' +
'</div>' +
'<div class="wows-ep-meta">' +
'<span class="wows-ep-days">' +
'<span class="wows-ep-dayno"></span>' +
'<span class="wows-ep-spend"></span>' +
'</span>' +
'<span class="wows-ep-tokens"></span>' +
'<span class="wows-ep-servers"></span>' +
'</div>';
var nameEl  = el.querySelector( '.wows-ep-name' );
var nameTxt = el.getAttribute( 'data-name' ) || 'Event';
var nameUrl = el.getAttribute( 'data-link' );
if ( nameUrl ) {
var a = document.createElement( 'a' );
a.href = nameUrl;
a.textContent = nameTxt;
nameEl.appendChild( a );
} else {
nameEl.textContent = nameTxt;
}
function num( attr, dflt ) {
var v = parseFloat( el.getAttribute( attr ) );
return isNaN( v ) ? dflt : v;
}
var o = {
el: el, win: win,
tLow:    num( 'data-token-low',  TOKEN_LOW ),
tHigh:    num( 'data-token-high', TOKEN_HIGH ),
tCap:    num( 'data-token-cap',  TOKEN_CAP ),
earnDays: num( 'data-earn-days',  0 ),
fill:    el.querySelector( '.wows-ep-fill' ),
tail:    el.querySelector( '.wows-ep-tail' ),
mark:    el.querySelector( '.wows-ep-mark' ),
track:  el.querySelector( '.wows-ep-track' ),
remain:  el.querySelector( '.wows-ep-remain' ),
sLabel:  el.querySelector( '.wows-ep-start' ),
eLabel:  el.querySelector( '.wows-ep-end' ),
pct:    el.querySelector( '.wows-ep-pct' ),
dayNo:  el.querySelector( '.wows-ep-dayno' ),
spend:  el.querySelector( '.wows-ep-spend' ),
tokens:  el.querySelector( '.wows-ep-tokens' ),
srvWrap: el.querySelector( '.wows-ep-servers' ),
srvBtns: {}
};
var avail = REGIONS.filter( function ( r ) { return win[ r ]; } );
if ( avail.length > 1 ) {
var lbl = document.createElement( 'span' );
lbl.className = 'wows-ep-srv-label';
lbl.textContent = 'Server:';
o.srvWrap.appendChild( lbl );
avail.forEach( function ( r ) {
var b = document.createElement( 'button' );
b.type = 'button';
b.className = 'wows-ep-srv';
b.textContent = LABELS[ r ];
b.setAttribute( 'aria-pressed', 'false' );
b.setAttribute( 'title', 'Show ' + LABELS[ r ] + ' server dates' );
b.addEventListener( 'click', function () { setRegion( r ); } );
o.srvWrap.appendChild( b );
o.srvBtns[ r ] = b;
} );
} else {
o.srvWrap.style.display = 'none';
}
return o;
}
function render( o ) {
var r = currentRegion();
var w = o.win[ r ] || o.win.all || o.win[ Object.keys( o.win )[ 0 ] ];
var t = Date.now();
var earnEnd  = o.earnDays ? Math.min( w.e, w.s + o.earnDays * DAY ) : w.e;
var earnDays = o.earnDays || Math.round( ( w.e - w.s ) / DAY );
var hasTail  = earnEnd < w.e - HOUR;
var pct = Math.max( 0, Math.min( 100, ( t - w.s ) / ( w.e - w.s ) * 100 ) );
var shown = Math.round( pct );
if ( t < w.e && shown === 100 ) { shown = 99; }
if ( t > w.s && shown === 0 )  { shown = 1; }
var earnPct = ( earnEnd - w.s ) / ( w.e - w.s ) * 100;
o.fill.style.width = Math.min( pct, earnPct ).toFixed( 2 ) + '%';
o.tail.style.left  = earnPct.toFixed( 2 ) + '%';
o.tail.style.width = Math.max( 0, pct - earnPct ).toFixed( 2 ) + '%';
o.track.setAttribute( 'aria-valuenow', shown );
o.pct.textContent = shown + '% complete';
if ( hasTail ) {
o.mark.style.display = 'block';
o.mark.style.left = earnPct.toFixed( 2 ) + '%';
o.mark.title = 'Token earning ends ' + fmtFull( earnEnd );
}
o.el.classList.remove( 'wows-ep--upcoming', 'wows-ep--active', 'wows-ep--spend', 'wows-ep--ended' );
if ( t < w.s ) {
o.el.classList.add( 'wows-ep--upcoming' );
o.remain.textContent = 'Starts in ' + span( w.s - t );
o.sLabel.textContent = 'Starts ' + fmtDate( w.s );
o.pct.textContent    = 'Not started';
} else if ( t < earnEnd ) {
o.el.classList.add( 'wows-ep--active' );
o.remain.textContent = span( earnEnd - t ) + ' left to earn';
o.sLabel.textContent = 'Started ' + fmtDate( w.s );
} else if ( t < w.e ) {
o.el.classList.add( 'wows-ep--spend' );
o.remain.textContent = 'Earning closed \u00b7 ' + span( w.e - t ) + ' left to spend';
o.sLabel.textContent = 'Started ' + fmtDate( w.s );
} else {
o.el.classList.add( 'wows-ep--ended' );
o.remain.textContent = 'Event has ended';
o.sLabel.textContent = 'Ran from ' + fmtDate( w.s );
o.pct.textContent    = 'Complete';
}
o.eLabel.textContent = hasTail
? ( t >= earnEnd ? 'Earning ended ' : 'Earning ends ' ) + fmtDate( earnEnd )
: ( t >= w.e ? 'Ended ' : 'Ends ' ) + fmtDate( w.e );
o.sLabel.title = fmtFull( w.s );
o.eLabel.title = hasTail ? fmtFull( earnEnd ) : fmtFull( w.e );
var day = t < w.s ? 0 : Math.min( earnDays, Math.floor( ( t - w.s ) / DAY ) + 1 );
o.dayNo.textContent = t >= earnEnd
? 'Earning closed after ' + earnDays + ' days'
: 'Day ' + day + ' of ' + earnDays;
o.spend.textContent = hasTail
? ( t >= w.e ? 'Spending closed ' + fmtDate( w.e ) : 'Spend by ' + fmtDate( w.e ) )
: '';
o.spend.title = hasTail ? fmtFull( w.e ) : '';
var left    = Math.max( 0, earnDays - day );
var doneLow  = Math.min( o.tCap, day * o.tLow );
var doneHigh = Math.min( o.tCap, day * o.tHigh );
var leftLow  = Math.max( 0, Math.min( left * o.tLow,  o.tCap - doneLow ) );
var leftHigh = Math.max( 0, Math.min( left * o.tHigh, o.tCap - doneHigh ) );
o.tokens.innerHTML =
'<span class="wows-ep-tgrid">' +
'<span class="wows-ep-tr">Mission tokens</span>' +
'<span class="wows-ep-th">1 battle/day</span>' +
'<span class="wows-ep-th">5 battles/day</span>' +
'<span class="wows-ep-tr">earnable to date</span>' +
'<b>' + n( doneLow ) + '</b><b>' + n( doneHigh ) + '</b>' +
'<span class="wows-ep-tr">still remaining</span>' +
'<b>' + n( leftLow ) + '</b><b>' + n( leftHigh ) + '</b>' +
'</span>';
o.tokens.title = TOKEN_NOTE;
Object.keys( o.srvBtns ).forEach( function ( k ) {
o.srvBtns[ k ].setAttribute( 'aria-pressed', k === r ? 'true' : 'false' );
} );
}
function init() {
Array.prototype.forEach.call( document.querySelectorAll( SELECTOR ), function ( el ) {
var o = build( el );
if ( o ) { bars.push( o ); render( o ); }
} );
if ( !bars.length ) { return; }
function all() { bars.forEach( render ); }
setInterval( all, TICK_MS );
document.addEventListener( 'visibilitychange', function () {
if ( !document.hidden ) { all(); }
} );
window.addEventListener( 'storage', function ( e ) {
if ( e.key === STORE ) { all(); }
} );
}
if ( document.readyState === 'loading' ) {
document.addEventListener( 'DOMContentLoaded', init );
} else {
init();
}
}() );

Latest revision as of 14:41, 14 August 2026

/* Any JavaScript here will be loaded for all users on every page load. */

/* ============================================================
 Light / Dark Mode Toggle
 Adds a fixed pill (top-right) that switches between the
 current dark theme and the live-wiki light theme.
 Styled to match the Wargaming sign-in page mode-switcher.
 Preference is remembered via localStorage.
============================================================ */
( function () {
'use strict';

var STORAGE_KEY = 'wows-theme-mode';
var STYLE_ID    = 'wows-light-mode-styles';

var LIGHT_CSS = [
  'html, body, .b-main {',
  '  background-color: transparent !important;',
  '  background-image: url("https://wiki.wargaming.net/skins/globalwiki/images/bg.jpg?2026-01-22T09:18:20Z") !important;',
  '  background-repeat: repeat !important;',
  '  color: rgb(0, 0, 0) !important;',
  '}',
  '#firstHeading { color: rgb(56, 56, 56) !important; }',
  '.b-game-info > b, .b-game-info .b-heading2 { color: rgb(56, 56, 56) !important; }',
  '.mw-parser-output, .mw-body-content, #mw-content-text { color: rgb(0, 0, 0) !important; }',
  '#mw-panel, #mw-panel a, .b-sidebar, .b-sidebar a, .b-left-menu_item, .b-portlet-link { color: rgb(67, 99, 115) !important; }',
  '#mw-head, #mw-head a, .b-header a { color: rgb(56, 56, 56) !important; }',
  '.b-tabs-wrp { border-color: initial !important; }',
  'table, .wikitable, .b-infobox { background-color: transparent !important; color: rgb(0,0,0) !important; }',
  'th { background-color: rgba(200,220,235,0.9) !important; color: rgb(0,0,0) !important; }',
  'td { color: rgb(0,0,0) !important; }',
  '.mw-body, #content { background-color: transparent !important; }',
  '.b-sidebar_title, #footer, .b-footer { color: rgb(56, 56, 56) !important; }',
  '.b-heading1, .b-heading2, .b-heading3, h1, h2, h3, h4, h5, h6, .mw-parser-output h1, .mw-parser-output h2, .mw-parser-output h3, .mw-parser-output h4, .mw-parser-output h5, .mw-parser-output h6 { color: rgb(56, 56, 56) !important; }',
  '.b-search-input, #searchInput { background-color: rgb(245,245,245) !important; color: rgb(0,0,0) !important; }',
  '.b-game-info { background-color: transparent !important; color: rgb(0,0,0) !important; border-color: transparent !important; }',
  '.b-description.gw-frame-1, .mw-parser-output .gw-frame-1 { background-color: rgba(255,255,255,0.6) !important; border-color: rgba(212,175,55,0.4) !important; box-shadow: none !important; color: rgb(0,0,0) !important; }',
  '.b-description.gw-frame-1 a, .mw-parser-output .gw-frame-1 a { color: rgb(2,93,177) !important; }'
].join( '\n' );

function applyLight() {
  if ( document.getElementById( STYLE_ID ) ) { return; }
  var s = document.createElement( 'style' );
  s.id          = STYLE_ID;
  s.textContent = LIGHT_CSS;
  document.head.appendChild( s );
}

function applyDark() {
  var s = document.getElementById( STYLE_ID );
  if ( s ) { s.remove(); }
}

var DAY_SVG   = 'https://wargaming.net/id/static/2026.4.0/wgnet/img/mode-switcher/day.svg';
var NIGHT_SVG = 'https://wargaming.net/id/static/2026.4.0/wgnet/img/mode-switcher/night.svg';

function buildButton() {
  var isDark = ( localStorage.getItem( STORAGE_KEY ) !== 'light' );

  if ( !isDark ) { applyLight(); }

  var wrap = document.createElement( 'div' );
  wrap.id = 'wows-theme-toggle';
  wrap.style.cssText = [
    'position:fixed', 'top:14px', 'right:16px', 'z-index:9999',
    'background:rgb(30,30,30)', 'border:1px solid rgb(52,52,52)',
    'border-radius:8px', 'padding:3px', 'display:flex', 'gap:4px',
    'align-items:center'
  ].join( ';' );

  var BTN_BASE = [
    'width:30px', 'height:24px', 'border:none', 'cursor:pointer', 'padding:0',
    'border-radius:5px', 'background-color:transparent',
    'background-repeat:no-repeat', 'background-position:center',
    'background-size:20px', 'transition:opacity 0.2s'
  ].join( ';' );

  var btnLight = document.createElement( 'button' );
  btnLight.title = 'Light mode';
  btnLight.style.cssText = BTN_BASE + ';background-image:url("' + DAY_SVG + '")';
  btnLight.style.opacity = isDark ? '0.5' : '1';

  var btnDark = document.createElement( 'button' );
  btnDark.title = 'Dark mode';
  btnDark.style.cssText = BTN_BASE + ';background-image:url("' + NIGHT_SVG + '")';
  btnDark.style.opacity = isDark ? '1' : '0.5';

  function updateOpacity() {
    btnLight.style.opacity = isDark ? '0.5' : '1';
    btnDark.style.opacity  = isDark ? '1'   : '0.5';
  }

  btnLight.addEventListener( 'click', function () {
    if ( !isDark ) { return; }
    isDark = false; applyLight();
    localStorage.setItem( STORAGE_KEY, 'light' ); updateOpacity();
  } );

  btnDark.addEventListener( 'click', function () {
    if ( isDark ) { return; }
    isDark = true; applyDark();
    localStorage.setItem( STORAGE_KEY, 'dark' ); updateOpacity();
  } );

  wrap.appendChild( btnLight );
  wrap.appendChild( btnDark );
  document.body.appendChild( wrap );
}

if ( document.readyState === 'loading' ) {
  document.addEventListener( 'DOMContentLoaded', buildButton );
} else {
  buildButton();
}

}() );


/* Theme class hook — exposes light/dark to CSS for readable theming */
(function () {
  function applyThemeClass() {
    var light = localStorage.getItem('wows-theme-mode') === 'light';
    var h = document.documentElement;
    h.classList.toggle('wows-theme-light', light);
    h.classList.toggle('wows-theme-dark', !light);
  }
  applyThemeClass();
  window.addEventListener('storage', function (e) {
    if (e.key === 'wows-theme-mode') applyThemeClass();
  });
  document.addEventListener('click', function (e) {
    var b = e.target.closest && e.target.closest('button');
    if (b && /Light mode|Dark mode/i.test(b.title || b.textContent)) {
      setTimeout(applyThemeClass, 0);
    }
  }, true);
})();

/* ============================================================
   Ship pages: contextual Nation + Class links in the sidebar nav
   On Ship: namespace pages (ns 10000), read the page's categories
   to find its nation ("Ships of X") and class, then add links to
   the matching overview pages into the navigation menu.
   ============================================================ */
( function () {
'use strict';
if ( !window.mw || mw.config.get( 'wgNamespaceNumber' ) !== 10000 ) { return; }
var cats = mw.config.get( 'wgCategories' ) || [];
var CLASSES = [ 'Aircraft Carriers', 'Battleships', 'Cruisers', 'Destroyers', 'Submarines' ];
var nation = null, klass = null;
cats.forEach( function ( c ) {
	if ( /^Ships of /.test( c ) ) { nation = c; }
	if ( CLASSES.indexOf( c ) !== -1 ) { klass = c; }
} );
if ( !nation && !klass ) { return; }
var artPath = mw.config.get( 'wgArticlePath' );
function url( t ) { return artPath.replace( '$1', t.replace( / /g, '_' ) ); }
function addItem( menu, id, label, target ) {
	if ( document.getElementById( id ) ) { return; }
	var li = document.createElement( 'li' );
	li.id = id;
	li.className = 'b-left-menu_item mw-list-item';
	var a = document.createElement( 'a' );
	a.className = 'b-left-menu_link b-darklink';
	a.href = url( target );
	var point = document.createElement( 'span' );
	point.className = 'b-left-menu_point';
	point.title = label;
	point.textContent = label;
	a.appendChild( point );
	li.appendChild( a );
	menu.appendChild( li );
}
function inject() {
	var menu = document.querySelector( '#p-navigation .b-left-menu' ) ||
		document.querySelector( '#mw-panel .b-left-menu' );
	if ( !menu ) { return; }
	if ( nation ) { addItem( menu, 'n-ship-nation', nation, 'Ship:' + nation ); }
	if ( klass ) { addItem( menu, 'n-ship-class', klass, 'Ship:' + klass ); }
}
if ( document.readyState === 'loading' ) {
	document.addEventListener( 'DOMContentLoaded', inject );
} else {
	inject();
}
}() );

/* ============================================================
   Article pages: move the table of contents into the sidebar
   On any page with a TOC (#toc), relocate it into #mw-panel so it
   scrolls with the reader (the sidebar is sticky). Pages without a
   TOC (e.g. ship stat pages using __NOTOC__) are unaffected.
   ============================================================ */
( function () {
'use strict';
var toc = document.getElementById( 'toc' );
var panel = document.getElementById( 'mw-panel' );
if ( !toc || !panel ) { return; }
var sec = document.createElement( 'div' );
sec.id = 'p-toc';
sec.className = 'b-sidebar_item';
sec.appendChild( toc );
panel.appendChild( sec );
}() );


/* ===================================================================
   Sidebar "Request Edit" link - open in new tab
   =================================================================== */
( function () {
	'use strict';
	function fixLink() {
		var link = document.querySelector( '#n-Request-Edit a' );
		if ( !link ) { return; }
		link.setAttribute( 'target', '_blank' );
		var relParts = ( link.getAttribute( 'rel' ) || '' ).split( /\s+/ ).filter( Boolean );
		if ( relParts.indexOf( 'noopener' ) === -1 ) { relParts.push( 'noopener' ); }
		link.setAttribute( 'rel', relParts.join( ' ' ) );
	}
	if ( document.readyState === 'loading' ) {
		document.addEventListener( 'DOMContentLoaded', fixLink );
	} else {
		fixLink();
	}
	// Re-apply shortly after load in case other scripts modify the link's attributes afterward.
	setTimeout( fixLink, 500 );
} )();


/**
 * ================================================================
 *  Tech-Tree Line Navigation  (added via Cowork, 2026-07-21)
 * ================================================================
 *  Adds a "Tech Tree Line" panel to the sidebar on Ship: pages,
 *  reusing the skin's b-left-menu markup so it matches Navigation.
 *  Data: MediaWiki:ShipLines.json (WG API encyclopedia/ships next_ships).
 *  Ship: namespace only (ns 10000); premium/non-tree ships skipped.
 *  Regenerate the JSON each patch and bump CONFIG.cacheVersion.
 * ================================================================
 */
( function () {
	'use strict';
	var CONFIG = {
		shipNamespace: 10000,
		dataPage: 'MediaWiki:ShipLines.json',
		cacheKey: 'wowsShipLines',
		cacheVersionKey: 'wowsShipLinesVer',
		cacheVersion: '15.7.0'
	};
	if ( mw.config.get( 'wgNamespaceNumber' ) !== CONFIG.shipNamespace ) { return; }
	var shipName = mw.config.get( 'wgTitle' );
	if ( !shipName ) { return; }
	mw.loader.using( [ 'mediawiki.util' ] ).then( function () {
		loadData().then( function ( data ) {
			$( function () { setTimeout( function () { try { render( data ); } catch ( e ) { mw.log.warn( '[ShipLineNav] render failed:', e ); } }, 0 ); } );
		} ).catch( function ( e ) { mw.log.warn( '[ShipLineNav] data load failed:', e ); } );
	} );
	function loadData() {
		try {
			if ( window.sessionStorage && sessionStorage.getItem( CONFIG.cacheVersionKey ) === CONFIG.cacheVersion ) {
				var cached = sessionStorage.getItem( CONFIG.cacheKey );
				if ( cached ) { return Promise.resolve( JSON.parse( cached ) ); }
			}
		} catch ( e ) {}
		var url = mw.util.getUrl( CONFIG.dataPage, { action: 'raw' } );
		return fetch( url, { credentials: 'same-origin' } ).then( function ( r ) { return r.json(); } ).then( function ( data ) {
			try { sessionStorage.setItem( CONFIG.cacheKey, JSON.stringify( data ) ); sessionStorage.setItem( CONFIG.cacheVersionKey, CONFIG.cacheVersion ); } catch ( e ) {}
			return data;
		} );
	}
	function norm( s ) { return ( s || '' ).normalize( 'NFD' ).replace( /[̀-ͯ]/g, '' ).replace( /[^a-z0-9]/gi, '' ).toLowerCase(); }
	function findShip( data ) {
		var ships = data.ships || {}, id, exact = null, loose = null, want = norm( shipName );
		for ( id in ships ) {
			if ( ships[ id ].name === shipName ) { exact = ships[ id ]; break; }
			if ( !loose && norm( ships[ id ].name ) === want ) { loose = ships[ id ]; }
		}
		return exact || loose;
	}
	function roman( t ) { var r = [ '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI' ]; return r[ t ] || String( t ); }
	function render( data ) {
		var ship = findShip( data );
		if ( !ship ) { return; }
		var lineIds = ship.lines || [];
		if ( !lineIds.length ) { return; }

		var wrap = document.createElement( 'div' );
		wrap.id = 'p-wows-line';
		wrap.className = 'b-sidebar_item wows-line-portlet';
		var h = document.createElement( 'h5' );
		h.className = 'b-sidebar_title b-sidebar_title__opened';
		h.textContent = 'Tech Tree Line';
		wrap.appendChild( h );
		var ul = document.createElement( 'ul' );
		ul.className = 'b-left-menu b-left-menu__opened';
		wrap.appendChild( ul );

		var multi = lineIds.length > 1;
		lineIds.forEach( function ( lid ) {
			var line = ( data.lines || [] )[ lid ];
			if ( !line ) { return; }
			if ( multi ) {
				var sub = document.createElement( 'li' );
				sub.className = 'b-left-menu_item mw-list-item wows-line-subhead';
				sub.textContent = line.line_name + ' line';
				sub.style.cssText = 'opacity:.6;font-size:.85em;padding:4px 0 2px 20px;';
				ul.appendChild( sub );
			}
			( line.ships || [] ).forEach( function ( s ) {
				var li = document.createElement( 'li' );
				li.className = 'b-left-menu_item mw-list-item';
				var isCurrent = ( s.ship_id === ship.ship_id );
				var a = document.createElement( 'a' );
				a.className = 'b-left-menu_link b-darklink' + ( isCurrent ? ' wows-line-current' : '' );
				if ( !isCurrent ) { a.href = mw.util.getUrl( 'Ship:' + s.name ); }
				var pt = document.createElement( 'span' );
				pt.className = 'b-left-menu_point';
				var tier = document.createElement( 'span' );
				tier.className = 'wows-line-tier';
				tier.textContent = roman( s.tier );
				tier.style.cssText = 'position:absolute;left:3px;top:50%;transform:translateY(-50%);width:22px;text-align:right;color:#AD7A07;font-weight:bold;';
				pt.appendChild( tier );
				pt.appendChild( document.createTextNode( s.name ) );
				if ( isCurrent ) { a.style.fontWeight = 'bold'; pt.style.color = '#d4af37'; tier.style.color = '#d4af37'; }
				a.appendChild( pt );
				li.appendChild( a );
				ul.appendChild( li );
			} );
		} );

		var nav = document.querySelector( '#p-navigation, .mw-portlet-navigation, #p-Navigation' );
		if ( nav && nav.parentNode ) { nav.parentNode.insertBefore( wrap, nav.nextSibling ); return; }
		var sidebar = document.querySelector( '#mw-panel, .mw-panel, #column-one, .sidebar' );
		if ( sidebar ) { sidebar.appendChild( wrap ); return; }
		var content = document.querySelector( '#mw-content-text' );
		if ( content && content.parentNode ) { content.parentNode.insertBefore( wrap, content ); }
	}
}() );

/**
 * ================================================================
 *  Event Progress Bar  (added 2026-08-06)
 * ================================================================
 *  Template:Event progress renders a static, server-side bar. This
 *  script replaces it with a live version: per-visitor local times,
 *  an EU/NA/ASIA server switcher and a 60-second refresh. If this
 *  script fails to run, the static bar remains and stays correct.
 *
 *  Attributes on div.wows-event-progress (ISO 8601, UTC, trailing Z):
 *    data-name                       event title
 *    data-eu-start / data-eu-end     per-server outer window (spend deadline)
 *    data-na-start / data-na-end
 *    data-asia-start / data-asia-end
 *    data-start / data-end           single window if no server pairs
 *    data-earn-days                  days currency can be earned; the
 *                                    remainder becomes a spend-only tail
 *    data-token-low / -high / -cap   pacing (300 / 1200 / 207600)
 * ================================================================
 */
( function () {
	'use strict';

	var SELECTOR = '.wows-event-progress';
	var TICK_MS  = 60000;
	var HOUR     = 3600000;
	var DAY      = 86400000;
	var STORE    = 'wows-event-region';
	var REGIONS  = [ 'eu', 'na', 'asia' ];
	var LABELS   = { eu: 'EU', na: 'NA', asia: 'ASIA' };

	var TOKEN_LOW  = 300;
	var TOKEN_HIGH = 1200;
	var TOKEN_CAP  = 207600;
	var TOKEN_NOTE = 'Ceilings from daily combat missions only, assuming every mission is ' +
	                 'completed on every remaining day. Excludes Coal, Steel and Research Point ' +
	                 'conversion.';

	var bars = [];

	function detectRegion() {
		var tz = '';
		try { tz = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; } catch ( e ) {}
		if ( /^(America|US|Canada|Mexico|Brazil|Chile|Cuba|Jamaica|Atlantic)\b/.test( tz ) ) { return 'na'; }
		if ( /^(Europe|Africa|Arctic|GB|Eire|Poland|Portugal|Turkey|Israel|Egypt|Libya|CET|EET|WET|MET)\b/.test( tz ) ) { return 'eu'; }
		if ( /^(Asia|Australia|Pacific|Indian|NZ|Japan|Singapore|Hongkong|ROK|PRC|Iran)\b/.test( tz ) ) { return 'asia'; }
		var off = -new Date().getTimezoneOffset();
		if ( off <= -180 ) { return 'na'; }
		if ( off >= 300 )  { return 'asia'; }
		return 'eu';
	}
	function currentRegion() {
		var v = null;
		try { v = localStorage.getItem( STORE ); } catch ( e ) {}
		return REGIONS.indexOf( v ) !== -1 ? v : detectRegion();
	}
	function setRegion( r ) {
		try { localStorage.setItem( STORE, r ); } catch ( e ) {}
		bars.forEach( render );
	}

	function fmtDate( ms ) {
		return new Date( ms ).toLocaleDateString( undefined, { year: 'numeric', month: 'short', day: 'numeric' } );
	}
	function fmtFull( ms ) {
		return new Date( ms ).toLocaleString( undefined, {
			year: 'numeric', month: 'long', day: 'numeric',
			hour: '2-digit', minute: '2-digit', timeZoneName: 'short'
		} );
	}
	function plural( v, word ) { return v + ' ' + word + ( v === 1 ? '' : 's' ); }
	function span( ms ) {
		if ( ms >= DAY )  { return plural( Math.floor( ms / DAY ), 'day' ); }
		if ( ms >= HOUR ) { return plural( Math.floor( ms / HOUR ), 'hour' ); }
		return plural( Math.max( 1, Math.round( ms / 60000 ) ), 'minute' );
	}
	function n( v ) { return v.toLocaleString(); }

	function readWindows( el ) {
		var out = {}, any = false;
		REGIONS.forEach( function ( r ) {
			var s = Date.parse( el.getAttribute( 'data-' + r + '-start' ) );
			var e = Date.parse( el.getAttribute( 'data-' + r + '-end' ) );
			if ( !isNaN( s ) && !isNaN( e ) && e > s ) { out[ r ] = { s: s, e: e }; any = true; }
		} );
		if ( any ) { return out; }
		var s1 = Date.parse( el.getAttribute( 'data-start' ) );
		var e1 = Date.parse( el.getAttribute( 'data-end' ) );
		if ( !isNaN( s1 ) && !isNaN( e1 ) && e1 > s1 ) { return { all: { s: s1, e: e1 } }; }
		return null;
	}

	function build( el ) {
		var win = readWindows( el );
		if ( !win ) { return null; }   // bad dates: leave the static bar alone

		el.innerHTML =
			'<div class="wows-ep-head">' +
				'<span class="wows-ep-name"></span>' +
				'<span class="wows-ep-remain"></span>' +
			'</div>' +
			'<div class="wows-ep-track" role="progressbar" aria-valuemin="0" aria-valuemax="100">' +
				'<div class="wows-ep-fill"></div>' +
				'<div class="wows-ep-tail"></div>' +
				'<div class="wows-ep-mark"></div>' +
			'</div>' +
			'<div class="wows-ep-foot">' +
				'<span class="wows-ep-start"></span>' +
				'<span class="wows-ep-pct"></span>' +
				'<span class="wows-ep-end"></span>' +
			'</div>' +
			'<div class="wows-ep-meta">' +
				'<span class="wows-ep-days">' +
					'<span class="wows-ep-dayno"></span>' +
					'<span class="wows-ep-spend"></span>' +
				'</span>' +
				'<span class="wows-ep-tokens"></span>' +
				'<span class="wows-ep-servers"></span>' +
			'</div>';

		var nameEl  = el.querySelector( '.wows-ep-name' );
		var nameTxt = el.getAttribute( 'data-name' ) || 'Event';
		var nameUrl = el.getAttribute( 'data-link' );
		if ( nameUrl ) {
			var a = document.createElement( 'a' );
			a.href = nameUrl;
			a.textContent = nameTxt;
			nameEl.appendChild( a );
		} else {
			nameEl.textContent = nameTxt;
		}

		function num( attr, dflt ) {
			var v = parseFloat( el.getAttribute( attr ) );
			return isNaN( v ) ? dflt : v;
		}

		var o = {
			el: el, win: win,
			tLow:     num( 'data-token-low',  TOKEN_LOW ),
			tHigh:    num( 'data-token-high', TOKEN_HIGH ),
			tCap:     num( 'data-token-cap',  TOKEN_CAP ),
			earnDays: num( 'data-earn-days',  0 ),
			fill:    el.querySelector( '.wows-ep-fill' ),
			tail:    el.querySelector( '.wows-ep-tail' ),
			mark:    el.querySelector( '.wows-ep-mark' ),
			track:   el.querySelector( '.wows-ep-track' ),
			remain:  el.querySelector( '.wows-ep-remain' ),
			sLabel:  el.querySelector( '.wows-ep-start' ),
			eLabel:  el.querySelector( '.wows-ep-end' ),
			pct:     el.querySelector( '.wows-ep-pct' ),
			dayNo:   el.querySelector( '.wows-ep-dayno' ),
			spend:   el.querySelector( '.wows-ep-spend' ),
			tokens:  el.querySelector( '.wows-ep-tokens' ),
			srvWrap: el.querySelector( '.wows-ep-servers' ),
			srvBtns: {}
		};

		var avail = REGIONS.filter( function ( r ) { return win[ r ]; } );
		if ( avail.length > 1 ) {
			var lbl = document.createElement( 'span' );
			lbl.className = 'wows-ep-srv-label';
			lbl.textContent = 'Server:';
			o.srvWrap.appendChild( lbl );
			avail.forEach( function ( r ) {
				var b = document.createElement( 'button' );
				b.type = 'button';
				b.className = 'wows-ep-srv';
				b.textContent = LABELS[ r ];
				b.setAttribute( 'aria-pressed', 'false' );
				b.setAttribute( 'title', 'Show ' + LABELS[ r ] + ' server dates' );
				b.addEventListener( 'click', function () { setRegion( r ); } );
				o.srvWrap.appendChild( b );
				o.srvBtns[ r ] = b;
			} );
		} else {
			o.srvWrap.style.display = 'none';
		}
		return o;
	}

	function render( o ) {
		var r = currentRegion();
		var w = o.win[ r ] || o.win.all || o.win[ Object.keys( o.win )[ 0 ] ];
		var t = Date.now();

		var earnEnd  = o.earnDays ? Math.min( w.e, w.s + o.earnDays * DAY ) : w.e;
		var earnDays = o.earnDays || Math.round( ( w.e - w.s ) / DAY );
		var hasTail  = earnEnd < w.e - HOUR;

		var pct = Math.max( 0, Math.min( 100, ( t - w.s ) / ( w.e - w.s ) * 100 ) );
		var shown = Math.round( pct );
		if ( t < w.e && shown === 100 ) { shown = 99; }
		if ( t > w.s && shown === 0 )   { shown = 1; }

		var earnPct = ( earnEnd - w.s ) / ( w.e - w.s ) * 100;
		o.fill.style.width = Math.min( pct, earnPct ).toFixed( 2 ) + '%';
		o.tail.style.left  = earnPct.toFixed( 2 ) + '%';
		o.tail.style.width = Math.max( 0, pct - earnPct ).toFixed( 2 ) + '%';
		o.track.setAttribute( 'aria-valuenow', shown );
		o.pct.textContent = shown + '% complete';

		if ( hasTail ) {
			o.mark.style.display = 'block';
			o.mark.style.left = earnPct.toFixed( 2 ) + '%';
			o.mark.title = 'Token earning ends ' + fmtFull( earnEnd );
		}

		o.el.classList.remove( 'wows-ep--upcoming', 'wows-ep--active', 'wows-ep--spend', 'wows-ep--ended' );
		if ( t < w.s ) {
			o.el.classList.add( 'wows-ep--upcoming' );
			o.remain.textContent = 'Starts in ' + span( w.s - t );
			o.sLabel.textContent = 'Starts ' + fmtDate( w.s );
			o.pct.textContent    = 'Not started';
		} else if ( t < earnEnd ) {
			o.el.classList.add( 'wows-ep--active' );
			o.remain.textContent = span( earnEnd - t ) + ' left to earn';
			o.sLabel.textContent = 'Started ' + fmtDate( w.s );
		} else if ( t < w.e ) {
			o.el.classList.add( 'wows-ep--spend' );
			o.remain.textContent = 'Earning closed \u00b7 ' + span( w.e - t ) + ' left to spend';
			o.sLabel.textContent = 'Started ' + fmtDate( w.s );
		} else {
			o.el.classList.add( 'wows-ep--ended' );
			o.remain.textContent = 'Event has ended';
			o.sLabel.textContent = 'Ran from ' + fmtDate( w.s );
			o.pct.textContent    = 'Complete';
		}
		o.eLabel.textContent = hasTail
			? ( t >= earnEnd ? 'Earning ended ' : 'Earning ends ' ) + fmtDate( earnEnd )
			: ( t >= w.e ? 'Ended ' : 'Ends ' ) + fmtDate( w.e );
		o.sLabel.title = fmtFull( w.s );
		o.eLabel.title = hasTail ? fmtFull( earnEnd ) : fmtFull( w.e );

		var day = t < w.s ? 0 : Math.min( earnDays, Math.floor( ( t - w.s ) / DAY ) + 1 );
		o.dayNo.textContent = t >= earnEnd
			? 'Earning closed after ' + earnDays + ' days'
			: 'Day ' + day + ' of ' + earnDays;
		o.spend.textContent = hasTail
			? ( t >= w.e ? 'Spending closed ' + fmtDate( w.e ) : 'Spend by ' + fmtDate( w.e ) )
			: '';
		o.spend.title = hasTail ? fmtFull( w.e ) : '';

		var left     = Math.max( 0, earnDays - day );
		var doneLow  = Math.min( o.tCap, day * o.tLow );
		var doneHigh = Math.min( o.tCap, day * o.tHigh );
		var leftLow  = Math.max( 0, Math.min( left * o.tLow,  o.tCap - doneLow ) );
		var leftHigh = Math.max( 0, Math.min( left * o.tHigh, o.tCap - doneHigh ) );

		o.tokens.innerHTML =
			'<span class="wows-ep-tgrid">' +
				'<span class="wows-ep-tr">Mission tokens</span>' +
				'<span class="wows-ep-th">1 battle/day</span>' +
				'<span class="wows-ep-th">5 battles/day</span>' +
				'<span class="wows-ep-tr">earnable to date</span>' +
				'<b>' + n( doneLow ) + '</b><b>' + n( doneHigh ) + '</b>' +
				'<span class="wows-ep-tr">still remaining</span>' +
				'<b>' + n( leftLow ) + '</b><b>' + n( leftHigh ) + '</b>' +
			'</span>';
		o.tokens.title = TOKEN_NOTE;

		Object.keys( o.srvBtns ).forEach( function ( k ) {
			o.srvBtns[ k ].setAttribute( 'aria-pressed', k === r ? 'true' : 'false' );
		} );
	}

	function init() {
		Array.prototype.forEach.call( document.querySelectorAll( SELECTOR ), function ( el ) {
			var o = build( el );
			if ( o ) { bars.push( o ); render( o ); }
		} );
		if ( !bars.length ) { return; }
		function all() { bars.forEach( render ); }
		setInterval( all, TICK_MS );
		document.addEventListener( 'visibilitychange', function () {
			if ( !document.hidden ) { all(); }
		} );
		window.addEventListener( 'storage', function ( e ) {
			if ( e.key === STORE ) { all(); }
		} );
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener( 'DOMContentLoaded', init );
	} else {
		init();
	}
}() );