text: /*
      TimelineJS - ver. 3.6.3 - 2018-12-20
      Copyright (c) 2012-2016 Northwestern University
      a project of the Northwestern University Knight Lab, originally created by Zach Wise
      https://github.com/NUKnightLab/TimelineJS3
      This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
      If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
  */
  
  /* **********************************************
       Begin TL.js
  ********************************************** */
  
  /*!
  	TL
  */
  
  (function (root) {
  	root.TL = {
  		VERSION: '0.1',
  		_originalL: root.TL
  	};
  }(this));
  
  /*	TL.Debug
  	Debug mode
  ================================================== */
  TL.debug = false;
  
  
  
  /*	TL.Bind
  ================================================== */
  TL.Bind = function (/*Function*/ fn, /*Object*/ obj) /*-> Object*/ {
  	return function () {
  		return fn.apply(obj, arguments);
  	};
  };
  
  
  
  /* Trace (console.log)
  ================================================== */
  trace = function( msg ) {
  	if (TL.debug) {
  		if (window.console) {
  			console.log(msg);
  		} else if ( typeof( jsTrace ) != 'undefined' ) {
  			jsTrace.send( msg );
  		} else {
  			//alert(msg);
  		}
  	}
  }
  
  
  /* **********************************************
       Begin TL.Error.js
  ********************************************** */
  
  /* Timeline Error class */
  
  function TL_Error(message_key, detail) {
      this.name = 'TL.Error';
      this.message = message_key || 'error';
      this.message_key = this.message;
      this.detail = detail || '';
    
      // Grab stack?
      var e = new Error();
      if(e.hasOwnProperty('stack')) {
          this.stack = e.stack;
      }
  }
  
  TL_Error.prototype = Object.create(Error.prototype);
  TL_Error.prototype.constructor = TL_Error;
  
  TL.Error = TL_Error;
  
  
  /* **********************************************
       Begin TL.Util.js
  ********************************************** */
  
  /*	TL.Util
  	Class of utilities
  ================================================== */
  
  TL.Util = {
  	mergeData: function(data_main, data_to_merge) {
  		var x;
  		for (x in data_to_merge) {
  			if (Object.prototype.hasOwnProperty.call(data_to_merge, x)) {
  				data_main[x] = data_to_merge[x];
  			}
  		}
  		return data_main;
  	},
  
  	// like TL.Util.mergeData but takes an arbitrarily long list of sources to merge.
  	extend: function (/*Object*/ dest) /*-> Object*/ {	// merge src properties into dest
  		var sources = Array.prototype.slice.call(arguments, 1);
  		for (var j = 0, len = sources.length, src; j < len; j++) {
  			src = sources[j] || {};
  			TL.Util.mergeData(dest, src);
  		}
  		return dest;
  	},
  
  	isEven: function(n) {
  	  return n == parseFloat(n)? !(n%2) : void 0;
  	},
  
  	isTrue: function(s) {
  		if (s == null) return false;
  		return s == true || String(s).toLowerCase() == 'true' || Number(s) == 1;
  	},
  
  	findArrayNumberByUniqueID: function(id, array, prop, defaultVal) {
  		var _n = defaultVal || 0;
  
  		for (var i = 0; i < array.length; i++) {
  			if (array[i].data[prop] == id) {
  				_n = i;
  			}
  		};
  
  		return _n;
  	},
  
  	convertUnixTime: function(str) {
  		var _date, _months, _year, _month, _day, _time, _date_array = [],
  			_date_str = {
  				ymd:"",
  				time:"",
  				time_array:[],
  				date_array:[],
  				full_array:[]
  			};
  
  		_date_str.ymd = str.split(" ")[0];
  		_date_str.time = str.split(" ")[1];
  		_date_str.date_array = _date_str.ymd.split("-");
  		_date_str.time_array = _date_str.time.split(":");
  		_date_str.full_array = _date_str.date_array.concat(_date_str.time_array)
  
  		for(var i = 0; i < _date_str.full_array.length; i++) {
  			_date_array.push( parseInt(_date_str.full_array[i]) )
  		}
  
  		_date = new Date(_date_array[0], _date_array[1], _date_array[2], _date_array[3], _date_array[4], _date_array[5]);
  		_months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  		_year = _date.getFullYear();
  		_month = _months[_date.getMonth()];
  		_day = _date.getDate();
  		_time = _month + ', ' + _day + ' ' + _year;
  
  		return _time;
  	},
  
  	setData: function (obj, data) {
  		obj.data = TL.Util.extend({}, obj.data, data);
  		if (obj.data.unique_id === "") {
  			obj.data.unique_id = TL.Util.unique_ID(6);
  		}
  	},
  
  	stamp: (function () {
  		var lastId = 0, key = '_tl_id';
  
  
  		return function (/*Object*/ obj) {
  			obj[key] = obj[key] || ++lastId;
  			return obj[key];
  		};
  	}()),
  
  	isArray: (function () {
  	    // Use compiler's own isArray when available
  	    if (Array.isArray) {
  	        return Array.isArray;
  	    }
  
  	    // Retain references to variables for performance
  	    // optimization
  	    var objectToStringFn = Object.prototype.toString,
  	        arrayToStringResult = objectToStringFn.call([]);
  
  	    return function (subject) {
  	        return objectToStringFn.call(subject) === arrayToStringResult;
  	    };
  	}()),
  
      getRandomNumber: function(range) {
     		return Math.floor(Math.random() * range);
     	},
  
  	unique_ID: function(size, prefix) {
  
  		var getRandomNumber = function(range) {
  			return Math.floor(Math.random() * range);
  		};
  
  		var getRandomChar = function() {
  			var chars = "abcdefghijklmnopqurstuvwxyz";
  			return chars.substr( getRandomNumber(32), 1 );
  		};
  
  		var randomID = function(size) {
  			var str = "";
  			for(var i = 0; i < size; i++) {
  				str += getRandomChar();
  			}
  			return str;
  		};
  
  		if (prefix) {
  			return prefix + "-" + randomID(size);
  		} else {
  			return "tl-" + randomID(size);
  		}
  	},
  
  	ensureUniqueKey: function(obj, candidate) {
  		if (!candidate) { candidate = TL.Util.unique_ID(6); }
  
  		if (!(candidate in obj)) { return candidate; }
  
  		var root = candidate.match(/^(.+)(-\d+)?$/)[1];
  		var similar_ids = [];
  		// get an alternative
  		for (key in obj) {
  			if (key.match(/^(.+?)(-\d+)?$/)[1] == root) {
  				similar_ids.push(key);
  			}
  		}
  		candidate = root + "-" + (similar_ids.length + 1);
  
  		for (var counter = similar_ids.length; similar_ids.indexOf(candidate) != -1; counter++) {
  			candidate = root + '-' + counter;
  		}
  
  		return candidate;
  	},
  
  
  	htmlify: function(str) {
  		//if (str.match(/<\s*p[^>]*>([^<]*)<\s*\/\s*p\s*>/)) {
  		if (str.match(/<p>[\s\S]*?<\/p>/)) {
  
  			return str;
  		} else {
  			return "<p>" + str + "</p>";
  		}
  	},
  
  	unhtmlify: function(str) {
  		str = str.replace(/(<[^>]*>)+/g, '');
  		return str.replace('"', "'");
  	},
  
  	/*	* Turns plain text links into real links
  	================================================== */
  	linkify: function(text,targets,is_touch) {
  
          var make_link = function(url, link_text, prefix) {
              if (!prefix) {
                  prefix = "";
              }
              var MAX_LINK_TEXT_LENGTH = 30;
              if (link_text && link_text.length > MAX_LINK_TEXT_LENGTH) {
                  link_text = link_text.substring(0,MAX_LINK_TEXT_LENGTH) + "\u2026"; // unicode ellipsis
              }
              return prefix + "<a class='tl-makelink' href='" + url + "' onclick='void(0)'>" + link_text + "</a>";
          }
  		// http://, https://, ftp://
  		var urlPattern = /\b(?:https?|ftp):\/\/([a-z0-9-+&@#\/%?=~_|!:,.;]*[a-z0-9-+&@#\/%=~_|])/gim;
  
  		// www. sans http:// or https://
  		var pseudoUrlPattern = /(^|[^\/>])(www\.[\S]+(\b|$))/gim;
  
  		// Email addresses
  		var emailAddressPattern = /([a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/gim;
  
  
  		return text
  			.replace(urlPattern, function(match, url_sans_protocol, offset, string) {
                  // Javascript doesn't support negative lookbehind assertions, so
                  // we need to handle risk of matching URLs in legit hrefs
                  if (offset > 0) {
                      var prechar = string[offset-1];
                      if (prechar == '"' || prechar == "'" || prechar == "=") {
                          return match;
                      }
                  }
                  return make_link(match, url_sans_protocol);
              })
  			.replace(pseudoUrlPattern, function(match, beforePseudo, pseudoUrl, offset, string) {
                  return make_link('http://' + pseudoUrl, pseudoUrl, beforePseudo);
              })
  			.replace(emailAddressPattern, function(match, email, offset, string) {
                  return make_link('mailto:' + email, email);
              });
  	},
  
  	unlinkify: function(text) {
  		if(!text) return text;
  		text = text.replace(/<a\b[^>]*>/i,"");
  		text = text.replace(/<\/a>/i, "");
  		return text;
  	},
  
  	getParamString: function (obj) {
  		var params = [];
  		for (var i in obj) {
  			if (obj.hasOwnProperty(i)) {
  				params.push(i + '=' + obj[i]);
  			}
  		}
  		return '?' + params.join('&');
  	},
  
  	formatNum: function (num, digits) {
  		var pow = Math.pow(10, digits || 5);
  		return Math.round(num * pow) / pow;
  	},
  
  	falseFn: function () {
  		return false;
  	},
  
  	requestAnimFrame: (function () {
  		function timeoutDefer(callback) {
  			window.setTimeout(callback, 1000 / 60);
  		}
  
  		var requestFn = window.requestAnimationFrame ||
  			window.webkitRequestAnimationFrame ||
  			window.mozRequestAnimationFrame ||
  			window.oRequestAnimationFrame ||
  			window.msRequestAnimationFrame ||
  			timeoutDefer;
  
  		return function (callback, context, immediate, contextEl) {
  			callback = context ? TL.Util.bind(callback, context) : callback;
  			if (immediate && requestFn === timeoutDefer) {
  				callback();
  			} else {
  				requestFn(callback, contextEl);
  			}
  		};
  	}()),
  
  	bind: function (/*Function*/ fn, /*Object*/ obj) /*-> Object*/ {
  		return function () {
  			return fn.apply(obj, arguments);
  		};
  	},
  
  	template: function (str, data) {
  		return str.replace(/\{ *([\w_]+) *\}/g, function (str, key) {
  			var value = data[key];
  			if (!data.hasOwnProperty(key)) {
  			    throw new TL.Error("template_value_err", str);
  			}
  			return value;
  		});
  	},
  
  	hexToRgb: function(hex) {
  	    // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
          if (TL.Util.css_named_colors[hex.toLowerCase()]) {
              hex = TL.Util.css_named_colors[hex.toLowerCase()];
          }
  	    var shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
  	    hex = hex.replace(shorthandRegex, function(m, r, g, b) {
  	        return r + r + g + g + b + b;
  	    });
  
  	    var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  	    return result ? {
  	        r: parseInt(result[1], 16),
  	        g: parseInt(result[2], 16),
  	        b: parseInt(result[3], 16)
  	    } : null;
  	},
  	// given an object with r, g, and b keys, or a string of the form 'rgb(mm,nn,ll)', return a CSS hex string including the leading '#' character
  	rgbToHex: function(rgb) {
  		var r,g,b;
  		if (typeof(rgb) == 'object') {
  			r = rgb.r;
  			g = rgb.g;
  			b = rgb.b;
  		} else if (typeof(rgb.match) == 'function'){
  			var parts = rgb.match(/^rgb\((\d+),(\d+),(\d+)\)$/);
  			if (parts) {
  				r = parts[1];
  				g = parts[2];
  				b = parts[3];
  			}
  		}
  		if (isNaN(r) || isNaN(b) || isNaN(g)) {
  			throw new TL.Error("invalid_rgb_err");
  		}
  		return "#" + TL.Util.intToHexString(r) + TL.Util.intToHexString(g) + TL.Util.intToHexString(b);
  	},
  	colorObjToHex: function(o) {
  		var parts = [o.r, o.g, o.b];
  		return TL.Util.rgbToHex("rgb(" + parts.join(',') + ")")
  	},
      css_named_colors: {
          "aliceblue": "#f0f8ff",
          "antiquewhite": "#faebd7",
          "aqua": "#00ffff",
          "aquamarine": "#7fffd4",
          "azure": "#f0ffff",
          "beige": "#f5f5dc",
          "bisque": "#ffe4c4",
          "black": "#000000",
          "blanchedalmond": "#ffebcd",
          "blue": "#0000ff",
          "blueviolet": "#8a2be2",
          "brown": "#a52a2a",
          "burlywood": "#deb887",
          "cadetblue": "#5f9ea0",
          "chartreuse": "#7fff00",
          "chocolate": "#d2691e",
          "coral": "#ff7f50",
          "cornflowerblue": "#6495ed",
          "cornsilk": "#fff8dc",
          "crimson": "#dc143c",
          "cyan": "#00ffff",
          "darkblue": "#00008b",
          "darkcyan": "#008b8b",
          "darkgoldenrod": "#b8860b",
          "darkgray": "#a9a9a9",
          "darkgreen": "#006400",
          "darkkhaki": "#bdb76b",
          "darkmagenta": "#8b008b",
          "darkolivegreen": "#556b2f",
          "darkorange": "#ff8c00",
          "darkorchid": "#9932cc",
          "darkred": "#8b0000",
          "darksalmon": "#e9967a",
          "darkseagreen": "#8fbc8f",
          "darkslateblue": "#483d8b",
          "darkslategray": "#2f4f4f",
          "darkturquoise": "#00ced1",
          "darkviolet": "#9400d3",
          "deeppink": "#ff1493",
          "deepskyblue": "#00bfff",
          "dimgray": "#696969",
          "dodgerblue": "#1e90ff",
          "firebrick": "#b22222",
          "floralwhite": "#fffaf0",
          "forestgreen": "#228b22",
          "fuchsia": "#ff00ff",
          "gainsboro": "#dcdcdc",
          "ghostwhite": "#f8f8ff",
          "gold": "#ffd700",
          "goldenrod": "#daa520",
          "gray": "#808080",
          "green": "#008000",
          "greenyellow": "#adff2f",
          "honeydew": "#f0fff0",
          "hotpink": "#ff69b4",
          "indianred": "#cd5c5c",
          "indigo": "#4b0082",
          "ivory": "#fffff0",
          "khaki": "#f0e68c",
          "lavender": "#e6e6fa",
          "lavenderblush": "#fff0f5",
          "lawngreen": "#7cfc00",
          "lemonchiffon": "#fffacd",
          "lightblue": "#add8e6",
          "lightcoral": "#f08080",
          "lightcyan": "#e0ffff",
          "lightgoldenrodyellow": "#fafad2",
          "lightgray": "#d3d3d3",
          "lightgreen": "#90ee90",
          "lightpink": "#ffb6c1",
          "lightsalmon": "#ffa07a",
          "lightseagreen": "#20b2aa",
          "lightskyblue": "#87cefa",
          "lightslategray": "#778899",
          "lightsteelblue": "#b0c4de",
          "lightyellow": "#ffffe0",
          "lime": "#00ff00",
          "limegreen": "#32cd32",
          "linen": "#faf0e6",
          "magenta": "#ff00ff",
          "maroon": "#800000",
          "mediumaquamarine": "#66cdaa",
          "mediumblue": "#0000cd",
          "mediumorchid": "#ba55d3",
          "mediumpurple": "#9370db",
          "mediumseagreen": "#3cb371",
          "mediumslateblue": "#7b68ee",
          "mediumspringgreen": "#00fa9a",
          "mediumturquoise": "#48d1cc",
          "mediumvioletred": "#c71585",
          "midnightblue": "#191970",
          "mintcream": "#f5fffa",
          "mistyrose": "#ffe4e1",
          "moccasin": "#ffe4b5",
          "navajowhite": "#ffdead",
          "navy": "#000080",
          "oldlace": "#fdf5e6",
          "olive": "#808000",
          "olivedrab": "#6b8e23",
          "orange": "#ffa500",
          "orangered": "#ff4500",
          "orchid": "#da70d6",
          "palegoldenrod": "#eee8aa",
          "palegreen": "#98fb98",
          "paleturquoise": "#afeeee",
          "palevioletred": "#db7093",
          "papayawhip": "#ffefd5",
          "peachpuff": "#ffdab9",
          "peru": "#cd853f",
          "pink": "#ffc0cb",
          "plum": "#dda0dd",
          "powderblue": "#b0e0e6",
          "purple": "#800080",
          "rebeccapurple": "#663399",
          "red": "#ff0000",
          "rosybrown": "#bc8f8f",
          "royalblue": "#4169e1",
          "saddlebrown": "#8b4513",
          "salmon": "#fa8072",
          "sandybrown": "#f4a460",
          "seagreen": "#2e8b57",
          "seashell": "#fff5ee",
          "sienna": "#a0522d",
          "silver": "#c0c0c0",
          "skyblue": "#87ceeb",
          "slateblue": "#6a5acd",
          "slategray": "#708090",
          "snow": "#fffafa",
          "springgreen": "#00ff7f",
          "steelblue": "#4682b4",
          "tan": "#d2b48c",
          "teal": "#008080",
          "thistle": "#d8bfd8",
          "tomato": "#ff6347",
          "turquoise": "#40e0d0",
          "violet": "#ee82ee",
          "wheat": "#f5deb3",
          "white": "#ffffff",
          "whitesmoke": "#f5f5f5",
          "yellow": "#ffff00",
          "yellowgreen": "#9acd32"
      },
  	ratio: {
  		square: function(size) {
  			var s = {
  				w: 0,
  				h: 0
  			}
  			if (size.w > size.h && size.h > 0) {
  				s.h = size.h;
  				s.w = size.h;
  			} else {
  				s.w = size.w;
  				s.h = size.w;
  			}
  			return s;
  		},
  
  		r16_9: function(size) {
  			if (size.w !== null && size.w !== "") {
  				return Math.round((size.w / 16) * 9);
  			} else if (size.h !== null && size.h !== "") {
  				return Math.round((size.h / 9) * 16);
  			} else {
  				return 0;
  			}
  		},
  		r4_3: function(size) {
  			if (size.w !== null && size.w !== "") {
  				return Math.round((size.w / 4) * 3);
  			} else if (size.h !== null && size.h !== "") {
  				return Math.round((size.h / 3) * 4);
  			}
  		}
  	},
  	getObjectAttributeByIndex: function(obj, index) {
  		if(typeof obj != 'undefined') {
  			var i = 0;
  			for (var attr in obj){
  				if (index === i){
  					return obj[attr];
  				}
  				i++;
  			}
  			return "";
  		} else {
  			return "";
  		}
  
  	},
  	getUrlVars: function(string) {
  		var str,
  			vars = [],
  			hash,
  			hashes;
  
  		str = string.toString();
  
  		if (str.match('&#038;')) {
  			str = str.replace("&#038;", "&");
  		} else if (str.match('&#38;')) {
  			str = str.replace("&#38;", "&");
  		} else if (str.match('&amp;')) {
  			str = str.replace("&amp;", "&");
  		}
  
  		hashes = str.slice(str.indexOf('?') + 1).split('&');
  
  		for(var i = 0; i < hashes.length; i++) {
  			hash = hashes[i].split('=');
  			vars.push(hash[0]);
  			vars[hash[0]] = hash[1];
  		}
  
  
  		return vars;
  	},
      /**
       * Remove any leading or trailing whitespace from the given string.
       * If `str` is undefined or does not have a `replace` function, return
       * an empty string.
       */
  	trim: function(str) {
          if (str && typeof(str.replace) == 'function') {
              return str.replace(/^\s+|\s+$/g, '');
          }
          return "";
  	},
  
  	slugify: function(str) {
  		// borrowed from http://stackoverflow.com/a/5782563/102476
  		str = TL.Util.trim(str);
  		str = str.toLowerCase();
  
  		// remove accents, swap ñ for n, etc
  		var from = "ãàáäâẽèéëêìíïîõòóöôùúüûñç·/_,:;";
  		var to   = "aaaaaeeeeeiiiiooooouuuunc------";
  		for (var i=0, l=from.length ; i<l ; i++) {
  		str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
  		}
  
  		str = str.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
  		.replace(/\s+/g, '-') // collapse whitespace and replace by -
  		.replace(/-+/g, '-'); // collapse dashes
  
  		str = str.replace(/^([0-9])/,'_$1');
  		return str;
  	},
  	maxDepth: function(ary) {
  		// given a sorted array of 2-tuples of numbers, count how many "deep" the items are.
  		// that is, what is the maximum number of tuples that occupy any one moment
  		// each tuple should also be sorted
  		var stack = [];
  		var max_depth = 0;
  		for (var i = 0; i < ary.length; i++) {
  
  			stack.push(ary[i]);
  			if (stack.length > 1) {
  				var top = stack[stack.length - 1]
  				var bottom_idx = -1;
  				for (var j = 0; j < stack.length - 1; j++) {
  					if (stack[j][1] < top[0]) {
  						bottom_idx = j;
  					}
  				};
  				if (bottom_idx >= 0) {
  					stack = stack.slice(bottom_idx + 1);
  				}
  
  			}
  
  			if (stack.length > max_depth) {
  				max_depth = stack.length;
  			}
  		};
  		return max_depth;
  	},
  
  	pad: function (val, len) {
  		val = String(val);
  		len = len || 2;
  		while (val.length < len) val = "0" + val;
  		return val;
  	},
  	intToHexString: function(i) {
  		return TL.Util.pad(parseInt(i,10).toString(16));
  	},
      findNextGreater: function(list, current, default_value) {
          // given a sorted list and a current value which *might* be in the list,
          // return the next greatest value if the current value is >= the last item in the list, return default,
          // or if default is undefined, return input value
          for (var i = 0; i < list.length; i++) {
              if (current < list[i]) {
                  return list[i];
              }
          }
  
          return (default_value) ? default_value : current;
      },
  
      findNextLesser: function(list, current, default_value) {
          // given a sorted list and a current value which *might* be in the list,
          // return the next lesser value if the current value is <= the last item in the list, return default,
          // or if default is undefined, return input value
          for (var i = list.length - 1; i >= 0; i--) {
              if (current > list[i]) {
                  return list[i];
              }
          }
  
          return (default_value) ? default_value : current;
      },
  
  	isEmptyObject: function(o) {
  		var properties = []
  		if (Object.keys) {
  			properties = Object.keys(o);
  		} else { // all this to support IE 8
  		    for (var p in o) if (Object.prototype.hasOwnProperty.call(o,p)) properties.push(p);
      }
  		for (var i = 0; i < properties.length; i++) {
  			var k = properties[i];
  			if (o[k] != null && typeof o[k] != "string") return false;
  			if (TL.Util.trim(o[k]).length != 0) return false;
  		}
  		return true;
  	},
  	parseYouTubeTime: function(s) {
  	    // given a YouTube start time string in a reasonable format, reduce it to a number of seconds as an integer.
  		if (typeof(s) == 'string') {
  			parts = s.match(/^\s*(\d+h)?(\d+m)?(\d+s)?\s*/i);
  			if (parts) {
  				var hours = parseInt(parts[1]) || 0;
  				var minutes = parseInt(parts[2]) || 0;
  				var seconds = parseInt(parts[3]) || 0;
  				return seconds + (minutes * 60) + (hours * 60 * 60);
  			}
  		} else if (typeof(s) == 'number') {
  			return s;
  		}
  		return 0;
  	},
  	/**
  	 * Try to make seamless the process of interpreting a URL to a web page which embeds an image for sharing purposes
  	 * as a direct image link. Some services have predictable transformations we can use rather than explain to people
  	 * this subtlety.
  	 */
  	transformImageURL: function(url) {
  		return url.replace(/(.*)www.dropbox.com\/(.*)/, '$1dl.dropboxusercontent.com/$2')
  	},
  
  	base58: (function(alpha) {
  	    var alphabet = alpha || '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ',
  	        base = alphabet.length;
  	    return {
  	        encode: function(enc) {
  	            if(typeof enc!=='number' || enc !== parseInt(enc))
  	                throw '"encode" only accepts integers.';
  	            var encoded = '';
  	            while(enc) {
  	                var remainder = enc % base;
  	                enc = Math.floor(enc / base);
  	                encoded = alphabet[remainder].toString() + encoded;
  	            }
  	            return encoded;
  	        },
  	        decode: function(dec) {
  	            if(typeof dec!=='string')
  	                throw '"decode" only accepts strings.';
  	            var decoded = 0;
  	            while(dec) {
  	                var alphabetPosition = alphabet.indexOf(dec[0]);
  	                if (alphabetPosition < 0)
  	                    throw '"decode" can\'t find "' + dec[0] + '" in the alphabet: "' + alphabet + '"';
  	                var powerOf = dec.length - 1;
  	                decoded += alphabetPosition * (Math.pow(base, powerOf));
  	                dec = dec.substring(1);
  	            }
  	            return decoded;
  	        }
  	    };
  	})()
  
  };
  
  
  /* **********************************************
       Begin TL.Data.js
  ********************************************** */
  
  // Expects TL to be visible in scope
  
  ;(function(TL){
      /* Zepto v1.1.2-15-g59d3fe5 - zepto event ajax form ie - zeptojs.com/license */
  
      var Zepto = (function() {
        var undefined, key, $, classList, emptyArray = [], slice = emptyArray.slice, filter = emptyArray.filter,
          document = window.document,
          elementDisplay = {}, classCache = {},
          cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1,'opacity': 1, 'z-index': 1, 'zoom': 1 },
          fragmentRE = /^\s*<(\w+|!)[^>]*>/,
          singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
          tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,
          rootNodeRE = /^(?:body|html)$/i,
          capitalRE = /([A-Z])/g,
  
          // special attributes that should be get/set via method calls
          methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'],
  
          adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ],
          table = document.createElement('table'),
          tableRow = document.createElement('tr'),
          containers = {
            'tr': document.createElement('tbody'),
            'tbody': table, 'thead': table, 'tfoot': table,
            'td': tableRow, 'th': tableRow,
            '*': document.createElement('div')
          },
          readyRE = /complete|loaded|interactive/,
          classSelectorRE = /^\.([\w-]+)$/,
          idSelectorRE = /^#([\w-]*)$/,
          simpleSelectorRE = /^[\w-]*$/,
          class2type = {},
          toString = class2type.toString,
          zepto = {},
          camelize, uniq,
          tempParent = document.createElement('div'),
          propMap = {
            'tabindex': 'tabIndex',
            'readonly': 'readOnly',
            'for': 'htmlFor',
            'class': 'className',
            'maxlength': 'maxLength',
            'cellspacing': 'cellSpacing',
            'cellpadding': 'cellPadding',
            'rowspan': 'rowSpan',
            'colspan': 'colSpan',
            'usemap': 'useMap',
            'frameborder': 'frameBorder',
            'contenteditable': 'contentEditable'
          },
          isArray = Array.isArray ||
            function(object){ return object instanceof Array }
  
        zepto.matches = function(element, selector) {
          if (!selector || !element || element.nodeType !== 1) return false
          var matchesSelector = element.webkitMatchesSelector || element.mozMatchesSelector ||
                                element.oMatchesSelector || element.matchesSelector
          if (matchesSelector) return matchesSelector.call(element, selector)
          // fall back to performing a selector:
          var match, parent = element.parentNode, temp = !parent
          if (temp) (parent = tempParent).appendChild(element)
          match = ~zepto.qsa(parent, selector).indexOf(element)
          temp && tempParent.removeChild(element)
          return match
        }
  
        function type(obj) {
          return obj == null ? String(obj) :
            class2type[toString.call(obj)] || "object"
        }
  
        function isFunction(value) { return type(value) == "function" }
        function isWindow(obj)     { return obj != null && obj == obj.window }
        function isDocument(obj)   { return obj != null && obj.nodeType == obj.DOCUMENT_NODE }
        function isObject(obj)     { return type(obj) == "object" }
        function isPlainObject(obj) {
          return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype
        }
        function likeArray(obj) { return typeof obj.length == 'number' }
  
        function compact(array) { return filter.call(array, function(item){ return item != null }) }
        function flatten(array) { return array.length > 0 ? $.fn.concat.apply([], array) : array }
        camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) }
        function dasherize(str) {
          return str.replace(/::/g, '/')
                 .replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
                 .replace(/([a-z\d])([A-Z])/g, '$1_$2')
                 .replace(/_/g, '-')
                 .toLowerCase()
        }
        uniq = function(array){ return filter.call(array, function(item, idx){ return array.indexOf(item) == idx }) }
  
        function classRE(name) {
          return name in classCache ?
            classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'))
        }
  
        function maybeAddPx(name, value) {
          return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value
        }
  
        function defaultDisplay(nodeName) {
          var element, display
          if (!elementDisplay[nodeName]) {
            element = document.createElement(nodeName)
            document.body.appendChild(element)
            display = getComputedStyle(element, '').getPropertyValue("display")
            element.parentNode.removeChild(element)
            display == "none" && (display = "block")
            elementDisplay[nodeName] = display
          }
          return elementDisplay[nodeName]
        }
  
        function children(element) {
          return 'children' in element ?
            slice.call(element.children) :
            $.map(element.childNodes, function(node){ if (node.nodeType == 1) return node })
        }
  
        // `$.zepto.fragment` takes a html string and an optional tag name
        // to generate DOM nodes nodes from the given html string.
        // The generated DOM nodes are returned as an array.
        // This function can be overriden in plugins for example to make
        // it compatible with browsers that don't support the DOM fully.
        zepto.fragment = function(html, name, properties) {
          var dom, nodes, container
  
          // A special case optimization for a single tag
          if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))
  
          if (!dom) {
            if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
            if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
            if (!(name in containers)) name = '*'
  
            container = containers[name]
            container.innerHTML = '' + html
            dom = $.each(slice.call(container.childNodes), function(){
              container.removeChild(this)
            })
          }
  
          if (isPlainObject(properties)) {
            nodes = $(dom)
            $.each(properties, function(key, value) {
              if (methodAttributes.indexOf(key) > -1) nodes[key](value)
              else nodes.attr(key, value)
            })
          }
  
          return dom
        }
  
        // `$.zepto.Z` swaps out the prototype of the given `dom` array
        // of nodes with `$.fn` and thus supplying all the Zepto functions
        // to the array. Note that `__proto__` is not supported on Internet
        // Explorer. This method can be overriden in plugins.
        zepto.Z = function(dom, selector) {
          dom = dom || []
          dom.__proto__ = $.fn
          dom.selector = selector || ''
          return dom
        }
  
        // `$.zepto.isZ` should return `true` if the given object is a Zepto
        // collection. This method can be overriden in plugins.
        zepto.isZ = function(object) {
          return object instanceof zepto.Z
        }
  
        // `$.zepto.init` is Zepto's counterpart to jQuery's `$.fn.init` and
        // takes a CSS selector and an optional context (and handles various
        // special cases).
        // This method can be overriden in plugins.
        zepto.init = function(selector, context) {
          var dom
          // If nothing given, return an empty Zepto collection
          if (!selector) return zepto.Z()
          // Optimize for string selectors
          else if (typeof selector == 'string') {
            selector = selector.trim()
            // If it's a html fragment, create nodes from it
            // Note: In both Chrome 21 and Firefox 15, DOM error 12
            // is thrown if the fragment doesn't begin with <
            if (selector[0] == '<' && fragmentRE.test(selector))
              dom = zepto.fragment(selector, RegExp.$1, context), selector = null
            // If there's a context, create a collection on that context first, and select
            // nodes from there
            else if (context !== undefined) return $(context).find(selector)
            // If it's a CSS selector, use it to select nodes.
            else dom = zepto.qsa(document, selector)
          }
          // If a function is given, call it when the DOM is ready
          else if (isFunction(selector)) return $(document).ready(selector)
          // If a Zepto collection is given, just return it
          else if (zepto.isZ(selector)) return selector
          else {
            // normalize array if an array of nodes is given
            if (isArray(selector)) dom = compact(selector)
            // Wrap DOM nodes.
            else if (isObject(selector))
              dom = [selector], selector = null
            // If it's a html fragment, create nodes from it
            else if (fragmentRE.test(selector))
              dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null
            // If there's a context, create a collection on that context first, and select
            // nodes from there
            else if (context !== undefined) return $(context).find(selector)
            // And last but no least, if it's a CSS selector, use it to select nodes.
            else dom = zepto.qsa(document, selector)
          }
          // create a new Zepto collection from the nodes found
          return zepto.Z(dom, selector)
        }
  
        // `$` will be the base `Zepto` object. When calling this
        // function just call `$.zepto.init, which makes the implementation
        // details of selecting nodes and creating Zepto collections
        // patchable in plugins.
        $ = function(selector, context){
          return zepto.init(selector, context)
        }
  
        function extend(target, source, deep) {
          for (key in source)
            if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
              if (isPlainObject(source[key]) && !isPlainObject(target[key]))
                target[key] = {}
              if (isArray(source[key]) && !isArray(target[key]))
                target[key] = []
              extend(target[key], source[key], deep)
            }
            else if (source[key] !== undefined) target[key] = source[key]
        }
  
        // Copy all but undefined properties from one or more
        // objects to the `target` object.
        $.extend = function(target){
          var deep, args = slice.call(arguments, 1)
          if (typeof target == 'boolean') {
            deep = target
            target = args.shift()
          }
          args.forEach(function(arg){ extend(target, arg, deep) })
          return target
        }
  
        // `$.zepto.qsa` is Zepto's CSS selector implementation which
        // uses `document.querySelectorAll` and optimizes for some special cases, like `#id`.
        // This method can be overriden in plugins.
        zepto.qsa = function(element, selector){
          var found,
              maybeID = selector[0] == '#',
              maybeClass = !maybeID && selector[0] == '.',
              nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked
              isSimple = simpleSelectorRE.test(nameOnly)
          return (isDocument(element) && isSimple && maybeID) ?
            ( (found = element.getElementById(nameOnly)) ? [found] : [] ) :
            (element.nodeType !== 1 && element.nodeType !== 9) ? [] :
            slice.call(
              isSimple && !maybeID ?
                maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class
                element.getElementsByTagName(selector) : // Or a tag
                element.querySelectorAll(selector) // Or it's not simple, and we need to query all
            )
        }
  
        function filtered(nodes, selector) {
          return selector == null ? $(nodes) : $(nodes).filter(selector)
        }
  
        $.contains = function(parent, node) {
          return parent !== node && parent.contains(node)
        }
  
        function funcArg(context, arg, idx, payload) {
          return isFunction(arg) ? arg.call(context, idx, payload) : arg
        }
  
        function setAttribute(node, name, value) {
          value == null ? node.removeAttribute(name) : node.setAttribute(name, value)
        }
  
        // access className property while respecting SVGAnimatedString
        function className(node, value){
          var klass = node.className,
              svg   = klass && klass.baseVal !== undefined
  
          if (value === undefined) return svg ? klass.baseVal : klass
          svg ? (klass.baseVal = value) : (node.className = value)
        }
  
        // "true"  => true
        // "false" => false
        // "null"  => null
        // "42"    => 42
        // "42.5"  => 42.5
        // "08"    => "08"
        // JSON    => parse if valid
        // String  => self
        function deserializeValue(value) {
          var num
          try {
            return value ?
              value == "true" ||
              ( value == "false" ? false :
                value == "null" ? null :
                !/^0/.test(value) && !isNaN(num = Number(value)) ? num :
                /^[\[\{]/.test(value) ? $.parseJSON(value) :
                value )
              : value
          } catch(e) {
            return value
          }
        }
  
        $.type = type
        $.isFunction = isFunction
        $.isWindow = isWindow
        $.isArray = isArray
        $.isPlainObject = isPlainObject
  
        $.isEmptyObject = function(obj) {
          var name
          for (name in obj) return false
          return true
        }
  
        $.inArray = function(elem, array, i){
          return emptyArray.indexOf.call(array, elem, i)
        }
  
        $.camelCase = camelize
        $.trim = function(str) {
          return str == null ? "" : String.prototype.trim.call(str)
        }
  
        // plugin compatibility
        $.uuid = 0
        $.support = { }
        $.expr = { }
  
        $.map = function(elements, callback){
          var value, values = [], i, key
          if (likeArray(elements))
            for (i = 0; i < elements.length; i++) {
              value = callback(elements[i], i)
              if (value != null) values.push(value)
            }
          else
            for (key in elements) {
              value = callback(elements[key], key)
              if (value != null) values.push(value)
            }
          return flatten(values)
        }
  
        $.each = function(elements, callback){
          var i, key
          if (likeArray(elements)) {
            for (i = 0; i < elements.length; i++)
              if (callback.call(elements[i], i, elements[i]) === false) return elements
          } else {
            for (key in elements)
              if (callback.call(elements[key], key, elements[key]) === false) return elements
          }
  
          return elements
        }
  
        $.grep = function(elements, callback){
          return filter.call(elements, callback)
        }
  
        if (window.JSON) $.parseJSON = JSON.parse
  
        // Populate the class2type map
        $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
          class2type[ "[object " + name + "]" ] = name.toLowerCase()
        })
  
        // Define methods that will be available on all
        // Zepto collections
        $.fn = {
          // Because a collection acts like an array
          // copy over these useful array functions.
          forEach: emptyArray.forEach,
          reduce: emptyArray.reduce,
          push: emptyArray.push,
          sort: emptyArray.sort,
          indexOf: emptyArray.indexOf,
          concat: emptyArray.concat,
  
          // `map` and `slice` in the jQuery API work differently
          // from their array counterparts
          map: function(fn){
            return $($.map(this, function(el, i){ return fn.call(el, i, el) }))
          },
          slice: function(){
            return $(slice.apply(this, arguments))
          },
  
          ready: function(callback){
            // need to check if document.body exists for IE as that browser reports
            // document ready when it hasn't yet created the body element
            if (readyRE.test(document.readyState) && document.body) callback($)
            else document.addEventListener('DOMContentLoaded', function(){ callback($) }, false)
            return this
          },
          get: function(idx){
            return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length]
          },
          toArray: function(){ return this.get() },
          size: function(){
            return this.length
          },
          remove: function(){
            return this.each(function(){
              if (this.parentNode != null)
                this.parentNode.removeChild(this)
            })
          },
          each: function(callback){
            emptyArray.every.call(this, function(el, idx){
              return callback.call(el, idx, el) !== false
            })
            return this
          },
          filter: function(selector){
            if (isFunction(selector)) return this.not(this.not(selector))
            return $(filter.call(this, function(element){
              return zepto.matches(element, selector)
            }))
          },
          add: function(selector,context){
            return $(uniq(this.concat($(selector,context))))
          },
          is: function(selector){
            return this.length > 0 && zepto.matches(this[0], selector)
          },
          not: function(selector){
            var nodes=[]
            if (isFunction(selector) && selector.call !== undefined)
              this.each(function(idx){
                if (!selector.call(this,idx)) nodes.push(this)
              })
            else {
              var excludes = typeof selector == 'string' ? this.filter(selector) :
                (likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector)
              this.forEach(function(el){
                if (excludes.indexOf(el) < 0) nodes.push(el)
              })
            }
            return $(nodes)
          },
          has: function(selector){
            return this.filter(function(){
              return isObject(selector) ?
                $.contains(this, selector) :
                $(this).find(selector).size()
            })
          },
          eq: function(idx){
            return idx === -1 ? this.slice(idx) : this.slice(idx, + idx + 1)
          },
          first: function(){
            var el = this[0]
            return el && !isObject(el) ? el : $(el)
          },
          last: function(){
            var el = this[this.length - 1]
            return el && !isObject(el) ? el : $(el)
          },
          find: function(selector){
            var result, $this = this
            if (typeof selector == 'object')
              result = $(selector).filter(function(){
                var node = this
                return emptyArray.some.call($this, function(parent){
                  return $.contains(parent, node)
                })
              })
            else if (this.length == 1) result = $(zepto.qsa(this[0], selector))
            else result = this.map(function(){ return zepto.qsa(this, selector) })
            return result
          },
          closest: function(selector, context){
            var node = this[0], collection = false
            if (typeof selector == 'object') collection = $(selector)
            while (node && !(collection ? collection.indexOf(node) >= 0 : zepto.matches(node, selector)))
              node = node !== context && !isDocument(node) && node.parentNode
            return $(node)
          },
          parents: function(selector){
            var ancestors = [], nodes = this
            while (nodes.length > 0)
              nodes = $.map(nodes, function(node){
                if ((node = node.parentNode) && !isDocument(node) && ancestors.indexOf(node) < 0) {
                  ancestors.push(node)
                  return node
                }
              })
            return filtered(ancestors, selector)
          },
          parent: function(selector){
            return filtered(uniq(this.pluck('parentNode')), selector)
          },
          children: function(selector){
            return filtered(this.map(function(){ return children(this) }), selector)
          },
          contents: function() {
            return this.map(function() { return slice.call(this.childNodes) })
          },
          siblings: function(selector){
            return filtered(this.map(function(i, el){
              return filter.call(children(el.parentNode), function(child){ return child!==el })
            }), selector)
          },
          empty: function(){
            return this.each(function(){ this.innerHTML = '' })
          },
          // `pluck` is borrowed from Prototype.js
          pluck: function(property){
            return $.map(this, function(el){ return el[property] })
          },
          show: function(){
            return this.each(function(){
              this.style.display == "none" && (this.style.display = '')
              if (getComputedStyle(this, '').getPropertyValue("display") == "none")
                this.style.display = defaultDisplay(this.nodeName)
            })
          },
          replaceWith: function(newContent){
            return this.before(newContent).remove()
          },
          wrap: function(structure){
            var func = isFunction(structure)
            if (this[0] && !func)
              var dom   = $(structure).get(0),
                  clone = dom.parentNode || this.length > 1
  
            return this.each(function(index){
              $(this).wrapAll(
                func ? structure.call(this, index) :
                  clone ? dom.cloneNode(true) : dom
              )
            })
          },
          wrapAll: function(structure){
            if (this[0]) {
              $(this[0]).before(structure = $(structure))
              var children
              // drill down to the inmost element
              while ((children = structure.children()).length) structure = children.first()
              $(structure).append(this)
            }
            return this
          },
          wrapInner: function(structure){
            var func = isFunction(structure)
            return this.each(function(index){
              var self = $(this), contents = self.contents(),
                  dom  = func ? structure.call(this, index) : structure
              contents.length ? contents.wrapAll(dom) : self.append(dom)
            })
          },
          unwrap: function(){
            this.parent().each(function(){
              $(this).replaceWith($(this).children())
            })
            return this
          },
          clone: function(){
            return this.map(function(){ return this.cloneNode(true) })
          },
          hide: function(){
            return this.css("display", "none")
          },
          toggle: function(setting){
            return this.each(function(){
              var el = $(this)
              ;(setting === undefined ? el.css("display") == "none" : setting) ? el.show() : el.hide()
            })
          },
          prev: function(selector){ return $(this.pluck('previousElementSibling')).filter(selector || '*') },
          next: function(selector){ return $(this.pluck('nextElementSibling')).filter(selector || '*') },
          html: function(html){
            return arguments.length === 0 ?
              (this.length > 0 ? this[0].innerHTML : null) :
              this.each(function(idx){
                var originHtml = this.innerHTML
                $(this).empty().append( funcArg(this, html, idx, originHtml) )
              })
          },
          text: function(text){
            return arguments.length === 0 ?
              (this.length > 0 ? this[0].textContent : null) :
              this.each(function(){ this.textContent = (text === undefined) ? '' : ''+text })
          },
          attr: function(name, value){
            var result
            return (typeof name == 'string' && value === undefined) ?
              (this.length == 0 || this[0].nodeType !== 1 ? undefined :
                (name == 'value' && this[0].nodeName == 'INPUT') ? this.val() :
                (!(result = this[0].getAttribute(name)) && name in this[0]) ? this[0][name] : result
              ) :
              this.each(function(idx){
                if (this.nodeType !== 1) return
                if (isObject(name)) for (key in name) setAttribute(this, key, name[key])
                else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
              })
          },
          removeAttr: function(name){
            return this.each(function(){ this.nodeType === 1 && setAttribute(this, name) })
          },
          prop: function(name, value){
            name = propMap[name] || name
            return (value === undefined) ?
              (this[0] && this[0][name]) :
              this.each(function(idx){
                this[name] = funcArg(this, value, idx, this[name])
              })
          },
          data: function(name, value){
            var data = this.attr('data-' + name.replace(capitalRE, '-$1').toLowerCase(), value)
            return data !== null ? deserializeValue(data) : undefined
          },
          val: function(value){
            return arguments.length === 0 ?
              (this[0] && (this[0].multiple ?
                 $(this[0]).find('option').filter(function(){ return this.selected }).pluck('value') :
                 this[0].value)
              ) :
              this.each(function(idx){
                this.value = funcArg(this, value, idx, this.value)
              })
          },
          offset: function(coordinates){
            if (coordinates) return this.each(function(index){
              var $this = $(this),
                  coords = funcArg(this, coordinates, index, $this.offset()),
                  parentOffset = $this.offsetParent().offset(),
                  props = {
                    top:  coords.top  - parentOffset.top,
                    left: coords.left - parentOffset.left
                  }
  
              if ($this.css('position') == 'static') props['position'] = 'relative'
              $this.css(props)
            })
            if (this.length==0) return null
            var obj = this[0].getBoundingClientRect()
            return {
              left: obj.left + window.pageXOffset,
              top: obj.top + window.pageYOffset,
              width: Math.round(obj.width),
              height: Math.round(obj.height)
            }
          },
          css: function(property, value){
            if (arguments.length < 2) {
              var element = this[0], computedStyle = getComputedStyle(element, '')
              if(!element) return
              if (typeof property == 'string')
                return element.style[camelize(property)] || computedStyle.getPropertyValue(property)
              else if (isArray(property)) {
                var props = {}
                $.each(isArray(property) ? property: [property], function(_, prop){
                  props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop))
                })
                return props
              }
            }
  
            var css = ''
            if (type(property) == 'string') {
              if (!value && value !== 0)
                this.each(function(){ this.style.removeProperty(dasherize(property)) })
              else
                css = dasherize(property) + ":" + maybeAddPx(property, value)
            } else {
              for (key in property)
                if (!property[key] && property[key] !== 0)
                  this.each(function(){ this.style.removeProperty(dasherize(key)) })
                else
                  css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'
            }
  
            return this.each(function(){ this.style.cssText += ';' + css })
          },
          index: function(element){
            return element ? this.indexOf($(element)[0]) : this.parent().children().indexOf(this[0])
          },
          hasClass: function(name){
            if (!name) return false
            return emptyArray.some.call(this, function(el){
              return this.test(className(el))
            }, classRE(name))
          },
          addClass: function(name){
            if (!name) return this
            return this.each(function(idx){
              classList = []
              var cls = className(this), newName = funcArg(this, name, idx, cls)
              newName.split(/\s+/g).forEach(function(klass){
                if (!$(this).hasClass(klass)) classList.push(klass)
              }, this)
              classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))
            })
          },
          removeClass: function(name){
            return this.each(function(idx){
              if (name === undefined) return className(this, '')
              classList = className(this)
              funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass){
                classList = classList.replace(classRE(klass), " ")
              })
              className(this, classList.trim())
            })
          },
          toggleClass: function(name, when){
            if (!name) return this
            return this.each(function(idx){
              var $this = $(this), names = funcArg(this, name, idx, className(this))
              names.split(/\s+/g).forEach(function(klass){
                (when === undefined ? !$this.hasClass(klass) : when) ?
                  $this.addClass(klass) : $this.removeClass(klass)
              })
            })
          },
          scrollTop: function(value){
            if (!this.length) return
            var hasScrollTop = 'scrollTop' in this[0]
            if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset
            return this.each(hasScrollTop ?
              function(){ this.scrollTop = value } :
              function(){ this.scrollTo(this.scrollX, value) })
          },
          scrollLeft: function(value){
            if (!this.length) return
            var hasScrollLeft = 'scrollLeft' in this[0]
            if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset
            return this.each(hasScrollLeft ?
              function(){ this.scrollLeft = value } :
              function(){ this.scrollTo(value, this.scrollY) })
          },
          position: function() {
            if (!this.length) return
  
            var elem = this[0],
              // Get *real* offsetParent
              offsetParent = this.offsetParent(),
              // Get correct offsets
              offset       = this.offset(),
              parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()
  
            // Subtract element margins
            // note: when an element has margin: auto the offsetLeft and marginLeft
            // are the same in Safari causing offset.left to incorrectly be 0
            offset.top  -= parseFloat( $(elem).css('margin-top') ) || 0
            offset.left -= parseFloat( $(elem).css('margin-left') ) || 0
  
            // Add offsetParent borders
            parentOffset.top  += parseFloat( $(offsetParent[0]).css('border-top-width') ) || 0
            parentOffset.left += parseFloat( $(offsetParent[0]).css('border-left-width') ) || 0
  
            // Subtract the two offsets
            return {
              top:  offset.top  - parentOffset.top,
              left: offset.left - parentOffset.left
            }
          },
          offsetParent: function() {
            return this.map(function(){
              var parent = this.offsetParent || document.body
              while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static")
                parent = parent.offsetParent
              return parent
            })
          }
        }
  
        // for now
        $.fn.detach = $.fn.remove
  
        // Generate the `width` and `height` functions
        ;['width', 'height'].forEach(function(dimension){
          var dimensionProperty =
            dimension.replace(/./, function(m){ return m[0].toUpperCase() })
  
          $.fn[dimension] = function(value){
            var offset, el = this[0]
            if (value === undefined) return isWindow(el) ? el['inner' + dimensionProperty] :
              isDocument(el) ? el.documentElement['scroll' + dimensionProperty] :
              (offset = this.offset()) && offset[dimension]
            else return this.each(function(idx){
              el = $(this)
              el.css(dimension, funcArg(this, value, idx, el[dimension]()))
            })
          }
        })
  
        function traverseNode(node, fun) {
          fun(node)
          for (var key in node.childNodes) traverseNode(node.childNodes[key], fun)
        }
  
        // Generate the `after`, `prepend`, `before`, `append`,
        // `insertAfter`, `insertBefore`, `appendTo`, and `prependTo` methods.
        adjacencyOperators.forEach(function(operator, operatorIndex) {
          var inside = operatorIndex % 2 //=> prepend, append
  
          $.fn[operator] = function(){
            // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings
            var argType, nodes = $.map(arguments, function(arg) {
                  argType = type(arg)
                  return argType == "object" || argType == "array" || arg == null ?
                    arg : zepto.fragment(arg)
                }),
                parent, copyByClone = this.length > 1
            if (nodes.length < 1) return this
  
            return this.each(function(_, target){
              parent = inside ? target : target.parentNode
  
              // convert all methods to a "before" operation
              target = operatorIndex == 0 ? target.nextSibling :
                       operatorIndex == 1 ? target.firstChild :
                       operatorIndex == 2 ? target :
                       null
  
              nodes.forEach(function(node){
                if (copyByClone) node = node.cloneNode(true)
                else if (!parent) return $(node).remove()
  
                traverseNode(parent.insertBefore(node, target), function(el){
                  if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
                     (!el.type || el.type === 'text/javascript') && !el.src)
                    window['eval'].call(window, el.innerHTML)
                })
              })
            })
          }
  
          // after    => insertAfter
          // prepend  => prependTo
          // before   => insertBefore
          // append   => appendTo
          $.fn[inside ? operator+'To' : 'insert'+(operatorIndex ? 'Before' : 'After')] = function(html){
            $(html)[operator](this)
            return this
          }
        })
  
        zepto.Z.prototype = $.fn
  
        // Export internal API functions in the `$.zepto` namespace
        zepto.uniq = uniq
        zepto.deserializeValue = deserializeValue
        $.zepto = zepto
  
        return $
      })()
  
      window.Zepto = Zepto
      window.$ === undefined && (window.$ = Zepto)
  
      ;(function($){
        var $$ = $.zepto.qsa, _zid = 1, undefined,
            slice = Array.prototype.slice,
            isFunction = $.isFunction,
            isString = function(obj){ return typeof obj == 'string' },
            handlers = {},
            specialEvents={},
            focusinSupported = 'onfocusin' in window,
            focus = { focus: 'focusin', blur: 'focusout' },
            hover = { mouseenter: 'mouseover', mouseleave: 'mouseout' }
  
        specialEvents.click = specialEvents.mousedown = specialEvents.mouseup = specialEvents.mousemove = 'MouseEvents'
  
        function zid(element) {
          return element._zid || (element._zid = _zid++)
        }
        function findHandlers(element, event, fn, selector) {
          event = parse(event)
          if (event.ns) var matcher = matcherFor(event.ns)
          return (handlers[zid(element)] || []).filter(function(handler) {
            return handler
              && (!event.e  || handler.e == event.e)
              && (!event.ns || matcher.test(handler.ns))
              && (!fn       || zid(handler.fn) === zid(fn))
              && (!selector || handler.sel == selector)
          })
        }
        function parse(event) {
          var parts = ('' + event).split('.')
          return {e: parts[0], ns: parts.slice(1).sort().join(' ')}
        }
        function matcherFor(ns) {
          return new RegExp('(?:^| )' + ns.replace(' ', ' .* ?') + '(?: |$)')
        }
  
        function eventCapture(handler, captureSetting) {
          return handler.del &&
            (!focusinSupported && (handler.e in focus)) ||
            !!captureSetting
        }
  
        function realEvent(type) {
          return hover[type] || (focusinSupported && focus[type]) || type
        }
  
        function add(element, events, fn, data, selector, delegator, capture){
          var id = zid(element), set = (handlers[id] || (handlers[id] = []))
          events.split(/\s/).forEach(function(event){
            if (event == 'ready') return $(document).ready(fn)
            var handler   = parse(event)
            handler.fn    = fn
            handler.sel   = selector
            // emulate mouseenter, mouseleave
            if (handler.e in hover) fn = function(e){
              var related = e.relatedTarget
              if (!related || (related !== this && !$.contains(this, related)))
                return handler.fn.apply(this, arguments)
            }
            handler.del   = delegator
            var callback  = delegator || fn
            handler.proxy = function(e){
              e = compatible(e)
              if (e.isImmediatePropagationStopped()) return
              e.data = data
              var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
              if (result === false) e.preventDefault(), e.stopPropagation()
              return result
            }
            handler.i = set.length
            set.push(handler)
            if ('addEventListener' in element)
              element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
          })
        }
        function remove(element, events, fn, selector, capture){
          var id = zid(element)
          ;(events || '').split(/\s/).forEach(function(event){
            findHandlers(element, event, fn, selector).forEach(function(handler){
              delete handlers[id][handler.i]
            if ('removeEventListener' in element)
              element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
            })
          })
        }
  
        $.event = { add: add, remove: remove }
  
        $.proxy = function(fn, context) {
          if (isFunction(fn)) {
            var proxyFn = function(){ return fn.apply(context, arguments) }
            proxyFn._zid = zid(fn)
            return proxyFn
          } else if (isString(context)) {
            return $.proxy(fn[context], fn)
          } else {
            throw new TypeError("expected function")
          }
        }
  
        $.fn.bind = function(event, data, callback){
          return this.on(event, data, callback)
        }
        $.fn.unbind = function(event, callback){
          return this.off(event, callback)
        }
        $.fn.one = function(event, selector, data, callback){
          return this.on(event, selector, data, callback, 1)
        }
  
        var returnTrue = function(){return true},
            returnFalse = function(){return false},
            ignoreProperties = /^([A-Z]|returnValue$|layer[XY]$)/,
            eventMethods = {
              preventDefault: 'isDefaultPrevented',
              stopImmediatePropagation: 'isImmediatePropagationStopped',
              stopPropagation: 'isPropagationStopped'
            }
  
        function compatible(event, source) {
          if (source || !event.isDefaultPrevented) {
            source || (source = event)
  
            $.each(eventMethods, function(name, predicate) {
              var sourceMethod = source[name]
              event[name] = function(){
                this[predicate] = returnTrue
                return sourceMethod && sourceMethod.apply(source, arguments)
              }
              event[predicate] = returnFalse
            })
  
            if (source.defaultPrevented !== undefined ? source.defaultPrevented :
                'returnValue' in source ? source.returnValue === false :
                source.getPreventDefault && source.getPreventDefault())
              event.isDefaultPrevented = returnTrue
          }
          return event
        }
  
        function createProxy(event) {
          var key, proxy = { originalEvent: event }
          for (key in event)
            if (!ignoreProperties.test(key) && event[key] !== undefined) proxy[key] = event[key]
  
          return compatible(proxy, event)
        }
  
        $.fn.delegate = function(selector, event, callback){
          return this.on(event, selector, callback)
        }
        $.fn.undelegate = function(selector, event, callback){
          return this.off(event, selector, callback)
        }
  
        $.fn.live = function(event, callback){
          $(document.body).delegate(this.selector, event, callback)
          return this
        }
        $.fn.die = function(event, callback){
          $(document.body).undelegate(this.selector, event, callback)
          return this
        }
  
        $.fn.on = function(event, selector, data, callback, one){
          var autoRemove, delegator, $this = this
          if (event && !isString(event)) {
            $.each(event, function(type, fn){
              $this.on(type, selector, data, fn, one)
            })
            return $this
          }
  
          if (!isString(selector) && !isFunction(callback) && callback !== false)
            callback = data, data = selector, selector = undefined
          if (isFunction(data) || data === false)
            callback = data, data = undefined
  
          if (callback === false) callback = returnFalse
  
          return $this.each(function(_, element){
            if (one) autoRemove = function(e){
              remove(element, e.type, callback)
              return callback.apply(this, arguments)
            }
  
            if (selector) delegator = function(e){
              var evt, match = $(e.target).closest(selector, element).get(0)
              if (match && match !== element) {
                evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element})
                return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments, 1)))
              }
            }
  
            add(element, event, callback, data, selector, delegator || autoRemove)
          })
        }
        $.fn.off = function(event, selector, callback){
          var $this = this
          if (event && !isString(event)) {
            $.each(event, function(type, fn){
              $this.off(type, selector, fn)
            })
            return $this
          }
  
          if (!isString(selector) && !isFunction(callback) && callback !== false)
            callback = selector, selector = undefined
  
          if (callback === false) callback = returnFalse
  
          return $this.each(function(){
            remove(this, event, callback, selector)
          })
        }
  
        $.fn.trigger = function(event, args){
          event = (isString(event) || $.isPlainObject(event)) ? $.Event(event) : compatible(event)
          event._args = args
          return this.each(function(){
            // items in the collection might not be DOM elements
            if('dispatchEvent' in this) this.dispatchEvent(event)
            else $(this).triggerHandler(event, args)
          })
        }
  
        // triggers event handlers on current element just as if an event occurred,
        // doesn't trigger an actual event, doesn't bubble
        $.fn.triggerHandler = function(event, args){
          var e, result
          this.each(function(i, element){
            e = createProxy(isString(event) ? $.Event(event) : event)
            e._args = args
            e.target = element
            $.each(findHandlers(element, event.type || event), function(i, handler){
              result = handler.proxy(e)
              if (e.isImmediatePropagationStopped()) return false
            })
          })
          return result
        }
  
        // shortcut methods for `.bind(event, fn)` for each event type
        ;('focusin focusout load resize scroll unload click dblclick '+
        'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave '+
        'change select keydown keypress keyup error').split(' ').forEach(function(event) {
          $.fn[event] = function(callback) {
            return callback ?
              this.bind(event, callback) :
              this.trigger(event)
          }
        })
  
        ;['focus', 'blur'].forEach(function(name) {
          $.fn[name] = function(callback) {
            if (callback) this.bind(name, callback)
            else this.each(function(){
              try { this[name]() }
              catch(e) {}
            })
            return this
          }
        })
  
        $.Event = function(type, props) {
          if (!isString(type)) props = type, type = props.type
          var event = document.createEvent(specialEvents[type] || 'Events'), bubbles = true
          if (props) for (var name in props) (name == 'bubbles') ? (bubbles = !!props[name]) : (event[name] = props[name])
          event.initEvent(type, bubbles, true)
          return compatible(event)
        }
  
      })(Zepto)
  
      ;(function($){
        var jsonpID = 0,
            document = window.document,
            key,
            name,
            rscript = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
            scriptTypeRE = /^(?:text|application)\/javascript/i,
            xmlTypeRE = /^(?:text|application)\/xml/i,
            jsonType = 'application/json',
            htmlType = 'text/html',
            blankRE = /^\s*$/
  
        // trigger a custom event and return false if it was cancelled
        function triggerAndReturn(context, eventName, data) {
          var event = $.Event(eventName)
          $(context).trigger(event, data)
          return !event.isDefaultPrevented()
        }
  
        // trigger an Ajax "global" event
        function triggerGlobal(settings, context, eventName, data) {
          if (settings.global) return triggerAndReturn(context || document, eventName, data)
        }
  
        // Number of active Ajax requests
        $.active = 0
  
        function ajaxStart(settings) {
          if (settings.global && $.active++ === 0) triggerGlobal(settings, null, 'ajaxStart')
        }
        function ajaxStop(settings) {
          if (settings.global && !(--$.active)) triggerGlobal(settings, null, 'ajaxStop')
        }
  
        // triggers an extra global event "ajaxBeforeSend" that's like "ajaxSend" but cancelable
        function ajaxBeforeSend(xhr, settings) {
          var context = settings.context
          if (settings.beforeSend.call(context, xhr, settings) === false ||
              triggerGlobal(settings, context, 'ajaxBeforeSend', [xhr, settings]) === false)
            return false
  
          triggerGlobal(settings, context, 'ajaxSend', [xhr, settings])
        }
        function ajaxSuccess(data, xhr, settings, deferred) {
          var context = settings.context, status = 'success'
          settings.success.call(context, data, status, xhr)
          if (deferred) deferred.resolveWith(context, [data, status, xhr])
          triggerGlobal(settings, context, 'ajaxSuccess', [xhr, settings, data])
          ajaxComplete(status, xhr, settings)
        }
        // type: "timeout", "error", "abort", "parsererror"
        function ajaxError(error, type, xhr, settings, deferred) {
          var context = settings.context
          settings.error.call(context, xhr, type, error)
          if (deferred) deferred.rejectWith(context, [xhr, type, error])
          triggerGlobal(settings, context, 'ajaxError', [xhr, settings, error || type])
          ajaxComplete(type, xhr, settings)
        }
        // status: "success", "notmodified", "error", "timeout", "abort", "parsererror"
        function ajaxComplete(status, xhr, settings) {
          var context = settings.context
          settings.complete.call(context, xhr, status)
          triggerGlobal(settings, context, 'ajaxComplete', [xhr, settings])
          ajaxStop(settings)
        }
  
        // Empty function, used as default callback
        function empty() {}
  
        $.ajaxJSONP = function(options, deferred){
          if (!('type' in options)) return $.ajax(options)
  
          var _callbackName = options.jsonpCallback,
            callbackName = ($.isFunction(_callbackName) ?
              _callbackName() : _callbackName) || ('jsonp' + (++jsonpID)),
            script = document.createElement('script'),
            originalCallback = window[callbackName],
            responseData,
            abort = function(errorType) {
              $(script).triggerHandler('error', errorType || 'abort')
            },
            xhr = { abort: abort }, abortTimeout
  
          if (deferred) deferred.promise(xhr)
  
          $(script).on('load error', function(e, errorType){
            clearTimeout(abortTimeout)
            $(script).off().remove()
  
            if (e.type == 'error' || !responseData) {
              ajaxError(null, errorType || 'error', xhr, options, deferred)
            } else {
              ajaxSuccess(responseData[0], xhr, options, deferred)
            }
  
            window[callbackName] = originalCallback
            if (responseData && $.isFunction(originalCallback))
              originalCallback(responseData[0])
  
            originalCallback = responseData = undefined
          })
  
          if (ajaxBeforeSend(xhr, options) === false) {
            abort('abort')
            return xhr
          }
  
          window[callbackName] = function(){
            responseData = arguments
          }
  
          script.src = options.url.replace(/\?(.+)=\?/, '?$1=' + callbackName)
          document.head.appendChild(script)
  
          if (options.timeout > 0) abortTimeout = setTimeout(function(){
            abort('timeout')
          }, options.timeout)
  
          return xhr
        }
  
        $.ajaxSettings = {
          // Default type of request
          type: 'GET',
          // Callback that is executed before request
          beforeSend: empty,
          // Callback that is executed if the request succeeds
          success: empty,
          // Callback that is executed the the server drops error
          error: empty,
          // Callback that is executed on request complete (both: error and success)
          complete: empty,
          // The context for the callbacks
          context: null,
          // Whether to trigger "global" Ajax events
          global: true,
          // Transport
          xhr: function () {
            return new window.XMLHttpRequest()
          },
          // MIME types mapping
          // IIS returns Javascript as "application/x-javascript"
          accepts: {
            script: 'text/javascript, application/javascript, application/x-javascript',
            json:   jsonType,
            xml:    'application/xml, text/xml',
            html:   htmlType,
            text:   'text/plain'
          },
          // Whether the request is to another domain
          crossDomain: false,
          // Default timeout
          timeout: 0,
          // Whether data should be serialized to string
          processData: true,
          // Whether the browser should be allowed to cache GET responses
          cache: true
        }
  
        function mimeToDataType(mime) {
          if (mime) mime = mime.split(';', 2)[0]
          return mime && ( mime == htmlType ? 'html' :
            mime == jsonType ? 'json' :
            scriptTypeRE.test(mime) ? 'script' :
            xmlTypeRE.test(mime) && 'xml' ) || 'text'
        }
  
        function appendQuery(url, query) {
          if (query == '') return url
          return (url + '&' + query).replace(/[&?]{1,2}/, '?')
        }
  
        // serialize payload and append it to the URL for GET requests
        function serializeData(options) {
          if (options.processData && options.data && $.type(options.data) != "string")
            options.data = $.param(options.data, options.traditional)
          if (options.data && (!options.type || options.type.toUpperCase() == 'GET'))
            options.url = appendQuery(options.url, options.data), options.data = undefined
        }
  
        $.ajax = function(options){
          var settings = $.extend({}, options || {}),
              deferred = $.Deferred && $.Deferred()
          for (key in $.ajaxSettings) if (settings[key] === undefined) settings[key] = $.ajaxSettings[key]
  
          ajaxStart(settings)
  
          if (!settings.crossDomain) settings.crossDomain = /^([\w-]+:)?\/\/([^\/]+)/.test(settings.url) &&
            RegExp.$2 != window.location.host
  
          if (!settings.url) settings.url = window.location.toString()
          serializeData(settings)
          if (settings.cache === false) settings.url = appendQuery(settings.url, '_=' + Date.now())
  
          var dataType = settings.dataType, hasPlaceholder = /\?.+=\?/.test(settings.url)
          if (dataType == 'jsonp' || hasPlaceholder) {
            if (!hasPlaceholder)
              settings.url = appendQuery(settings.url,
                settings.jsonp ? (settings.jsonp + '=?') : settings.jsonp === false ? '' : 'callback=?')
            return $.ajaxJSONP(settings, deferred)
          }
  
          var mime = settings.accepts[dataType],
              headers = { },
              setHeader = function(name, value) { headers[name.toLowerCase()] = [name, value] },
              protocol = /^([\w-]+:)\/\//.test(settings.url) ? RegExp.$1 : window.location.protocol,
              xhr = settings.xhr(),
              nativeSetHeader = xhr.setRequestHeader,
              abortTimeout
  
          if (deferred) deferred.promise(xhr)
  
          if (!settings.crossDomain) setHeader('X-Requested-With', 'XMLHttpRequest')
          setHeader('Accept', mime || '*/*')
          if (mime = settings.mimeType || mime) {
            if (mime.indexOf(',') > -1) mime = mime.split(',', 2)[0]
            xhr.overrideMimeType && xhr.overrideMimeType(mime)
          }
          if (settings.contentType || (settings.contentType !== false && settings.data && settings.type.toUpperCase() != 'GET'))
            setHeader('Content-Type', settings.contentType || 'application/x-www-form-urlencoded')
  
          if (settings.headers) for (name in settings.headers) setHeader(name, settings.headers[name])
          xhr.setRequestHeader = setHeader
  
          xhr.onreadystatechange = function(){
            if (xhr.readyState == 4) {
              xhr.onreadystatechange = empty
              clearTimeout(abortTimeout)
              var result, error = false
              if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304 || (xhr.status == 0 && protocol == 'file:')) {
                dataType = dataType || mimeToDataType(settings.mimeType || xhr.getResponseHeader('content-type'))
                result = xhr.responseText
  
                try {
                  // http://perfectionkills.com/global-eval-what-are-the-options/
                  if (dataType == 'script')    (1,eval)(result)
                  else if (dataType == 'xml')  result = xhr.responseXML
                  else if (dataType == 'json') result = blankRE.test(result) ? null : $.parseJSON(result)
                } catch (e) { error = e }
  
                if (error) ajaxError(error, 'parsererror', xhr, settings, deferred)
                else ajaxSuccess(result, xhr, settings, deferred)
              } else {
                ajaxError(xhr.statusText || null, xhr.status ? 'error' : 'abort', xhr, settings, deferred)
              }
            }
          }
  
          if (ajaxBeforeSend(xhr, settings) === false) {
            xhr.abort()
            ajaxError(null, 'abort', xhr, settings, deferred)
            return xhr
          }
  
          if (settings.xhrFields) for (name in settings.xhrFields) xhr[name] = settings.xhrFields[name]
  
          var async = 'async' in settings ? settings.async : true
          xhr.open(settings.type, settings.url, async, settings.username, settings.password)
  
          for (name in headers) nativeSetHeader.apply(xhr, headers[name])
  
          if (settings.timeout > 0) abortTimeout = setTimeout(function(){
              xhr.onreadystatechange = empty
              xhr.abort()
              ajaxError(null, 'timeout', xhr, settings, deferred)
            }, settings.timeout)
  
          // avoid sending empty string (#319)
          xhr.send(settings.data ? settings.data : null)
          return xhr
        }
  
        // handle optional data/success arguments
        function parseArguments(url, data, success, dataType) {
          var hasData = !$.isFunction(data)
          return {
            url:      url,
            data:     hasData  ? data : undefined,
            success:  !hasData ? data : $.isFunction(success) ? success : undefined,
            dataType: hasData  ? dataType || success : success
          }
        }
  
        $.get = function(url, data, success, dataType){
          return $.ajax(parseArguments.apply(null, arguments))
        }
  
        $.post = function(url, data, success, dataType){
          var options = parseArguments.apply(null, arguments)
          options.type = 'POST'
          return $.ajax(options)
        }
  
        $.getJSON = function(url, data, success){
          var options = parseArguments.apply(null, arguments)
          options.dataType = 'json'
          return $.ajax(options)
        }
  
        $.fn.load = function(url, data, success){
          if (!this.length) return this
          var self = this, parts = url.split(/\s/), selector,
              options = parseArguments(url, data, success),
              callback = options.success
          if (parts.length > 1) options.url = parts[0], selector = parts[1]
          options.success = function(response){
            self.html(selector ?
              $('<div>').html(response.replace(rscript, "")).find(selector)
              : response)
            callback && callback.apply(self, arguments)
          }
          $.ajax(options)
          return this
        }
  
        var escape = encodeURIComponent
  
        function serialize(params, obj, traditional, scope){
          var type, array = $.isArray(obj), hash = $.isPlainObject(obj)
          $.each(obj, function(key, value) {
            type = $.type(value)
            if (scope) key = traditional ? scope :
              scope + '[' + (hash || type == 'object' || type == 'array' ? key : '') + ']'
            // handle data in serializeArray() format
            if (!scope && array) params.add(value.name, value.value)
            // recurse into nested objects
            else if (type == "array" || (!traditional && type == "object"))
              serialize(params, value, traditional, key)
            else params.add(key, value)
          })
        }
  
        $.param = function(obj, traditional){
          var params = []
          params.add = function(k, v){ this.push(escape(k) + '=' + escape(v)) }
          serialize(params, obj, traditional)
          return params.join('&').replace(/%20/g, '+')
        }
      })(Zepto)
  
      ;(function($){
        $.fn.serializeArray = function() {
          var result = [], el
          $([].slice.call(this.get(0).elements)).each(function(){
            el = $(this)
            var type = el.attr('type')
            if (this.nodeName.toLowerCase() != 'fieldset' &&
              !this.disabled && type != 'submit' && type != 'reset' && type != 'button' &&
              ((type != 'radio' && type != 'checkbox') || this.checked))
              result.push({
                name: el.attr('name'),
                value: el.val()
              })
          })
          return result
        }
  
        $.fn.serialize = function(){
          var result = []
          this.serializeArray().forEach(function(elm){
            result.push(encodeURIComponent(elm.name) + '=' + encodeURIComponent(elm.value))
          })
          return result.join('&')
        }
  
        $.fn.submit = function(callback) {
          if (callback) this.bind('submit', callback)
          else if (this.length) {
            var event = $.Event('submit')
            this.eq(0).trigger(event)
            if (!event.isDefaultPrevented()) this.get(0).submit()
          }
          return this
        }
  
      })(Zepto)
  
      ;(function($){
        // __proto__ doesn't exist on IE<11, so redefine
        // the Z function to use object extension instead
        if (!('__proto__' in {})) {
          $.extend($.zepto, {
            Z: function(dom, selector){
              dom = dom || []
              $.extend(dom, $.fn)
              dom.selector = selector || ''
              dom.__Z = true
              return dom
            },
            // this is a kludge but works
            isZ: function(object){
              return $.type(object) === 'array' && '__Z' in object
            }
          })
        }
  
        // getComputedStyle shouldn't freak out when called
        // without a valid element as argument
        try {
          getComputedStyle(undefined)
        } catch(e) {
          var nativeGetComputedStyle = getComputedStyle;
          window.getComputedStyle = function(element, pseudoElement){
            try {
              return nativeGetComputedStyle(element, pseudoElement)
            } catch(e) {
              return null
            }
          }
        }
      })(Zepto)
  
  
    TL.getJSON = Zepto.getJSON;
  	TL.ajax = Zepto.ajax;
  })(TL)
  
  //     Based on https://github.com/madrobby/zepto/blob/5585fe00f1828711c04208372265a5d71e3238d1/src/ajax.js
  //     Zepto.js
  //     (c) 2010-2012 Thomas Fuchs
  //     Zepto.js may be freely distributed under the MIT license.
  /*
  Copyright (c) 2010-2012 Thomas Fuchs
  http://zeptojs.com
  
  Permission is hereby granted, free of charge, to any person obtaining a copy 
  of this software and associated documentation files (the "Software"), to deal 
  in the Software without restriction, including without limitation the rights 
  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
  copies of the Software, and to permit persons to whom the Software is 
  furnished to do so, subject to the following conditions:
  
  The above copyright notice and this permission notice shall be included in all 
  copies or substantial portions of the Software.
  
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
  SOFTWARE.
  */
  
  
  /* **********************************************
       Begin TL.Class.js
  ********************************************** */
  
  /*	TL.Class
  	Class powers the OOP facilities of the library.
  ================================================== */
  TL.Class = function () {};
  
  TL.Class.extend = function (/*Object*/ props) /*-> Class*/ {
   
  	// extended class with the new prototype
  	var NewClass = function () {
  		if (this.initialize) {
  			this.initialize.apply(this, arguments);
  		}
  	};
  
  	// instantiate class without calling constructor
  	var F = function () {};
  	F.prototype = this.prototype;
  	var proto = new F();
  
  	proto.constructor = NewClass;
  	NewClass.prototype = proto;
  
  	// add superclass access
  	NewClass.superclass = this.prototype;
  
  	// add class name
  	//proto.className = props;
  
  	//inherit parent's statics
  	for (var i in this) {
  		if (this.hasOwnProperty(i) && i !== 'prototype' && i !== 'superclass') {
  			NewClass[i] = this[i];
  		}
  	}
  
  	// mix static properties into the class
  	if (props.statics) {
  		TL.Util.extend(NewClass, props.statics);
  		delete props.statics;
  	}
  
  	// mix includes into the prototype
  	if (props.includes) {
  		TL.Util.extend.apply(null, [proto].concat(props.includes));
  		delete props.includes;
  	}
  
  	// merge options
  	if (props.options && proto.options) {
  		props.options = TL.Util.extend({}, proto.options, props.options);
  	}
  
  	// mix given properties into the prototype
  	TL.Util.extend(proto, props);
  
  	// allow inheriting further
  	NewClass.extend = TL.Class.extend;
  
  	// method for adding properties to prototype
  	NewClass.include = function (props) {
  		TL.Util.extend(this.prototype, props);
  	};
  
  	return NewClass;
  };
  
  
  /* **********************************************
       Begin TL.Events.js
  ********************************************** */
  
  /*	TL.Events
  	adds custom events functionality to TL classes
  ================================================== */
  TL.Events = {
  	addEventListener: function (/*String*/ type, /*Function*/ fn, /*(optional) Object*/ context) {
  		var events = this._tl_events = this._tl_events || {};
  		events[type] = events[type] || [];
  		events[type].push({
  			action: fn,
  			context: context || this
  		});
  		return this;
  	},
  
  	hasEventListeners: function (/*String*/ type) /*-> Boolean*/ {
  		var k = '_tl_events';
  		return (k in this) && (type in this[k]) && (this[k][type].length > 0);
  	},
  
  	removeEventListener: function (/*String*/ type, /*Function*/ fn, /*(optional) Object*/ context) {
  		if (!this.hasEventListeners(type)) {
  			return this;
  		}
  
  		for (var i = 0, events = this._tl_events, len = events[type].length; i < len; i++) {
  			if (
  				(events[type][i].action === fn) &&
  				(!context || (events[type][i].context === context))
  			) {
  				events[type].splice(i, 1);
  				return this;
  			}
  		}
  		return this;
  	},
  
  	fireEvent: function (/*String*/ type, /*(optional) Object*/ data) {
  		if (!this.hasEventListeners(type)) {
  			return this;
  		}
  
  		var event = TL.Util.mergeData({
  			type: type,
  			target: this
  		}, data);
  
  		var listeners = this._tl_events[type].slice();
  
  		for (var i = 0, len = listeners.length; i < len; i++) {
  			listeners[i].action.call(listeners[i].context || this, event);
  		}
  
  		return this;
  	}
  };
  
  TL.Events.on	= TL.Events.addEventListener;
  TL.Events.off	= TL.Events.removeEventListener;
  TL.Events.fire = TL.Events.fireEvent;
  
  
  /* **********************************************
       Begin TL.Browser.js
  ********************************************** */
  
  /*
  	Based on Leaflet Browser
  	TL.Browser handles different browser and feature detections for internal  use.
  */
  
  
  (function() {
  
  	var ua = navigator.userAgent.toLowerCase(),
  		doc = document.documentElement,
  
  		ie = 'ActiveXObject' in window,
  
  		webkit = ua.indexOf('webkit') !== -1,
  		phantomjs = ua.indexOf('phantom') !== -1,
  		android23 = ua.search('android [23]') !== -1,
  
  		mobile = typeof orientation !== 'undefined',
  		msPointer = navigator.msPointerEnabled && navigator.msMaxTouchPoints && !window.PointerEvent,
  		pointer = (window.PointerEvent && navigator.pointerEnabled && navigator.maxTouchPoints) || msPointer,
  
  		ie3d = ie && ('transition' in doc.style),
  		webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23,
  		gecko3d = 'MozPerspective' in doc.style,
  		opera3d = 'OTransition' in doc.style,
  		opera = window.opera;
  
  
  	var retina = 'devicePixelRatio' in window && window.devicePixelRatio > 1;
  
  	if (!retina && 'matchMedia' in window) {
  		var matches = window.matchMedia('(min-resolution:144dpi)');
  		retina = matches && matches.matches;
  	}
  
  	var touch = !window.L_NO_TOUCH && !phantomjs && (pointer || 'ontouchstart' in window || (window.DocumentTouch && document instanceof window.DocumentTouch));
  
  	TL.Browser = {
  		ie: ie,
  		ua: ua,
  		ie9: Boolean(ie && ua.match(/MSIE 9/i)),
  		ielt9: ie && !document.addEventListener,
  		webkit: webkit,
  		//gecko: (ua.indexOf('gecko') !== -1) && !webkit && !window.opera && !ie,
  		firefox: (ua.indexOf('gecko') !== -1) && !webkit && !window.opera && !ie,
  		android: ua.indexOf('android') !== -1,
  		android23: android23,
  		chrome: ua.indexOf('chrome') !== -1,
  		edge: ua.indexOf('edge/') !== -1,
  
  		ie3d: ie3d,
  		webkit3d: webkit3d,
  		gecko3d: gecko3d,
  		opera3d: opera3d,
  		any3d: !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d || opera3d) && !phantomjs,
  
  		mobile: mobile,
  		mobileWebkit: mobile && webkit,
  		mobileWebkit3d: mobile && webkit3d,
  		mobileOpera: mobile && window.opera,
  
  		touch: !! touch,
  		msPointer: !! msPointer,
  		pointer: !! pointer,
  
  		retina: !! retina,
  		orientation: function() {
  			var w = window.innerWidth,
  				h = window.innerHeight,
  				_orientation = "portrait";
  
  			if (w > h) {
  				_orientation = "landscape";
  			}
  			if (Math.abs(window.orientation) == 90) {
  				//_orientation = "landscape";
  			}
  			trace(_orientation);
  			return _orientation;
  		}
  	};
  
  }());
  
  
  /* **********************************************
       Begin TL.Load.js
  ********************************************** */
  
  /*	TL.Load
  	Loads External Javascript and CSS
  ================================================== */
  
  TL.Load = (function (doc) {
  	var loaded	= [];
  	
  	function isLoaded(url) {
  		
  		var i			= 0,
  			has_loaded	= false;
  		
  		for (i = 0; i < loaded.length; i++) {
  			if (loaded[i] == url) {
  				has_loaded = true;
  			}
  		}
  		
  		if (has_loaded) {
  			return true;
  		} else {
  			loaded.push(url);
  			return false;
  		}
  		
  	}
  	
  	return {
  		
  		css: function (urls, callback, obj, context) {
  			if (!isLoaded(urls)) {
  				TL.LoadIt.css(urls, callback, obj, context);
  			} else {
  				callback();
  			}
  		},
  
  		js: function (urls, callback, obj, context) {
  			if (!isLoaded(urls)) {
  				TL.LoadIt.js(urls, callback, obj, context);
  			} else {
  				callback();
  			}
  		}
      };
  	
  })(this.document);
  
  
  /*jslint browser: true, eqeqeq: true, bitwise: true, newcap: true, immed: true, regexp: false */
  
  /*
  LazyLoad makes it easy and painless to lazily load one or more external
  JavaScript or CSS files on demand either during or after the rendering of a web
  page.
  
  Supported browsers include Firefox 2+, IE6+, Safari 3+ (including Mobile
  Safari), Google Chrome, and Opera 9+. Other browsers may or may not work and
  are not officially supported.
  
  Visit https://github.com/rgrove/lazyload/ for more info.
  
  Copyright (c) 2011 Ryan Grove <ryan@wonko.com>
  All rights reserved.
  
  Permission is hereby granted, free of charge, to any person obtaining a copy of
  this software and associated documentation files (the 'Software'), to deal in
  the Software without restriction, including without limitation the rights to
  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  the Software, and to permit persons to whom the Software is furnished to do so,
  subject to the following conditions:
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
  
  THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  
  @module lazyload
  @class LazyLoad
  @static
  @version 2.0.3 (git)
  */
  
  TL.LoadIt = (function (doc) {
    // -- Private Variables ------------------------------------------------------
  
    // User agent and feature test information.
    var env,
  
    // Reference to the <head> element (populated lazily).
    head,
  
    // Requests currently in progress, if any.
    pending = {},
  
    // Number of times we've polled to check whether a pending stylesheet has
    // finished loading. If this gets too high, we're probably stalled.
    pollCount = 0,
  
    // Queued requests.
    queue = {css: [], js: []},
  
    // Reference to the browser's list of stylesheets.
    styleSheets = doc.styleSheets;
  
    // -- Private Methods --------------------------------------------------------
  
    /**
    Creates and returns an HTML element with the specified name and attributes.
  
    @method createNode
    @param {String} name element name
    @param {Object} attrs name/value mapping of element attributes
    @return {HTMLElement}
    @private
    */
    function createNode(name, attrs) {
      var node = doc.createElement(name), attr;
  
      for (attr in attrs) {
        if (attrs.hasOwnProperty(attr)) {
          node.setAttribute(attr, attrs[attr]);
        }
      }
  
      return node;
    }
  
    /**
    Called when the current pending resource of the specified type has finished
    loading. Executes the associated callback (if any) and loads the next
    resource in the queue.
  
    @method finish
    @param {String} type resource type ('css' or 'js')
    @private
    */
    function finish(type) {
      var p = pending[type],
          callback,
          urls;
  
      if (p) {
        callback = p.callback;
        urls     = p.urls;
  
        urls.shift();
        pollCount = 0;
  
        // If this is the last of the pending URLs, execute the callback and
        // start the next request in the queue (if any).
        if (!urls.length) {
          callback && callback.call(p.context, p.obj);
          pending[type] = null;
          queue[type].length && load(type);
        }
      }
    }
  
    /**
    Populates the <code>env</code> variable with user agent and feature test
    information.
  
    @method getEnv
    @private
    */
    function getEnv() {
      var ua = navigator.userAgent;
  
      env = {
        // True if this browser supports disabling async mode on dynamically
        // created script nodes. See
        // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order
        async: doc.createElement('script').async === true
      };
  
      (env.webkit = /AppleWebKit\//.test(ua))
        || (env.ie = /MSIE/.test(ua))
        || (env.opera = /Opera/.test(ua))
        || (env.gecko = /Gecko\//.test(ua))
        || (env.unknown = true);
    }
  
    /**
    Loads the specified resources, or the next resource of the specified type
    in the queue if no resources are specified. If a resource of the specified
    type is already being loaded, the new request will be queued until the
    first request has been finished.
  
    When an array of resource URLs is specified, those URLs will be loaded in
    parallel if it is possible to do so while preserving execution order. All
    browsers support parallel loading of CSS, but only Firefox and Opera
    support parallel loading of scripts. In other browsers, scripts will be
    queued and loaded one at a time to ensure correct execution order.
  
    @method load
    @param {String} type resource type ('css' or 'js')
    @param {String|Array} urls (optional) URL or array of URLs to load
    @param {Function} callback (optional) callback function to execute when the
      resource is loaded
    @param {Object} obj (optional) object to pass to the callback function
    @param {Object} context (optional) if provided, the callback function will
      be executed in this object's context
    @private
    */
    function load(type, urls, callback, obj, context) {
      var _finish = function () { finish(type); },
          isCSS   = type === 'css',
          nodes   = [],
          i, len, node, p, pendingUrls, url;
  
      env || getEnv();
  
      if (urls) {
        // If urls is a string, wrap it in an array. Otherwise assume it's an
        // array and create a copy of it so modifications won't be made to the
        // original.
        urls = typeof urls === 'string' ? [urls] : urls.concat();
  
        // Create a request object for each URL. If multiple URLs are specified,
        // the callback will only be executed after all URLs have been loaded.
        //
        // Sadly, Firefox and Opera are the only browsers capable of loading
        // scripts in parallel while preserving execution order. In all other
        // browsers, scripts must be loaded sequentially.
        //
        // All browsers respect CSS specificity based on the order of the link
        // elements in the DOM, regardless of the order in which the stylesheets
        // are actually downloaded.
        if (isCSS || env.async || env.gecko || env.opera) {
          // Load in parallel.
          queue[type].push({
            urls    : urls,
            callback: callback,
            obj     : obj,
            context : context
          });
        } else {
          // Load sequentially.
          for (i = 0, len = urls.length; i < len; ++i) {
            queue[type].push({
              urls    : [urls[i]],
              callback: i === len - 1 ? callback : null, // callback is only added to the last URL
              obj     : obj,
              context : context
            });
          }
        }
      }
  
      // If a previous load request of this type is currently in progress, we'll
      // wait our turn. Otherwise, grab the next item in the queue.
      if (pending[type] || !(p = pending[type] = queue[type].shift())) {
        return;
      }
  
      head || (head = doc.head || doc.getElementsByTagName('head')[0]);
      pendingUrls = p.urls;
  
      for (i = 0, len = pendingUrls.length; i < len; ++i) {
        url = pendingUrls[i];
  
        if (isCSS) {
            node = env.gecko ? createNode('style') : createNode('link', {
              href: url,
              rel : 'stylesheet'
            });
        } else {
          node = createNode('script', {src: url});
          node.async = false;
        }
  
        node.className = 'lazyload';
        node.setAttribute('charset', 'utf-8');
  
        if (env.ie && !isCSS) {
          node.onreadystatechange = function () {
            if (/loaded|complete/.test(node.readyState)) {
              node.onreadystatechange = null;
              _finish();
            }
          };
        } else if (isCSS && (env.gecko || env.webkit)) {
          // Gecko and WebKit don't support the onload event on link nodes.
          if (env.webkit) {
            // In WebKit, we can poll for changes to document.styleSheets to
            // figure out when stylesheets have loaded.
            p.urls[i] = node.href; // resolve relative URLs (or polling won't work)
            pollWebKit();
          } else {
            // In Gecko, we can import the requested URL into a <style> node and
            // poll for the existence of node.sheet.cssRules. Props to Zach
            // Leatherman for calling my attention to this technique.
            node.innerHTML = '@import "' + url + '";';
            pollGecko(node);
          }
        } else {
          node.onload = node.onerror = _finish;
        }
  
        nodes.push(node);
      }
  
      for (i = 0, len = nodes.length; i < len; ++i) {
        head.appendChild(nodes[i]);
      }
    }
  
    /**
    Begins polling to determine when the specified stylesheet has finished loading
    in Gecko. Polling stops when all pending stylesheets have loaded or after 10
    seconds (to prevent stalls).
  
    Thanks to Zach Leatherman for calling my attention to the @import-based
    cross-domain technique used here, and to Oleg Slobodskoi for an earlier
    same-domain implementation. See Zach's blog for more details:
    http://www.zachleat.com/web/2010/07/29/load-css-dynamically/
  
    @method pollGecko
    @param {HTMLElement} node Style node to poll.
    @private
    */
    function pollGecko(node) {
      var hasRules;
  
      try {
        // We don't really need to store this value or ever refer to it again, but
        // if we don't store it, Closure Compiler assumes the code is useless and
        // removes it.
        hasRules = !!node.sheet.cssRules;
      } catch (ex) {
        // An exception means the stylesheet is still loading.
        pollCount += 1;
  
        if (pollCount < 200) {
          setTimeout(function () { pollGecko(node); }, 50);
        } else {
          // We've been polling for 10 seconds and nothing's happened. Stop
          // polling and finish the pending requests to avoid blocking further
          // requests.
          hasRules && finish('css');
        }
  
        return;
      }
  
      // If we get here, the stylesheet has loaded.
      finish('css');
    }
  
    /**
    Begins polling to determine when pending stylesheets have finished loading
    in WebKit. Polling stops when all pending stylesheets have loaded or after 10
    seconds (to prevent stalls).
  
    @method pollWebKit
    @private
    */
    function pollWebKit() {
      var css = pending.css, i;
  
      if (css) {
        i = styleSheets.length;
  
        // Look for a stylesheet matching the pending URL.
        while (--i >= 0) {
          if (styleSheets[i].href === css.urls[0]) {
            finish('css');
            break;
          }
        }
  
        pollCount += 1;
  
        if (css) {
          if (pollCount < 200) {
            setTimeout(pollWebKit, 50);
          } else {
            // We've been polling for 10 seconds and nothing's happened, which may
            // indicate that the stylesheet has been removed from the document
            // before it had a chance to load. Stop polling and finish the pending
            // request to prevent blocking further requests.
            finish('css');
          }
        }
      }
    }
  
    return {
  
      /**
      Requests the specified CSS URL or URLs and executes the specified
      callback (if any) when they have finished loading. If an array of URLs is
      specified, the stylesheets will be loaded in parallel and the callback
      will be executed after all stylesheets have finished loading.
  
      @method css
      @param {String|Array} urls CSS URL or array of CSS URLs to load
      @param {Function} callback (optional) callback function to execute when
        the specified stylesheets are loaded
      @param {Object} obj (optional) object to pass to the callback function
      @param {Object} context (optional) if provided, the callback function
        will be executed in this object's context
      @static
      */
      css: function (urls, callback, obj, context) {
        load('css', urls, callback, obj, context);
      },
  
      /**
      Requests the specified JavaScript URL or URLs and executes the specified
      callback (if any) when they have finished loading. If an array of URLs is
      specified and the browser supports it, the scripts will be loaded in
      parallel and the callback will be executed after all scripts have
      finished loading.
  
      Currently, only Firefox and Opera support parallel loading of scripts while
      preserving execution order. In other browsers, scripts will be
      queued and loaded one at a time to ensure correct execution order.
  
      @method js
      @param {String|Array} urls JS URL or array of JS URLs to load
      @param {Function} callback (optional) callback function to execute when
        the specified scripts are loaded
      @param {Object} obj (optional) object to pass to the callback function
      @param {Object} context (optional) if provided, the callback function
        will be executed in this object's context
      @static
      */
      js: function (urls, callback, obj, context) {
        load('js', urls, callback, obj, context);
      }
  
    };
  })(this.document);
  
  
  /* **********************************************
       Begin TL.TimelineConfig.js
  ********************************************** */
  
  /*  TL.TimelineConfig
  separate the configuration from the display (TL.Timeline)
  to make testing easier
  ================================================== */
  TL.TimelineConfig = TL.Class.extend({
  
  	includes: [],
  	initialize: function (data) {
  		this.title = '';
  		this.scale = '';
  		this.events = [];
  		this.eras = [];
  		this.event_dict = {}; // despite name, all slides (events + title) indexed by slide.unique_id
  		this.messages = {
  			errors: [],
  			warnings: []
  		};
  
  		// Initialize the data
  		if (typeof data === 'object' && data.events) {
  			this.scale = data.scale;
  			this.events = [];
  			this._ensureValidScale(data.events);
  
  			if (data.title) {
  				var title_id = this._assignID(data.title);
  				this._tidyFields(data.title);
  				this.title = data.title;
  				this.event_dict[title_id] = this.title;
  			}
  
  			for (var i = 0; i < data.events.length; i++) {
  				try {
  					this.addEvent(data.events[i], true);
  				} catch (e) {
  				    this.logError(e);
  				}
  			}
  
  			if (data.eras) {
  				for (var i = 0; i < data.eras.length; i++) {
  					try {
  						this.addEra(data.eras[i], true);
  					} catch (e) {
  						this.logError("Era " + i + ": " + e);
  					}
  				}
  			}
  
  			TL.DateUtil.sortByDate(this.events);
  			TL.DateUtil.sortByDate(this.eras);
  
  		}
  	},
  	logError: function(msg) {
  		trace(msg);
  		this.messages.errors.push(msg);
  	},
  	/*
  	 * Return any accumulated error messages. If `sep` is passed, it should be a string which will be used to join all messages, resulting in a string return value. Otherwise,
  	 * errors will be returned as an array.
  	 */
  	getErrors: function(sep) {
  		if (sep) {
  			return this.messages.errors.join(sep);
  		} else {
  			return this.messages.errors;
  		}
  	},
  	/*
  	 * Perform any sanity checks we can before trying to use this to make a timeline. Returns nothing, but errors will be logged
  	 * such that after this is called, one can test `this.isValid()` to see if everything is OK.
  	 */
  	validate: function() {
  		if (typeof(this.events) == "undefined" || typeof(this.events.length) == "undefined" || this.events.length == 0) {
  			this.logError("Timeline configuration has no events.")
  		}
  
  		// make sure all eras have start and end dates
  		for (var i = 0; i < this.eras.length; i++) {
  			if (typeof(this.eras[i].start_date) == 'undefined' || typeof(this.eras[i].end_date) == 'undefined') {
  				var era_identifier;
  				if (this.eras[i].text && this.eras[i].text.headline) {
  					era_identifier = this.eras[i].text.headline
  				} else {
  					era_identifier = "era " + (i+1);
  				}
  				this.logError("All eras must have start and end dates. [" + era_identifier + "]") // add internationalization (I18N) and context
  			}
  		};
  	},
  
  	isValid: function() {
  		return this.messages.errors.length == 0;
  	},
  	/* Add an event (including cleaning/validation) and return the unique id.
  	* All event data validation should happen in here.
  	* Throws: TL.Error for any validation problems.
  	*/
  	addEvent: function(data, defer_sort) {
  		var event_id = this._assignID(data);
  
  		if (typeof(data.start_date) == 'undefined') {
  		    throw new TL.Error("missing_start_date_err", event_id);
  		} else {
  			this._processDates(data);
  			this._tidyFields(data);
  		}
  
  		this.events.push(data);
  		this.event_dict[event_id] = data;
  
  		if (!defer_sort) {
  			TL.DateUtil.sortByDate(this.events);
  		}
  		return event_id;
  	},
  
  	addEra: function(data, defer_sort) {
  		var event_id = this._assignID(data);
  
  		if (typeof(data.start_date) == 'undefined') {
  		    throw new TL.Error("missing_start_date_err", event_id);
  		} else {
  			this._processDates(data);
  			this._tidyFields(data);
  		}
  
  		this.eras.push(data);
  		this.event_dict[event_id] = data;
  
  		if (!defer_sort) {
  			TL.DateUtil.sortByDate(this.eras);
  		}
  		return event_id;
  	},
  
  	/**
  	 * Given a slide, verify that its ID is unique, or assign it one which is.
  	 * The assignment happens in this function, and the assigned ID is also
  	 * the return value. Not thread-safe, because ids are not reserved
  	 * when assigned here.
  	 */
  	_assignID: function(slide) {
  		var slide_id = slide.unique_id;
  		if (!TL.Util.trim(slide_id)) {
  			// give it an ID if it doesn't have one
  			slide_id = (slide.text) ? TL.Util.slugify(slide.text.headline) : null;
  		}
  		// make sure it's unique and add it.
  		slide.unique_id = TL.Util.ensureUniqueKey(this.event_dict,slide_id);
  		return slide.unique_id
  	},
  
  	/**
  	 * Given an array of slide configs (the events), ensure that each one has a distinct unique_id. The id of the title
  	 * is also passed in because in most ways it functions as an event slide, and the event IDs must also all be unique
  	 * from the title ID.
  	 */
  	_makeUniqueIdentifiers: function(title_id, array) {
  		var used = [title_id];
  
  		// establish which IDs are assigned and if any appear twice, clear out successors.
  		for (var i = 0; i < array.length; i++) {
  			if (TL.Util.trim(array[i].unique_id)) {
  				array[i].unique_id = TL.Util.slugify(array[i].unique_id); // enforce valid
  				if (used.indexOf(array[i].unique_id) == -1) {
  					used.push(array[i].unique_id);
  				} else { // it was already used, wipe it out
  					array[i].unique_id = '';
  				}
  			}
  		};
  
  		if (used.length != (array.length + 1)) {
  			// at least some are yet to be assigned
  			for (var i = 0; i < array.length; i++) {
  				if (!array[i].unique_id) {
  					// use the headline for the unique ID if it's available
  					var slug = (array[i].text) ? TL.Util.slugify(array[i].text.headline) : null;
  					if (!slug) {
  						slug = TL.Util.unique_ID(6); // or generate a random ID
  					}
  					if (used.indexOf(slug) != -1) {
  						slug = slug + '-' + i; // use the index to get a unique ID.
  					}
  					used.push(slug);
  					array[i].unique_id = slug;
  				}
  			}
  		}
  	},
  	_ensureValidScale: function(events) {
  		if(!this.scale) {
  			trace("Determining scale dynamically");
  			this.scale = "human"; // default to human unless there's a slide which is explicitly 'cosmological' or one which has a cosmological year
  
  			for (var i = 0; i < events.length; i++) {
  				if (events[i].scale == 'cosmological') {
  					this.scale = 'cosmological';
  					break;
  				}
  				if (events[i].start_date && typeof(events[i].start_date.year) != "undefined") {
  					var d = new TL.BigDate(events[i].start_date);
  					var year = d.data.date_obj.year;
  					if(year < -271820 || year >  275759) {
  						this.scale = "cosmological";
  						break;
  					}
  				}
  			}
  		}
  		var dateCls = TL.DateUtil.SCALE_DATE_CLASSES[this.scale];
  		if (!dateCls) { this.logError("Don't know how to process dates on scale "+this.scale); }
  	},
  	/*
  	   Given a thing which has a start_date and optionally an end_date, make sure that it is an instance
  		 of the correct date class (for human or cosmological scale). For slides, remove redundant end dates
  		 (people frequently configure an end date which is the same as the start date).
  	 */
  	_processDates: function(slide_or_era) {
  		var dateCls = TL.DateUtil.SCALE_DATE_CLASSES[this.scale];
  		if(!(slide_or_era.start_date instanceof dateCls)) {
  			var start_date = slide_or_era.start_date;
  			slide_or_era.start_date = new dateCls(start_date);
  
  			// eliminate redundant end dates.
  			if (typeof(slide_or_era.end_date) != 'undefined' && !(slide_or_era.end_date instanceof dateCls)) {
  				var end_date = slide_or_era.end_date;
  				var equal = true;
  				for (property in start_date) {
  					equal = equal && (start_date[property] == end_date[property]);
  				}
  				if (equal) {
  					trace("End date same as start date is redundant; dropping end date");
  					delete slide_or_era.end_date;
  				} else {
  					slide_or_era.end_date = new dateCls(end_date);
  				}
  
  			}
  		}
  
  	},
  	/**
  	 * Return the earliest date that this config knows about, whether it's a slide or an era
  	 */
  	getEarliestDate: function() {
  		// counting that dates were sorted in initialization
  		var date = this.events[0].start_date;
  		if (this.eras && this.eras.length > 0) {
  			if (this.eras[0].start_date.isBefore(date)) {
  				return this.eras[0].start_date;
  			}
  		}
  		return date;
  
  	},
  	/**
  	 * Return the latest date that this config knows about, whether it's a slide or an era, taking end_dates into account.
  	 */
  	getLatestDate: function() {
  		var dates = [];
  		for (var i = 0; i < this.events.length; i++) {
  			if (this.events[i].end_date) {
  				dates.push({ date: this.events[i].end_date });
  			} else {
  				dates.push({ date: this.events[i].start_date });
  			}
  		}
  		for (var i = 0; i < this.eras.length; i++) {
  			if (this.eras[i].end_date) {
  				dates.push({ date: this.eras[i].end_date });
  			} else {
  				dates.push({ date: this.eras[i].start_date });
  			}
  		}
  		TL.DateUtil.sortByDate(dates, 'date');
  		return dates.slice(-1)[0].date;
  	},
  	_tidyFields: function(slide) {
  
  		function fillIn(obj,key,default_value) {
  			if (!default_value) default_value = '';
  			if (!obj.hasOwnProperty(key)) { obj[key] = default_value }
  		}
  
  		if (slide.group) {
  			slide.group = TL.Util.trim(slide.group);
  		}
  
  		if (!slide.text) {
  			slide.text = {};
  		}
  		fillIn(slide.text,'text');
  		fillIn(slide.text,'headline');
  	}
  });
  
  
  /* **********************************************
       Begin TL.ConfigFactory.js
  ********************************************** */
  
  /* TL.ConfigFactory.js
   * Build TimelineConfig objects from other data sources
   */
  ;(function(TL){
      /*
       * Convert a URL to a Google Spreadsheet (typically a /pubhtml version but somewhat flexible) into an object with the spreadsheet key (ID) and worksheet ID.
  
       If `url` is actually a string which is only letters, numbers, '-' and '_', then it's assumed to be an ID already. If we had a more precise way of testing to see if the input argument was a valid key, we might apply it, but I don't know where that's documented.
  
       If we're pretty sure this isn't a bare key or a url that could be used to find a Google spreadsheet then return null.
       */
      function parseGoogleSpreadsheetURL(url) {
          parts = {
              key: null,
              worksheet: 0 // not really sure how to use this to get the feed for that sheet, so this is not ready except for first sheet right now
          }
          // key as url parameter (old-fashioned)
          var key_pat = /\bkey=([-_A-Za-z0-9]+)&?/i;
          var url_pat = /docs.google.com\/spreadsheets(.*?)\/d\//; // fixing issue of URLs with u/0/d
  
          if (url.match(key_pat)) {
              parts.key = url.match(key_pat)[1];
              // can we get a worksheet from this form?
          } else if (url.match(url_pat)) {
              var pos = url.search(url_pat) + url.match(url_pat)[0].length;
              var tail = url.substr(pos);
              parts.key = tail.split('/')[0]
              if (url.match(/\?gid=(\d+)/)) {
                  parts.worksheet = url.match(/\?gid=(\d+)/)[1];
              }
          } else if (url.match(/^\b[-_A-Za-z0-9]+$/)) {
              parts.key = url;
          }
  
          if (parts.key) {
              return parts;
          } else {
              return null;
          }
      }
  
      function extractGoogleEntryData_V1(item) {
          var item_data = {}
          for (k in item) {
              if (k.indexOf('gsx$') == 0) {
                  item_data[k.substr(4)] = item[k].$t;
              }
          }
          if (TL.Util.isEmptyObject(item_data)) return null;
          var d = {
              media: {
                  caption: item_data.mediacaption || '',
                  credit: item_data.mediacredit || '',
                  url: item_data.media || '',
                  thumbnail: item_data.mediathumbnail || ''
              },
              text: {
                  headline: item_data.headline || '',
                  text: item_data.text || ''
              },
              group: item_data.tag || '',
              type: item_data.type || ''
          }
          if (item_data.startdate) {
              d['start_date'] = TL.Date.parseDate(item_data.startdate);
          }
          if (item_data.enddate) {
              d['end_date'] = TL.Date.parseDate(item_data.enddate);
          }
  
  
          return d;
      }
  
      function extractGoogleEntryData_V3(item) {
  
          function clean_integer(s) {
              if (s) {
                  return s.replace(/[\s,]+/g,''); // doesn't handle '.' as comma separator, but how to distinguish that from decimal separator?
              }
          }
  
          var item_data = {}
          for (k in item) {
              if (k.indexOf('gsx$') == 0) {
                  item_data[k.substr(4)] = TL.Util.trim(item[k].$t);
              }
          }
          if (TL.Util.isEmptyObject(item_data)) return null;
          var d = {
              media: {
                  caption: item_data.mediacaption || '',
                  credit: item_data.mediacredit || '',
                  url: item_data.media || '',
                  thumbnail: item_data.mediathumbnail || ''
              },
              text: {
                  headline: item_data.headline || '',
                  text: item_data.text || ''
              },
              start_date: {
                  year: clean_integer(item_data.year),
                  month: clean_integer(item_data.month) || '',
                  day: clean_integer(item_data.day) || ''
              },
              end_date: {
                  year: clean_integer(item_data.endyear) || '',
                  month: clean_integer(item_data.endmonth) || '',
                  day: clean_integer(item_data.endday) || ''
              },
              display_date: item_data.displaydate || '',
  
              type: item_data.type || ''
          }
  
          if (item_data.time) {
              TL.Util.mergeData(d.start_date,TL.DateUtil.parseTime(item_data.time));
          }
  
          if (item_data.endtime) {
              TL.Util.mergeData(d.end_date,TL.DateUtil.parseTime(item_data.endtime));
          }
  
  
          if (item_data.group) {
              d.group = item_data.group;
          }
  
          if (d.end_date.year == '') {
              var bad_date = d.end_date;
              delete d.end_date;
              if (bad_date.month != '' || bad_date.day != '' || bad_date.time != '') {
                  var label = d.text.headline ||
                  trace("Invalid end date for spreadsheet row. Must have a year if any other date fields are specified.");
                  trace(item);
              }
          }
  
          if (item_data.background) {
              if (item_data.background.match(/^(https?:)?\/\/?/)) { // support http, https, protocol relative, site relative
                  d['background'] = { 'url': item_data.background }
              } else { // for now we'll trust it's a color
                  d['background'] = { 'color': item_data.background }
              }
          }
  
          return d;
      }
  
      var getGoogleItemExtractor = function(data) {
          if (typeof data.feed.entry === 'undefined'
                  || data.feed.entry.length == 0) {
              throw new TL.Error("empty_feed_err");
          }
          var entry = data.feed.entry[0];
  
          if (typeof entry.gsx$startdate !== 'undefined') {
              // check headers V1
              // var headers_V1 = ['startdate', 'enddate', 'headline','text','media','mediacredit','mediacaption','mediathumbnail','media','type','tag'];
              // for (var i = 0; i < headers_V1.length; i++) {
              //     if (typeof entry['gsx$' + headers_V1[i]] == 'undefined') {
              //         throw new TL.Error("invalid_data_format_err");
              //     }
              // }
              return extractGoogleEntryData_V1;
          } else if (typeof entry.gsx$year !== 'undefined') {
              // check rest of V3 headers
              var headers_V3 = ['month', 'day', 'time', 'endmonth', 'endyear', 'endday', 'endtime', 'displaydate', 'headline','text','media','mediacredit','mediacaption','mediathumbnail','type','group','background'];
              // for (var i = 0; i < headers_V3.length; i++) {
              //     if (typeof entry['gsx$' + headers_V3[i]] == 'undefined') {
              //         throw new TL.Error("invalid_data_format_err");
              //     }
              // }
              return extractGoogleEntryData_V3;
          }
          throw new TL.Error("invalid_data_format_err");
      }
  
      var buildGoogleFeedURL = function(parts) {
          return "https://spreadsheets.google.com/feeds/list/" + parts.key + "/1/public/values?alt=json";
  
      }
  
      var jsonFromGoogleURL = function(url) {
          var url = buildGoogleFeedURL(parseGoogleSpreadsheetURL(url));
              var timeline_config = { 'events': [] };
              var data = TL.ajax({
                  url: url,
                  async: false
              });
              data = JSON.parse(data.responseText);
              return googleFeedJSONtoTimelineJSON(data);
          }
  
      var googleFeedJSONtoTimelineJSON = function(data) {
          var timeline_config = { 'events': [], 'errors': [], 'warnings': [], 'eras': [] }
          var extract = getGoogleItemExtractor(data);
          for (var i = 0; i < data.feed.entry.length; i++) {
              try {
                  var event = extract(data.feed.entry[i]);
                  if (event) { // blank rows return null
                    var row_type = 'event';
                    if (typeof(event.type) != 'undefined') {
                        row_type = event.type;
                        delete event.type;
                    }
                    if (row_type == 'title') {
                      if (!timeline_config.title) {
                        timeline_config.title = event;
                      } else {
                        timeline_config.warnings.push("Multiple title slides detected.");
                        timeline_config.events.push(event);
                      }
                    } else if (row_type == 'era') {
                      timeline_config.eras.push(event);
                    } else {
                        timeline_config.events.push(event);
                    }
                  }
              } catch(e) {
                  if (e.message) {
                      e = e.message;
                  }
                  timeline_config.errors.push(e + " ["+ i +"]");
              }
          };
          return timeline_config;
  
      }
  
      var makeConfig = function(url, callback) {
          var tc,
              key = parseGoogleSpreadsheetURL(url);
  
          if (key) {
              try {
                  var json = jsonFromGoogleURL(url);
              } catch(e) {
                  tc = new TL.TimelineConfig();
                  if (e.name == 'NetworkError') {
                      tc.logError(new TL.Error("network_err"));
                  } else if(e.name == 'TL.Error') {
                      tc.logError(e);
                  } else {
                      tc.logError(new TL.Error("unknown_read_err", e.name));
                  }
                  callback(tc);
                  return;
              }
              tc = new TL.TimelineConfig(json);
              if (json.errors) {
                  for (var i = 0; i < json.errors.length; i++) {
                      tc.logError(json.errors[i]);
                  };
              }
              callback(tc);
          } else {
            TL.ajax({
              url: url,
              dataType: 'json',
              success: function(data){
              try {
                  tc = new TL.TimelineConfig(data);
              } catch(e) {
                  tc = new TL.TimelineConfig();
                  tc.logError(e);
              }
              callback(tc);
              },
              error: function(xhr, errorType, error) {
                tc = new TL.TimelineConfig();
                if (errorType == 'parsererror') {
                  var error = new TL.Error("invalid_url_err");
                } else {
                  var error = new TL.Error("unknown_read_err", errorType)
                }
                tc.logError(error);
                callback(tc);
              }
            });
  
          }
      }
  
      TL.ConfigFactory = {
          // export for unit testing and use by authoring tool
          parseGoogleSpreadsheetURL: parseGoogleSpreadsheetURL,
          // export for unit testing
          googleFeedJSONtoTimelineJSON: googleFeedJSONtoTimelineJSON,
  
  
          fromGoogle: function(url) {
              console.warn("TL.ConfigFactory.fromGoogle is deprecated and will be removed soon. Use TL.ConfigFactory.makeConfig(url,callback)")
              return jsonFromGoogleURL(url);
  
          },
  
          /*
           * Given a URL to a Timeline data source, read the data, create a TimelineConfig
           * object, and call the given `callback` function passing the created config as
           * the only argument. This should be the main public interface to getting configs
           * from any kind of URL, Google or direct JSON.
           */
          makeConfig: makeConfig,
      }
  })(TL)
  
  
  /* **********************************************
       Begin TL.Language.js
  ********************************************** */
  
  TL.Language = function(options) {
  	// borrowed from http://stackoverflow.com/a/14446414/102476
  	for (k in TL.Language.languages.en) {
  		this[k] = TL.Language.languages.en[k];
  	}
  	if (options && options.language && typeof(options.language) == 'string' && options.language != 'en') {
  		var code = options.language;
  		if (!(code in TL.Language.languages)) {
  			if (/\.json$/.test(code)) {
  				var url = code;
  			} else {
  				var fragment = "/locale/" + code + ".json";
  				var script_path = options.script_path || TL.Timeline.source_path;
  				if (/\/$/.test(script_path)) { fragment = fragment.substr(1)}
  				var url = script_path + fragment;
  			}
  			var self = this;
  			var xhr = TL.ajax({
  				url: url, async: false
  			});
  			if (xhr.status == 200) {
  				TL.Language.languages[code] = JSON.parse(xhr.responseText);
  			} else {
  				throw "Could not load language [" + code + "]: " + xhr.statusText;
  			}
  		}
  		TL.Util.mergeData(this,TL.Language.languages[code]);
  
  	}
  }
  
  TL.Language.formatNumber = function(val,mask) {
  		if (mask.match(/%(\.(\d+))?f/)) {
  			var match = mask.match(/%(\.(\d+))?f/);
  			var token = match[0];
  			if (match[2]) {
  				val = val.toFixed(match[2]);
  			}
  			return mask.replace(token,val);
  		}
  		// use mask as literal display value.
  		return mask;
  	}
  
  
  
  /* TL.Util.mergeData is shallow, we have nested dicts.
     This is a simplistic handling but should work.
   */
  TL.Language.prototype.mergeData = function(lang_json) {
  	for (k in TL.Language.languages.en) {
  		if (lang_json[k]) {
  			if (typeof(this[k]) == 'object') {
  				TL.Util.mergeData(lang_json[k], this[k]);
  			} else {
  				this[k] = lang_json[k]; // strings, mostly
  			}
  		}
  	}
  }
  
  TL.Language.fallback = { messages: {} }; // placeholder to satisfy IE8 early compilation
  TL.Language.prototype.getMessage = function(k) {
  	return this.messages[k] || TL.Language.fallback.messages[k] || k;
  }
  
  TL.Language.prototype._ = TL.Language.prototype.getMessage; // keep it concise
  
  TL.Language.prototype.formatDate = function(date, format_name) {
  
  	if (date.constructor == Date) {
  		return this.formatJSDate(date, format_name);
  	}
  
  	if (date.constructor == TL.BigYear) {
  		return this.formatBigYear(date, format_name);
  	}
  
  	if (date.data && date.data.date_obj) {
  		return this.formatDate(date.data.date_obj, format_name);
  	}
  
  	trace("Unfamiliar date presented for formatting");
  	return date.toString();
  }
  
  TL.Language.prototype.formatBigYear = function(bigyear, format_name) {
  	var the_year = bigyear.year;
  	var format_list = this.bigdateformats[format_name] || this.bigdateformats['fallback'];
  
  	if (format_list) {
  		for (var i = 0; i < format_list.length; i++) {
  			var tuple = format_list[i];
  			if (Math.abs(the_year / tuple[0]) > 1) {
  				// will we ever deal with distant future dates?
  				return TL.Language.formatNumber(Math.abs(the_year / tuple[0]),tuple[1])
  			}
  		};
  
  		return the_year.toString();
  
  	} else {
  	    trace("Language file dateformats missing cosmological. Falling back.");
  	    return TL.Language.formatNumber(the_year,format_name);
  	}
  }
  
  TL.Language.prototype.formatJSDate = function(js_date, format_name) {
  	// ultimately we probably want this to work with TL.Date instead of (in addition to?) JS Date
  	// utc, timezone and timezoneClip are carry over from Steven Levithan implementation. We probably aren't going to use them.
  	var self = this;
  	var formatPeriod = function(fmt, value) {
  		var formats = self.period_labels[fmt];
  		if (formats) {
  			var fmt = (value < 12) ? formats[0] : formats[1];
  		}
  		return "<span class='tl-timeaxis-timesuffix'>" + fmt + "</span>";
  	}
  
  	var utc = false,
  		timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
  		timezoneClip = /[^-+\dA-Z]/g;
  
  
  	if (!format_name) {
  		format_name = 'full';
  	}
  
  	var mask = this.dateformats[format_name] || TL.Language.fallback.dateformats[format_name];
  	if (!mask) {
  		mask = format_name; // allow custom format strings
  	}
  
  
  	var	_ = utc ? "getUTC" : "get",
  		d = js_date[_ + "Date"](),
  		D = js_date[_ + "Day"](),
  		m = js_date[_ + "Month"](),
  		y = js_date[_ + "FullYear"](),
  		H = js_date[_ + "Hours"](),
  		M = js_date[_ + "Minutes"](),
  		s = js_date[_ + "Seconds"](),
  		L = js_date[_ + "Milliseconds"](),
  		o = utc ? 0 : js_date.getTimezoneOffset(),
  		year = "",
  		flags = {
  			d:    d,
  			dd:   TL.Util.pad(d),
  			ddd:  this.date.day_abbr[D],
  			dddd: this.date.day[D],
  			m:    m + 1,
  			mm:   TL.Util.pad(m + 1),
  			mmm:  this.date.month_abbr[m],
  			mmmm: this.date.month[m],
  			yy:   String(y).slice(2),
  			yyyy: (y < 0 && this.has_negative_year_modifier()) ? Math.abs(y) : y,
  			h:    H % 12 || 12,
  			hh:   TL.Util.pad(H % 12 || 12),
  			H:    H,
  			HH:   TL.Util.pad(H),
  			M:    M,
  			MM:   TL.Util.pad(M),
  			s:    s,
  			ss:   TL.Util.pad(s),
  			l:    TL.Util.pad(L, 3),
  			L:    TL.Util.pad(L > 99 ? Math.round(L / 10) : L),
  			t:    formatPeriod('t',H),
  			tt:   formatPeriod('tt',H),
  			T:    formatPeriod('T',H),
  			TT:   formatPeriod('TT',H),
  			Z:    utc ? "UTC" : (String(js_date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
  			o:    (o > 0 ? "-" : "+") + TL.Util.pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
  			S:    ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
  		};
  
  		var formatted = mask.replace(TL.Language.DATE_FORMAT_TOKENS, function ($0) {
  			return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
  		});
  
  		return this._applyEra(formatted, y);
  }
  
  TL.Language.prototype.has_negative_year_modifier = function() {
  	return Boolean(this.era_labels.negative_year.prefix || this.era_labels.negative_year.suffix);
  }
  
  
  TL.Language.prototype._applyEra = function(formatted_date, original_year) {
  	// trusts that the formatted_date was property created with a non-negative year if there are
  	// negative affixes to be applied
  	var labels = (original_year < 0) ? this.era_labels.negative_year : this.era_labels.positive_year;
  	var result = '';
  	if (labels.prefix) { result += '<span>' + labels.prefix + '</span> ' }
  	result += formatted_date;
  	if (labels.suffix) { result += ' <span>' + labels.suffix + '</span>' }
  	return result;
  }
  
  
  TL.Language.DATE_FORMAT_TOKENS = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g;
  
  TL.Language.languages = {
  /*
  	This represents the canonical list of message keys which translation files should handle. The existence of the 'en.json' file should not mislead you.
  	It is provided more as a starting point for someone who wants to provide a
  	new translation since the form for non-default languages (JSON not JS) is slightly different from what appears below. Also, those files have some message keys grandfathered in from TimelineJS2 which we'd rather not have to
  	get "re-translated" if we use them.
  */
  	en: {
  		name: 					"English",
  		lang: 					"en",
          api: {
              wikipedia:          "en" // the two letter code at the beginning of the Wikipedia subdomain for this language
          },
  		messages: {
  			loading: 			            		  "Loading",
  			wikipedia: 			            		"From Wikipedia, the free encyclopedia",
  			error: 				            			"Error",
        contract_timeline:              "Contract Timeline",
        return_to_title:                "Return to Title",
        loading_content:                "Loading Content",
        expand_timeline:                "Expand Timeline",
        loading_timeline:               "Loading Timeline... ",
        swipe_to_navigate:              "Swipe to Navigate<br><span class='tl-button'>OK</span>",
        unknown_read_err:               "An unexpected error occurred trying to read your spreadsheet data",
  			invalid_url_err: 								"Unable to read Timeline data. Make sure your URL is for a Google Spreadsheet or a Timeline JSON file.",
        network_err:                    "Unable to read your Google Spreadsheet. Make sure you have published it to the web.",
        empty_feed_err:                 "No data entries found",
        missing_start_date_err:         "Missing start_date",
        invalid_data_format_err:        "Header row has been modified.",
        date_compare_err:               "Can't compare TL.Dates on different scales",
        invalid_scale_err:              "Invalid scale",
        invalid_date_err:               "Invalid date: month, day and year must be numbers.",
        invalid_separator_error:        "Invalid time: misuse of : or . as separator.",
        invalid_hour_err:               "Invalid time (hour)",
        invalid_minute_err:             "Invalid time (minute)",
        invalid_second_err:             "Invalid time (second)",
        invalid_fractional_err:         "Invalid time (fractional seconds)",
        invalid_second_fractional_err:  "Invalid time (seconds and fractional seconds)",
        invalid_year_err:               "Invalid year",
        flickr_notfound_err:            "Photo not found or private",
        flickr_invalidurl_err:          "Invalid Flickr URL",
        imgur_invalidurl_err:           "Invalid Imgur URL",
        twitter_invalidurl_err:         "Invalid Twitter URL",
        twitter_load_err:               "Unable to load Tweet",
        twitterembed_invalidurl_err:    "Invalid Twitter Embed url",
        wikipedia_load_err:             "Unable to load Wikipedia entry",
        youtube_invalidurl_err:         "Invalid YouTube URL",
        spotify_invalid_url:            "Invalid Spotify URL",
        template_value_err:             "No value provided for variable",
        invalid_rgb_err:                "Invalid RGB argument",
        time_scale_scale_err:           "Don't know how to get date from time for scale",
        axis_helper_no_options_err:     "Axis helper must be configured with options",
        axis_helper_scale_err:          "No AxisHelper available for scale",
        invalid_integer_option:       	"Invalid option value—must be a whole number."
  		},
  		date: {
        month: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
        month_abbr: ["Jan.", "Feb.", "March", "April", "May", "June", "July", "Aug.", "Sept.", "Oct.", "Nov.", "Dec."],
        day: ["Sunday","Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
        day_abbr: ["Sun.","Mon.", "Tues.", "Wed.", "Thurs.", "Fri.", "Sat."]
  		},
  		era_labels: { // specify prefix or suffix to apply to formatted date. Blanks mean no change.
  	        positive_year: {
  	        	prefix: '',
  	        	suffix: ''
  	        },
  	        negative_year: { // if either of these is specified, the year will be converted to positive before they are applied
  	        	prefix: '',
  	        	suffix: 'BCE'
  	        }
          },
          period_labels: {  // use of t/tt/T/TT legacy of original Timeline date format
  			t: ['a', 'p'],
  			tt: ['am', 'pm'],
  			T: ['A', 'P'],
  			TT: ['AM', 'PM']
  		},
  		dateformats: {
  			year: "yyyy",
  			month_short: "mmm",
  			month: "mmmm yyyy",
  			full_short: "mmm d",
  			full: "mmmm d',' yyyy",
  			time: "h:MM:ss TT' <small>'mmmm d',' yyyy'</small>'",
  			time_short: "h:MM:ss TT",
  			time_no_seconds_short: "h:MM TT",
  			time_no_minutes_short: "h TT",
  			time_no_seconds_small_date: "h:MM TT' <small>'mmmm d',' yyyy'</small>'",
  			time_milliseconds: "l",
  			full_long: "mmm d',' yyyy 'at' h:MM TT",
  			full_long_small_date: "h:MM TT' <small>mmm d',' yyyy'</small>'"
  		},
  		bigdateformats: {
  			fallback: [ // a list of tuples, with t[0] an order of magnitude and t[1] a format string. format string syntax may change...
  				[1000000000,"%.2f billion years ago"],
  				[1000000,"%.1f million years ago"],
  				[1000,"%.1f thousand years ago"],
  				[1, "%f years ago"]
  			],
  		    compact: [
  				[1000000000,"%.2f bya"],
  				[1000000,"%.1f mya"],
  				[1000,"%.1f kya"],
  				[1, "%f years ago"]
  			],
  		    verbose: [
  				[1000000000,"%.2f billion years ago"],
  				[1000000,"%.1f million years ago"],
  				[1000,"%.1f thousand years ago"],
  				[1, "%f years ago"]
  			]
  		}
  	}
  }
  
  TL.Language.fallback = new TL.Language();
  
  
  /* **********************************************
       Begin TL.I18NMixins.js
  ********************************************** */
  
  /*  TL.I18NMixins
      assumes that its class has an options object with a TL.Language instance    
  ================================================== */
  TL.I18NMixins = {
      getLanguage: function() {
          if (this.options && this.options.language) {
              return this.options.language;
          }
          trace("Expected a language option");
          return TL.Language.fallback;
      },
  
      _: function(msg) {
          return this.getLanguage()._(msg);
      }
  }
  
  
  /* **********************************************
       Begin TL.Ease.js
  ********************************************** */
  
  /* The equations defined here are open source under BSD License.
   * http://www.robertpenner.com/easing_terms_of_use.html (c) 2003 Robert Penner
   * Adapted to single time-based by
   * Brian Crescimanno <brian.crescimanno@gmail.com>
   * Ken Snyder <kendsnyder@gmail.com>
   */
  
  /** MIT License
   *
   * KeySpline - use bezier curve for transition easing function
   * Copyright (c) 2012 Gaetan Renaudeau <renaudeau.gaetan@gmail.com>
   * 
   * Permission is hereby granted, free of charge, to any person obtaining a
   * copy of this software and associated documentation files (the "Software"),
   * to deal in the Software without restriction, including without limitation
   * the rights to use, copy, modify, merge, publish, distribute, sublicense,
   * and/or sell copies of the Software, and to permit persons to whom the
   * Software is furnished to do so, subject to the following conditions:
   * 
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   * 
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
   * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
   * DEALINGS IN THE SOFTWARE.
   */
  /**
   * KeySpline - use bezier curve for transition easing function
   * is inspired from Firefox's nsSMILKeySpline.cpp
   * Usage:
   * var spline = new KeySpline(0.25, 0.1, 0.25, 1.0)
   * spline.get(x) => returns the easing value | x must be in [0, 1] range
   */
  
  TL.Easings = {
      ease:        [0.25, 0.1, 0.25, 1.0], 
      linear:      [0.00, 0.0, 1.00, 1.0],
      easein:     [0.42, 0.0, 1.00, 1.0],
      easeout:    [0.00, 0.0, 0.58, 1.0],
      easeinout: [0.42, 0.0, 0.58, 1.0]
  };
  
  TL.Ease = {
  	KeySpline: function(a) {
  	//KeySpline: function(mX1, mY1, mX2, mY2) {
  		this.get = function(aX) {
  			if (a[0] == a[1] && a[2] == a[3]) return aX; // linear
  			return CalcBezier(GetTForX(aX), a[1], a[3]);
  		}
  
  		function A(aA1, aA2) {
  			return 1.0 - 3.0 * aA2 + 3.0 * aA1;
  		}
  
  		function B(aA1, aA2) {
  			return 3.0 * aA2 - 6.0 * aA1;
  		}
  
  		function C(aA1) {
  			return 3.0 * aA1;
  		}
  
  		// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
  
  		function CalcBezier(aT, aA1, aA2) {
  			return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
  		}
  
  		// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
  
  		function GetSlope(aT, aA1, aA2) {
  			return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
  		}
  
  		function GetTForX(aX) {
  			// Newton raphson iteration
  			var aGuessT = aX;
  			for (var i = 0; i < 4; ++i) {
  				var currentSlope = GetSlope(aGuessT, a[0], a[2]);
  				if (currentSlope == 0.0) return aGuessT;
  				var currentX = CalcBezier(aGuessT, a[0], a[2]) - aX;
  				aGuessT -= currentX / currentSlope;
  			}
  			return aGuessT;
  		}
  	},
  	
  	easeInSpline: function(t) {
  		var spline = new TL.Ease.KeySpline(TL.Easings.easein);
  		return spline.get(t);
  	},
  	
  	easeInOutExpo: function(t) {
  		var spline = new TL.Ease.KeySpline(TL.Easings.easein);
  		return spline.get(t);
  	},
  	
  	easeOut: function(t) {
  		return Math.sin(t * Math.PI / 2);
  	},
  	easeOutStrong: function(t) {
  		return (t == 1) ? 1 : 1 - Math.pow(2, - 10 * t);
  	},
  	easeIn: function(t) {
  		return t * t;
  	},
  	easeInStrong: function(t) {
  		return (t == 0) ? 0 : Math.pow(2, 10 * (t - 1));
  	},
  	easeOutBounce: function(pos) {
  		if ((pos) < (1 / 2.75)) {
  			return (7.5625 * pos * pos);
  		} else if (pos < (2 / 2.75)) {
  			return (7.5625 * (pos -= (1.5 / 2.75)) * pos + .75);
  		} else if (pos < (2.5 / 2.75)) {
  			return (7.5625 * (pos -= (2.25 / 2.75)) * pos + .9375);
  		} else {
  			return (7.5625 * (pos -= (2.625 / 2.75)) * pos + .984375);
  		}
  	},
  	easeInBack: function(pos) {
  		var s = 1.70158;
  		return (pos) * pos * ((s + 1) * pos - s);
  	},
  	easeOutBack: function(pos) {
  		var s = 1.70158;
  		return (pos = pos - 1) * pos * ((s + 1) * pos + s) + 1;
  	},
  	bounce: function(t) {
  		if (t < (1 / 2.75)) {
  			return 7.5625 * t * t;
  		}
  		if (t < (2 / 2.75)) {
  			return 7.5625 * (t -= (1.5 / 2.75)) * t + 0.75;
  		}
  		if (t < (2.5 / 2.75)) {
  			return 7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375;
  		}
  		return 7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375;
  	},
  	bouncePast: function(pos) {
  		if (pos < (1 / 2.75)) {
  			return (7.5625 * pos * pos);
  		} else if (pos < (2 / 2.75)) {
  			return 2 - (7.5625 * (pos -= (1.5 / 2.75)) * pos + .75);
  		} else if (pos < (2.5 / 2.75)) {
  			return 2 - (7.5625 * (pos -= (2.25 / 2.75)) * pos + .9375);
  		} else {
  			return 2 - (7.5625 * (pos -= (2.625 / 2.75)) * pos + .984375);
  		}
  	},
  	swingTo: function(pos) {
  		var s = 1.70158;
  		return (pos -= 1) * pos * ((s + 1) * pos + s) + 1;
  	},
  	swingFrom: function(pos) {
  		var s = 1.70158;
  		return pos * pos * ((s + 1) * pos - s);
  	},
  	elastic: function(pos) {
  		return -1 * Math.pow(4, - 8 * pos) * Math.sin((pos * 6 - 1) * (2 * Math.PI) / 2) + 1;
  	},
  	spring: function(pos) {
  		return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
  	},
  	blink: function(pos, blinks) {
  		return Math.round(pos * (blinks || 5)) % 2;
  	},
  	pulse: function(pos, pulses) {
  		return (-Math.cos((pos * ((pulses || 5) - .5) * 2) * Math.PI) / 2) + .5;
  	},
  	wobble: function(pos) {
  		return (-Math.cos(pos * Math.PI * (9 * pos)) / 2) + 0.5;
  	},
  	sinusoidal: function(pos) {
  		return (-Math.cos(pos * Math.PI) / 2) + 0.5;
  	},
  	flicker: function(pos) {
  		var pos = pos + (Math.random() - 0.5) / 5;
  		return easings.sinusoidal(pos < 0 ? 0 : pos > 1 ? 1 : pos);
  	},
  	mirror: function(pos) {
  		if (pos < 0.5) return easings.sinusoidal(pos * 2);
  		else return easings.sinusoidal(1 - (pos - 0.5) * 2);
  	},
  	// accelerating from zero velocity
  	easeInQuad: function (t) { return t*t },
  	// decelerating to zero velocity
  	easeOutQuad: function (t) { return t*(2-t) },
  	// acceleration until halfway, then deceleration
  	easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
  	// accelerating from zero velocity 
  	easeInCubic: function (t) { return t*t*t },
  	// decelerating to zero velocity 
  	easeOutCubic: function (t) { return (--t)*t*t+1 },
  	// acceleration until halfway, then deceleration 
  	easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
  	// accelerating from zero velocity 
  	easeInQuart: function (t) { return t*t*t*t },
  	// decelerating to zero velocity 
  	easeOutQuart: function (t) { return 1-(--t)*t*t*t },
  	// acceleration until halfway, then deceleration
  	easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
  	// accelerating from zero velocity
  	easeInQuint: function (t) { return t*t*t*t*t },
  	// decelerating to zero velocity
  	easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
  	// acceleration until halfway, then deceleration 
  	easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
  };
  
  /*
  Math.easeInExpo = function (t, b, c, d) {
  	return c * Math.pow( 2, 10 * (t/d - 1) ) + b;
  };
  
  		
  
  // exponential easing out - decelerating to zero velocity
  
  
  Math.easeOutExpo = function (t, b, c, d) {
  	return c * ( -Math.pow( 2, -10 * t/d ) + 1 ) + b;
  };
  
  		
  
  // exponential easing in/out - accelerating until halfway, then decelerating
  
  
  Math.easeInOutExpo = function (t, b, c, d) {
  	t /= d/2;
  	if (t < 1) return c/2 * Math.pow( 2, 10 * (t - 1) ) + b;
  	t--;
  	return c/2 * ( -Math.pow( 2, -10 * t) + 2 ) + b;
  };
  */
  
  /* **********************************************
       Begin TL.Animate.js
  ********************************************** */
  
  /*	TL.Animate
  	Basic animation
  ================================================== */
  
  TL.Animate = function(el, options) {
  	var animation = new tlanimate(el, options),
  		webkit_timeout;
  		/*
  		// POSSIBLE ISSUE WITH WEBKIT FUTURE BUILDS
  	var onWebKitTimeout = function() {
  
  		animation.stop(true);
  	}
  	if (TL.Browser.webkit) {
  		webkit_timeout = setTimeout(function(){onWebKitTimeout()}, options.duration);
  	}
  	*/
  	return animation;
  };
  
  
  /*	Based on: Morpheus
  	https://github.com/ded/morpheus - (c) Dustin Diaz 2011
  	License MIT
  ================================================== */
  window.tlanimate = (function() {
  
  	var doc = document,
  		win = window,
  		perf = win.performance,
  		perfNow = perf && (perf.now || perf.webkitNow || perf.msNow || perf.mozNow),
  		now = perfNow ? function () { return perfNow.call(perf) } : function () { return +new Date() },
  		html = doc.documentElement,
  		fixTs = false, // feature detected below
  		thousand = 1000,
  		rgbOhex = /^rgb\(|#/,
  		relVal = /^([+\-])=([\d\.]+)/,
  		numUnit = /^(?:[\+\-]=?)?\d+(?:\.\d+)?(%|in|cm|mm|em|ex|pt|pc|px)$/,
  		rotate = /rotate\(((?:[+\-]=)?([\-\d\.]+))deg\)/,
  		scale = /scale\(((?:[+\-]=)?([\d\.]+))\)/,
  		skew = /skew\(((?:[+\-]=)?([\-\d\.]+))deg, ?((?:[+\-]=)?([\-\d\.]+))deg\)/,
  		translate = /translate\(((?:[+\-]=)?([\-\d\.]+))px, ?((?:[+\-]=)?([\-\d\.]+))px\)/,
  		// these elements do not require 'px'
  		unitless = { lineHeight: 1, zoom: 1, zIndex: 1, opacity: 1, transform: 1};
  
    // which property name does this browser use for transform
  	var transform = function () {
  		var styles = doc.createElement('a').style,
  			props = ['webkitTransform', 'MozTransform', 'OTransform', 'msTransform', 'Transform'],
  			i;
  
  		for (i = 0; i < props.length; i++) {
  			if (props[i] in styles) return props[i]
  		};
  	}();
  
  	// does this browser support the opacity property?
  	var opacity = function () {
  		return typeof doc.createElement('a').style.opacity !== 'undefined'
  	}();
  
  	// initial style is determined by the elements themselves
  	var getStyle = doc.defaultView && doc.defaultView.getComputedStyle ?
  	function (el, property) {
  		property = property == 'transform' ? transform : property
  		property = camelize(property)
  		var value = null,
  			computed = doc.defaultView.getComputedStyle(el, '');
  
  		computed && (value = computed[property]);
  		return el.style[property] || value;
  	} : html.currentStyle ?
  
      function (el, property) {
  		property = camelize(property)
  
  		if (property == 'opacity') {
  			var val = 100
  			try {
  				val = el.filters['DXImageTransform.Microsoft.Alpha'].opacity
  			} catch (e1) {
  				try {
  					val = el.filters('alpha').opacity
  				} catch (e2) {
  
  				}
  			}
  			return val / 100
  		}
  		var value = el.currentStyle ? el.currentStyle[property] : null
  		return el.style[property] || value
  	} :
  
      function (el, property) {
  		return el.style[camelize(property)]
      }
  
    var frame = function () {
      // native animation frames
      // http://webstuff.nfshost.com/anim-timing/Overview.html
      // http://dev.chromium.org/developers/design-documents/requestanimationframe-implementation
      return win.requestAnimationFrame  ||
        win.webkitRequestAnimationFrame ||
        win.mozRequestAnimationFrame    ||
        win.msRequestAnimationFrame     ||
        win.oRequestAnimationFrame      ||
        function (callback) {
          win.setTimeout(function () {
            callback(+new Date())
          }, 17) // when I was 17..
        }
    }()
  
    var children = []
  
  	frame(function(timestamp) {
  	  	// feature-detect if rAF and now() are of the same scale (epoch or high-res),
  		// if not, we have to do a timestamp fix on each frame
  		fixTs = timestamp > 1e12 != now() > 1e12
  	})
  
    function has(array, elem, i) {
      if (Array.prototype.indexOf) return array.indexOf(elem)
      for (i = 0; i < array.length; ++i) {
        if (array[i] === elem) return i
      }
    }
  
    function render(timestamp) {
      var i, count = children.length
      // if we're using a high res timer, make sure timestamp is not the old epoch-based value.
      // http://updates.html5rocks.com/2012/05/requestAnimationFrame-API-now-with-sub-millisecond-precision
      if (perfNow && timestamp > 1e12) timestamp = now()
  	if (fixTs) timestamp = now()
      for (i = count; i--;) {
        children[i](timestamp)
      }
      children.length && frame(render)
    }
  
    function live(f) {
      if (children.push(f) === 1) frame(render)
    }
  
    function die(f) {
      var rest, index = has(children, f)
      if (index >= 0) {
        rest = children.slice(index + 1)
        children.length = index
        children = children.concat(rest)
      }
    }
  
    function parseTransform(style, base) {
      var values = {}, m
      if (m = style.match(rotate)) values.rotate = by(m[1], base ? base.rotate : null)
      if (m = style.match(scale)) values.scale = by(m[1], base ? base.scale : null)
      if (m = style.match(skew)) {values.skewx = by(m[1], base ? base.skewx : null); values.skewy = by(m[3], base ? base.skewy : null)}
      if (m = style.match(translate)) {values.translatex = by(m[1], base ? base.translatex : null); values.translatey = by(m[3], base ? base.translatey : null)}
      return values
    }
  
    function formatTransform(v) {
      var s = ''
      if ('rotate' in v) s += 'rotate(' + v.rotate + 'deg) '
      if ('scale' in v) s += 'scale(' + v.scale + ') '
      if ('translatex' in v) s += 'translate(' + v.translatex + 'px,' + v.translatey + 'px) '
      if ('skewx' in v) s += 'skew(' + v.skewx + 'deg,' + v.skewy + 'deg)'
      return s
    }
  
    function rgb(r, g, b) {
      return '#' + (1 << 24 | r << 16 | g << 8 | b).toString(16).slice(1)
    }
  
    // convert rgb and short hex to long hex
    function toHex(c) {
      var m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
      return (m ? rgb(m[1], m[2], m[3]) : c)
        .replace(/#(\w)(\w)(\w)$/, '#$1$1$2$2$3$3') // short skirt to long jacket
    }
  
    // change font-size => fontSize etc.
    function camelize(s) {
      return s.replace(/-(.)/g, function (m, m1) {
        return m1.toUpperCase()
      })
    }
  
    // aren't we having it?
    function fun(f) {
      return typeof f == 'function'
    }
  
    function nativeTween(t) {
      // default to a pleasant-to-the-eye easeOut (like native animations)
      return Math.sin(t * Math.PI / 2)
    }
  
    /**
      * Core tween method that requests each frame
      * @param duration: time in milliseconds. defaults to 1000
      * @param fn: tween frame callback function receiving 'position'
      * @param done {optional}: complete callback function
      * @param ease {optional}: easing method. defaults to easeOut
      * @param from {optional}: integer to start from
      * @param to {optional}: integer to end at
      * @returns method to stop the animation
      */
    function tween(duration, fn, done, ease, from, to) {
      ease = fun(ease) ? ease : morpheus.easings[ease] || nativeTween
      var time = duration || thousand
        , self = this
        , diff = to - from
        , start = now()
        , stop = 0
        , end = 0
  
      function run(t) {
        var delta = t - start
        if (delta > time || stop) {
          to = isFinite(to) ? to : 1
          stop ? end && fn(to) : fn(to)
          die(run)
          return done && done.apply(self)
        }
        // if you don't specify a 'to' you can use tween as a generic delta tweener
        // cool, eh?
        isFinite(to) ?
          fn((diff * ease(delta / time)) + from) :
          fn(ease(delta / time))
      }
  
      live(run)
  
      return {
        stop: function (jump) {
          stop = 1
          end = jump // jump to end of animation?
          if (!jump) done = null // remove callback if not jumping to end
        }
      }
    }
  
    /**
      * generic bezier method for animating x|y coordinates
      * minimum of 2 points required (start and end).
      * first point start, last point end
      * additional control points are optional (but why else would you use this anyway ;)
      * @param points: array containing control points
         [[0, 0], [100, 200], [200, 100]]
      * @param pos: current be(tween) position represented as float  0 - 1
      * @return [x, y]
      */
    function bezier(points, pos) {
      var n = points.length, r = [], i, j
      for (i = 0; i < n; ++i) {
        r[i] = [points[i][0], points[i][1]]
      }
      for (j = 1; j < n; ++j) {
        for (i = 0; i < n - j; ++i) {
          r[i][0] = (1 - pos) * r[i][0] + pos * r[parseInt(i + 1, 10)][0]
          r[i][1] = (1 - pos) * r[i][1] + pos * r[parseInt(i + 1, 10)][1]
        }
      }
      return [r[0][0], r[0][1]]
    }
  
    // this gets you the next hex in line according to a 'position'
    function nextColor(pos, start, finish) {
      var r = [], i, e, from, to
      for (i = 0; i < 6; i++) {
        from = Math.min(15, parseInt(start.charAt(i),  16))
        to   = Math.min(15, parseInt(finish.charAt(i), 16))
        e = Math.floor((to - from) * pos + from)
        e = e > 15 ? 15 : e < 0 ? 0 : e
        r[i] = e.toString(16)
      }
      return '#' + r.join('')
    }
  
    // this retreives the frame value within a sequence
    function getTweenVal(pos, units, begin, end, k, i, v) {
      if (k == 'transform') {
        v = {}
        for (var t in begin[i][k]) {
          v[t] = (t in end[i][k]) ? Math.round(((end[i][k][t] - begin[i][k][t]) * pos + begin[i][k][t]) * thousand) / thousand : begin[i][k][t]
        }
        return v
      } else if (typeof begin[i][k] == 'string') {
        return nextColor(pos, begin[i][k], end[i][k])
      } else {
        // round so we don't get crazy long floats
        v = Math.round(((end[i][k] - begin[i][k]) * pos + begin[i][k]) * thousand) / thousand
        // some css properties don't require a unit (like zIndex, lineHeight, opacity)
        if (!(k in unitless)) v += units[i][k] || 'px'
        return v
      }
    }
  
    // support for relative movement via '+=n' or '-=n'
    function by(val, start, m, r, i) {
      return (m = relVal.exec(val)) ?
        (i = parseFloat(m[2])) && (start + (m[1] == '+' ? 1 : -1) * i) :
        parseFloat(val)
    }
  
    /**
      * morpheus:
      * @param element(s): HTMLElement(s)
      * @param options: mixed bag between CSS Style properties & animation options
      *  - {n} CSS properties|values
      *     - value can be strings, integers,
      *     - or callback function that receives element to be animated. method must return value to be tweened
      *     - relative animations start with += or -= followed by integer
      *  - duration: time in ms - defaults to 1000(ms)
      *  - easing: a transition method - defaults to an 'easeOut' algorithm
      *  - complete: a callback method for when all elements have finished
      *  - bezier: array of arrays containing x|y coordinates that define the bezier points. defaults to none
      *     - this may also be a function that receives element to be animated. it must return a value
      */
    function morpheus(elements, options) {
      var els = elements ? (els = isFinite(elements.length) ? elements : [elements]) : [], i
        , complete = options.complete
        , duration = options.duration
        , ease = options.easing
        , points = options.bezier
        , begin = []
        , end = []
        , units = []
        , bez = []
        , originalLeft
        , originalTop
  
      if (points) {
        // remember the original values for top|left
        originalLeft = options.left;
        originalTop = options.top;
        delete options.right;
        delete options.bottom;
        delete options.left;
        delete options.top;
      }
  
      for (i = els.length; i--;) {
  
        // record beginning and end states to calculate positions
        begin[i] = {}
        end[i] = {}
        units[i] = {}
  
        // are we 'moving'?
        if (points) {
  
          var left = getStyle(els[i], 'left')
            , top = getStyle(els[i], 'top')
            , xy = [by(fun(originalLeft) ? originalLeft(els[i]) : originalLeft || 0, parseFloat(left)),
                    by(fun(originalTop) ? originalTop(els[i]) : originalTop || 0, parseFloat(top))]
  
          bez[i] = fun(points) ? points(els[i], xy) : points
          bez[i].push(xy)
          bez[i].unshift([
            parseInt(left, 10),
            parseInt(top, 10)
          ])
        }
  
        for (var k in options) {
          switch (k) {
          case 'complete':
          case 'duration':
          case 'easing':
          case 'bezier':
            continue
          }
          var v = getStyle(els[i], k), unit
            , tmp = fun(options[k]) ? options[k](els[i]) : options[k]
          if (typeof tmp == 'string' &&
              rgbOhex.test(tmp) &&
              !rgbOhex.test(v)) {
            delete options[k]; // remove key :(
            continue; // cannot animate colors like 'orange' or 'transparent'
                      // only #xxx, #xxxxxx, rgb(n,n,n)
          }
  
          begin[i][k] = k == 'transform' ? parseTransform(v) :
            typeof tmp == 'string' && rgbOhex.test(tmp) ?
              toHex(v).slice(1) :
              parseFloat(v)
          end[i][k] = k == 'transform' ? parseTransform(tmp, begin[i][k]) :
            typeof tmp == 'string' && tmp.charAt(0) == '#' ?
              toHex(tmp).slice(1) :
              by(tmp, parseFloat(v));
          // record original unit
          (typeof tmp == 'string') && (unit = tmp.match(numUnit)) && (units[i][k] = unit[1])
        }
      }
      // ONE TWEEN TO RULE THEM ALL
      return tween.apply(els, [duration, function (pos, v, xy) {
        // normally not a fan of optimizing for() loops, but we want something
        // fast for animating
        for (i = els.length; i--;) {
          if (points) {
            xy = bezier(bez[i], pos)
            els[i].style.left = xy[0] + 'px'
            els[i].style.top = xy[1] + 'px'
          }
          for (var k in options) {
            v = getTweenVal(pos, units, begin, end, k, i)
            k == 'transform' ?
              els[i].style[transform] = formatTransform(v) :
              k == 'opacity' && !opacity ?
                (els[i].style.filter = 'alpha(opacity=' + (v * 100) + ')') :
                (els[i].style[camelize(k)] = v)
          }
        }
      }, complete, ease])
    }
  
    // expose useful methods
    morpheus.tween = tween
    morpheus.getStyle = getStyle
    morpheus.bezier = bezier
    morpheus.transform = transform
    morpheus.parseTransform = parseTransform
    morpheus.formatTransform = formatTransform
    morpheus.easings = {}
  
    return morpheus
  })();
  
  
  /* **********************************************
       Begin TL.Point.js
  ********************************************** */
  
  /*	TL.Point
  	Inspired by Leaflet
  	TL.Point represents a point with x and y coordinates.
  ================================================== */
  
  TL.Point = function (/*Number*/ x, /*Number*/ y, /*Boolean*/ round) {
  	this.x = (round ? Math.round(x) : x);
  	this.y = (round ? Math.round(y) : y);
  };
  
  TL.Point.prototype = {
  	add: function (point) {
  		return this.clone()._add(point);
  	},
  
  	_add: function (point) {
  		this.x += point.x;
  		this.y += point.y;
  		return this;
  	},
  
  	subtract: function (point) {
  		return this.clone()._subtract(point);
  	},
  
  	// destructive subtract (faster)
  	_subtract: function (point) {
  		this.x -= point.x;
  		this.y -= point.y;
  		return this;
  	},
  
  	divideBy: function (num, round) {
  		return new TL.Point(this.x / num, this.y / num, round);
  	},
  
  	multiplyBy: function (num) {
  		return new TL.Point(this.x * num, this.y * num);
  	},
  
  	distanceTo: function (point) {
  		var x = point.x - this.x,
  			y = point.y - this.y;
  		return Math.sqrt(x * x + y * y);
  	},
  
  	round: function () {
  		return this.clone()._round();
  	},
  
  	// destructive round
  	_round: function () {
  		this.x = Math.round(this.x);
  		this.y = Math.round(this.y);
  		return this;
  	},
  
  	clone: function () {
  		return new TL.Point(this.x, this.y);
  	},
  
  	toString: function () {
  		return 'Point(' +
  				TL.Util.formatNum(this.x) + ', ' +
  				TL.Util.formatNum(this.y) + ')';
  	}
  };
  
  /* **********************************************
       Begin TL.DomMixins.js
  ********************************************** */
  
  /*	TL.DomMixins
  	DOM methods used regularly
  	Assumes there is a _el.container and animator
  ================================================== */
  TL.DomMixins = {
  	
  	/*	Adding, Hiding, Showing etc
  	================================================== */
  	show: function(animate) {
  		if (animate) {
  			/*
  			this.animator = TL.Animate(this._el.container, {
  				left: 		-(this._el.container.offsetWidth * n) + "px",
  				duration: 	this.options.duration,
  				easing: 	this.options.ease
  			});
  			*/
  		} else {
  			this._el.container.style.display = "block";
  		}
  	},
  	
  	hide: function(animate) {
  		this._el.container.style.display = "none";
  	},
  	
  	addTo: function(container) {
  		container.appendChild(this._el.container);
  		this.onAdd();
  	},
  	
  	removeFrom: function(container) {
  		container.removeChild(this._el.container);
  		this.onRemove();
  	},
  	
  	/*	Animate to Position
  	================================================== */
  	animatePosition: function(pos, el) {
  		var ani = {
  			duration: 	this.options.duration,
  			easing: 	this.options.ease
  		};
  		for (var name in pos) {
  			if (pos.hasOwnProperty(name)) {
  				ani[name] = pos[name] + "px";
  			}
  		}
  		
  		if (this.animator) {
  			this.animator.stop();
  		}
  		this.animator = TL.Animate(el, ani);
  	},
  	
  	/*	Events
  	================================================== */
  	
  	onLoaded: function() {
  		this.fire("loaded", this.data);
  	},
  	
  	onAdd: function() {
  		this.fire("added", this.data);
  	},
  
  	onRemove: function() {
  		this.fire("removed", this.data);
  	},
  	
  	/*	Set the Position
  	================================================== */
  	setPosition: function(pos, el) {
  		for (var name in pos) {
  			if (pos.hasOwnProperty(name)) {
  				if (el) {
  					el.style[name] = pos[name] + "px";
  				} else {
  					this._el.container.style[name] = pos[name] + "px";
  				};
  			}
  		}
  	},
  	
  	getPosition: function() {
  		return TL.Dom.getPosition(this._el.container);
  	}
  	
  };
  
  
  /* **********************************************
       Begin TL.Dom.js
  ********************************************** */
  
  /*	TL.Dom
  	Utilities for working with the DOM
  ================================================== */
  
  TL.Dom = {
  
  	get: function(id) {
  		return (typeof id === 'string' ? document.getElementById(id) : id);
  	},
  
  	getByClass: function(id) {
  		if (id) {
  			return document.getElementsByClassName(id);
  		}
  	},
  
  	create: function(tagName, className, container) {
  		var el = document.createElement(tagName);
  		el.className = className;
  		if (container) {
  			container.appendChild(el);
  		}
  		return el;
  	},
  
  	createText: function(content, container) {
  		var el = document.createTextNode(content);
  		if (container) {
  			container.appendChild(el);
  		}
  		return el;
  	},
  
  	getTranslateString: function (point) {
  		return TL.Dom.TRANSLATE_OPEN +
  				point.x + 'px,' + point.y + 'px' +
  				TL.Dom.TRANSLATE_CLOSE;
  	},
  
  	setPosition: function (el, point) {
  		el._tl_pos = point;
  		if (TL.Browser.webkit3d) {
  			el.style[TL.Dom.TRANSFORM] =  TL.Dom.getTranslateString(point);
  
  			if (TL.Browser.android) {
  				el.style['-webkit-perspective'] = '1000';
  				el.style['-webkit-backface-visibility'] = 'hidden';
  			}
  		} else {
  			el.style.left = point.x + 'px';
  			el.style.top = point.y + 'px';
  		}
  	},
  
  	getPosition: function(el){
  	    var pos = {
  	    	x: 0,
  			y: 0
  	    }
  	    while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
  	        pos.x += el.offsetLeft// - el.scrollLeft;
  	        pos.y += el.offsetTop// - el.scrollTop;
  	        el = el.offsetParent;
  	    }
  	    return pos;
  	},
  
  	testProp: function(props) {
  		var style = document.documentElement.style;
  
  		for (var i = 0; i < props.length; i++) {
  			if (props[i] in style) {
  				return props[i];
  			}
  		}
  		return false;
  	}
  
  };
  
  TL.Util.mergeData(TL.Dom, {
  	TRANSITION: TL.Dom.testProp(['transition', 'webkitTransition', 'OTransition', 'MozTransition', 'msTransition']),
  	TRANSFORM: TL.Dom.testProp(['transformProperty', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']),
  
  	TRANSLATE_OPEN: 'translate' + (TL.Browser.webkit3d ? '3d(' : '('),
  	TRANSLATE_CLOSE: TL.Browser.webkit3d ? ',0)' : ')'
  });
  
  
  /* **********************************************
       Begin TL.DomUtil.js
  ********************************************** */
  
  /*	TL.DomUtil
  	Inspired by Leaflet
  	TL.DomUtil contains various utility functions for working with DOM
  ================================================== */
  
  
  TL.DomUtil = {
  	get: function (id) {
  		return (typeof id === 'string' ? document.getElementById(id) : id);
  	},
  
  	getStyle: function (el, style) {
  		var value = el.style[style];
  		if (!value && el.currentStyle) {
  			value = el.currentStyle[style];
  		}
  		if (!value || value === 'auto') {
  			var css = document.defaultView.getComputedStyle(el, null);
  			value = css ? css[style] : null;
  		}
  		return (value === 'auto' ? null : value);
  	},
  
  	getViewportOffset: function (element) {
  		var top = 0,
  			left = 0,
  			el = element,
  			docBody = document.body;
  
  		do {
  			top += el.offsetTop || 0;
  			left += el.offsetLeft || 0;
  
  			if (el.offsetParent === docBody &&
  					TL.DomUtil.getStyle(el, 'position') === 'absolute') {
  				break;
  			}
  			el = el.offsetParent;
  		} while (el);
  
  		el = element;
  
  		do {
  			if (el === docBody) {
  				break;
  			}
  
  			top -= el.scrollTop || 0;
  			left -= el.scrollLeft || 0;
  
  			el = el.parentNode;
  		} while (el);
  
  		return new TL.Point(left, top);
  	},
  
  	create: function (tagName, className, container) {
  		var el = document.createElement(tagName);
  		el.className = className;
  		if (container) {
  			container.appendChild(el);
  		}
  		return el;
  	},
  
  	disableTextSelection: function () {
  		if (document.selection && document.selection.empty) {
  			document.selection.empty();
  		}
  		if (!this._onselectstart) {
  			this._onselectstart = document.onselectstart;
  			document.onselectstart = TL.Util.falseFn;
  		}
  	},
  
  	enableTextSelection: function () {
  		document.onselectstart = this._onselectstart;
  		this._onselectstart = null;
  	},
  
  	hasClass: function (el, name) {
  		return (el.className.length > 0) &&
  				new RegExp("(^|\\s)" + name + "(\\s|$)").test(el.className);
  	},
  
  	addClass: function (el, name) {
  		if (!TL.DomUtil.hasClass(el, name)) {
  			el.className += (el.className ? ' ' : '') + name;
  		}
  	},
  
  	removeClass: function (el, name) {
  		el.className = el.className.replace(/(\S+)\s*/g, function (w, match) {
  			if (match === name) {
  				return '';
  			}
  			return w;
  		}).replace(/^\s+/, '');
  	},
  
  	setOpacity: function (el, value) {
  		if (TL.Browser.ie) {
  			el.style.filter = 'alpha(opacity=' + Math.round(value * 100) + ')';
  		} else {
  			el.style.opacity = value;
  		}
  	},
  
  
  	testProp: function (props) {
  		var style = document.documentElement.style;
  
  		for (var i = 0; i < props.length; i++) {
  			if (props[i] in style) {
  				return props[i];
  			}
  		}
  		return false;
  	},
  
  	getTranslateString: function (point) {
  
  		return TL.DomUtil.TRANSLATE_OPEN +
  				point.x + 'px,' + point.y + 'px' +
  				TL.DomUtil.TRANSLATE_CLOSE;
  	},
  
  	getScaleString: function (scale, origin) {
  		var preTranslateStr = TL.DomUtil.getTranslateString(origin),
  			scaleStr = ' scale(' + scale + ') ',
  			postTranslateStr = TL.DomUtil.getTranslateString(origin.multiplyBy(-1));
  
  		return preTranslateStr + scaleStr + postTranslateStr;
  	},
  
  	setPosition: function (el, point) {
  		el._tl_pos = point;
  		if (TL.Browser.webkit3d) {
  			el.style[TL.DomUtil.TRANSFORM] =  TL.DomUtil.getTranslateString(point);
  
  			if (TL.Browser.android) {
  				el.style['-webkit-perspective'] = '1000';
  				el.style['-webkit-backface-visibility'] = 'hidden';
  			}
  		} else {
  			el.style.left = point.x + 'px';
  			el.style.top = point.y + 'px';
  		}
  	},
  
  	getPosition: function (el) {
  		return el._tl_pos;
  	}
  };
  
  /* **********************************************
       Begin TL.DomEvent.js
  ********************************************** */
  
  /*	TL.DomEvent
  	Inspired by Leaflet 
  	DomEvent contains functions for working with DOM events.
  ================================================== */
  // TODO stamp
  
  TL.DomEvent = {
  	/* inpired by John Resig, Dean Edwards and YUI addEvent implementations */
  	addListener: function (/*HTMLElement*/ obj, /*String*/ type, /*Function*/ fn, /*Object*/ context) {
  		var id = TL.Util.stamp(fn),
  			key = '_tl_' + type + id;
  
  		if (obj[key]) {
  			return;
  		}
  
  		var handler = function (e) {
  			return fn.call(context || obj, e || TL.DomEvent._getEvent());
  		};
  
  		if (TL.Browser.touch && (type === 'dblclick') && this.addDoubleTapListener) {
  			this.addDoubleTapListener(obj, handler, id);
  		} else if ('addEventListener' in obj) {
  			if (type === 'mousewheel') {
  				obj.addEventListener('DOMMouseScroll', handler, false);
  				obj.addEventListener(type, handler, false);
  			} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
  				var originalHandler = handler,
  					newType = (type === 'mouseenter' ? 'mouseover' : 'mouseout');
  				handler = function (e) {
  					if (!TL.DomEvent._checkMouse(obj, e)) {
  						return;
  					}
  					return originalHandler(e);
  				};
  				obj.addEventListener(newType, handler, false);
  			} else {
  				obj.addEventListener(type, handler, false);
  			}
  		} else if ('attachEvent' in obj) {
  			obj.attachEvent("on" + type, handler);
  		}
  
  		obj[key] = handler;
  	},
  
  	removeListener: function (/*HTMLElement*/ obj, /*String*/ type, /*Function*/ fn) {
  		var id = TL.Util.stamp(fn),
  			key = '_tl_' + type + id,
  			handler = obj[key];
  
  		if (!handler) {
  			return;
  		}
  
  		if (TL.Browser.touch && (type === 'dblclick') && this.removeDoubleTapListener) {
  			this.removeDoubleTapListener(obj, id);
  		} else if ('removeEventListener' in obj) {
  			if (type === 'mousewheel') {
  				obj.removeEventListener('DOMMouseScroll', handler, false);
  				obj.removeEventListener(type, handler, false);
  			} else if ((type === 'mouseenter') || (type === 'mouseleave')) {
  				obj.removeEventListener((type === 'mouseenter' ? 'mouseover' : 'mouseout'), handler, false);
  			} else {
  				obj.removeEventListener(type, handler, false);
  			}
  		} else if ('detachEvent' in obj) {
  			obj.detachEvent("on" + type, handler);
  		}
  		obj[key] = null;
  	},
  
  	_checkMouse: function (el, e) {
  		var related = e.relatedTarget;
  
  		if (!related) {
  			return true;
  		}
  
  		try {
  			while (related && (related !== el)) {
  				related = related.parentNode;
  			}
  		} catch (err) {
  			return false;
  		}
  
  		return (related !== el);
  	},
  
  	/*jshint noarg:false */ // evil magic for IE
  	_getEvent: function () {
  		var e = window.event;
  		if (!e) {
  			var caller = arguments.callee.caller;
  			while (caller) {
  				e = caller['arguments'][0];
  				if (e && window.Event === e.constructor) {
  					break;
  				}
  				caller = caller.caller;
  			}
  		}
  		return e;
  	},
  	/*jshint noarg:false */
  
  	stopPropagation: function (/*Event*/ e) {
  		if (e.stopPropagation) {
  			e.stopPropagation();
  		} else {
  			e.cancelBubble = true;
  		}
  	},
  	
  	// TODO TL.Draggable.START
  	disableClickPropagation: function (/*HTMLElement*/ el) {
  		TL.DomEvent.addListener(el, TL.Draggable.START, TL.DomEvent.stopPropagation);
  		TL.DomEvent.addListener(el, 'click', TL.DomEvent.stopPropagation);
  		TL.DomEvent.addListener(el, 'dblclick', TL.DomEvent.stopPropagation);
  	},
  
  	preventDefault: function (/*Event*/ e) {
  		if (e.preventDefault) {
  			e.preventDefault();
  		} else {
  			e.returnValue = false;
  		}
  	},
  
  	stop: function (e) {
  		TL.DomEvent.preventDefault(e);
  		TL.DomEvent.stopPropagation(e);
  	},
  
  
  	getWheelDelta: function (e) {
  		var delta = 0;
  		if (e.wheelDelta) {
  			delta = e.wheelDelta / 120;
  		}
  		if (e.detail) {
  			delta = -e.detail / 3;
  		}
  		return delta;
  	}
  };
  
  
  
  
  /* **********************************************
       Begin TL.StyleSheet.js
  ********************************************** */
  
  /*	TL.StyleSheet
  	Style Sheet Object
  ================================================== */
  
  TL.StyleSheet = TL.Class.extend({
  	
  	includes: [TL.Events],
  	
  	_el: {},
  	
  	/*	Constructor
  	================================================== */
  	initialize: function() {
  		// Borrowed from: http://davidwalsh.name/add-rules-stylesheets
  		this.style = document.createElement("style");
  		
  		// WebKit hack :(
  		this.style.appendChild(document.createTextNode(""));
  		
  		// Add the <style> element to the page
  		document.head.appendChild(this.style);
  		
  		this.sheet = this.style.sheet;
  		
  	},
  	
  	addRule: function(selector, rules, index) {
  		var _index = 0;
  		
  		if (index) {
  			_index = index;
  		}
  		
  		if("insertRule" in this.sheet) {
  			this.sheet.insertRule(selector + "{" + rules + "}", _index);
  		}
  		else if("addRule" in this.sheet) {
  			this.sheet.addRule(selector, rules, _index);
  		}
  	},
  	
  
  	/*	Events
  	================================================== */
  	onLoaded: function(error) {
  		this._state.loaded = true;
  		this.fire("loaded", this.data);
  	}
  	
  });
  
  /* **********************************************
       Begin TL.Date.js
  ********************************************** */
  
  /*	TL.Date
  	Date object
  	MONTHS are 1-BASED, not 0-BASED (different from Javascript date objects)
  ================================================== */
  
  //
  // Class for human dates
  //
  
  TL.Date = TL.Class.extend({
  
      // @data = ms, JS Date object, or JS dictionary with date properties
  	initialize: function (data, format, format_short) {
  	    if (typeof(data) == 'number') {
  			this.data = {
  				format:     "yyyy mmmm",
  				date_obj:   new Date(data)
  			};
  	    } else if(Date == data.constructor) {
  			this.data = {
  				format:     "yyyy mmmm",
  				date_obj:   data
  			};
  	    } else {
  	        this.data = JSON.parse(JSON.stringify(data)); // clone don't use by reference.
              this._createDateObj();
  	    }
  
  		this._setFormat(format, format_short);
      },
  
  	setDateFormat: function(format) {
  		this.data.format = format;
  	},
  
  	getDisplayDate: function(language, format) {
  	    if (this.data.display_date) {
  	        return this.data.display_date;
  	    }
          if (!language) {
              language = TL.Language.fallback;
          }
          if (language.constructor != TL.Language) {
              trace("First argument to getDisplayDate must be TL.Language");
              language = TL.Language.fallback;
          }
  
          var format_key = format || this.data.format;
          return language.formatDate(this.data.date_obj, format_key);
  	},
  
  	getMillisecond: function() {
  		return this.getTime();
  	},
  
  	getTime: function() {
  		return this.data.date_obj.getTime();
  	},
  
  	isBefore: function(other_date) {
          if (!this.data.date_obj.constructor == other_date.data.date_obj.constructor) {
              throw new TL.Error("date_compare_err") // but should be able to compare 'cosmological scale' dates once we get to that...
          }
          if ('isBefore' in this.data.date_obj) {
              return this.data.date_obj['isBefore'](other_date.data.date_obj);
          }
          return this.data.date_obj < other_date.data.date_obj
  	},
  
  	isAfter: function(other_date) {
          if (!this.data.date_obj.constructor == other_date.data.date_obj.constructor) {
              throw new TL.Error("date_compare_err") // but should be able to compare 'cosmological scale' dates once we get to that...
          }
          if ('isAfter' in this.data.date_obj) {
              return this.data.date_obj['isAfter'](other_date.data.date_obj);
          }
          return this.data.date_obj > other_date.data.date_obj
  	},
  
      // Return a new TL.Date which has been 'floored' at the given scale.
      // @scale = string value from TL.Date.SCALES
      floor: function(scale) {
          var d = new Date(this.data.date_obj.getTime());
          for (var i = 0; i < TL.Date.SCALES.length; i++) {
               // for JS dates, we iteratively apply flooring functions
              TL.Date.SCALES[i][2](d);
              if (TL.Date.SCALES[i][0] == scale) return new TL.Date(d);
          };
  
          throw new TL.Error("invalid_scale_err", scale);
      },
  
  	/*	Private Methods
  	================================================== */
  
      _getDateData: function() {
          var _date = {
              year: 			0,
              month: 			1, // stupid JS dates
              day: 			1,
              hour: 			0,
              minute: 		0,
              second: 		0,
              millisecond: 	0
  		};
  
  		// Merge data
  		TL.Util.mergeData(_date, this.data);
  
   		// Make strings into numbers
  		var DATE_PARTS = TL.Date.DATE_PARTS;
  
   		for (var ix in DATE_PARTS) {
   		    var x = TL.Util.trim(_date[DATE_PARTS[ix]]);
   		    if (!x.match(/^-?\d*$/)) {
   		        throw new TL.Error("invalid_date_err", DATE_PARTS[ix] + " = '" + _date[DATE_PARTS[ix]] + "'");
   		    }
   		    
  			var parsed = parseInt(_date[DATE_PARTS[ix]]);
  			if (isNaN(parsed)) {
                  parsed = (ix == 4 || ix == 5) ? 1 : 0; // month and day have diff baselines
              }
  			_date[DATE_PARTS[ix]] = parsed;
  		}
  
  		if (_date.month > 0 && _date.month <= 12) { // adjust for JS's weirdness
  			_date.month = _date.month - 1;
  		}
  
  		return _date;
      },
  
  	_createDateObj: function() {
  	    var _date = this._getDateData();
          this.data.date_obj = new Date(_date.year, _date.month, _date.day, _date.hour, _date.minute, _date.second, _date.millisecond);
          if (this.data.date_obj.getFullYear() != _date.year) {
              // Javascript has stupid defaults for two-digit years
              this.data.date_obj.setFullYear(_date.year);
          }
  	},
  
      /*  Find Best Format
       * this may not work with 'cosmologic' dates, or with TL.Date if we
       * support constructing them based on JS Date and time
      ================================================== */
      findBestFormat: function(variant) {
          var eval_array = TL.Date.DATE_PARTS,
              format = "";
  
          for (var i = 0; i < eval_array.length; i++) {
              if ( this.data[eval_array[i]]) {
                  if (variant) {
                      if (!(variant in TL.Date.BEST_DATEFORMATS)) {
                          variant = 'short'; // legacy
                      }
                  } else {
                      variant = 'base'
                  }
                  return TL.Date.BEST_DATEFORMATS[variant][eval_array[i]];
              }
          };
          return "";
      },
      _setFormat: function(format, format_short) {
  		if (format) {
  			this.data.format = format;
  		} else if (!this.data.format) {
  			this.data.format = this.findBestFormat();
  		}
  
  		if (format_short) {
  			this.data.format_short = format_short;
  		} else if (!this.data.format_short) {
  			this.data.format_short = this.findBestFormat(true);
  		}
      }
  });
  
  // offer something that can figure out the right date class to return
  TL.Date.makeDate = function(data) {
      var date = new TL.Date(data);
      if (!isNaN(date.getTime())) {
          return date;
      }
      return new TL.BigDate(data);
  }
  
  TL.BigYear = TL.Class.extend({
      initialize: function (year) {
          this.year = parseInt(year);
          if (isNaN(this.year)) {
              throw new TL.Error('invalid_year_err', year);
          }
      },
  
      isBefore: function(that) {
          return this.year < that.year;
      },
  
      isAfter: function(that) {
          return this.year > that.year;
      },
  
      getTime: function() {
          return this.year;
      }
  });
  
  (function(cls){
      // human scales
      cls.SCALES = [ // ( name, units_per_tick, flooring function )
          ['millisecond',1, function(d) { }],
          ['second',1000, function(d) { d.setMilliseconds(0);}],
          ['minute',1000 * 60, function(d) { d.setSeconds(0);}],
          ['hour',1000 * 60 * 60, function(d) { d.setMinutes(0);}],
          ['day',1000 * 60 * 60 * 24, function(d) { d.setHours(0);}],
          ['month',1000 * 60 * 60 * 24 * 30, function(d) { d.setDate(1);}],
          ['year',1000 * 60 * 60 * 24 * 365, function(d) { d.setMonth(0);}],
          ['decade',1000 * 60 * 60 * 24 * 365 * 10, function(d) {
              var real_year = d.getFullYear();
              d.setFullYear( real_year - (real_year % 10))
          }],
          ['century',1000 * 60 * 60 * 24 * 365 * 100, function(d) {
              var real_year = d.getFullYear();
              d.setFullYear( real_year - (real_year % 100))
          }],
          ['millennium',1000 * 60 * 60 * 24 * 365 * 1000, function(d) {
              var real_year = d.getFullYear();
              d.setFullYear( real_year - (real_year % 1000))
          }]
      ];
  
      // Date parts from highest to lowest precision
      cls.DATE_PARTS = ["millisecond", "second", "minute", "hour", "day", "month", "year"];
  
      var ISO8601_SHORT_PATTERN = /^([\+-]?\d+?)(-\d{2}?)?(-\d{2}?)?$/;
      // regex below from
      // http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/
      var ISO8601_PATTERN = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24\:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/;
  
      /* For now, rather than extract parts from regexp, lets trust the browser.
       * Famous last words...
       * What about UTC vs local time?
       * see also http://stackoverflow.com/questions/10005374/ecmascript-5-date-parse-results-for-iso-8601-test-cases
       */
      cls.parseISODate = function(str) {
          var d = new Date(str);
          if (isNaN(d)) {
              throw new TL.Error("invalid_date_err", str);
          }
          return {
              year: d.getFullYear(),
              month: d.getMonth() + 1,
              day: d.getDate(),
              hour: d.getHours(),
              minute: d.getMinutes(),
              second: d.getSeconds(),
              millisecond: d.getMilliseconds()
          }
  
      }
  
      cls.parseDate = function(str) {
  
          if (str.match(ISO8601_SHORT_PATTERN)) {
              // parse short specifically to avoid timezone offset confusion
              // most browsers assume short is UTC, not local time.
              var parts = str.match(ISO8601_SHORT_PATTERN).slice(1);
              var d = { year: parts[0].replace('+','')} // year can be negative
              if (parts[1]) { d['month'] = parts[1].replace('-',''); }
              if (parts[2]) { d['day'] = parts[2].replace('-',''); }
              return d;
          }
  
          if (str.match(ISO8601_PATTERN)) {
              return cls.parseISODate(str);
          }
  
          if (str.match(/^\-?\d+$/)) {
              return { year: str }
          }
  
          var parsed = {}
          if (str.match(/\d+\/\d+\/\d+/)) { // mm/yy/dddd
              var date = str.match(/\d+\/\d+\/\d+/)[0];
              str = TL.Util.trim(str.replace(date,''));
              var date_parts = date.split('/');
              parsed.month = date_parts[0];
              parsed.day = date_parts[1];
              parsed.year = date_parts[2];
          }
  
          if (str.match(/\d+\/\d+/)) { // mm/yy
              var date = str.match(/\d+\/\d+/)[0];
              str = TL.Util.trim(str.replace(date,''));
              var date_parts = date.split('/');
              parsed.month = date_parts[0];
              parsed.year = date_parts[1];
          }
          // todo: handle hours, minutes, seconds, millis other date formats, etc...
          if (str.match(':')) {
              var time_parts = str.split(':');
              parsed.hour = time_parts[0];
              parsed.minute = time_parts[1];
              if (time_parts[2]) {
                  second_parts = time_parts[2].split('.');
                  parsed.second = second_parts[0];
                  parsed.millisecond = second_parts[1];
              }
          }
          return parsed;
      }
  
      cls.BEST_DATEFORMATS = {
          base: {
              millisecond: 'time_short',
              second: 'time',
              minute: 'time_no_seconds_small_date',
              hour: 'time_no_seconds_small_date',
              day: 'full',
              month: 'month',
              year: 'year',
              decade: 'year',
              century: 'year',
              millennium: 'year',
              age: 'fallback',
              epoch: 'fallback',
              era: 'fallback',
              eon: 'fallback',
              eon2: 'fallback'
          },
  
          short: {
              millisecond: 'time_short',
              second: 'time_short',
              minute: 'time_no_seconds_short',
              hour: 'time_no_minutes_short',
              day: 'full_short',
              month: 'month_short',
              year: 'year',
              decade: 'year',
              century: 'year',
              millennium: 'year',
              age: 'fallback',
              epoch: 'fallback',
              era: 'fallback',
              eon: 'fallback',
              eon2: 'fallback'
          }
      }
  
  
  })(TL.Date)
  
  
  //
  // Class for cosmological dates
  //
  TL.BigDate = TL.Date.extend({
  
      // @data = TL.BigYear object or JS dictionary with date properties
      initialize: function(data, format, format_short) {
          if (TL.BigYear == data.constructor) {
              this.data = {
                  date_obj:   data
              }
          } else {
              this.data = JSON.parse(JSON.stringify(data));
              this._createDateObj();
          }
  
          this._setFormat(format, format_short);
      },
  
      // Create date_obj
      _createDateObj: function() {
  	    var _date = this._getDateData();
          this.data.date_obj = new TL.BigYear(_date.year);
      },
  
      // Return a new TL.BigDate which has been 'floored' at the given scale.
      // @scale = string value from TL.BigDate.SCALES
      floor: function(scale) {
          for (var i = 0; i < TL.BigDate.SCALES.length; i++) {
              if (TL.BigDate.SCALES[i][0] == scale) {
                  var floored = TL.BigDate.SCALES[i][2](this.data.date_obj);
                  return new TL.BigDate(floored);
              }
          };
  
          throw new TL.Error("invalid_scale_err", scale);
      }
  });
  
  (function(cls){
      // cosmo units are years, not millis
      var AGE = 1000000;
      var EPOCH = AGE * 10;
      var ERA = EPOCH * 10;
      var EON = ERA * 10;
  
      var Floorer = function(unit) {
          return function(a_big_year) {
              var year = a_big_year.getTime();
              return new TL.BigYear(Math.floor(year/unit) * unit);
          }
      }
  
      // cosmological scales
      cls.SCALES = [ // ( name, units_per_tick, flooring function )
  				['year',1, new Floorer(1)],
  				['decade',10, new Floorer(10)],
  				['century',100, new Floorer(100)],
  				['millennium',1000, new Floorer(1000)],
          ['age',AGE, new Floorer(AGE)],          // 1M years
          ['epoch',EPOCH, new Floorer(EPOCH)],    // 10M years
          ['era',ERA, new Floorer(ERA)],          // 100M years
          ['eon',EON, new Floorer(EON)]           // 1B years
      ];
  
  })(TL.BigDate)
  
  
  /* **********************************************
       Begin TL.DateUtil.js
  ********************************************** */
  
  /*	TL.DateUtil
  	Utilities for parsing time
  ================================================== */
  
  
  TL.DateUtil = {
  	get: function (id) {
  		return (typeof id === 'string' ? document.getElementById(id) : id);
  	},
  
  	sortByDate: function(array,prop_name) { // only for use with slide data objects
  		var prop_name = prop_name || 'start_date';
  		array.sort(function(a,b){
  			if (a[prop_name].isBefore(b[prop_name])) return -1;
  			if (a[prop_name].isAfter(b[prop_name])) return 1;
  			return 0;
  		});
  	},
  
  	parseTime: function(time_str) {
  		var parsed = {
  			hour: null, minute: null, second: null, millisecond: null // conform to keys in TL.Date
  		}
  		var period = null;
  		var match = time_str.match(/(\s*[AaPp]\.?[Mm]\.?\s*)$/);
  		if (match) {
  			period = TL.Util.trim(match[0]);
  			time_str = TL.Util.trim(time_str.substring(0,time_str.lastIndexOf(period)));
  		}
  
  		var parts = [];
  		var no_separators = time_str.match(/^\s*(\d{1,2})(\d{2})\s*$/);
  		if (no_separators) {
  			parts = no_separators.slice(1);
  		} else {
  			parts = time_str.split(':');
  			if (parts.length == 1) {
  				parts = time_str.split('.');
  			}
  		}
  
  		if (parts.length > 4) { 
  		    throw new TL.Error("invalid_separator_error");
  		}
  
  		parsed.hour = parseInt(parts[0]);
  
  		if (period && period.toLowerCase()[0] == 'p' && parsed.hour != 12) {
  			parsed.hour += 12;
  		} else if (period && period.toLowerCase()[0] == 'a' && parsed.hour == 12) {
  			parsed.hour = 0;
  		}
  
  
  		if (isNaN(parsed.hour) || parsed.hour < 0 || parsed.hour > 23) {
  			throw new TL.Error("invalid_hour_err", parsed.hour);
  		}
  
  		if (parts.length > 1) {
  			parsed.minute = parseInt(parts[1]);
  			if (isNaN(parsed.minute)) { 
  			    throw new TL.Error("invalid_minute_err", parsed.minute); 
  			}
  		}
  
  		if (parts.length > 2) {
  			var sec_parts = parts[2].split(/[\.,]/);
  			parts = sec_parts.concat(parts.slice(3)) // deal with various methods of specifying fractional seconds
  			if (parts.length > 2) { 
  			    throw new TL.Error("invalid_second_fractional_err");
  			}
  			parsed.second = parseInt(parts[0]);
  			if (isNaN(parsed.second)) { 
  			    throw new TL.Error("invalid_second_err");
  			}
  			if (parts.length == 2) {
  				var frac_secs = parseInt(parts[1]);
  				if (isNaN(frac_secs)) { 
  				    throw new TL.Error("invalid_fractional_err");
  				}
  				parsed.millisecond = 100 * frac_secs;
  			}
  		}
  
  		return parsed;
  	},
  
  	SCALE_DATE_CLASSES: {
  		human: TL.Date,
  		cosmological: TL.BigDate
  	}
  
  
  };
  
  
  /* **********************************************
       Begin TL.Draggable.js
  ********************************************** */
  
  /*	TL.Draggable
  	TL.Draggable allows you to add dragging capabilities to any element. Supports mobile devices too.
  	TODO Enable constraints
  ================================================== */
  
  TL.Draggable = TL.Class.extend({
  	
  	includes: TL.Events,
  	
  	_el: {},
  	
  	mousedrag: {
  		down:		"mousedown",
  		up:			"mouseup",
  		leave:		"mouseleave",
  		move:		"mousemove"
  	},
  	
  	touchdrag: {
  		down:		"touchstart",
  		up:			"touchend",
  		leave:		"mouseleave",
  		move:		"touchmove"
  	},
  
  	initialize: function (drag_elem, options, move_elem) {
  		
  		// DOM ELements 
  		this._el = {
  			drag: drag_elem,
  			move: drag_elem
  		};
  		
  		if (move_elem) {
  			this._el.move = move_elem;
  		}
  		
  		
  		//Options
  		this.options = {
  			enable:	{
  				x: true,
  				y: true
  			},
  			constraint: {
  				top: false,
  				bottom: false,
  				left: false,
  				right: false
  			},
  			momentum_multiplier: 	2000,
  			duration: 				1000,
  			ease: 					TL.Ease.easeInOutQuint
  		};
  		
  		
  		// Animation Object
  		this.animator = null;
  		
  		// Drag Event Type
  		this.dragevent = this.mousedrag;
  		
  		if (TL.Browser.touch) {
  			this.dragevent = this.touchdrag;
  		}
  		
  		// Draggable Data
  		this.data = {
  			sliding:		false,
  			direction: 		"none",
  			pagex: {
  				start:		0,
  				end:		0
  			},
  			pagey: {
  				start:		0,
  				end:		0
  			},
  			pos: {
  				start: {
  					x: 0,
  					y:0
  				},
  				end: {
  					x: 0,
  					y:0
  				}
  			},
  			new_pos: {
  				x: 0,
  				y: 0
  			},
  			new_pos_parent: {
  				x: 0,
  				y: 0
  			},
  			time: {
  				start:		0,
  				end:		0
  			},
  			touch:			false
  		};
  		
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  		
  		
  	},
  	
  	enable: function(e) {
  		
  		this.data.pos.start = 0; 
  		this._el.move.style.left = this.data.pos.start.x + "px";
  		this._el.move.style.top = this.data.pos.start.y + "px";
  		this._el.move.style.position = "absolute";
  	},
  	
  	disable: function() {
  		TL.DomEvent.removeListener(this._el.drag, this.dragevent.down, this._onDragStart, this);
  		TL.DomEvent.removeListener(this._el.drag, this.dragevent.up, this._onDragEnd, this);
  	},
  	
  	stopMomentum: function() {
  		if (this.animator) {
  			this.animator.stop();
  		}
  
  	},
  	
  	updateConstraint: function(c) {
  		this.options.constraint = c;
  		
  	},
  	
  	/*	Private Methods
  	================================================== */
  	_onDragStart: function(e) {
  		if (TL.Browser.touch) {
  			if (e.originalEvent) {
  				this.data.pagex.start = e.originalEvent.touches[0].screenX;
  				this.data.pagey.start = e.originalEvent.touches[0].screenY;
  			} else {
  				this.data.pagex.start = e.targetTouches[0].screenX;
  				this.data.pagey.start = e.targetTouches[0].screenY;
  			}
  		} else {
  			this.data.pagex.start = e.pageX;
  			this.data.pagey.start = e.pageY;
  		}
  		
  		// Center element to finger or mouse
  		if (this.options.enable.x) {
  			this._el.move.style.left = this.data.pagex.start - (this._el.move.offsetWidth / 2) + "px";
  		}
  		
  		if (this.options.enable.y) {
  			this._el.move.style.top = this.data.pagey.start - (this._el.move.offsetHeight / 2) + "px";
  		}
  		
  		this.data.pos.start = TL.Dom.getPosition(this._el.drag);
  		this.data.time.start = new Date().getTime();
  		
  		this.fire("dragstart", this.data);
  		TL.DomEvent.addListener(this._el.drag, this.dragevent.move, this._onDragMove, this);
  		TL.DomEvent.addListener(this._el.drag, this.dragevent.leave, this._onDragEnd, this);
  	},
  	
  	_onDragEnd: function(e) {
  		this.data.sliding = false;
  		TL.DomEvent.removeListener(this._el.drag, this.dragevent.move, this._onDragMove, this);
  		TL.DomEvent.removeListener(this._el.drag, this.dragevent.leave, this._onDragEnd, this);
  		this.fire("dragend", this.data);
  		
  		//  momentum
  		this._momentum();
  	},
  	
  	_onDragMove: function(e) {
  		e.preventDefault();
  		this.data.sliding = true;
  		
  		if (TL.Browser.touch) {
  			if (e.originalEvent) {
  				this.data.pagex.end = e.originalEvent.touches[0].screenX;
  				this.data.pagey.end = e.originalEvent.touches[0].screenY;
  			} else {
  				this.data.pagex.end = e.targetTouches[0].screenX;
  				this.data.pagey.end = e.targetTouches[0].screenY;
  			}
  
  		} else {
  			this.data.pagex.end = e.pageX;
  			this.data.pagey.end = e.pageY;
  		}
  		
  		this.data.pos.end = TL.Dom.getPosition(this._el.drag);
  		this.data.new_pos.x = -(this.data.pagex.start - this.data.pagex.end - this.data.pos.start.x);
  		this.data.new_pos.y = -(this.data.pagey.start - this.data.pagey.end - this.data.pos.start.y );
  		
  		if (this.options.enable.x) {
  			this._el.move.style.left = this.data.new_pos.x + "px";
  		}
  		
  		if (this.options.enable.y) {
  			this._el.move.style.top = this.data.new_pos.y + "px";
  		}
  		
  		this.fire("dragmove", this.data);
  	},
  	
  	_momentum: function() {
  		var pos_adjust = {
  				x: 0,
  				y: 0,
  				time: 0
  			},
  			pos_change = {
  				x: 0,
  				y: 0,
  				time: 0
  			},
  			swipe = false,
  			swipe_direction = "";
  		
  		
  		if (TL.Browser.touch) {
  			// Treat mobile multiplier differently
  			//this.options.momentum_multiplier = this.options.momentum_multiplier * 2;
  		}
  		
  		pos_adjust.time = (new Date().getTime() - this.data.time.start) * 10;
  		pos_change.time = (new Date().getTime() - this.data.time.start) * 10;
  		
  		pos_change.x = this.options.momentum_multiplier * (Math.abs(this.data.pagex.end) - Math.abs(this.data.pagex.start));
  		pos_change.y = this.options.momentum_multiplier * (Math.abs(this.data.pagey.end) - Math.abs(this.data.pagey.start));
  		
  		pos_adjust.x = Math.round(pos_change.x / pos_change.time);
  		pos_adjust.y = Math.round(pos_change.y / pos_change.time);
  		
  		this.data.new_pos.x = Math.min(this.data.pos.end.x + pos_adjust.x);
  		this.data.new_pos.y = Math.min(this.data.pos.end.y + pos_adjust.y);
  
  		
  		if (!this.options.enable.x) {
  			this.data.new_pos.x = this.data.pos.start.x;
  		} else if (this.data.new_pos.x < 0) {
  			this.data.new_pos.x = 0;
  		}
  		
  		if (!this.options.enable.y) {
  			this.data.new_pos.y = this.data.pos.start.y;
  		} else if (this.data.new_pos.y < 0) {
  			this.data.new_pos.y = 0;
  		}
  		
  		// Detect Swipe
  		if (pos_change.time < 3000) {
  			swipe = true;
  		}
  		
  		// Detect Direction
  		if (Math.abs(pos_change.x) > 10000) {
  			this.data.direction = "left";
  			if (pos_change.x > 0) {
  				this.data.direction = "right";
  			}
  		}
  		// Detect Swipe
  		if (Math.abs(pos_change.y) > 10000) {
  			this.data.direction = "up";
  			if (pos_change.y > 0) {
  				this.data.direction = "down";
  			}
  		}
  		this._animateMomentum();
  		if (swipe) {
  			this.fire("swipe_" + this.data.direction, this.data);
  		}
  		
  	},
  	
  	
  	_animateMomentum: function() {
  		var pos = {
  				x: this.data.new_pos.x,
  				y: this.data.new_pos.y
  			},
  			animate = {
  				duration: 	this.options.duration,
  				easing: 	TL.Ease.easeOutStrong
  			};
  		
  		if (this.options.enable.y) {
  			if (this.options.constraint.top || this.options.constraint.bottom) {
  				if (pos.y > this.options.constraint.bottom) {
  					pos.y = this.options.constraint.bottom;
  				} else if (pos.y < this.options.constraint.top) {
  					pos.y = this.options.constraint.top;
  				}
  			}
  			animate.top = Math.floor(pos.y) + "px";
  		}
  		
  		if (this.options.enable.x) {
  			if (this.options.constraint.left || this.options.constraint.right) {
  				if (pos.x > this.options.constraint.left) {
  					pos.x = this.options.constraint.left;
  				} else if (pos.x < this.options.constraint.right) {
  					pos.x = this.options.constraint.right;
  				}
  			}
  			animate.left = Math.floor(pos.x) + "px";
  		}
  		
  		this.animator = TL.Animate(this._el.move, animate);
  		
  		this.fire("momentum", this.data);
  	}
  });
  
  
  /* **********************************************
       Begin TL.Swipable.js
  ********************************************** */
  
  /*	TL.Swipable
  	TL.Draggable allows you to add dragging capabilities to any element. Supports mobile devices too.
  	TODO Enable constraints
  ================================================== */
  
  TL.Swipable = TL.Class.extend({
  	
  	includes: TL.Events,
  	
  	_el: {},
  	
  	mousedrag: {
  		down:		"mousedown",
  		up:			"mouseup",
  		leave:		"mouseleave",
  		move:		"mousemove"
  	},
  	
  	touchdrag: {
  		down:		"touchstart",
  		up:			"touchend",
  		leave:		"mouseleave",
  		move:		"touchmove"
  	},
  
  	initialize: function (drag_elem, move_elem, options) {
  		
  		// DOM ELements 
  		this._el = {
  			drag: drag_elem,
  			move: drag_elem
  		};
  		
  		if (move_elem) {
  			this._el.move = move_elem;
  		}
  		
  		
  		//Options
  		this.options = {
  			snap: false,
  			enable:	{
  				x: true,
  				y: true
  			},
  			constraint: {
  				top: false,
  				bottom: false,
  				left: 0,
  				right: false
  			},
  			momentum_multiplier: 	2000,
  			duration: 				1000,
  			ease: 					TL.Ease.easeInOutQuint
  		};
  		
  		
  		// Animation Object
  		this.animator = null;
  		
  		// Drag Event Type
  		this.dragevent = this.mousedrag;
  		
  		if (TL.Browser.touch) {
  			this.dragevent = this.touchdrag;
  		}
  		
  		// Draggable Data
  		this.data = {
  			sliding:		false,
  			direction: 		"none",
  			pagex: {
  				start:		0,
  				end:		0
  			},
  			pagey: {
  				start:		0,
  				end:		0
  			},
  			pos: {
  				start: {
  					x: 0,
  					y:0
  				},
  				end: {
  					x: 0,
  					y:0
  				}
  			},
  			new_pos: {
  				x: 0,
  				y: 0
  			},
  			new_pos_parent: {
  				x: 0,
  				y: 0
  			},
  			time: {
  				start:		0,
  				end:		0
  			},
  			touch:			false
  		};
  		
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  		
  		
  	},
  	
  	enable: function(e) {
  		TL.DomEvent.addListener(this._el.drag, this.dragevent.down, this._onDragStart, this);
  		TL.DomEvent.addListener(this._el.drag, this.dragevent.up, this._onDragEnd, this);
  		
  		this.data.pos.start = 0; //TL.Dom.getPosition(this._el.move);
  		this._el.move.style.left = this.data.pos.start.x + "px";
  		this._el.move.style.top = this.data.pos.start.y + "px";
  		this._el.move.style.position = "absolute";
  		//this._el.move.style.zIndex = "11";
  		//this._el.move.style.cursor = "move";
  	},
  	
  	disable: function() {
  		TL.DomEvent.removeListener(this._el.drag, this.dragevent.down, this._onDragStart, this);
  		TL.DomEvent.removeListener(this._el.drag, this.dragevent.up, this._onDragEnd, this);
  	},
  	
  	stopMomentum: function() {
  		if (this.animator) {
  			this.animator.stop();
  		}
  
  	},
  	
  	updateConstraint: function(c) {
  		this.options.constraint = c;
  		
  		// Temporary until issues are fixed
  		
  	},
  	
  	/*	Private Methods
  	================================================== */
  	_onDragStart: function(e) {
  		
  		if (this.animator) {
  			this.animator.stop();
  		}
  		
  		if (TL.Browser.touch) {
  			if (e.originalEvent) {
  				this.data.pagex.start = e.originalEvent.touches[0].screenX;
  				this.data.pagey.start = e.originalEvent.touches[0].screenY;
  			} else {
  				this.data.pagex.start = e.targetTouches[0].screenX;
  				this.data.pagey.start = e.targetTouches[0].screenY;
  			}
  		} else {
  			this.data.pagex.start = e.pageX;
  			this.data.pagey.start = e.pageY;
  		}
  		
  		// Center element to finger or mouse
  		if (this.options.enable.x) {
  			//this._el.move.style.left = this.data.pagex.start - (this._el.move.offsetWidth / 2) + "px";
  		}
  		
  		if (this.options.enable.y) {
  			//this._el.move.style.top = this.data.pagey.start - (this._el.move.offsetHeight / 2) + "px";
  		}
  		
  		this.data.pos.start = {x:this._el.move.offsetLeft, y:this._el.move.offsetTop};
  		
  		
  		this.data.time.start 			= new Date().getTime();
  		
  		this.fire("dragstart", this.data);
  		TL.DomEvent.addListener(this._el.drag, this.dragevent.move, this._onDragMove, this);
  		TL.DomEvent.addListener(this._el.drag, this.dragevent.leave, this._onDragEnd, this);
  	},
  	
  	_onDragEnd: function(e) {
  		this.data.sliding = false;
  		TL.DomEvent.removeListener(this._el.drag, this.dragevent.move, this._onDragMove, this);
  		TL.DomEvent.removeListener(this._el.drag, this.dragevent.leave, this._onDragEnd, this);
  		this.fire("dragend", this.data);
  		
  		//  momentum
  		this._momentum();
  	},
  	
  	_onDragMove: function(e) {
  		var change = {
  			x:0,
  			y:0
  		}
  		//e.preventDefault();
  		this.data.sliding = true;
  		
  		if (TL.Browser.touch) {
  			if (e.originalEvent) {
  				this.data.pagex.end = e.originalEvent.touches[0].screenX;
  				this.data.pagey.end = e.originalEvent.touches[0].screenY;
  			} else {
  				this.data.pagex.end = e.targetTouches[0].screenX;
  				this.data.pagey.end = e.targetTouches[0].screenY;
  			}
  
  		} else {
  			this.data.pagex.end = e.pageX;
  			this.data.pagey.end = e.pageY;
  		}
  		
  		change.x = this.data.pagex.start - this.data.pagex.end;
  		change.y = this.data.pagey.start - this.data.pagey.end;
  		
  		this.data.pos.end = {x:this._el.drag.offsetLeft, y:this._el.drag.offsetTop};
  		
  		this.data.new_pos.x = -(change.x - this.data.pos.start.x);
  		this.data.new_pos.y = -(change.y - this.data.pos.start.y );
  		
  		if (this.options.enable.x && ( Math.abs(change.x) > Math.abs(change.y) ) ) {
  			e.preventDefault();
  			this._el.move.style.left = this.data.new_pos.x + "px";
  		}
  		
  		if (this.options.enable.y && ( Math.abs(change.y) > Math.abs(change.y) ) ) {
  			e.preventDefault();
  			this._el.move.style.top = this.data.new_pos.y + "px";
  		}
  		
  		this.fire("dragmove", this.data);
  	},
  	
  	_momentum: function() {
  		var pos_adjust = {
  				x: 0,
  				y: 0,
  				time: 0
  			},
  			pos_change = {
  				x: 0,
  				y: 0,
  				time: 0
  			},
  			swipe_detect = {
  				x: false,
  				y: false
  			},
  			swipe = false,
  			swipe_direction = "";
  		
  		
  		this.data.direction = null;
  		
  		pos_adjust.time = (new Date().getTime() - this.data.time.start) * 10;
  		pos_change.time = (new Date().getTime() - this.data.time.start) * 10;
  		
  		pos_change.x = this.options.momentum_multiplier * (Math.abs(this.data.pagex.end) - Math.abs(this.data.pagex.start));
  		pos_change.y = this.options.momentum_multiplier * (Math.abs(this.data.pagey.end) - Math.abs(this.data.pagey.start));
  		
  		pos_adjust.x = Math.round(pos_change.x / pos_change.time);
  		pos_adjust.y = Math.round(pos_change.y / pos_change.time);
  		
  		this.data.new_pos.x = Math.min(this.data.new_pos.x + pos_adjust.x);
  		this.data.new_pos.y = Math.min(this.data.new_pos.y + pos_adjust.y);
  		
  		if (!this.options.enable.x) {
  			this.data.new_pos.x = this.data.pos.start.x;
  		} else if (this.options.constraint.left && this.data.new_pos.x > this.options.constraint.left) {
  			this.data.new_pos.x = this.options.constraint.left;
  		}
  		
  		if (!this.options.enable.y) {
  			this.data.new_pos.y = this.data.pos.start.y;
  		} else if (this.data.new_pos.y < 0) {
  			this.data.new_pos.y = 0;
  		}
  		
  		// Detect Swipe
  		if (pos_change.time < 2000) {
  			swipe = true;
  		}
  		
  		
  		if (this.options.enable.x && this.options.enable.y) {
  			if (Math.abs(pos_change.x) > Math.abs(pos_change.y)) {
  				swipe_detect.x = true;
  			} else {
  				swipe_detect.y = true;
  			}
  		} else if (this.options.enable.x) {
  			if (Math.abs(pos_change.x) > Math.abs(pos_change.y)) {
  				swipe_detect.x = true;
  			}
  		} else {
  			if (Math.abs(pos_change.y) > Math.abs(pos_change.x)) {
  				swipe_detect.y = true;
  			}
  		}
  		
  		// Detect Direction and long swipe
  		if (swipe_detect.x) {
  			
  			// Long Swipe
  			if (Math.abs(pos_change.x) > (this._el.drag.offsetWidth/2)) {
  				swipe = true;
  			}
  			
  			if (Math.abs(pos_change.x) > 10000) {
  				this.data.direction = "left";
  				if (pos_change.x > 0) {
  					this.data.direction = "right";
  				}
  			}
  		}
  		
  		if (swipe_detect.y) {
  			
  			// Long Swipe
  			if (Math.abs(pos_change.y) > (this._el.drag.offsetHeight/2)) {
  				swipe = true;
  			}
  			
  			if (Math.abs(pos_change.y) > 10000) {
  				this.data.direction = "up";
  				if (pos_change.y > 0) {
  					this.data.direction = "down";
  				}
  			}
  		}
  		
  		if (pos_change.time < 1000 ) {
  			
  		} else {
  			this._animateMomentum();
  		}
  		
  		if (swipe && this.data.direction) {
  			this.fire("swipe_" + this.data.direction, this.data);
  		} else if (this.data.direction) {
  			this.fire("swipe_nodirection", this.data);
  		} else if (this.options.snap) {
  			this.animator.stop();
  			
  			this.animator = TL.Animate(this._el.move, {
  				top: 		this.data.pos.start.y,
  				left: 		this.data.pos.start.x,
  				duration: 	this.options.duration,
  				easing: 	TL.Ease.easeOutStrong
  			});
  		}
  		
  	},
  	
  	
  	_animateMomentum: function() {
  		var pos = {
  				x: this.data.new_pos.x,
  				y: this.data.new_pos.y
  			},
  			animate = {
  				duration: 	this.options.duration,
  				easing: 	TL.Ease.easeOutStrong
  			};
  		
  		if (this.options.enable.y) {
  			if (this.options.constraint.top || this.options.constraint.bottom) {
  				if (pos.y > this.options.constraint.bottom) {
  					pos.y = this.options.constraint.bottom;
  				} else if (pos.y < this.options.constraint.top) {
  					pos.y = this.options.constraint.top;
  				}
  			}
  			animate.top = Math.floor(pos.y) + "px";
  		}
  		
  		if (this.options.enable.x) {
  			if (this.options.constraint.left && pos.x >= this.options.constraint.left) {
  				pos.x = this.options.constraint.left;
  			}
  			if (this.options.constraint.right && pos.x < this.options.constraint.right) {
  				pos.x = this.options.constraint.right;
  			}
  
  			animate.left = Math.floor(pos.x) + "px";
  		}
  		
  		this.animator = TL.Animate(this._el.move, animate);
  		
  		this.fire("momentum", this.data);
  	}
  });
  
  
  /* **********************************************
       Begin TL.MenuBar.js
  ********************************************** */
  
  /*	TL.MenuBar
  	Draggable component to control size
  ================================================== */
  
  TL.MenuBar = TL.Class.extend({
  
  	includes: [TL.Events, TL.DomMixins],
  
  	_el: {},
  
  	/*	Constructor
  	================================================== */
  	initialize: function(elem, parent_elem, options) {
  		// DOM ELEMENTS
  		this._el = {
  			parent: {},
  			container: {},
  			button_backtostart: {},
  			button_zoomin: {},
  			button_zoomout: {},
  			arrow: {},
  			line: {},
  			coverbar: {},
  			grip: {}
  		};
  
  		this.collapsed = false;
  
  		if (typeof elem === 'object') {
  			this._el.container = elem;
  		} else {
  			this._el.container = TL.Dom.get(elem);
  		}
  
  		if (parent_elem) {
  			this._el.parent = parent_elem;
  		}
  
  		//Options
  		this.options = {
  			width: 					600,
  			height: 				600,
  			duration: 				1000,
  			ease: 					TL.Ease.easeInOutQuint,
  			menubar_default_y: 		0
  		};
  
  		// Animation
  		this.animator = {};
  
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  
  		this._initLayout();
  		this._initEvents();
  	},
  
  	/*	Public
  	================================================== */
  	show: function(d) {
  
  		var duration = this.options.duration;
  		if (d) {
  			duration = d;
  		}
  		/*
  		this.animator = TL.Animate(this._el.container, {
  			top: 		this.options.menubar_default_y + "px",
  			duration: 	duration,
  			easing: 	TL.Ease.easeOutStrong
  		});
  		*/
  	},
  
  	hide: function(top) {
  		/*
  		this.animator = TL.Animate(this._el.container, {
  			top: 		top,
  			duration: 	this.options.duration,
  			easing: 	TL.Ease.easeOutStrong
  		});
  		*/
  	},
  
  	toogleZoomIn: function(show) {
  		if (show) {
        TL.DomUtil.removeClass(this._el.button_zoomin,'tl-menubar-button-inactive');
  		} else {
        TL.DomUtil.addClass(this._el.button_zoomin,'tl-menubar-button-inactive');
  		}
  	},
  
  	toogleZoomOut: function(show) {
  		if (show) {
        TL.DomUtil.removeClass(this._el.button_zoomout,'tl-menubar-button-inactive');
  		} else {
        TL.DomUtil.addClass(this._el.button_zoomout,'tl-menubar-button-inactive');
  		}
  	},
  
  	setSticky: function(y) {
  		this.options.menubar_default_y = y;
  	},
  
  	/*	Color
  	================================================== */
  	setColor: function(inverted) {
  		if (inverted) {
  			this._el.container.className = 'tl-menubar tl-menubar-inverted';
  		} else {
  			this._el.container.className = 'tl-menubar';
  		}
  	},
  
  	/*	Update Display
  	================================================== */
  	updateDisplay: function(w, h, a, l) {
  		this._updateDisplay(w, h, a, l);
  	},
  
  
  	/*	Events
  	================================================== */
  	_onButtonZoomIn: function(e) {
  		this.fire("zoom_in", e);
  	},
  
  	_onButtonZoomOut: function(e) {
  		this.fire("zoom_out", e);
  	},
  
  	_onButtonBackToStart: function(e) {
  		this.fire("back_to_start", e);
  	},
  
  
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  
  		// Create Layout
  		this._el.button_zoomin 							= TL.Dom.create('span', 'tl-menubar-button', this._el.container);
  		this._el.button_zoomout 						= TL.Dom.create('span', 'tl-menubar-button', this._el.container);
  		this._el.button_backtostart 					= TL.Dom.create('span', 'tl-menubar-button', this._el.container);
  
  		if (TL.Browser.mobile) {
  			this._el.container.setAttribute("ontouchstart"," ");
  		}
  
  		this._el.button_backtostart.innerHTML		= "<span class='tl-icon-goback'></span>";
  		this._el.button_zoomin.innerHTML			= "<span class='tl-icon-zoom-in'></span>";
  		this._el.button_zoomout.innerHTML			= "<span class='tl-icon-zoom-out'></span>";
  
  
  	},
  
  	_initEvents: function () {
  		TL.DomEvent.addListener(this._el.button_backtostart, 'click', this._onButtonBackToStart, this);
  		TL.DomEvent.addListener(this._el.button_zoomin, 'click', this._onButtonZoomIn, this);
  		TL.DomEvent.addListener(this._el.button_zoomout, 'click', this._onButtonZoomOut, this);
  	},
  
  	// Update Display
  	_updateDisplay: function(width, height, animate) {
  
  		if (width) {
  			this.options.width = width;
  		}
  		if (height) {
  			this.options.height = height;
  		}
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Message.js
  ********************************************** */
  
  /*	TL.Message
  	
  ================================================== */
   
  TL.Message = TL.Class.extend({
  	
  	includes: [TL.Events, TL.DomMixins, TL.I18NMixins],
  	
  	_el: {},
  	
  	/*	Constructor
  	================================================== */
  	initialize: function(data, options, add_to_container) {
  		// DOM ELEMENTS
  		this._el = {
  			parent: {},
  			container: {},
  			message_container: {},
  			loading_icon: {},
  			message: {}
  		};
  	
  		//Options
  		this.options = {
  			width: 					600,
  			height: 				600,
  			message_class: 			"tl-message",
  			message_icon_class: 	"tl-loading-icon"
  		};
  		
  		this._add_to_container = add_to_container || {}; // save ref
  		
  		// Merge Data and Options
  		TL.Util.mergeData(this.data, data);
  		TL.Util.mergeData(this.options, options);
  		
  		this._el.container = TL.Dom.create("div", this.options.message_class);
  		
  		if (add_to_container) {
  			add_to_container.appendChild(this._el.container);
  			this._el.parent = add_to_container;
  		}
  		
  		// Animation
  		this.animator = {};
  				
  		this._initLayout();
  		this._initEvents();
  	},
  	
  	/*	Public
  	================================================== */
  	updateMessage: function(t) {
  		this._updateMessage(t);
  	},
  	
  	
  	/*	Update Display
  	================================================== */
  	updateDisplay: function(w, h) {
  		this._updateDisplay(w, h);
  	},
  	
  	_updateMessage: function(t) {
  		if (!t) {
  			this._el.message.innerHTML = this._('loading');
  		} else {
  			this._el.message.innerHTML = t;
  		}
  		
  		// Re-add to DOM?
  		if(!this._el.parent.atrributes && this._add_to_container.attributes) {
  		    this._add_to_container.appendChild(this._el.container);
  		    this._el.parent = this._add_to_container;
  		}
  	},
  	
  
  	/*	Events
  	================================================== */
  
  	
  	_onMouseClick: function() {
  		this.fire("clicked", this.options);
  	},
  	
  	_onRemove: function() {
  	    this._el.parent = {};
  	},
  
  
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  		
  		// Create Layout
  		this._el.message_container = TL.Dom.create("div", "tl-message-container", this._el.container);
  		this._el.loading_icon = TL.Dom.create("div", this.options.message_icon_class, this._el.message_container);
  		this._el.message = TL.Dom.create("div", "tl-message-content", this._el.message_container);
  		
  		this._updateMessage();
  		
  	},
  	
  	_initEvents: function () {
  		TL.DomEvent.addListener(this._el.container, 'click', this._onMouseClick, this);
  		TL.DomEvent.addListener(this, 'removed', this._onRemove, this);
  	},
  	
  	// Update Display
  	_updateDisplay: function(width, height, animate) {
  		
  	}
  	
  });
  
  /* **********************************************
       Begin TL.MediaType.js
  ********************************************** */
  
  /*    TL.MediaType
      Determines the type of media the url string is.
      returns an object with .type and .id
      You can add new media types by adding a regex
      to match and the media class name to use to
      render the media
  
      The image_only parameter indicates that the
      call only wants an image-based media type
      that can be resolved to an image URL.
  
      TODO
      Allow array so a slideshow can be a mediatype
  ================================================== */
  TL.MediaType = function(m, image_only) {
      var media = {},
          media_types =     [
              {
                  type:         "youtube",
                  name:         "YouTube",
                  match_str:     "^(https?:)?\/*(www.)?youtube|youtu\.be",
                  cls:         TL.Media.YouTube
              },
              {
                  type:         "vimeo",
                  name:         "Vimeo",
                  match_str:     "^(https?:)?\/*(player.)?vimeo\.com",
                  cls:         TL.Media.Vimeo
              },
              {
                  type:         "dailymotion",
                  name:         "DailyMotion",
                  match_str:     "^(https?:)?\/*(www.)?dailymotion\.com",
                  cls:         TL.Media.DailyMotion
              },
              {
                  type:         "vine",
                  name:         "Vine",
                  match_str:     "^(https?:)?\/*(www.)?vine\.co",
                  cls:         TL.Media.Vine
              },
              {
                  type:         "soundcloud",
                  name:         "SoundCloud",
                  match_str:     "^(https?:)?\/*(player.)?soundcloud\.com",
                  cls:         TL.Media.SoundCloud
              },
              {
                  type:         "twitter",
                  name:         "Twitter",
                  match_str:     "^(https?:)?\/*(www.)?twitter\.com",
                  cls:         TL.Media.Twitter
              },
              {
                  type:         "twitterembed",
                  name:         "TwitterEmbed",
                  match_str:     "<blockquote class=['\"]twitter-tweet['\"]",
                  cls:         TL.Media.Twitter
              },
              {
                  type:         "googlemaps",
                  name:         "Google Map",
                  match_str:     /google.+?\/maps\/@([-\d.]+),([-\d.]+),((?:[-\d.]+[zmayht],?)*)|google.+?\/maps\/search\/([\w\W]+)\/@([-\d.]+),([-\d.]+),((?:[-\d.]+[zmayht],?)*)|google.+?\/maps\/place\/([\w\W]+)\/@([-\d.]+),([-\d.]+),((?:[-\d.]+[zmayht],?)*)|google.+?\/maps\/dir\/([\w\W]+)\/([\w\W]+)\/@([-\d.]+),([-\d.]+),((?:[-\d.]+[zmayht],?)*)/,
                  cls:         TL.Media.GoogleMap
              },
              {
                  type:         "googleplus",
                  name:         "Google+",
                  match_str:     "^(https?:)?\/*plus.google",
                  cls:         TL.Media.GooglePlus
              },
              {
                  type:         "flickr",
                  name:         "Flickr",
                  match_str:     "^(https?:)?\/*(www.)?flickr.com\/photos",
                  cls:         TL.Media.Flickr
              },
              {
                  type:         "flickr",
                  name:         "Flickr",
                  match_str:     "^(https?:\/\/)?flic.kr\/.*",
                  cls:         TL.Media.Flickr
              },
              {
                  type:         "instagram",
                  name:         "Instagram",
                  match_str:     /^(https?:)?\/*(www.)?(instagr.am|^(https?:)?\/*(www.)?instagram.com)\/p\//,
                  cls:         TL.Media.Instagram
              },
              {
                  type:         "profile",
                  name:         "Profile",
                  match_str:     /^(https?:)?\/*(www.)?instagr.am\/[a-zA-Z0-9]{2,}|^(https?:)?\/*(www.)?instagram.com\/[a-zA-Z0-9]{2,}/,
                  cls:         TL.Media.Profile
              },
              {
                  type:       "documentcloud",
                  name:       "Document Cloud",
                  match_str:  /documentcloud.org\//,
                  cls:        TL.Media.DocumentCloud
              },
              {
                  type:         "image",
                  name:         "Image",
                  match_str:     /(jpg|jpeg|png|gif|svg)(\?.*)?$/i,
                  cls:         TL.Media.Image
              },
              {
                  type:         "imgur",
                  name:         "Imgur",
                  match_str:     /^.*imgur.com\/.+$|<blockquote class=['\"]imgur-embed-pub['\"]/i,
                  cls:         TL.Media.Imgur
              },
              {
                  type:         "googledocs",
                  name:         "Google Doc",
                  match_str:     "^(https?:)?\/*[^.]*.google.com\/[^\/]*\/d\/[^\/]*\/[^\/]*\?usp=sharing|^(https?:)?\/*drive.google.com\/open\?id=[^\&]*\&authuser=0|^(https?:)?\/*drive.google.com\/open\?id=[^\&]*|^(https?:)?\/*[^.]*.googledrive.com\/host\/[^\/]*\/",
                  cls:         TL.Media.GoogleDoc
              },
              {
                  type:         "pdf",
                  name:         "PDF",
                  match_str:     /^.*\.pdf(\?.*)?(\#.*)?/,
                  cls:         TL.Media.PDF
              },
              {
                  type:         "wikipedia",
                  name:         "Wikipedia",
                  match_str:     "^(https?:)?\/*(www.)?wikipedia\.org|^(https?:)?\/*([a-z][a-z].)?wikipedia\.org",
                  cls:         TL.Media.Wikipedia
              },
              {
                  type:         "spotify",
                  name:         "spotify",
                  match_str:     "spotify",
                  cls:         TL.Media.Spotify
              },
              {
                  type:         "iframe",
                  name:         "iFrame",
                  match_str:     "iframe",
                  cls:         TL.Media.IFrame
              },
              {
                  type:         "storify",
                  name:         "Storify",
                  match_str:     "storify",
                  cls:         TL.Media.Storify
              },
              {
                  type:         "blockquote",
                  name:         "Quote",
                  match_str:     "blockquote",
                  cls:         TL.Media.Blockquote
              },
              // {
              //     type:         "website",
              //     name:         "Website",
              //     match_str:     "https?://",
              //     cls:         TL.Media.Website
              // },
              {
                  type:         "video",
                  name:         "Video",
                  match_str:     /(mp4)(\?.*)?$/i,
                  cls:         TL.Media.Video
              },
              {
                type:         "wistia",
                name:         "Wistia",
                match_str:     /https?:\/\/(.+)?(wistia\.com|wi\.st)\/.*/i,
                cls:         TL.Media.Wistia
              },
              {
                  type:         "audio",
                  name:         "Audio",
                  match_str:     /(mp3|wav|m4a)(\?.*)?$/i,
                  cls:         TL.Media.Audio
              },
              {
                  type:         "imageblank",
                  name:         "Imageblank",
                  match_str:     "",
                  cls:         TL.Media.Image
              }
          ];
  
      if(image_only) {
          if (m instanceof Array) {
              return false;
          }
          for (var i = 0; i < media_types.length; i++) {
              switch(media_types[i].type) {
                  case "flickr":
                  case "image":
                  case "instagram":
                      if (m.url.match(media_types[i].match_str)) {
                          media = media_types[i];
                          return media;
                      }
                      break;
                  default:
                      break;
              }
          }
  
      } else {
          for (var i = 0; i < media_types.length; i++) {
              if (m instanceof Array) {
                  return media = {
                      type:         "slider",
                      cls:         TL.Media.Slider
                  };
              } else if (m.url.match(media_types[i].match_str)) {
                  media         = media_types[i];
                  return media;
              }
          }
      }
      return false;
  }
  
  
  /* **********************************************
       Begin TL.Media.js
  ********************************************** */
  
  /*	TL.Media
  	Main media template for media assets.
  	Takes a data object and populates a dom object
  ================================================== */
  // TODO add link
  
  TL.Media = TL.Class.extend({
  
  	includes: [TL.Events, TL.I18NMixins],
  
  	_el: {},
  
  	/*	Constructor
  	================================================== */
  	initialize: function(data, options, add_to_container) {
  		// DOM ELEMENTS
  		this._el = {
  			container: {},
  			content_container: {},
  			content: {},
  			content_item: {},
  			content_link: {},
  			caption: null,
  			credit: null,
  			parent: {},
  			link: null
  		};
  
  		// Player (If Needed)
  		this.player = null;
  
  		// Timer (If Needed)
  		this.timer = null;
  		this.load_timer = null;
  
  		// Message
  		this.message = null;
  
  		// Media ID
  		this.media_id = null;
  
  		// State
  		this._state = {
  			loaded: false,
  			show_meta: false,
  			media_loaded: false
  		};
  
  		// Data
  		this.data = {
  			unique_id: 			null,
  			url: 				null,
  			credit:				null,
  			caption:			null,
  			credit_alternate: 	null,
  			caption_alternate: 	null,
  			link: 				null,
  			link_target: 		null
  		};
  
  		//Options
  		this.options = {
  			api_key_flickr: 		"f2cc870b4d233dd0a5bfe73fd0d64ef0",
  			api_key_googlemaps: "AIzaSyB9dW8e_iRrATFa8g24qB6BDBGdkrLDZYI",
  			api_key_embedly: 		"", // ae2da610d1454b66abdf2e6a4c44026d
  			credit_height: 			0,
  			caption_height: 		0,
  			background:         0   // is background media (for slide)
  		};
  
  		this.animator = {};
  
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  		TL.Util.mergeData(this.data, data);
  
          // Don't create DOM elements if this is background media
          if(!this.options.background) {
              this._el.container = TL.Dom.create("div", "tl-media");
  
              if (this.data.unique_id) {
                  this._el.container.id = this.data.unique_id;
              }
  
              this._initLayout();
  
              if (add_to_container) {
                  add_to_container.appendChild(this._el.container);
                  this._el.parent = add_to_container;
              }
          }
  	},
  
  	loadMedia: function() {
  		var self = this;
  
  		if (!this._state.loaded) {
  			try {
  				this.load_timer = setTimeout(function() {
  		            self.loadingMessage();
  					self._loadMedia();
  					// self._state.loaded = true; handled in onLoaded()
  					self._updateDisplay();
  				}, 1200);
  			} catch (e) {
  				trace("Error loading media for ", this._media);
  				trace(e);
  			}
  		}
  	},
  
    _updateMessage: function(msg) {
        if(this.message) {
            this.message.updateMessage(msg);
        }
    },
  
  	loadingMessage: function() {
  	    this._updateMessage(this._('loading') + " " + this.options.media_name);
  	},
  
  	errorMessage: function(msg) {
  		if (msg) {
  			msg = this._('error') + ": " + msg;
  		} else {
  			msg = this._('error');
  		}
  		this._updateMessage(msg);
  	},
  
  	updateMediaDisplay: function(layout) {
  		if (this._state.loaded && !this.options.background) {
  
  			if (TL.Browser.mobile) {
  				this._el.content_item.style.maxHeight = (this.options.height/2) + "px";
  			} else {
  				this._el.content_item.style.maxHeight = this.options.height - this.options.credit_height - this.options.caption_height - 30 + "px";
  			}
  
  			//this._el.content_item.style.maxWidth = this.options.width + "px";
  			this._el.container.style.maxWidth = this.options.width + "px";
  			// Fix for max-width issues in Firefox
  			if (TL.Browser.firefox) {
  				if (this._el.content_item.offsetWidth > this._el.content_item.offsetHeight) {
  					//this._el.content_item.style.width = "100%";
  				}
  			}
  
  			this._updateMediaDisplay(layout);
  
  			if (this._state.media_loaded) {
  				if (this._el.credit) {
  					this._el.credit.style.width		= this._el.content_item.offsetWidth + "px";
  				}
  				if (this._el.caption) {
  					this._el.caption.style.width		= this._el.content_item.offsetWidth + "px";
  				}
  			}
  
  		}
  	},
  
  	/*	Media Specific
  	================================================== */
      _loadMedia: function() {
          // All overrides must call this.onLoaded() to set state
          this.onLoaded();
      },
  
      _updateMediaDisplay: function(l) {
          //this._el.content_item.style.maxHeight = (this.options.height - this.options.credit_height - this.options.caption_height - 16) + "px";
          if(TL.Browser.firefox) {
              this._el.content_item.style.maxWidth = this.options.width + "px";
              this._el.content_item.style.width = "auto";
          }
      },
  
      _getMeta: function() {
  
      },
  
      _getImageURL: function(w, h) {
          // Image-based media types should return <img>-compatible src url
          return "";
      },
  
  	/*	Public
  	================================================== */
  	show: function() {
  
  	},
  
  	hide: function() {
  
  	},
  
  	addTo: function(container) {
  		container.appendChild(this._el.container);
  		this.onAdd();
  	},
  
  	removeFrom: function(container) {
  		container.removeChild(this._el.container);
  		this.onRemove();
  	},
  
    getImageURL: function(w, h) {
        return this._getImageURL(w, h);
    },
  
  	// Update Display
  	updateDisplay: function(w, h, l) {
  		this._updateDisplay(w, h, l);
  	},
  
  	stopMedia: function() {
  		this._stopMedia();
  	},
  
  	loadErrorDisplay: function(message) {
  		try {
  			this._el.content.removeChild(this._el.content_item);
  		} catch(e) {
  			// if this._el.content_item isn't a child of this._el then just keep truckin
  		}
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-loaderror", this._el.content);
  		this._el.content_item.innerHTML = "<div class='tl-icon-" + this.options.media_type + "'></div><p>" + message + "</p>";
  
  		// After Loaded
  		this.onLoaded(true);
  	},
  
  	/*	Events
  	================================================== */
  	onLoaded: function(error) {
  		this._state.loaded = true;
  		this.fire("loaded", this.data);
  		if (this.message) {
  			this.message.hide();
  		}
  		if (!(error || this.options.background)) {
  			this.showMeta();
  		}
  		this.updateDisplay();
  	},
  
  	onMediaLoaded: function(e) {
  		this._state.media_loaded = true;
  		this.fire("media_loaded", this.data);
  		if (this._el.credit) {
  			this._el.credit.style.width		= this._el.content_item.offsetWidth + "px";
  		}
  		if (this._el.caption) {
  			this._el.caption.style.width		= this._el.content_item.offsetWidth + "px";
  		}
  	},
  
  	showMeta: function(credit, caption) {
  		this._state.show_meta = true;
  		// Credit
  		if (this.data.credit && this.data.credit != "") {
  			this._el.credit					= TL.Dom.create("div", "tl-credit", this._el.content_container);
  			this._el.credit.innerHTML		= this.options.autolink == true ? TL.Util.linkify(this.data.credit) : this.data.credit;
  			this.options.credit_height 		= this._el.credit.offsetHeight;
  		}
  
  		// Caption
  		if (this.data.caption && this.data.caption != "") {
  			this._el.caption				= TL.Dom.create("div", "tl-caption", this._el.content_container);
  			this._el.caption.innerHTML		= this.options.autolink == true ? TL.Util.linkify(this.data.caption) : this.data.caption;
  			this.options.caption_height 	= this._el.caption.offsetHeight;
  		}
  
  		if (!this.data.caption || !this.data.credit) {
  			this.getMeta();
  		}
  
  	},
  
  	getMeta: function() {
  		this._getMeta();
  	},
  
  	updateMeta: function() {
  		if (!this.data.credit && this.data.credit_alternate) {
  			this._el.credit					= TL.Dom.create("div", "tl-credit", this._el.content_container);
  			this._el.credit.innerHTML		= this.data.credit_alternate;
  			this.options.credit_height 		= this._el.credit.offsetHeight;
  		}
  
  		if (!this.data.caption && this.data.caption_alternate) {
  			this._el.caption				= TL.Dom.create("div", "tl-caption", this._el.content_container);
  			this._el.caption.innerHTML		= this.data.caption_alternate;
  			this.options.caption_height 	= this._el.caption.offsetHeight;
  		}
  
  		this.updateDisplay();
  	},
  
  	onAdd: function() {
  		this.fire("added", this.data);
  	},
  
  	onRemove: function() {
  		this.fire("removed", this.data);
  	},
  
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  
  		// Message
  		this.message = new TL.Message({}, this.options);
  		this.message.addTo(this._el.container);
  
  		// Create Layout
  		this._el.content_container = TL.Dom.create("div", "tl-media-content-container", this._el.container);
  
  		// Link
  		if (this.data.link && this.data.link != "") {
  
  			this._el.link = TL.Dom.create("a", "tl-media-link", this._el.content_container);
  			this._el.link.href = this.data.link;
  			if (this.data.link_target && this.data.link_target != "") {
  				this._el.link.target = this.data.link_target;
  			} else {
  				this._el.link.target = "_blank";
  			}
  
  			this._el.content = TL.Dom.create("div", "tl-media-content", this._el.link);
  
  		} else {
  			this._el.content = TL.Dom.create("div", "tl-media-content", this._el.content_container);
  		}
  
  
  	},
  
  	// Update Display
  	_updateDisplay: function(w, h, l) {
  		if (w) {
  			this.options.width = w;
  
  		}
  		//this._el.container.style.width = this.options.width + "px";
  		if (h) {
  			this.options.height = h;
  		}
  
  		if (l) {
  			this.options.layout = l;
  		}
  
  		if (this._el.credit) {
  			this.options.credit_height 		= this._el.credit.offsetHeight;
  		}
  		if (this._el.caption) {
  			this.options.caption_height 	= this._el.caption.offsetHeight + 5;
  		}
  
  		this.updateMediaDisplay(this.options.layout);
  
  	},
  
  	_stopMedia: function() {
  
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Blockquote.js
  ********************************************** */
  
  /*	TL.Media.Blockquote
  ================================================== */
  
  TL.Media.Blockquote = TL.Media.extend({
  	
  	includes: [TL.Events],
  	
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {		
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-blockquote", this._el.content);
  		this._el.content_container.className = "tl-media-content-container tl-media-content-container-text";
  		
  		// Get Media ID
  		this.media_id = this.data.url;
  		
  		// API Call
  		this._el.content_item.innerHTML = this.media_id;
  		
  		// After Loaded
  		this.onLoaded();
  	},
  	
  	updateMediaDisplay: function() {
  		
  	},
  	
  	_updateMediaDisplay: function() {
  		
  	}
  
  	
  });
  
  
  /* **********************************************
       Begin TL.Media.DailyMotion.js
  ********************************************** */
  
  /*	TL.Media.DailyMotion
  ================================================== */
  
  TL.Media.DailyMotion = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe tl-media-dailymotion", this._el.content);
  
  		// Get Media ID
  		if (this.data.url.match("video")) {
  			this.media_id = this.data.url.split("video\/")[1].split(/[?&]/)[0];
  		} else {
  			this.media_id = this.data.url.split("embed\/")[1].split(/[?&]/)[0];
  		}
  
  		// API URL
  		api_url = "https://www.dailymotion.com/embed/video/" + this.media_id+"?api=postMessage";
  
  		// API Call
  		this._el.content_item.innerHTML = "<iframe autostart='false' frameborder='0' width='100%' height='100%' src='" + api_url + "'></iframe>"
  
  		// After Loaded
  		this.onLoaded();
  	},
  
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		this._el.content_item.style.height = TL.Util.ratio.r16_9({w:this._el.content_item.offsetWidth}) + "px";
  	},
  
  	_stopMedia: function() {
  		this._el.content_item.querySelector("iframe").contentWindow.postMessage('{"command":"pause","parameters":[]}', "*");
  
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.DocumentCloud.js
  ********************************************** */
  
  /*	TL.Media.DocumentCloud
  ================================================== */
  
  TL.Media.DocumentCloud = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var self = this;
  
  		// Create Dom elements
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-documentcloud tl-media-shadow", this._el.content);
  		this._el.content_item.id = TL.Util.unique_ID(7)
  
  		// Check url
  		if(this.data.url.match(/\.html$/)) {
  		    this.data.url = this._transformURL(this.data.url);
  		} else if(!(this.data.url.match(/.(json|js)$/))) {
  		    trace("DOCUMENT CLOUD IN URL BUT INVALID SUFFIX");
  		}
  
  		// Load viewer API
          TL.Load.js([
  					'https://assets.documentcloud.org/viewer/loader.js',
  					'https://assets.documentcloud.org/viewer/viewer.js'],
              function() {	
  	            self.createMedia();
  			}
  		);
  	},
  
  	// Viewer API needs js, not html
  	_transformURL: function(url) {
          return url.replace(/(.*)\.html$/, '$1.js')
  	},
  
  	// Update Media Display
  	_updateMediaDisplay: function() {
          this._el.content_item.style.height = this.options.height + "px";
  		//this._el.content_item.style.width = this.options.width + "px";
  	},
  
  	createMedia: function() {
  		// DocumentCloud API call
  		DV.load(this.data.url, {
  		    container: '#'+this._el.content_item.id,
  		    showSidebar: false
  		});
  		this.onLoaded();
  	},
  
  
  
  	/*	Events
  	================================================== */
  
  
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Flickr.js
  ********************************************** */
  
  /*	TL.Media.Flickr
  
  ================================================== */
  
  TL.Media.Flickr = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  
  		try {
  		    // Get Media ID
  		    this.establishMediaID();
  
              // API URL
              api_url = "https://api.flickr.com/services/rest/?method=flickr.photos.getSizes&api_key=" + this.options.api_key_flickr + "&photo_id=" + this.media_id + "&format=json&jsoncallback=?";
  
              // API Call
              TL.getJSON(api_url, function(d) {
                  if (d.stat == "ok") {
                      self.sizes = d.sizes.size; // store sizes info
  
                      if(!self.options.background) {
                          self.createMedia();
                      }
  
                      self.onLoaded();
                  } else {
                      self.loadErrorDisplay(self._("flickr_notfound_err"));
                  }
              });
  		} catch(e) {
  		    self.loadErrorDisplay(self._(e.message_key));
  		}
  	},
  
  	establishMediaID: function() {
  		if (this.data.url.match(/flic.kr\/.+/i)) {
  			var encoded = this.data.url.split('/').slice(-1)[0];
  			this.media_id = TL.Util.base58.decode(encoded);
  		} else {
  			var marker = 'flickr.com/photos/';
  			var idx = this.data.url.indexOf(marker);
  			if (idx == -1) { throw new TL.Error("flickr_invalidurl_err"); }
  			var pos = idx + marker.length;
  			this.media_id = this.data.url.substr(pos).split("/")[1];
  		}
  	},
  
  	createMedia: function() {
  	    var self = this;
  
  		// Link
  		this._el.content_link = TL.Dom.create("a", "", this._el.content);
  		this._el.content_link.href = this.data.url;
  		this._el.content_link.target = "_blank";
  
  		// Photo
  		this._el.content_item = TL.Dom.create("img", "tl-media-item tl-media-image tl-media-flickr tl-media-shadow", this._el.content_link);
  
  		if (this.data.alt) {
  			this._el.content_item.alt = this.data.alt;
  		} else if (this.data.caption) {
  			this._el.content_item.alt = TL.Util.unhtmlify(this.data.caption);
  		}
  
  		if (this.data.title) {
  			this._el.content_item.title = this.data.title;
  		} else if (this.data.caption) {
  			this._el.content_item.title = TL.Util.unhtmlify(this.data.caption);
  		}
  
  		// Media Loaded Event
  		this._el.content_item.addEventListener('load', function(e) {
  			self.onMediaLoaded();
  		});
  
  		// Set Image Source
  		this._el.content_item.src = this.getImageURL(this.options.width, this.options.height);
  	},
  
      getImageURL: function(w, h) {
          var best_size 	= this.size_label(h),
              source = this.sizes[this.sizes.length - 2].source;
  
  		for(var i = 0; i < this.sizes.length; i++) {
  			if (this.sizes[i].label == best_size) {
  				source = this.sizes[i].source;
  			}
  		}
  
  		return source;
      },
  
  	_getMeta: function() {
  		var self = this,
  		api_url;
  
  		// API URL
  		api_url = "https://api.flickr.com/services/rest/?method=flickr.photos.getInfo&api_key=" + this.options.api_key_flickr + "&photo_id=" + this.media_id + "&format=json&jsoncallback=?";
  
  		// API Call
  		TL.getJSON(api_url, function(d) {
  			self.data.credit_alternate = "<a href='" + self.data.url + "' target='_blank'>" + d.photo.owner.realname + "</a>";
  			self.data.caption_alternate = d.photo.title._content + " " + d.photo.description._content;
  			self.updateMeta();
  		});
  	},
  
  	size_label: function(s) {
  		var _size = "";
  
  		if (s <= 75) {
  			if (s <= 0) {
  				_size = "Large";
  			} else {
  				_size = "Thumbnail";
  			}
  		} else if (s <= 180) {
  			_size = "Small";
  		} else if (s <= 240) {
  			_size = "Small 320";
  		} else if (s <= 375) {
  			_size = "Medium";
  		} else if (s <= 480) {
  			_size = "Medium 640";
  		} else if (s <= 600) {
  			_size = "Large";
  		} else {
  			_size = "Large";
  		}
  
  		return _size;
  	}
  
  
  
  });
  
  
  /* **********************************************
       Begin TL.Media.GoogleDoc.js
  ********************************************** */
  
  /*	TL.Media.GoogleDoc
  
  ================================================== */
  
  TL.Media.GoogleDoc = TL.Media.extend({
  	
  	includes: [TL.Events],
  	
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var url,
  			self = this;
  		
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe", this._el.content);
  		
  		// Get Media ID
  		if (this.data.url.match("open\?id\=")) {
  			this.media_id = this.data.url.split("open\?id\=")[1];
  			if (this.data.url.match("\&authuser\=0")) {
  				url = this.media_id.match("\&authuser\=0")[0];
  			};
  		} else if (this.data.url.match(/file\/d\/([^/]*)\/?/)) {
  			var doc_id = this.data.url.match(/file\/d\/([^/]*)\/?/)[1];
  			url = 'https://drive.google.com/file/d/' + doc_id + '/preview'
  		} else {
  			url = this.data.url;
  		}
  		
  		// this URL makes something suitable for an img src but what if it's not an image?
  		// api_url = "http://www.googledrive.com/host/" + this.media_id + "/";
  		
  		this._el.content_item.innerHTML	=	"<iframe class='doc' frameborder='0' width='100%' height='100%' src='" + url + "'></iframe>";
  		
  		// After Loaded
  		this.onLoaded();
  	},
  	
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		this._el.content_item.style.height = this.options.height + "px";
  	}
  
  	
  });
  
  
  /* **********************************************
       Begin TL.Media.GooglePlus.js
  ********************************************** */
  
  /*	TL.Media.GooglePlus
  ================================================== */
  
  TL.Media.GooglePlus = TL.Media.extend({
  	
  	includes: [TL.Events],
  	
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  		
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-googleplus", this._el.content);
  		
  		// Get Media ID
  		this.media_id = this.data.url;
  		
  		// API URL
  		api_url = this.media_id;
  		
  		// API Call
  		this._el.content_item.innerHTML = "<iframe frameborder='0' width='100%' height='100%' src='" + api_url + "'></iframe>"		
  		
  		// After Loaded
  		this.onLoaded();
  	},
  	
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		this._el.content_item.style.height = this.options.height + "px";
  	}
  
  	
  });
  
  
  /* **********************************************
       Begin TL.Media.IFrame.js
  ********************************************** */
  
  /*	TL.Media.IFrame
  ================================================== */
  
  TL.Media.IFrame = TL.Media.extend({
  	
  	includes: [TL.Events],
  	
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  				
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe", this._el.content);
  		
  		// Get Media ID
  		this.media_id = this.data.url;
  		
  		// API URL
  		api_url = this.media_id;
  		
  		// API Call
  		this._el.content_item.innerHTML = api_url;
  		
  		// After Loaded
  		this.onLoaded();
  	},
  	
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		this._el.content_item.style.height = this.options.height + "px";
  	}
  	
  });
  
  
  /* **********************************************
       Begin TL.Media.Image.js
  ********************************************** */
  
  /*	TL.Media.Image
  	Produces image assets.
  	Takes a data object and populates a dom object
  ================================================== */
  
  TL.Media.Image = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		// Loading Message
  		this.loadingMessage();
  
          // Create media?
          if(!this.options.background) {
              this.createMedia();
          }
  
          // After loaded
  		this.onLoaded();
  	},
  
      createMedia: function() {
          var self = this,
              image_class = "tl-media-item tl-media-image tl-media-shadow";
  
  		if (this.data.url.match(/.png(\?.*)?$/) || this.data.url.match(/.svg(\?.*)?$/)) {
  			image_class = "tl-media-item tl-media-image"
  		}
  
   		// Link
  		if (this.data.link) {
  			this._el.content_link 				= TL.Dom.create("a", "", this._el.content);
  			this._el.content_link.href 			= this.data.link;
  			this._el.content_link.target 		= "_blank";
  			this._el.content_item				= TL.Dom.create("img", image_class, this._el.content_link);
  		} else {
  			this._el.content_item				= TL.Dom.create("img", image_class, this._el.content);
  		}
  
  		if (this.data.alt) {
  			this._el.content_item.alt = this.data.alt;
  		} else if (this.data.caption) {
  			this._el.content_item.alt = TL.Util.unhtmlify(this.data.caption);
  		}
  
  		if (this.data.title) {
  			this._el.content_item.title = this.data.title;
  		} else if (this.data.caption) {
  			this._el.content_item.title = TL.Util.unhtmlify(this.data.caption);
  		}
  
  		// Media Loaded Event
  		this._el.content_item.addEventListener('load', function(e) {
  			self.onMediaLoaded();
  		});
  
  		this._el.content_item.src			= this.getImageURL();
      },
  
      getImageURL: function(w, h) {
          return TL.Util.transformImageURL(this.data.url);
      },
  
  	_updateMediaDisplay: function(layout) {
  		if(TL.Browser.firefox) {
  			//this._el.content_item.style.maxWidth = (this.options.width/2) - 40 + "px";
  			this._el.content_item.style.width = "auto";
  		}
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Imgur.js
  ********************************************** */
  
  /*	TL.Media.Flickr
  
  ================================================== */
  
  TL.Media.Imgur = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		try {
  			var self = this;
  
  			if (this.data.url.match("<blockquote class=['\"]imgur-embed-pub['\"]")){
  				var found = this.data.url.match(/(imgur\.com)\/(\w+)/);
  				this.media_id = found[2];
  				this.data.url = "http://imgur.com/gallery/" + this.media_id;
  			}
  			else if (this.data.url){
  				this.media_id = this.data.url.split('/').slice(-1)[0];
  			}
  
  	        TL.Load.js([
  						'https://s.imgur.com/min/embed.js'], 
  					function(){
  						self.createMedia();
  					}
  			);
  
  		} catch(e) {
  		    this.loadErrorDisplay(this._("imgur_invalidurl_err"));
  		}
  	},
  
  	createMedia: function() {
  	    var self = this;
  		var api_url = "https://api.imgur.com/oembed.json?url=" + this.data.url;
  
  		// Content div
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-image tl-media-imgur",
  																								this._el.content);
  
  		// API Call
  
            TL.ajax({
            	type: 'GET',
              url: api_url,
              dataType: 'json',
              success: function(data){
              try {
                  self._el.content_item.innerHTML	= data.html;
              	setInterval(function(){
              		if(document.querySelector("blockquote.imgur-embed-pub") == null){
              			clearInterval();
              		}
              		else{
              			imgurEmbed.createIframe();
              			document.getElementById("imageElement").removeAttribute("style");
              			document.getElementById("image").removeAttribute("style");
              		}
              	}, 2000);
              } catch(e) {
              }
              },
              error: function(xhr, errorType, error) {
                tc = new TL.TimelineConfig();
                if (errorType == 'parsererror') {
                  var error = new TL.Error("invalid_url_err");
                } else {
                  var error = new TL.Error("unknown_read_err", errorType);
                }
                self.loadErrorDisplay(self._("imgur_invalidurl_err"));
                tc.logError(error);
              }
            });
  
           this.onLoaded();
  
  	},
  
  
  
  	_updateMediaDisplay: function() {
  		//this.el.content_item = document.getElementById(this._el.content_item.id);
  		this._el.content_item.style.width = this.options.width + "px";
  		this._el.content_item.style.height = TL.Util.ratio.r16_9({w:this.options.width}) + "px";
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Instagram.js
  ********************************************** */
  
  /*	TL.Media.Instagram
  
  ================================================== */
  
  TL.Media.Instagram = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		// Get Media ID
  		this.media_id = this.data.url.split("\/p\/")[1].split("/")[0];
  
  		if(!this.options.background) {
  		    this.createMedia();
  		}
  
  		// After Loaded
  		this.onLoaded();
  	},
  
      createMedia: function() {
          var self = this;
  
  		// Link
  		this._el.content_link 				= TL.Dom.create("a", "", this._el.content);
  		this._el.content_link.href 			= this.data.url;
  		this._el.content_link.target 		= "_blank";
  
  		// Photo
  		this._el.content_item				= TL.Dom.create("img", "tl-media-item tl-media-image tl-media-instagram tl-media-shadow", this._el.content_link);
  
  		if (this.data.alt) {
  			this._el.content_item.alt = this.data.alt;
  		} else if (this.data.caption) {
  			this._el.content_item.alt = TL.Util.unhtmlify(this.data.caption);
  		}
  
  		if (this.data.title) {
  			this._el.content_item.title = this.data.title;
  		} else if (this.data.caption) {
  			this._el.content_item.title = TL.Util.unhtmlify(this.data.caption);
  		}
  
  		// Media Loaded Event
  		this._el.content_item.addEventListener('load', function(e) {
  			self.onMediaLoaded();
  		});
  
  	    this._el.content_item.src = this.getImageURL(this._el.content.offsetWidth);
      },
  
      getImageURL: function(w, h) {
          return "https://instagram.com/p/" + this.media_id + "/media/?size=" + this.sizes(w);
      },
  
  	_getMeta: function() {
  		var self = this,
  		    api_url;
  
  		// API URL
  		api_url = "https://api.instagram.com/oembed?url=https://instagr.am/p/" + this.media_id + "&callback=?";
  
  		// API Call
  		TL.getJSON(api_url, function(d) {
  			self.data.credit_alternate = "<a href='" + d.author_url + "' target='_blank'>" + d.author_name + "</a>";
  			self.data.caption_alternate = d.title;
  			self.updateMeta();
  		});
  	},
  
  	sizes: function(s) {
  		var _size = "";
  		if (s <= 150) {
  			_size = "t";
  		} else if (s <= 306) {
  			_size = "m";
  		} else {
  			_size = "l";
  		}
  
  		return _size;
  	}
  
  
  
  });
  
  
  /* **********************************************
       Begin TL.Media.GoogleMap.js
  ********************************************** */
  
  /*  TL.Media.Map
  ================================================== */
  
  TL.Media.GoogleMap = TL.Media.extend({
  	includes: [TL.Events],
  
  	/*  Load the media
  	================================================== */
  	_loadMedia: function() {
  
  		// Create Dom element
  		this._el.content_item   = TL.Dom.create("div", "tl-media-item tl-media-map tl-media-shadow", this._el.content);
  
  		// Get Media ID
  		this.media_id = this.data.url;
  
  		// API Call
  		this.mapframe = TL.Dom.create("iframe", "", this._el.content_item);
  		window.stash = this;
  		this.mapframe.width       = "100%";
  		this.mapframe.height      = "100%";
  		this.mapframe.frameBorder = "0";
  		this.mapframe.src         = this.makeGoogleMapsEmbedURL(this.media_id, this.options.api_key_googlemaps);
  		
  		
  		// After Loaded
  		this.onLoaded();
  	},
  
  	_updateMediaDisplay: function() {
  		if (this._state.loaded) {
  			var dimensions = TL.Util.ratio.square({w:this._el.content_item.offsetWidth});
  			this._el.content_item.style.height = dimensions.h + "px";
  		}
  	},
  	
  	makeGoogleMapsEmbedURL: function(url,api_key) {
  		// Test with https://docs.google.com/spreadsheets/d/1zCpvtRdftlR5fBPppmy_-SkGIo7RMwoPUiGFZDAXbTc/edit
  		var Streetview = false;
  
  		function determineMapMode(url){
  			function parseDisplayMode(display_mode, param_string) {
  				// Set the zoom param
  				if (display_mode.slice(-1) == "z") {
  					param_string["zoom"] = display_mode;
  					// Set the maptype to something other than "roadmap"
  				} else if (display_mode.slice(-1) == "m") {
  					// TODO: make this somehow interpret the correct zoom level
  					// until then fake it by using Google's default zoom level
  					param_string["zoom"] = 14;
  					param_string["maptype"] = "satellite";
  					// Set all the fun streetview params
  				} else if (display_mode.slice(-1) == "t") {
  					Streetview = true;
  					// streetview uses "location" instead of "center"
  					// "place" mode doesn't have the center param, so we may need to grab that now
  					if (mapmode == "place") {
  						var center = url.match(regexes["place"])[3] + "," + url.match(regexes["place"])[4];
  					} else {
  						var center = param_string["center"];
  						delete param_string["center"];
  					}
  					// Clear out all the other params -- this is so hacky
  					param_string = {};
  					param_string["location"] = center;
  					streetview_params = display_mode.split(",");
  					for (param in param_defs["streetview"]) {
  						var i = parseInt(param) + 1;
  						if (param_defs["streetview"][param] == "pitch" && streetview_params[i] == "90t"){
  							// Although 90deg is the horizontal default in the URL, 0 is horizontal default for embed URL. WHY??
  							// https://developers.google.com/maps/documentation/javascript/streetview
  							param_string[param_defs["streetview"][param]] = 0;
  						} else {
  							param_string[param_defs["streetview"][param]] = streetview_params[i].slice(0,-1);
  						}
  					}
  
  				}
  				return param_string;
  			}
  			function determineMapModeURL(mapmode, match) {
  				var param_string = {};
  				var url_root = match[1], display_mode = match[match.length - 1];
  				for (param in param_defs[mapmode]) {
  					// skip first 2 matches, because they reflect the URL and not params
  					var i = parseInt(param)+2;
  					if (param_defs[mapmode][param] == "center") {
  						param_string[param_defs[mapmode][param]] = match[i] + "," + match[++i];
  					} else {
  						param_string[param_defs[mapmode][param]] = match[i];
  					}
  				}
  
  				param_string = parseDisplayMode(display_mode, param_string);
  				param_string["key"] = api_key;
  				if (Streetview == true) {
  					mapmode = "streetview";
  				} else {
  				}
  				return (url_root + "/embed/v1/" + mapmode + TL.Util.getParamString(param_string));
  			}
  
  
  			mapmode = "view";
  			if (url.match(regexes["place"])) {
  				mapmode = "place";
  			} else if (url.match(regexes["directions"])) {
  				mapmode = "directions";
  			} else if (url.match(regexes["search"])) {
  				mapmode = "search";
  			}
  			return determineMapModeURL(mapmode, url.match(regexes[mapmode]));
  
  		}
  
  		// These must be in the order they appear in the original URL
  		// "key" param not included since it's not in the URL structure
  		// Streetview "location" param not included since it's captured as "center"
  		// Place "center" param ...um...
  		var param_defs = {
  			"view": ["center"],
  			"place": ["q", "center"],
  			"directions": ["origin", "destination", "center"],
  			"search": ["q", "center"],
  			"streetview": ["fov", "heading", "pitch"]
  		};
  		// Set up regex parts to make updating these easier if Google changes them
  		var root_url_regex = /(https:\/\/.+google.+?\/maps)/;
  		var coords_regex = /@([-\d.]+),([-\d.]+)/;
  		var address_regex = /([\w\W]+)/;
  
  		// Data doesn't seem to get used for anything
  		var data_regex = /data=[\S]*/;
  
  		// Capture the parameters that determine what map tiles to use
  		// In roadmap view, mode URLs include zoom paramater (e.g. "14z")
  		// In satellite (or "earth") view, URLs include a distance parameter (e.g. "84511m")
  		// In streetview, URLs include paramaters like "3a,75y,49.76h,90t" -- see http://stackoverflow.com/a/22988073
  		var display_mode_regex = /,((?:[-\d.]+[zmayht],?)*)/;
  
  		var regexes = {
  			view: new RegExp(root_url_regex.source + "/" + coords_regex.source + display_mode_regex.source),
  			place: new RegExp(root_url_regex.source + "/place/" + address_regex.source + "/" + coords_regex.source + display_mode_regex.source),
  			directions: new RegExp(root_url_regex.source + "/dir/" + address_regex.source + "/" + address_regex.source + "/" + coords_regex.source + display_mode_regex.source),
  			search: new RegExp(root_url_regex.source + "/search/" + address_regex.source + "/" + coords_regex.source + display_mode_regex.source)
  		};
  		return determineMapMode(url);
  	}
     
  });
  
  
  /* **********************************************
       Begin TL.Media.PDF.js
  ********************************************** */
  
  /*	TL.Media.PDF
   * Chrome and Firefox on both OSes and Safari all support PDFs as iframe src.
   * This prompts for a download on IE10/11. We should investigate using
   * https://mozilla.github.io/pdf.js/ to support showing PDFs on IE.
  ================================================== */
  
  TL.Media.PDF = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var url = TL.Util.transformImageURL(this.data.url),
  			self = this;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe", this._el.content);
  		var markup = "";
  		// not assigning media_id attribute. Seems like a holdover which is no longer used.
  		if (TL.Browser.ie || TL.Browser.edge || url.match(/dl.dropboxusercontent.com/)) {
  			markup = "<iframe class='doc' frameborder='0' width='100%' height='100%' src='//docs.google.com/viewer?url=" + url + "&amp;embedded=true'></iframe>";
  		} else {
  			markup = "<iframe class='doc' frameborder='0' width='100%' height='100%' src='" + url + "'></iframe>"
  		}
  		this._el.content_item.innerHTML	= markup
  		this.onLoaded();
  	},
  
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		this._el.content_item.style.height = this.options.height + "px";
  	}
  
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Profile.js
  ********************************************** */
  
  /*	TL.Media.Profile
  
  ================================================== */
  
  TL.Media.Profile = TL.Media.extend({
  	
  	includes: [TL.Events],
  	
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		
  		this._el.content_item				= TL.Dom.create("img", "tl-media-item tl-media-image tl-media-profile tl-media-shadow", this._el.content);
  		this._el.content_item.src			= this.data.url;
  		
  		this.onLoaded();
  	},
  	
  	_updateMediaDisplay: function(layout) {
  		
  		
  		if(TL.Browser.firefox) {
  			this._el.content_item.style.maxWidth = (this.options.width/2) - 40 + "px";
  		}
  	}
  	
  });
  
  /* **********************************************
       Begin TL.Media.Slider.js
  ********************************************** */
  
  /*	TL.Media.SLider
  	Produces a Slider
  	Takes a data object and populates a dom object
  	TODO
  	Placeholder
  ================================================== */
  
  TL.Media.Slider = TL.Media.extend({
  	
  	includes: [TL.Events],
  	
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		
  		this._el.content_item				= TL.Dom.create("img", "tl-media-item tl-media-image", this._el.content);
  		this._el.content_item.src			= this.data.url;
  		
  		this.onLoaded();
  	}
  	
  });
  
  /* **********************************************
       Begin TL.Media.SoundCloud.js
  ********************************************** */
  
  /*	TL.Media.SoundCloud
  ================================================== */
  
  var soundCoudCreated = false;
  
  TL.Media.SoundCloud = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe tl-media-soundcloud tl-media-shadow", this._el.content);
  
  		// Get Media ID
  		this.media_id = this.data.url;
  
  		// API URL
  		api_url = "https://soundcloud.com/oembed?url=" + this.media_id + "&format=js&callback=?"
  
  		// API Call
  		TL.getJSON(api_url, function(d) {
  			TL.Load.js("https://w.soundcloud.com/player/api.js", function() {//load soundcloud api for pausing.
  				self.createMedia(d);
  			});
  		});
  
  	},
  
  	createMedia: function(d) {
  		this._el.content_item.innerHTML = d.html;
  
  		this.soundCloudCreated = true;
  
  		self.widget = SC.Widget(this._el.content_item.querySelector("iframe"));//create widget for api use
  
  		// After Loaded
  		this.onLoaded();
  	},
  
  	_stopMedia: function() {
  		if (this.soundCloudCreated)
  		{
  			self.widget.pause();
  		}
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Spotify.js
  ********************************************** */
  
  /*	TL.Media.Spotify
  ================================================== */
  
  TL.Media.Spotify = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe tl-media-spotify", this._el.content);
  
  		// Get Media ID
  		if (this.data.url.match(/^spotify:track/) || this.data.url.match(/^spotify:album/) || this.data.url.match(/^spotify:user:.+:playlist:/)) {
  			this.media_id = this.data.url;
  		}
  
  		if (this.data.url.match(/spotify\.com\/track\/(.+)/)) {
  			this.media_id = "spotify:track:" + this.data.url.match(/spotify\.com\/track\/(.+)/)[1];
  		} else if (this.data.url.match(/spotify\.com\/album\/(.+)/)) {
  			this.media_id = "spotify:album:" + this.data.url.match(/spotify\.com\/album\/(.+)/)[1];
  		} else if (this.data.url.match(/spotify\.com\/user\/(.+?)\/playlist\/(.+)/)) {
  			var user = this.data.url.match(/spotify\.com\/user\/(.+?)\/playlist\/(.+)/)[1];
  			var playlist = this.data.url.match(/spotify\.com\/user\/(.+?)\/playlist\/(.+)/)[2];
  			this.media_id = "spotify:user:" + user + ":playlist:" + playlist;
  		} else if (this.data.url.match(/spotify\.com\/artist\/(.+)/)) {
  			var artist = this.data.url.match(/spotify\.com\/artist\/(.+)/)[1];
  			this.media_id = "spotify:artist:" + artist;
  		}
  
  
  		if (this.media_id) {
  			// API URL
  			api_url = "https://embed.spotify.com/?uri=" + this.media_id + "&theme=white&view=coverart";
  
  			this.player = TL.Dom.create("iframe", "tl-media-shadow", this._el.content_item);
  			this.player.width 		= "100%";
  			this.player.height 		= "100%";
  			this.player.frameBorder = "0";
  			this.player.src 		= api_url;
  
  			// After Loaded
  			this.onLoaded();
  
  		} else {
  				this.loadErrorDisplay(this._('spotify_invalid_url'));
  		}
  	},
  
  	// Update Media Display
  
  	_updateMediaDisplay: function(l) {
  		var _height = this.options.height,
  			_player_height = 0,
  			_player_width = 0;
  
  		if (TL.Browser.mobile) {
  			_height = (this.options.height/2);
  		} else {
  			_height = this.options.height - this.options.credit_height - this.options.caption_height - 30;
  		}
  
  		this._el.content_item.style.maxHeight = "none";
  		trace(_height);
  		trace(this.options.width)
  		if (_height > this.options.width) {
  			trace("height is greater")
  			_player_height = this.options.width + 80 + "px";
  			_player_width = this.options.width + "px";
  		} else {
  			trace("width is greater")
  			trace(this.options.width)
  			_player_height = _height + "px";
  			_player_width = _height - 80 + "px";
  		}
  
  
  		this.player.style.width = _player_width;
  		this.player.style.height = _player_height;
  
  		if (this._el.credit) {
  			this._el.credit.style.width		= _player_width;
  		}
  		if (this._el.caption) {
  			this._el.caption.style.width		= _player_width;
  		}
  	},
  
  
  	_stopMedia: function() {
  		// Need spotify stop code
  
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Storify.js
  ********************************************** */
  
  /*	TL.Media.Storify
  ================================================== */
  
  TL.Media.Storify = TL.Media.extend({
  	
  	includes: [TL.Events],
  	
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var content;
  				
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe tl-media-storify", this._el.content);
  		
  		// Get Media ID
  		this.media_id = this.data.url;
  		
  		// Content
  		content =	"<iframe frameborder='0' width='100%' height='100%' src='" + this.media_id + "/embed'></iframe>";
  		content +=	"<script src='" + this.media_id + ".js'></script>";
  		
  		// API Call
  		this._el.content_item.innerHTML = content;
  		
  		// After Loaded
  		this.onLoaded();
  	},
  	
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		this._el.content_item.style.height = this.options.height + "px";
  	}
  	
  	
  });
  
  
  /* **********************************************
       Begin TL.Media.Text.js
  ********************************************** */
  
  TL.Media.Text = TL.Class.extend({
  	
  	includes: [TL.Events],
  	
  	// DOM ELEMENTS
  	_el: {
  		container: {},
  		content_container: {},
  		content: {},
  		headline: {},
  		date: {}
  	},
  	
  	// Data
  	data: {
  		unique_id: 			"",
  		headline: 			"headline",
  		text: 				"text"
  	},
  	
  	// Options
  	options: {
  		title: 			false
  	},
  	
  	/*	Constructor
  	================================================== */
  	initialize: function(data, options, add_to_container) {
  		
  		TL.Util.setData(this, data);
  		
  		// Merge Options
  		TL.Util.mergeData(this.options, options);
  		
  		this._el.container = TL.Dom.create("div", "tl-text");
  		this._el.container.id = this.data.unique_id;
  		
  		this._initLayout();
  		
  		if (add_to_container) {
  			add_to_container.appendChild(this._el.container);
  		};
  		
  	},
  	
  	/*	Adding, Hiding, Showing etc
  	================================================== */
  	show: function() {
  		
  	},
  	
  	hide: function() {
  		
  	},
  	
  	addTo: function(container) {
  		container.appendChild(this._el.container);
  		//this.onAdd();
  	},
  	
  	removeFrom: function(container) {
  		container.removeChild(this._el.container);
  	},
  	
  	headlineHeight: function() {
  		return this._el.headline.offsetHeight + 40;
  	},
  	
  	addDateText: function(str) {
  		this._el.date.innerHTML = str;
  	},
  	
  	/*	Events
  	================================================== */
  	onLoaded: function() {
  		this.fire("loaded", this.data);
  	},
  	
  	onAdd: function() {
  		this.fire("added", this.data);
  	},
  
  	onRemove: function() {
  		this.fire("removed", this.data);
  	},
  	
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  		
  		// Create Layout
  		this._el.content_container			= TL.Dom.create("div", "tl-text-content-container", this._el.container);
  		
  		// Date
  		this._el.date 				= TL.Dom.create("h3", "tl-headline-date", this._el.content_container);
  		
  		// Headline
  		if (this.data.headline != "") {
  			var headline_class = "tl-headline";
  			if (this.options.title) {
  				headline_class = "tl-headline tl-headline-title";
  			}
  			this._el.headline				= TL.Dom.create("h2", headline_class, this._el.content_container);
  			this._el.headline.innerHTML		= this.data.headline;
  		}
  
  		// Text
  		if (this.data.text != "") {
  			var text_content = "";
  
  			text_content += TL.Util.htmlify(this.options.autolink == true ? TL.Util.linkify(this.data.text) : this.data.text);
  			trace(this.data.text);
  			this._el.content				= TL.Dom.create("div", "tl-text-content", this._el.content_container);
  			this._el.content.innerHTML		= text_content;
  			trace(text_content);
  			trace(this._el.content)
  		}
  
  		// Fire event that the slide is loaded
  		this.onLoaded();
  
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Twitter.js
  ********************************************** */
  
  /*	TL.Media.Twitter
  	Produces Twitter Display
  ================================================== */
  
  TL.Media.Twitter = TL.Media.extend({
  	
  	includes: [TL.Events],
      
  
  	
  	/*	Load the media
  	================================================== */
  
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  					
  		// Create Dom element
  		this._el.content_item = TL.Dom.create("div", "tl-media-twitter", this._el.content);
  		this._el.content_container.className = "tl-media-content-container tl-media-content-container-text";
          		
  		// Get Media ID
          if(this.data.url.match("^(https?:)?\/*(www.)?twitter\.com"))
          {
  		if (this.data.url.match("status\/")) {
  			this.media_id = this.data.url.split("status\/")[1];
  		} else if (this.data.url.match("statuses\/")) {
  			this.media_id = this.data.url.split("statuses\/")[1];
  		} else {
  			this.media_id = "";
  		}
          }
          
          else if(this.data.url.match("<blockquote class=['\"]twitter-tweet['\"]")) {
  		
          var found = this.data.url.match(/(status|statuses)\/(\d+)/);
  		if (found && found.length > 2) {
  		    this.media_id = found[2];
  		} else {
  		    self.loadErrorDisplay(self._("twitterembed_invalidurl_err"));
  		    return;
  		}
      }
          
  		// API URL
  		api_url = "https://api.twitter.com/1/statuses/oembed.json?id=" + this.media_id + "&omit_script=true&include_entities=true&callback=?";
  		
  		// API Call
  		TL.ajax({
  			type: 'GET',
  			url: api_url,
  			dataType: 'json', //json data type
  			success: function(d){
  				self.createMedia(d);
  			},
  			error:function(xhr, type){
  				var error_text = "";
  				error_text += self._("twitter_load_err") + "<br/>" + self.media_id + "<br/>" + type;
  				self.loadErrorDisplay(error_text);
  			}
  		});
  		 
  	},
  	
  	createMedia: function(d) {
          trace("create_media")	
  		var tweet				= "",
  			tweet_text			= "",
  			tweetuser			= "",
  			tweet_status_temp 	= "",
  			tweet_status_url 	= "",
  			tweet_status_date 	= "",
              self = this;
  			
  		//	TWEET CONTENT
  		tweet_text 			= d.html.split("<\/p>\&mdash;")[0] + "</p></blockquote>";
  		tweetuser			= d.author_url.split("twitter.com\/")[1];
  		tweet_status_temp 	= d.html.split("<\/p>\&mdash;")[1].split("<a href=\"")[1];
  		tweet_status_url 	= tweet_status_temp.split("\"\>")[0];
  		tweet_status_date 	= tweet_status_temp.split("\"\>")[1].split("<\/a>")[0];
  		
  		// Open links in new window
  		tweet_text = tweet_text.replace(/<a href/ig, '<a target="_blank" href');
          
          if (tweet_text.includes("pic.twitter.com")) {
              
              TL.Load.js('https://platform.twitter.com/widgets.js', function() {
                  twttr.widgets.createTweet(self.media_id, self._el.content_item,
                  {
                      conversation : 'none',    // or all
                      linkColor    : '#cc0000', // default is blue
                      theme        : 'light'    // or dark
                  })
              });
              
              this.onLoaded();
              
          } else {
  
              // 	TWEET CONTENT
              tweet += tweet_text;
  
              //	TWEET AUTHOR
              tweet += "<div class='vcard'>";
              tweet += "<a href='" + tweet_status_url + "' class='twitter-date' target='_blank'>" + tweet_status_date + "</a>";
              tweet += "<img src='" + "' class='tl-media-item tl-media-image' target='_blank'>" + "</a>";
              tweet += "<div class='author'>";
              tweet += "<a class='screen-name url' href='" + d.author_url + "' target='_blank'>";
              tweet += "<span class='avatar'></span>";
              tweet += "<span class='fn'>" + d.author_name + " <span class='tl-icon-twitter'></span></span>";
              tweet += "<span class='nickname'>@" + tweetuser + "<span class='thumbnail-inline'></span></span>";
              tweet += "</a>";
              tweet += "</div>";
              tweet += "</div>";
  
  
              // Add to DOM
              this._el.content_item.innerHTML	= tweet;
  
              // After Loaded
              this.onLoaded();
          }
      },
  	
      
  	updateMediaDisplay: function() {
  		
  	},
  	
  	_updateMediaDisplay: function() {
  		
  	},
      
  	
  	
  	
  });
  
  
  /* **********************************************
       Begin TL.Media.TwitterEmbed.js
  ********************************************** */
  
  /*	TL.Media.TwitterEmbed
  	Produces Twitter Display
  ================================================== */
  
          var mediaID;
  
  TL.Media.TwitterEmbed = TL.Media.extend({
  	includes: [TL.Events],
      
  
  	
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  					
  		// Create Dom element
  		this._el.content_item = TL.Dom.create("div", "tl-media-twitter", this._el.content);
  		this._el.content_container.className = "tl-media-content-container tl-media-content-container-text";
  		
  		// Get Media ID
  		var found = this.data.url.match(/(status|statuses)\/(\d+)/);
  		if (found && found.length > 2) {
  		    this.media_id = found[2];
  		} else {
  		    self.loadErrorDisplay(self._("twitterembed_invalidurl_err"));
  		    return;
  		}
  
  		// API URL
  		api_url = "https://api.twitter.com/1/statuses/oembed.json?id=" + this.media_id + "&omit_script=true&include_entities=true&callback=?";
          
          window.twttr = (function(d, s, id) {
              var js, fjs = d.getElementsByTagName(s)[0],
              t = window.twttr || {};
              if (d.getElementById(id)) return t;
              js = d.createElement(s);
              js.id = id;
              js.src = "https://platform.twitter.com/widgets.js";
              fjs.parentNode.insertBefore(js, fjs);
  
              t._e = [];
              t.ready = function(f) {
              t._e.push(f);
              };
  
              return t;
          }(document, "script", "twitter-wjs"));
          
          mediaID = this.media_id;
  		
  		// API Call
  		TL.ajax({
  			type: 'GET',
  			url: api_url,
  			dataType: 'json', //json data type
  			success: function(d){
  				self.createMedia(d);
  			},
  			error:function(xhr, type){
  				var error_text = "";
  				error_text += self._("twitter_load_err") + "<br/>" + self.media_id + "<br/>" + type;
  				self.loadErrorDisplay(error_text);
  			}
  		});
  		 
  	},
  	
  	createMedia: function(d) {	
  		trace("create_media")	
  		var tweet				= "",
  			tweet_text			= "",
  			tweetuser			= "",
  			tweet_status_temp 	= "",
  			tweet_status_url 	= "",
  			tweet_status_date 	= "";
  			
  		//	TWEET CONTENT
  		tweet_text 			= d.html.split("<\/p>\&mdash;")[0] + "</p></blockquote>";
          console.log(tweet_text);
  		tweetuser			= d.author_url.split("twitter.com\/")[1];
  		tweet_status_temp 	= d.html.split("<\/p>\&mdash;")[1].split("<a href=\"")[1];
  		tweet_status_url 	= tweet_status_temp.split("\"\>")[0];
  		tweet_status_date 	= tweet_status_temp.split("\"\>")[1].split("<\/a>")[0];
  		
  		// Open links in new window
  		tweet_text = tweet_text.replace(/<a href/ig, '<a target="_blank" href');
          
          if (tweet_text.includes("pic.twitter.com")) {
              twttr.ready(
                  function(evt) {
                      tweet = document.getElementsByClassName("tl-media-twitter")[0];
                      var id = String(mediaID);
                      twttr.widgets.createTweet(id, tweet,
                          {
                              conversation : 'none',    // or all
                              linkColor    : '#cc0000', // default is blue
                              theme        : 'light'    // or dark
                          })
                      .then(function (evt) {
                          this.onLoaded();
                      });
                  }
              );
              this._el.content_item.innerHTML	= tweet;
              this.onLoaded();
          } else{
              // 	TWEET CONTENT
              tweet += tweet_text;
  
              //	TWEET AUTHOR
              tweet += "<div class='vcard'>";
              tweet += "<a href='" + tweet_status_url + "' class='twitter-date' target='_blank'>" + tweet_status_date + "</a>";
              tweet += "<div class='author'>";
              tweet += "<a class='screen-name url' href='" + d.author_url + "' target='_blank'>";
              tweet += "<span class='avatar'></span>";
              tweet += "<span class='fn'>" + d.author_name + " <span class='tl-icon-twitter'></span></span>";
              tweet += "<span class='nickname'>@" + tweetuser + "<span class='thumbnail-inline'></span></span>";
              tweet += "</a>";
              tweet += "</div>";
              tweet += "</div>";
  
  
              // Add to DOM
              this._el.content_item.innerHTML	= tweet;
  
              // After Loaded
              this.onLoaded();
          }
  			
  	},
  	
  	updateMediaDisplay: function() {
  		
  	},
  	
  	_updateMediaDisplay: function() {
  		
  	}
  	
  	
  	
  });
  
  /* **********************************************
       Begin TL.Media.Vimeo.js
  ********************************************** */
  
  /*	TL.Media.Vimeo
  ================================================== */
  
  TL.Media.Vimeo = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe tl-media-vimeo tl-media-shadow", this._el.content);
  
  		// Get Media ID
  		this.media_id = this.data.url.split(/video\/|\/\/vimeo\.com\//)[1].split(/[?&]/)[0];
  
  		// API URL
  		api_url = "https://player.vimeo.com/video/" + this.media_id + "?api=1&title=0&amp;byline=0&amp;portrait=0&amp;color=ffffff";
  
  		this.player = TL.Dom.create("iframe", "", this._el.content_item);
  
  		// Media Loaded Event
  		this.player.addEventListener('load', function(e) {
  			self.onMediaLoaded();
  		});
  
  		this.player.width 		= "100%";
  		this.player.height 		= "100%";
  		this.player.frameBorder = "0";
  		this.player.src 		= api_url;
  
  		this.player.setAttribute('allowfullscreen', '');
  		this.player.setAttribute('webkitallowfullscreen', '');
  		this.player.setAttribute('mozallowfullscreen', '');
  
  		// After Loaded
  		this.onLoaded();
  	},
  
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		this._el.content_item.style.height = TL.Util.ratio.r16_9({w:this._el.content_item.offsetWidth}) + "px";
  	},
  
  	_stopMedia: function() {
  
  		try {
  			this.player.contentWindow.postMessage(JSON.stringify({method: "pause"}), "https://player.vimeo.com");
  		}
  		catch(err) {
  			trace(err);
  		}
  	}
  });
  
  
  /* **********************************************
       Begin TL.Media.Vine.js
  ********************************************** */
  
  /*	TL.Media.Vine
  
  ================================================== */
  
  TL.Media.Vine = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe tl-media-vine tl-media-shadow", this._el.content);
  
  		// Get Media ID
  		this.media_id = this.data.url.split("vine.co/v/")[1];
  
  		// API URL
  		api_url = "https://vine.co/v/" + this.media_id + "/embed/simple";
  
  		// API Call
  		this._el.content_item.innerHTML = "<iframe frameborder='0' width='100%' height='100%' src='" + api_url + "'></iframe><script async src='https://platform.vine.co/static/scripts/embed.js' charset='utf-8'></script>"		
  
  		// After Loaded
  		this.onLoaded();
  	},
  
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		var size = TL.Util.ratio.square({w:this._el.content_item.offsetWidth , h:this.options.height});
  		this._el.content_item.style.height = size.h + "px";
  	},
  
  	_stopMedia: function() {
  		this._el.content_item.querySelector("iframe").contentWindow.postMessage('pause', '*');
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Website.js
  ********************************************** */
  
  /*	TL.Media.Website
  	Uses Embedly
  	http://embed.ly/docs/api/extract/endpoints/1/extract
  ================================================== */
  
  TL.Media.Website = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var self = this;
  
  		// Get Media ID
  		this.media_id = this.data.url.replace(/.*?:\/\//g, "");
  
  		if (this.options.api_key_embedly) {
  			// API URL
  			api_url = "https://api.embed.ly/1/extract?key=" + this.options.api_key_embedly + "&url=" + this.media_id + "&callback=?";
  
  			// API Call
  			TL.getJSON(api_url, function(d) {
  				self.createMedia(d);
  			});
  		} else {
  			this.createCardContent();
  		}
  	},
  
  	createCardContent: function() {
  		(function(w, d){
  			var id='embedly-platform', n = 'script';
  			if (!d.getElementById(id)){
  			 w.embedly = w.embedly || function() {(w.embedly.q = w.embedly.q || []).push(arguments);};
  			 var e = d.createElement(n); e.id = id; e.async=1;
  			 e.src = ('https:' === document.location.protocol ? 'https' : 'http') + '://cdn.embedly.com/widgets/platform.js';
  			 var s = d.getElementsByTagName(n)[0];
  			 s.parentNode.insertBefore(e, s);
  			}
  		})(window, document);
  
  		var content = "<a href=\"" + this.data.url + "\" class=\"embedly-card\">" + this.data.url + "</a>";
  		this._setContent(content);
  
  	},
  	createMedia: function(d) { // this costs API credits...
  		var content = "";
  
  
  		content		+=	"<h4><a href='" + this.data.url + "' target='_blank'>" + d.title + "</a></h4>";
  		if (d.images) {
  			if (d.images[0]) {
  				trace(d.images[0].url);
  				content		+=	"<img src='" + d.images[0].url + "' />";
  			}
  		}
  		if (d.favicon_url) {
  			content		+=	"<img class='tl-media-website-icon' src='" + d.favicon_url + "' />";
  		}
  		content		+=	"<span class='tl-media-website-description'>" + d.provider_name + "</span><br/>";
  		content		+=	"<p>" + d.description + "</p>";
  
  		this._setContent(content);
  	},
  
  	_setContent: function(content) {
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-website", this._el.content);
  		this._el.content_container.className = "tl-media-content-container tl-media-content-container-text";
  		this._el.content_item.innerHTML = content;
  
  		// After Loaded
  		this.onLoaded();
  
  	},
  
  	updateMediaDisplay: function() {
  
  	},
  
  	_updateMediaDisplay: function() {
  
  	}
  
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Wikipedia.js
  ********************************************** */
  
  /*	TL.Media.Wikipedia
  ================================================== */
  
  TL.Media.Wikipedia = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			api_language,
  			self = this;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-wikipedia", this._el.content);
  		this._el.content_container.className = "tl-media-content-container tl-media-content-container-text";
  
  		// Get Media ID
  		this.media_id	 = this.data.url.split("wiki\/")[1].split("#")[0].replace("_", " ");
  		this.media_id	 = this.media_id.replace(" ", "%20");
  		api_language	 = this.data.url.split("//")[1].split(".wikipedia")[0];
  
  		// API URL
  		api_url = "https://" + api_language + ".wikipedia.org/w/api.php?action=query&prop=extracts|pageimages&redirects=&titles=" + this.media_id + "&exintro=1&format=json&callback=?";
  
  		// API Call
  		TL.ajax({
  			type: 'GET',
  			url: api_url,
  			dataType: 'json', //json data type
  
  			success: function(d){
  				self.createMedia(d);
  			},
  			error:function(xhr, type){
  				var error_text = "";
  				error_text += self._("wikipedia_load_err") + "<br/>" + self.media_id + "<br/>" + type;
  				self.loadErrorDisplay(error_text);
  			}
  		});
  
  	},
  
  	createMedia: function(d) {
  		var wiki = "";
  
  		if (d.query) {
  			var content = "",
  				wiki = {
  					entry: {},
  					title: "",
  					text: "",
  					extract: "",
  					paragraphs: 1,
  					page_image: "",
  					text_array: []
  				};
  
  			wiki.entry		 = TL.Util.getObjectAttributeByIndex(d.query.pages, 0);
  			wiki.extract	 = wiki.entry.extract;
  			wiki.title		 = wiki.entry.title;
  			wiki.page_image	 = wiki.entry.thumbnail;
  
  			if (wiki.extract.match("<p>")) {
  				wiki.text_array = wiki.extract.split("<p>");
  			} else {
  				wiki.text_array.push(wiki.extract);
  			}
  
  			for(var i = 0; i < wiki.text_array.length; i++) {
  				if (i+1 <= wiki.paragraphs && i+1 < wiki.text_array.length) {
  					wiki.text	+= "<p>" + wiki.text_array[i+1];
  				}
  			}
  
  
  			content		+=	"<span class='tl-icon-wikipedia'></span>";
  			content		+=	"<div class='tl-wikipedia-title'><h4><a href='" + this.data.url + "' target='_blank'>" + wiki.title + "</a></h4>";
  			content		+=	"<span class='tl-wikipedia-source'>" + this._('wikipedia') + "</span></div>";
  
  			if (wiki.page_image) {
  				//content 	+= 	"<img class='tl-wikipedia-pageimage' src='" + wiki.page_image.source +"'>";
  			}
  
  			content		+=	wiki.text;
  
  			if (wiki.extract.match("REDIRECT")) {
  
  			} else {
  				// Add to DOM
  				this._el.content_item.innerHTML	= content;
  				// After Loaded
  				this.onLoaded();
  			}
  
  
  		}
  
  	},
  
  	updateMediaDisplay: function() {
  
  	},
  
  	_updateMediaDisplay: function() {
  
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Wistia.js
  ********************************************** */
  
  /*	TL.Media.Wistia
  ================================================== */
  
  TL.Media.Wistia = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var api_url,
  			self = this;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-iframe tl-media-wistia tl-media-shadow", this._el.content);
  
  		// Get Media ID
  		this.media_id = this.data.url.split(/https?:\/\/(.+)?(wistia\.com|wi\.st)\/medias\/(.*)/)[3];
  
  		// API URL
  		api_url = "https://fast.wistia.com/embed/iframe/" + this.media_id + "?version=v1&controlsVisibleOnLoad=true&playerColor=aae3d8";
  
      this.player = TL.Dom.create("iframe", "", this._el.content_item);
  
      // Media Loaded Event
  		this.player.addEventListener('load', function(e) {
  			self.onMediaLoaded();
  		});
  
  		this.player.width 		= "100%";
  		this.player.height 		= "100%";
  		this.player.frameBorder = "0";
  		this.player.src 		= api_url;
  
  		this.player.setAttribute('allowfullscreen', '');
  		this.player.setAttribute('webkitallowfullscreen', '');
  		this.player.setAttribute('mozallowfullscreen', '');
  
  		// After Loaded
  		this.onLoaded();
  	},
  
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		this._el.content_item.style.height = TL.Util.ratio.r16_9({w:this._el.content_item.offsetWidth}) + "px";
  	},
  
  	_stopMedia: function() {
  		try {
  			this.player.contentWindow.postMessage(JSON.stringify({method: "pause"}), "https://player.vimeo.com");
  		}
  		catch(err) {
  			trace(err);
  		}
  	}
  });
  
  
  /* **********************************************
       Begin TL.Media.YouTube.js
  ********************************************** */
  
  /*	TL.Media.YouTube
  ================================================== */
  
  TL.Media.YouTube = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		var self = this,
  			url_vars;
  
  		this.youtube_loaded = false;
  
  		// Create Dom element
  		this._el.content_item	= TL.Dom.create("div", "tl-media-item tl-media-youtube tl-media-shadow", this._el.content);
  		this._el.content_item.id = TL.Util.unique_ID(7)
  
  		// URL Vars
  		url_vars = TL.Util.getUrlVars(this.data.url);
  
  		// Get Media ID
  		this.media_id = {};
  
  		if (this.data.url.match('v=')) {
  			this.media_id.id	= url_vars["v"];
  		} else if (this.data.url.match('\/embed\/')) {
  			this.media_id.id	= this.data.url.split("embed\/")[1].split(/[?&]/)[0];
  		} else if (this.data.url.match(/v\/|v=|youtu\.be\//)){
  			this.media_id.id	= this.data.url.split(/v\/|v=|youtu\.be\//)[1].split(/[?&]/)[0];
  		} else {
  			trace("YOUTUBE IN URL BUT NOT A VALID VIDEO");
  		}
  
  		this.media_id.start		= parseInt(url_vars["start"]);	
  
  		if (isNaN(this.media_id.start)){
  			this.media_id.start		= TL.Util.parseYouTubeTime(url_vars["t"]);
  		}
  
  		this.media_id.end		= parseInt(url_vars["end"]);
  
  		this.media_id.hd		= Boolean(typeof(url_vars["hd"]) != 'undefined');
  
  
  		// API Call
  		TL.Load.js('https://www.youtube.com/iframe_api', function() {
  			self.createMedia();
  		});
  
  	},
  
  	// Update Media Display
  	_updateMediaDisplay: function() {
  		//this.el.content_item = document.getElementById(this._el.content_item.id);
  		this._el.content_item.style.height = TL.Util.ratio.r16_9({w:this.options.width}) + "px";
  		this._el.content_item.style.width = this.options.width + "px";
  	},
  
  	_stopMedia: function() {
  		if (this.youtube_loaded) {
  			try {
  			    if(this.player.getPlayerState() == YT.PlayerState.PLAYING) {
  			        this.player.pauseVideo();
  			    }
  			}
  			catch(err) {
  				trace(err);
  			}
  
  		}
  	},
  	createMedia: function() {
  		var self = this;
  
  		clearTimeout(this.timer);
  
  		if(typeof YT != 'undefined' && typeof YT.Player != 'undefined') {
  			// Create Player
  			this.player = new YT.Player(this._el.content_item.id, {
  				playerVars: {
  					enablejsapi:		1,
  					color: 				'white',
  					autohide: 			1,
  					showinfo:			0,
  					theme:				'light',
  					start:				this.media_id.start,
  					end:  				this.media_id.end,
  					fs: 				0,
  					rel:				0
  				},
  				videoId: this.media_id.id,
  				events: {
  					onReady: 			function() {
  						self.onPlayerReady();
  						// After Loaded
  						self.onLoaded();
  					},
  					'onStateChange': 	self.onStateChange
  				}
  			});
  		} else {
  			this.timer = setTimeout(function() {
  				self.createMedia();
  			}, 1000);
  		}
  	},
  
  	/*	Events
  	================================================== */
  	onPlayerReady: function(e) {
  		this.youtube_loaded = true;
  		this._el.content_item = document.getElementById(this._el.content_item.id);
  		this.onMediaLoaded();
  
  	},
  
  	onStateChange: function(e) {
          if(e.data == YT.PlayerState.ENDED) {
              e.target.seekTo(0);
              e.target.pauseVideo();
          }				
  	}
  
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Audio.js
  ********************************************** */
  
  /*	TL.Media.Audio
  	Produces audio assets.
  	Takes a data object and populates a dom object
  ================================================== */
  
  TL.Media.Audio = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		// Loading Message
  		this.loadingMessage();
  
          // Create media?
          if(!this.options.background) {
              this.createMedia();
          }
  
          // After loaded
  		this.onLoaded();
  	},
  
    createMedia: function() {
      var self = this,
          audio_class = "tl-media-item tl-media-audio tl-media-shadow";
  
   		// Link
  		if (this.data.link) {
  			this._el.content_link 				= TL.Dom.create("a", "", this._el.content);
  			this._el.content_link.href 		= this.data.link;
  			this._el.content_link.target 	= "_blank";
  			this._el.content_item					= TL.Dom.create("audio", audio_class, this._el.content_link);
  		} else {
  			this._el.content_item					= TL.Dom.create("audio", audio_class, this._el.content);
  		}
  
  		this._el.content_item.controls = true;
  		this._el.source_item = TL.Dom.create("source", "", this._el.content_item);
  
  		// Media Loaded Event
  		this._el.content_item.addEventListener('load', function(e) {
  			self.onMediaLoaded();
  		});
  
  		this._el.source_item.src = this.data.url;
  		this._el.source_item.type = this._getType(this.data.url, this.data.mediatype.match_str);
  		this._el.content_item.innerHTML += "Your browser doesn't support HTML5 audio with " + this._el.source_item.type;
    },
  
  	_updateMediaDisplay: function(layout) {
  		if(TL.Browser.firefox) {
  			this._el.content_item.style.width = "auto";
  		}
  	},
  
  	_getType: function(url, reg) {
  		var ext = url.match(reg);
  		var type = "audio/"
  		switch(ext[1]) {
  			case "mp3":
  				type += "mpeg";
  				break;
  			case "wav":
  				type += "wav";
  				break;
  			case "m4a":
  				type += "mp4";
  				break;
  			default:
  				type = "audio";
  				break;
  		}
  		return type
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Media.Video.js
  ********************************************** */
  
  /*	TL.Media.Video
  	Produces video assets.
  	Takes a data object and populates a dom object
  ================================================== */
  
  TL.Media.Video = TL.Media.extend({
  
  	includes: [TL.Events],
  
  	/*	Load the media
  	================================================== */
  	_loadMedia: function() {
  		// Loading Message
  		this.loadingMessage();
  
          // Create media?
          if(!this.options.background) {
              this.createMedia();
          }
  
          // After loaded
  		this.onLoaded();
  	},
  
    createMedia: function() {
      var self = this,
          video_class = "tl-media-item tl-media-video tl-media-shadow";
  
   		// Link
  		if (this.data.link) {
  			this._el.content_link 				= TL.Dom.create("a", "", this._el.content);
  			this._el.content_link.href 		= this.data.link;
  			this._el.content_link.target 	= "_blank";
  			this._el.content_item					= TL.Dom.create("video", video_class, this._el.content_link);
  		} else {
  			this._el.content_item					= TL.Dom.create("video", video_class, this._el.content);
  		}
  
  		this._el.content_item.controls = true;
  		this._el.source_item = TL.Dom.create("source", "", this._el.content_item);
  
  		// Media Loaded Event
  		this._el.content_item.addEventListener('load', function(e) {
  			self.onMediaLoaded();
  		});
  
  		this._el.source_item.src = this.data.url;
  		this._el.source_item.type = this._getType(this.data.url, this.data.mediatype.match_str);
  		this._el.content_item.innerHTML += "Your browser doesn't support HTML5 video with " + this._el.source_item.type;
    },
  
  	_updateMediaDisplay: function(layout) {
  		if(TL.Browser.firefox) {
  			this._el.content_item.style.width = "auto";
  		}
  	},
  
  	_getType: function(url, reg) {
  		var ext = url.match(reg);
  		var type = "video/"
  		switch(ext[1]) {
  			case "mp4":
  				type += "mp4";
  				break;
  			default:
  				type = "video";
  				break;
  		}
  		return type
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.Slide.js
  ********************************************** */
  
  /*	TL.Slide
  	Creates a slide. Takes a data object and
  	populates the slide with content.
  ================================================== */
  
  TL.Slide = TL.Class.extend({
  
  	includes: [TL.Events, TL.DomMixins, TL.I18NMixins],
  
  	_el: {},
  
  	/*	Constructor
  	================================================== */
  	initialize: function(data, options, title_slide) {
  		// DOM Elements
  		this._el = {
  			container: {},
  			scroll_container: {},
  			background: {},
  			content_container: {},
  			content: {}
  		};
  
  		// Components
  		this._media 		= null;
  		this._mediaclass	= {};
  		this._text			= {};
  		this._background_media = null;
  
  		// State
  		this._state = {
  			loaded: 		false
  		};
  
  		this.has = {
  			headline: 	false,
  			text: 		false,
  			media: 		false,
  			title: 		false,
  			background: {
  				image: false,
  				color: false,
  				color_value :""
  			}
  		}
  
  		this.has.title = title_slide;
  
  		// Data
  		this.data = {
  			unique_id: 				null,
  			background: 			null,
  			start_date: 			null,
  			end_date: 				null,
  			location: 				null,
  			text: 					null,
  			media: 					null,
              autolink: true
  		};
  
  		// Options
  		this.options = {
  			// animation
  			duration: 			1000,
  			slide_padding_lr: 	40,
  			ease: 				TL.Ease.easeInSpline,
  			width: 				600,
  			height: 			600,
  			skinny_size: 		650,
  			media_name: 		""
  		};
  
  		// Actively Displaying
  		this.active = false;
  
  		// Animation Object
  		this.animator = {};
  
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  		TL.Util.mergeData(this.data, data);
  
  		this._initLayout();
  		this._initEvents();
  
  
  	},
  
  	/*	Adding, Hiding, Showing etc
  	================================================== */
  	show: function() {
  		this.animator = TL.Animate(this._el.slider_container, {
  			left: 		-(this._el.container.offsetWidth * n) + "px",
  			duration: 	this.options.duration,
  			easing: 	this.options.ease
  		});
  	},
  
  	hide: function() {
  
  	},
  
  	setActive: function(is_active) {
  		this.active = is_active;
  
  		if (this.active) {
  			if (this.data.background) {
  				this.fire("background_change", this.has.background);
  			}
  			this.loadMedia();
  		} else {
  			this.stopMedia();
  		}
  	},
  
  	addTo: function(container) {
  		container.appendChild(this._el.container);
  		//this.onAdd();
  	},
  
  	removeFrom: function(container) {
  		container.removeChild(this._el.container);
  	},
  
  	updateDisplay: function(w, h, l) {
  		this._updateDisplay(w, h, l);
  	},
  
  	loadMedia: function() {
          var self = this;
  
  		if (this._media && !this._state.loaded) {
  			this._media.loadMedia();
  			this._state.loaded = true;
  		}
  
  		if(this._background_media && !this._background_media._state.loaded) {
  		    this._background_media.on("loaded", function() {
  		        self._updateBackgroundDisplay();
  		    });
  		    this._background_media.loadMedia();
  		}
  	},
  
  	stopMedia: function() {
  		if (this._media && this._state.loaded) {
  			this._media.stopMedia();
  		}
  	},
  
  	getBackground: function() {
  		return this.has.background;
  	},
  
  	scrollToTop: function() {
  		this._el.container.scrollTop = 0;
  	},
  
  	getFormattedDate: function() {
  
  		if (TL.Util.trim(this.data.display_date).length > 0) {
  			return this.data.display_date;
  		}
  		var date_text = "";
  
  		if(!this.has.title) {
              if (this.data.end_date) {
                  date_text = " &mdash; " + this.data.end_date.getDisplayDate(this.getLanguage());
              }
              if (this.data.start_date) {
                  date_text = this.data.start_date.getDisplayDate(this.getLanguage()) + date_text;
              }
          }
  		return date_text;
  	},
  
  	/*	Events
  	================================================== */
  
  
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  		// Create Layout
  		this._el.container 				= TL.Dom.create("div", "tl-slide");
  
  		if (this.has.title) {
  			this._el.container.className = "tl-slide tl-slide-titleslide";
  		}
  
  		if (this.data.unique_id) {
  			this._el.container.id 		= this.data.unique_id;
  		}
  		this._el.scroll_container 		= TL.Dom.create("div", "tl-slide-scrollable-container", this._el.container);
  		this._el.content_container		= TL.Dom.create("div", "tl-slide-content-container", this._el.scroll_container);
  		this._el.content				= TL.Dom.create("div", "tl-slide-content", this._el.content_container);
  		this._el.background				= TL.Dom.create("div", "tl-slide-background", this._el.container);
  		// Style Slide Background
  		if (this.data.background) {
  			if (this.data.background.url) {
  		    var media_type = TL.MediaType(this.data.background, true);
  		    if(media_type) {
            this._background_media = new media_type.cls(this.data.background, {background: 1});
  
            this.has.background.image 					= true;
            this._el.container.className 				+= ' tl-full-image-background';
            this.has.background.color_value 		= "#000";
            this._el.background.style.display 	= "block";
          }
  			}
  			if (this.data.background.color) {
  				this.has.background.color 					= true;
  				this._el.container.className 				+= ' tl-full-color-background';
  				this.has.background.color_value 			= this.data.background.color;
  				//this._el.container.style.backgroundColor = this.data.background.color;
  				//this._el.background.style.backgroundColor 	= this.data.background.color;
  				//this._el.background.style.display 			= "block";
  			}
  			if (this.data.background.text_background) {
  				this._el.container.className 				+= ' tl-text-background';
  			}
  
  		}
  
  
  
  		// Determine Assets for layout and loading
  		if (this.data.media && this.data.media.url && this.data.media.url != "") {
  			this.has.media = true;
  		}
  		if (this.data.text && this.data.text.text) {
  			this.has.text = true;
  		}
  		if (this.data.text && this.data.text.headline) {
  			this.has.headline = true;
  		}
  
  		// Create Media
  		if (this.has.media) {
  			// Determine the media type
  			this.data.media.mediatype = TL.MediaType(this.data.media);
  			this.options.media_name 	= this.data.media.mediatype.name;
  			this.options.media_type 	= this.data.media.mediatype.type;
        this.options.autolink 		= this.data.autolink;
  
  			// Create a media object using the matched class name
  			this._media = new this.data.media.mediatype.cls(this.data.media, this.options);
  		}
  
  		// Create Text
  		if (this.has.text || this.has.headline) {
  			this._text = new TL.Media.Text(this.data.text, {title:this.has.title,language: this.options.language, autolink: this.data.autolink });
  			this._text.addDateText(this.getFormattedDate());
  		}
  
  
  
  		// Add to DOM
  		if (!this.has.text && !this.has.headline && this.has.media) {
  			TL.DomUtil.addClass(this._el.container, 'tl-slide-media-only');
  			this._media.addTo(this._el.content);
  		} else if (this.has.headline && this.has.media && !this.has.text) {
  			TL.DomUtil.addClass(this._el.container, 'tl-slide-media-only');
  			this._text.addTo(this._el.content);
  			this._media.addTo(this._el.content);
  		} else if (this.has.text && this.has.media) {
  			this._media.addTo(this._el.content);
  			this._text.addTo(this._el.content);
  		} else if (this.has.text || this.has.headline) {
  			TL.DomUtil.addClass(this._el.container, 'tl-slide-text-only');
  			this._text.addTo(this._el.content);
  		}
  
  		// Fire event that the slide is loaded
  		this.onLoaded();
  
  	},
  
  	_initEvents: function() {
  
  	},
  
  	// Update Display
  	_updateDisplay: function(width, height, layout) {
  		var content_width,
  			content_padding_left = this.options.slide_padding_lr,
  			content_padding_right = this.options.slide_padding_lr;
  
  		if (width) {
  			this.options.width 					= width;
  		} else {
  			this.options.width 					= this._el.container.offsetWidth;
  		}
  
  		content_width = this.options.width - (this.options.slide_padding_lr * 2);
  
  		if(TL.Browser.mobile && (this.options.width <= this.options.skinny_size)) {
  			content_padding_left = 0;
  			content_padding_right = 0;
  			content_width = this.options.width;
  		} else if (layout == "landscape") {
  
  		} else if (this.options.width <= this.options.skinny_size) {
  			content_padding_left = 50;
  			content_padding_right = 50;
  			content_width = this.options.width - content_padding_left - content_padding_right;
  		} else {
  
  		}
  
  		this._el.content.style.paddingLeft 	= content_padding_left + "px";
  		this._el.content.style.paddingRight = content_padding_right + "px";
  		this._el.content.style.width		= content_width + "px";
  
  		if (height) {
  			this.options.height = height;
  			//this._el.scroll_container.style.height		= this.options.height + "px";
  
  		} else {
  			this.options.height = this._el.container.offsetHeight;
  		}
  
  		if (this._media) {
  
  			if (!this.has.text && this.has.headline) {
  				this._media.updateDisplay(content_width, (this.options.height - this._text.headlineHeight()), layout);
  			} else if (!this.has.text && !this.has.headline) {
  				this._media.updateDisplay(content_width, this.options.height, layout);
  			} else if (this.options.width <= this.options.skinny_size) {
  				this._media.updateDisplay(content_width, this.options.height, layout);
  			} else {
  				this._media.updateDisplay(content_width/2, this.options.height, layout);
  			}
  		}
  
  		this._updateBackgroundDisplay();
  	},
  
  	_updateBackgroundDisplay: function() {
  	    if(this._background_media && this._background_media._state.loaded) {
  	        this._el.background.style.backgroundImage 	= "url('" + this._background_media.getImageURL(this.options.width, this.options.height) + "')";
  	    }
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.SlideNav.js
  ********************************************** */
  
  /*	TL.SlideNav
  	encapsulate DOM display/events for the 
  	'next' and 'previous' buttons on a slide.
  ================================================== */
  // TODO null out data
  
  TL.SlideNav = TL.Class.extend({
  	
  	includes: [TL.Events, TL.DomMixins],
  	
  	_el: {},
  	
  	/*	Constructor
  	================================================== */
  	initialize: function(data, options, add_to_container) {
  		// DOM ELEMENTS
  		this._el = {
  			container: {},
  			content_container: {},
  			icon: {},
  			title: {},
  			description: {}
  		};
  	
  		// Media Type
  		this.mediatype = {};
  		
  		// Data
  		this.data = {
  			title: "Navigation",
  			description: "Description",
  			date: "Date"
  		};
  	
  		//Options
  		this.options = {
  			direction: 			"previous"
  		};
  	
  		this.animator = null;
  		
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  		TL.Util.mergeData(this.data, data);
  		
  		
  		this._el.container = TL.Dom.create("div", "tl-slidenav-" + this.options.direction);
  		
  		if (TL.Browser.mobile) {
  			this._el.container.setAttribute("ontouchstart"," ");
  		}
  		
  		this._initLayout();
  		this._initEvents();
  		
  		if (add_to_container) {
  			add_to_container.appendChild(this._el.container);
  		};
  		
  	},
  	
  	/*	Update Content
  	================================================== */
  	update: function(slide) {
  		var d = {
  			title: "",
  			description: "",
  			date: slide.getFormattedDate()
  		};
  		
  		if (slide.data.text) {
  			if (slide.data.text.headline) {
  				d.title = slide.data.text.headline;
  			}
  		}
  
  		this._update(d);
  	},
  	
  	/*	Color
  	================================================== */
  	setColor: function(inverted) {
  		if (inverted) {
  			this._el.content_container.className = 'tl-slidenav-content-container tl-slidenav-inverted';
  		} else {
  			this._el.content_container.className = 'tl-slidenav-content-container';
  		}
  	},
  	
  	/*	Events
  	================================================== */
  	_onMouseClick: function() {
  		this.fire("clicked", this.options);
  	},
  	
  	/*	Private Methods
  	================================================== */
  	_update: function(d) {
  		// update data
  		this.data = TL.Util.mergeData(this.data, d);
  		
  		// Title
  		this._el.title.innerHTML = TL.Util.unlinkify(this.data.title);
  		
  		// Date
  		this._el.description.innerHTML	= TL.Util.unlinkify(this.data.date);
  	},
  	
  	_initLayout: function () {
  		
  		// Create Layout
  		this._el.content_container			= TL.Dom.create("div", "tl-slidenav-content-container", this._el.container);
  		this._el.icon						= TL.Dom.create("div", "tl-slidenav-icon", this._el.content_container);
  		this._el.title						= TL.Dom.create("div", "tl-slidenav-title", this._el.content_container);
  		this._el.description				= TL.Dom.create("div", "tl-slidenav-description", this._el.content_container);
  		
  		this._el.icon.innerHTML				= "&nbsp;"
  		
  		this._update();
  	},
  	
  	_initEvents: function () {
  		TL.DomEvent.addListener(this._el.container, 'click', this._onMouseClick, this);
  	}
  	
  	
  });
  
  /* **********************************************
       Begin TL.StorySlider.js
  ********************************************** */
  
  /*	StorySlider
  	is the central class of the API - it is used to create a StorySlider
  
  	Events:
  	nav_next
  	nav_previous
  	slideDisplayUpdate
  	loaded
  	slideAdded
  	slideLoaded
  	slideRemoved
  
  
  ================================================== */
  
  TL.StorySlider = TL.Class.extend({
  
  	includes: [TL.Events, TL.I18NMixins],
  
  	/*	Private Methods
  	================================================== */
  	initialize: function (elem, data, options, init) {
  
  		// DOM ELEMENTS
  		this._el = {
  			container: {},
  			background: {},
  			slider_container_mask: {},
  			slider_container: {},
  			slider_item_container: {}
  		};
  
  		this._nav = {};
  		this._nav.previous = {};
  		this._nav.next = {};
  
  		// Slide Spacing
  		this.slide_spacing = 0;
  
  		// Slides Array
  		this._slides = [];
  
  		// Swipe Object
  		this._swipable;
  
  		// Preload Timer
  		this.preloadTimer;
  
  		// Message
  		this._message;
  
  		// Current Slide
  		this.current_id = '';
  
  		// Data Object
  		this.data = {};
  
  		this.options = {
  			id: 					"",
  			layout: 				"portrait",
  			width: 					600,
  			height: 				600,
  			default_bg_color: 		{r:255, g:255, b:255},
  			slide_padding_lr: 		40, 			// padding on slide of slide
  			start_at_slide: 		1,
  			slide_default_fade: 	"0%", 			// landscape fade
  			// animation
  			duration: 				1000,
  			ease: 					TL.Ease.easeInOutQuint,
  			// interaction
  			dragging: 				true,
  			trackResize: 			true
  		};
  
  		// Main element ID
  		if (typeof elem === 'object') {
  			this._el.container = elem;
  			this.options.id = TL.Util.unique_ID(6, "tl");
  		} else {
  			this.options.id = elem;
  			this._el.container = TL.Dom.get(elem);
  		}
  
  		if (!this._el.container.id) {
  			this._el.container.id = this.options.id;
  		}
  
  		// Animation Object
  		this.animator = null;
  
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  		TL.Util.mergeData(this.data, data);
  
  		if (init) {
  			this.init();
  		}
  	},
  
  	init: function() {
  		this._initLayout();
  		this._initEvents();
  		this._initData();
  		this._updateDisplay();
  
  		// Go to initial slide
  		this.goTo(this.options.start_at_slide);
  
  		this._onLoaded();
  	},
  
  	/* Slides
  	================================================== */
  	_addSlide:function(slide) {
  		slide.addTo(this._el.slider_item_container);
  		slide.on('added', this._onSlideAdded, this);
  		slide.on('background_change', this._onBackgroundChange, this);
  	},
  
  	_createSlide: function(d, title_slide, n) {
  		var slide = new TL.Slide(d, this.options, title_slide);
  		this._addSlide(slide);
  		if(n < 0) {
  		    this._slides.push(slide);
  		} else {
  		    this._slides.splice(n, 0, slide);
  		}
  	},
  
  	_createSlides: function(array) {
  		for (var i = 0; i < array.length; i++) {
  			if (array[i].unique_id == "") {
  				array[i].unique_id = TL.Util.unique_ID(6, "tl-slide");
  			}
              this._createSlide(array[i], false, -1);
  		}
  	},
  
  	_removeSlide: function(slide) {
  		slide.removeFrom(this._el.slider_item_container);
  		slide.off('added', this._onSlideRemoved, this);
  		slide.off('background_change', this._onBackgroundChange);
  	},
  
  	_destroySlide: function(n) {
  		this._removeSlide(this._slides[n]);
  		this._slides.splice(n, 1);
  	},
  
      _findSlideIndex: function(n) {
          var _n = n;
  		if (typeof n == 'string' || n instanceof String) {
  			_n = TL.Util.findArrayNumberByUniqueID(n, this._slides, "unique_id");
  		}
  		return _n;
      },
  
  	/*	Public
  	================================================== */
  	updateDisplay: function(w, h, a, l) {
  		this._updateDisplay(w, h, a, l);
  	},
  
  	// Create a slide
  	createSlide: function(d, n) {
  		this._createSlide(d, false, n);
  	},
  
  	// Create Many Slides from an array
  	createSlides: function(array) {
  		this._createSlides(array);
  	},
  
  	// Destroy slide by index
  	destroySlide: function(n) {
  	    this._destroySlide(n);
  	},
  
  	// Destroy slide by id
  	destroySlideId: function(id) {
  	    this.destroySlide(this._findSlideIndex(id));
  	},
  
  	/*	Navigation
  	================================================== */
  	goTo: function(n, fast, displayupdate) {
  		n = parseInt(n);
  		if (isNaN(n)) n = 0;
  
  		var self = this;
  
  		this.changeBackground({color_value:"", image:false});
  
  		// Clear Preloader Timer
  		if (this.preloadTimer) {
  			clearTimeout(this.preloadTimer);
  		}
  
  		// Set Slide Active State
  		for (var i = 0; i < this._slides.length; i++) {
  			this._slides[i].setActive(false);
  		}
  
  		if (n < this._slides.length && n >= 0) {
  			this.current_id = this._slides[n].data.unique_id;
  
  			// Stop animation
  			if (this.animator) {
  				this.animator.stop();
  			}
  			if (this._swipable) {
  				this._swipable.stopMomentum();
  			}
  
  			if (fast) {
  				this._el.slider_container.style.left = -(this.slide_spacing * n) + "px";
  				this._onSlideChange(displayupdate);
  			} else {
  				this.animator = TL.Animate(this._el.slider_container, {
  					left: 		-(this.slide_spacing * n) + "px",
  					duration: 	this.options.duration,
  					easing: 	this.options.ease,
  					complete: 	this._onSlideChange(displayupdate)
  				});
  			}
  
  			// Set Slide Active State
  			this._slides[n].setActive(true);
  
  			// Update Navigation and Info
  			if (this._slides[n + 1]) {
  				this.showNav(this._nav.next, true);
  				this._nav.next.update(this._slides[n + 1]);
  			} else {
  				this.showNav(this._nav.next, false);
  			}
  			if (this._slides[n - 1]) {
  				this.showNav(this._nav.previous, true);
  				this._nav.previous.update(this._slides[n - 1]);
  			} else {
  				this.showNav(this._nav.previous, false);
  			}
  
  			// Preload Slides
  			this.preloadTimer = setTimeout(function() {
  				self.preloadSlides(n);
  			}, this.options.duration);
  		}
  	},
  
  	goToId: function(id, fast, displayupdate) {
  		this.goTo(this._findSlideIndex(id), fast, displayupdate);
  	},
  
  	preloadSlides: function(n) {
  		if (this._slides[n + 1]) {
  			this._slides[n + 1].loadMedia();
  			this._slides[n + 1].scrollToTop();
  		}
  		if (this._slides[n + 2]) {
  			this._slides[n + 2].loadMedia();
  			this._slides[n + 2].scrollToTop();
  		}
  		if (this._slides[n - 1]) {
  			this._slides[n - 1].loadMedia();
  			this._slides[n - 1].scrollToTop();
  		}
  		if (this._slides[n - 2]) {
  			this._slides[n - 2].loadMedia();
  			this._slides[n - 2].scrollToTop();
  		}
  	},
  
  	next: function() {
  	    var n = this._findSlideIndex(this.current_id);
  		if ((n + 1) < (this._slides.length)) {
  			this.goTo(n + 1);
  		} else {
  			this.goTo(n);
  		}
  	},
  
  	previous: function() {
  	    var n = this._findSlideIndex(this.current_id);
  		if (n - 1 >= 0) {
  			this.goTo(n - 1);
  		} else {
  			this.goTo(n);
  		}
  	},
  
  	showNav: function(nav_obj, show) {
  
  		if (this.options.width <= 500 && TL.Browser.mobile) {
  
  		} else {
  			if (show) {
  				nav_obj.show();
  			} else {
  				nav_obj.hide();
  			}
  
  		}
  	},
  
  
  
  	changeBackground: function(bg) {
  		var bg_color = {r:256, g:256, b:256},
  			bg_color_rgb;
  
  		if (bg.color_value && bg.color_value != "") {
  			bg_color = TL.Util.hexToRgb(bg.color_value);
  			if (!bg_color) {
  				trace("Invalid color value " + bg.color_value);
  				bg_color = this.options.default_bg_color;
  			}
  		} else {
  			bg_color = this.options.default_bg_color;
  			bg.color_value = "rgb(" + bg_color.r + " , " + bg_color.g + ", " + bg_color.b + ")";
  		}
  
  		bg_color_rgb 	= bg_color.r + "," + bg_color.g + "," + bg_color.b;
  		this._el.background.style.backgroundImage = "none";
  
  
  		if (bg.color_value) {
  			this._el.background.style.backgroundColor = bg.color_value;
  		} else {
  			this._el.background.style.backgroundColor = "transparent";
  		}
  
  		if (bg_color.r < 255 || bg_color.g < 255 || bg_color.b < 255 || bg.image) {
  			this._nav.next.setColor(true);
  			this._nav.previous.setColor(true);
  		} else {
  			this._nav.next.setColor(false);
  			this._nav.previous.setColor(false);
  		}
  	},
  	/*	Private Methods
  	================================================== */
  
  	// Update Display
  	_updateDisplay: function(width, height, animate, layout) {
  		var nav_pos, _layout;
  
  		if(typeof layout === 'undefined'){
  			_layout = this.options.layout;
  		} else {
  			_layout = layout;
  		}
  
  		this.options.layout = _layout;
  
  		this.slide_spacing = this.options.width*2;
  
  		if (width) {
  			this.options.width = width;
  		} else {
  			this.options.width = this._el.container.offsetWidth;
  		}
  
  		if (height) {
  			this.options.height = height;
  		} else {
  			this.options.height = this._el.container.offsetHeight;
  		}
  
  		//this._el.container.style.height = this.options.height;
  
  		// position navigation
  		nav_pos = (this.options.height/2);
  		this._nav.next.setPosition({top:nav_pos});
  		this._nav.previous.setPosition({top:nav_pos});
  
  
  		// Position slides
  		for (var i = 0; i < this._slides.length; i++) {
  			this._slides[i].updateDisplay(this.options.width, this.options.height, _layout);
  			this._slides[i].setPosition({left:(this.slide_spacing * i), top:0});
  
  		};
  
  		// Go to the current slide
  		this.goToId(this.current_id, true, true);
  	},
  
  	// Reposition and redraw slides
      _updateDrawSlides: function() {
  	    var _layout = this.options.layout;
  
  		for (var i = 0; i < this._slides.length; i++) {
  			this._slides[i].updateDisplay(this.options.width, this.options.height, _layout);
  			this._slides[i].setPosition({left:(this.slide_spacing * i), top:0});
  		};
  
  		this.goToId(this.current_id, true, false);
  	},
  
  
  	/*	Init
  	================================================== */
  	_initLayout: function () {
  
  		TL.DomUtil.addClass(this._el.container, 'tl-storyslider');
  
  		// Create Layout
  		this._el.slider_container_mask		= TL.Dom.create('div', 'tl-slider-container-mask', this._el.container);
  		this._el.background 				= TL.Dom.create('div', 'tl-slider-background tl-animate', this._el.container);
  		this._el.slider_container			= TL.Dom.create('div', 'tl-slider-container tlanimate', this._el.slider_container_mask);
  		this._el.slider_item_container		= TL.Dom.create('div', 'tl-slider-item-container', this._el.slider_container);
  
  
  		// Update Size
  		this.options.width = this._el.container.offsetWidth;
  		this.options.height = this._el.container.offsetHeight;
  
  		// Create Navigation
  		this._nav.previous = new TL.SlideNav({title: "Previous", description: "description"}, {direction:"previous"});
  		this._nav.next = new TL.SlideNav({title: "Next",description: "description"}, {direction:"next"});
  
  		// add the navigation to the dom
  		this._nav.next.addTo(this._el.container);
  		this._nav.previous.addTo(this._el.container);
  
  
  
  		this._el.slider_container.style.left="0px";
  
  		if (TL.Browser.touch) {
  			//this._el.slider_touch_mask = TL.Dom.create('div', 'tl-slider-touch-mask', this._el.slider_container_mask);
  			this._swipable = new TL.Swipable(this._el.slider_container_mask, this._el.slider_container, {
  				enable: {x:true, y:false},
  				snap: 	true
  			});
  			this._swipable.enable();
  
  			// Message
  			this._message = new TL.Message({}, {
  				message_class: 		"tl-message-full",
  				message_icon_class: "tl-icon-swipe-left"
  			});
  			this._message.updateMessage(this._("swipe_to_navigate"));
  			this._message.addTo(this._el.container);
  		}
  
  	},
  
  	_initEvents: function () {
  		this._nav.next.on('clicked', this._onNavigation, this);
  		this._nav.previous.on('clicked', this._onNavigation, this);
  
  		if (this._message) {
  			this._message.on('clicked', this._onMessageClick, this);
  		}
  
  		if (this._swipable) {
  			this._swipable.on('swipe_left', this._onNavigation, this);
  			this._swipable.on('swipe_right', this._onNavigation, this);
  			this._swipable.on('swipe_nodirection', this._onSwipeNoDirection, this);
  		}
  
  
  	},
  
  	_initData: function() {
  	    if(this.data.title) {
  	        this._createSlide(this.data.title, true, -1);
  	    }
          this._createSlides(this.data.events);
  	},
  
  	/*	Events
  	================================================== */
  	_onBackgroundChange: function(e) {
  	    var n = this._findSlideIndex(this.current_id);
  		var slide_background = this._slides[n].getBackground();
  		this.changeBackground(e);
  		this.fire("colorchange", slide_background);
  	},
  
  	_onMessageClick: function(e) {
  		this._message.hide();
  	},
  
  	_onSwipeNoDirection: function(e) {
  		this.goToId(this.current_id);
  	},
  
  	_onNavigation: function(e) {
  
  		if (e.direction == "next" || e.direction == "left") {
  			this.next();
  		} else if (e.direction == "previous" || e.direction == "right") {
  			this.previous();
  		}
  		this.fire("nav_" + e.direction, this.data);
  	},
  
  	_onSlideAdded: function(e) {
  		trace("slideadded")
  		this.fire("slideAdded", this.data);
  	},
  
  	_onSlideRemoved: function(e) {
  		this.fire("slideRemoved", this.data);
  	},
  
  	_onSlideChange: function(displayupdate) {
  		if (!displayupdate) {
  			this.fire("change", {unique_id: this.current_id});
  		}
  	},
  
  	_onMouseClick: function(e) {
  
  	},
  
  	_fireMouseEvent: function (e) {
  		if (!this._loaded) {
  			return;
  		}
  
  		var type = e.type;
  		type = (type === 'mouseenter' ? 'mouseover' : (type === 'mouseleave' ? 'mouseout' : type));
  
  		if (!this.hasEventListeners(type)) {
  			return;
  		}
  
  		if (type === 'contextmenu') {
  			TL.DomEvent.preventDefault(e);
  		}
  
  		this.fire(type, {
  			latlng: "something", //this.mouseEventToLatLng(e),
  			layerPoint: "something else" //this.mouseEventToLayerPoint(e)
  		});
  	},
  
  	_onLoaded: function() {
  		this.fire("loaded", this.data);
  	}
  
  
  });
  
  
  /* **********************************************
       Begin TL.TimeNav.js
  ********************************************** */
  
  /*	TL.TimeNav
  
  ================================================== */
  
  TL.TimeNav = TL.Class.extend({
  
  	includes: [TL.Events, TL.DomMixins],
  
  	_el: {},
  
  	/*	Constructor
  	================================================== */
  	initialize: function (elem, timeline_config, options, init) {
  		// DOM ELEMENTS
  		this._el = {
  			parent: {},
  			container: {},
  			slider: {},
  			slider_background: {},
  			line: {},
  			marker_container_mask: {},
  			marker_container: {},
  			marker_item_container: {},
  			timeaxis: {},
  			timeaxis_background: {},
  			attribution: {}
  		};
  
  		this.collapsed = false;
  
  		if (typeof elem === 'object') {
  			this._el.container = elem;
  		} else {
  			this._el.container = TL.Dom.get(elem);
  		}
  
  		this.config = timeline_config;
  
  		//Options
  		this.options = {
  			width: 					600,
  			height: 				600,
  			duration: 				1000,
  			ease: 					TL.Ease.easeInOutQuint,
  			has_groups: 			false,
  			optimal_tick_width: 	50,
  			scale_factor: 			2, 				// How many screen widths wide should the timeline be
  			marker_padding: 		5,
  			timenav_height_min: 	150, 			// Minimum timenav height
  			marker_height_min: 		30, 			// Minimum Marker Height
  			marker_width_min: 		100, 			// Minimum Marker Width
  			zoom_sequence:          [0.5, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89] // Array of Fibonacci numbers for TimeNav zoom levels http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibtable.html
  		};
  
  		// Animation
  		this.animator = null;
  
  		// Ready state
  		this.ready = false;
  
  		// Markers Array
  		this._markers = [];
  
  		// Eras Array
  		this._eras = [];
  		this.has_eras = false;
  
  		// Groups Array
  		this._groups = [];
  
  		// Row Height
  		this._calculated_row_height = 100;
  
  		// Current Marker
  		this.current_id = "";
  
  		// TimeScale
  		this.timescale = {};
  
  		// TimeAxis
  		this.timeaxis = {};
  		this.axishelper = {};
  
  		// Max Rows
  		this.max_rows = 6;
  
  		// Animate CSS
  		this.animate_css = false;
  
  		// Swipe Object
  		this._swipable;
  
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  
  		if (init) {
  			this.init();
  		}
  	},
  
  	init: function() {
  		this._initLayout();
  		this._initEvents();
  		this._initData();
  		this._updateDisplay();
  
  		this._onLoaded();
  	},
  
  	/*	Public
  	================================================== */
  	positionMarkers: function() {
  		this._positionMarkers();
  	},
  
  	/*	Update Display
  	================================================== */
  	updateDisplay: function(w, h, a, l) {
  		this._updateDisplay(w, h, a, l);
  	},
  
  
  	/*	TimeScale
  	================================================== */
  	_getTimeScale: function() {
  		/* maybe the establishing config values (marker_height_min and max_rows) should be
  		separated from making a TimeScale object, which happens in another spot in this file with duplicate mapping of properties of this TimeNav into the TimeScale options object? */
  		// Set Max Rows
  		var marker_height_min = 0;
  		try {
  			marker_height_min = parseInt(this.options.marker_height_min);
  		} catch(e) {
  			trace("Invalid value for marker_height_min option.");
  			marker_height_min = 30;
  		}
  		if (marker_height_min == 0) {
  			trace("marker_height_min option must not be zero.")
  			marker_height_min = 30;
  		}
  		this.max_rows = Math.round((this.options.height - this._el.timeaxis_background.offsetHeight - (this.options.marker_padding)) / marker_height_min);
  		if (this.max_rows < 1) {
  			this.max_rows = 1;
  		}
  		return new TL.TimeScale(this.config, {
              display_width: this._el.container.offsetWidth,
              screen_multiplier: this.options.scale_factor,
              max_rows: this.max_rows
  
  		});
  	},
  
  	_updateTimeScale: function(new_scale) {
  		this.options.scale_factor = new_scale;
  		this._updateDrawTimeline();
  	},
  
  	zoomIn: function() { // move the the next "higher" scale factor
  		var new_scale = TL.Util.findNextGreater(this.options.zoom_sequence, this.options.scale_factor);
  		this.setZoomFactor(new_scale);
  	},
  
  	zoomOut: function() { // move the the next "lower" scale factor
  		var new_scale = TL.Util.findNextLesser(this.options.zoom_sequence, this.options.scale_factor);
  		this.setZoomFactor(new_scale);
  	},
  
  	setZoom: function(level) {
  		var zoom_factor = this.options.zoom_sequence[level];
  		if (typeof(zoom_factor) == 'number') {
  			this.setZoomFactor(zoom_factor);
  		} else {
  			console.warn("Invalid zoom level. Please use an index number between 0 and " + (this.options.zoom_sequence.length - 1));
  		}
  	},
  
  	setZoomFactor: function(factor) {
  		if (factor <= this.options.zoom_sequence[0]) {
  			this.fire("zoomtoggle", {zoom:"out", show:false});
  		} else {
  			this.fire("zoomtoggle", {zoom:"out", show:true});
  		}
  
  		if (factor >= this.options.zoom_sequence[this.options.zoom_sequence.length-1]) {
  			this.fire("zoomtoggle", {zoom:"in", show:false});
  		} else {
  			this.fire("zoomtoggle", {zoom:"in", show:true});
  		}
  
  		if (factor == 0) {
  			console.warn("Zoom factor must be greater than zero. Using 0.1");
  			factor = 0.1;
  		}
  		this.options.scale_factor = factor;
  		//this._updateDrawTimeline(true);
  		this.goToId(this.current_id, !this._updateDrawTimeline(true), true);
  	},
  
  	/*	Groups
  	================================================== */
  	_createGroups: function() {
  		this._groups = [];
  		var group_labels = this.timescale.getGroupLabels();
  
  		if (group_labels) {
  			this.options.has_groups = true;
  			for (var i = 0; i < group_labels.length; i++) {
  				this._createGroup(group_labels[i]);
  			}
  		}
  
  	},
  
  	_createGroup: function(group_label) {
  		var group = new TL.TimeGroup(group_label);
  		this._addGroup(group);
  		this._groups.push(group);
  	},
  
  	_addGroup:function(group) {
  		group.addTo(this._el.container);
  
  	},
  
  	_positionGroups: function() {
  		if (this.options.has_groups) {
  			var available_height 	= (this.options.height - this._el.timeaxis_background.offsetHeight ),
  				group_height 		= Math.floor((available_height /this.timescale.getNumberOfRows()) - this.options.marker_padding),
  				group_labels		= this.timescale.getGroupLabels();
  
  			for (var i = 0, group_rows = 0; i < this._groups.length; i++) {
  				var group_y = Math.floor(group_rows * (group_height + this.options.marker_padding));
  				var group_hide = false;
  				if (group_y > (available_height- this.options.marker_padding)) {
  					group_hide = true;
  				}
  
  				this._groups[i].setRowPosition(group_y, this._calculated_row_height + this.options.marker_padding/2);
  				this._groups[i].setAlternateRowColor(TL.Util.isEven(i), group_hide);
  
  				group_rows += this._groups[i].data.rows;    // account for groups spanning multiple rows
  			}
  		}
  	},
  
  	/*	Markers
  	================================================== */
  	_addMarker:function(marker) {
  		marker.addTo(this._el.marker_item_container);
  		marker.on('markerclick', this._onMarkerClick, this);
  		marker.on('added', this._onMarkerAdded, this);
  	},
  
  	_createMarker: function(data, n) {
  		var marker = new TL.TimeMarker(data, this.options);
  		this._addMarker(marker);
  		if(n < 0) {
  		    this._markers.push(marker);
  		} else {
  		    this._markers.splice(n, 0, marker);
  		}
  	},
  
  	_createMarkers: function(array) {
  		for (var i = 0; i < array.length; i++) {
  			this._createMarker(array[i], -1);
  		}
  	},
  
  	_removeMarker: function(marker) {
  		marker.removeFrom(this._el.marker_item_container);
  		//marker.off('added', this._onMarkerRemoved, this);
  	},
  
  	_destroyMarker: function(n) {
  	    this._removeMarker(this._markers[n]);
  	    this._markers.splice(n, 1);
  	},
  
  	_positionMarkers: function(fast) {
  		// POSITION X
  		for (var i = 0; i < this._markers.length; i++) {
  			var pos = this.timescale.getPositionInfo(i);
  			if (fast) {
  				this._markers[i].setClass("tl-timemarker tl-timemarker-fast");
  			} else {
  				this._markers[i].setClass("tl-timemarker");
  			}
  			this._markers[i].setPosition({left:pos.start});
  			this._markers[i].setWidth(pos.width);
  		};
  
  	},
  
  	_calculateMarkerHeight: function(h) {
  		return ((h /this.timescale.getNumberOfRows()) - this.options.marker_padding);
  	},
  
  	_calculateRowHeight: function(h) {
  		return (h /this.timescale.getNumberOfRows());
  	},
  
  	_calculateAvailableHeight: function() {
  		return (this.options.height - this._el.timeaxis_background.offsetHeight - (this.options.marker_padding));
  	},
  
  	_calculateMinimumTimeNavHeight: function() {
  		return (this.timescale.getNumberOfRows() * this.options.marker_height_min) + this._el.timeaxis_background.offsetHeight + (this.options.marker_padding);
  
  	},
  
  	getMinimumHeight: function() {
  		return this._calculateMinimumTimeNavHeight();
  	},
  
  	_assignRowsToMarkers: function() {
  		var available_height 	= this._calculateAvailableHeight(),
  			marker_height 		= this._calculateMarkerHeight(available_height);
  
  
  		this._positionGroups();
  
  		this._calculated_row_height = this._calculateRowHeight(available_height);
  
  		for (var i = 0; i < this._markers.length; i++) {
  
  			// Set Height
  			this._markers[i].setHeight(marker_height);
  
  			//Position by Row
  			var row = this.timescale.getPositionInfo(i).row;
  
  			var marker_y = Math.floor(row * (marker_height + this.options.marker_padding)) + this.options.marker_padding;
  
  			var remainder_height = available_height - marker_y + this.options.marker_padding;
  			this._markers[i].setRowPosition(marker_y, remainder_height);
  		};
  
  	},
  
  	_resetMarkersActive: function() {
  		for (var i = 0; i < this._markers.length; i++) {
  			this._markers[i].setActive(false);
  		};
  	},
  
  	_findMarkerIndex: function(n) {
  	    var _n = -1;
  		if (typeof n == 'string' || n instanceof String) {
  			_n = TL.Util.findArrayNumberByUniqueID(n, this._markers, "unique_id", _n);
  		}
  		return _n;
  	},
  
  	/*	ERAS
  	================================================== */
  	_createEras: function(array) {
  		for (var i = 0; i < array.length; i++) {
  			this._createEra(array[i], -1);
  		}
  	},
  
  	_createEra: function(data, n) {
  		var era = new TL.TimeEra(data, this.options);
  		this._addEra(era);
  		if(n < 0) {
  		    this._eras.push(era);
  		} else {
  		    this._eras.splice(n, 0, era);
  		}
  	},
  
  	_addEra:function(era) {
  		era.addTo(this._el.marker_item_container);
  		era.on('added', this._onEraAdded, this);
  	},
  
  	_removeEra: function(era) {
  		era.removeFrom(this._el.marker_item_container);
  		//marker.off('added', this._onMarkerRemoved, this);
  	},
  
  	_destroyEra: function(n) {
  	    this._removeEra(this._eras[n]);
  	    this._eras.splice(n, 1);
  	},
  
  	_positionEras: function(fast) {
  
  		var era_color = 0;
  		// POSITION X
  		for (var i = 0; i < this._eras.length; i++) {
  			var pos = {
  				start:0,
  				end:0,
  				width:0
  			};
  
  			pos.start = this.timescale.getPosition(this._eras[i].data.start_date.getTime());
  			pos.end = this.timescale.getPosition(this._eras[i].data.end_date.getTime());
  			pos.width = pos.end - pos.start;
  
  			if (fast) {
  				this._eras[i].setClass("tl-timeera tl-timeera-fast");
  			} else {
  				this._eras[i].setClass("tl-timeera");
  			}
  			this._eras[i].setPosition({left:pos.start});
  			this._eras[i].setWidth(pos.width);
  
  			era_color++;
  			if (era_color > 5) {
  				era_color = 0;
  			}
  			this._eras[i].setColor(era_color);
  		};
  
  	},
  
  	/*	Public
  	================================================== */
  
  	// Create a marker
  	createMarker: function(d, n) {
  	    this._createMarker(d, n);
  	},
  
  	// Create many markers from an array
  	createMarkers: function(array) {
  	    this._createMarkers(array);
  	},
  
  	// Destroy marker by index
  	destroyMarker: function(n) {
  	    this._destroyMarker(n);
  	},
  
  	// Destroy marker by id
  	destroyMarkerId: function(id) {
  	    this.destroyMarker(this._findMarkerIndex(id));
  	},
  
  	/*	Navigation
  	================================================== */
  	goTo: function(n, fast, css_animation) {
  		var self = 	this,
  			_ease = this.options.ease,
  			_duration = this.options.duration,
  			_n = (n < 0) ? 0 : n;
  
  		// Set Marker active state
  		this._resetMarkersActive();
  		if(n >= 0 && n < this._markers.length) {
  		    this._markers[n].setActive(true);
  		}
  		// Stop animation
  		if (this.animator) {
  			this.animator.stop();
  		}
  
  		if (fast) {
  			this._el.slider.className = "tl-timenav-slider";
  			this._el.slider.style.left = -this._markers[_n].getLeft() + (this.options.width/2) + "px";
  		} else {
  			if (css_animation) {
  				this._el.slider.className = "tl-timenav-slider tl-timenav-slider-animate";
  				this.animate_css = true;
  				this._el.slider.style.left = -this._markers[_n].getLeft() + (this.options.width/2) + "px";
  			} else {
  				this._el.slider.className = "tl-timenav-slider";
  				this.animator = TL.Animate(this._el.slider, {
  					left: 		-this._markers[_n].getLeft() + (this.options.width/2) + "px",
  					duration: 	_duration,
  					easing: 	_ease
  				});
  			}
  		}
  
  		if(n >= 0 && n < this._markers.length) {
  		    this.current_id = this._markers[n].data.unique_id;
  		} else {
  		    this.current_id = '';
  		}
  	},
  
  	goToId: function(id, fast, css_animation) {
  		this.goTo(this._findMarkerIndex(id), fast, css_animation);
  	},
  
  	/*	Events
  	================================================== */
  	_onLoaded: function() {
  		this.ready = true;
  		this.fire("loaded", this.config);
  	},
  
  	_onMarkerAdded: function(e) {
  		this.fire("dateAdded", this.config);
  	},
  
  	_onEraAdded: function(e) {
  		this.fire("eraAdded", this.config);
  	},
  
  	_onMarkerRemoved: function(e) {
  		this.fire("dateRemoved", this.config);
  	},
  
  	_onMarkerClick: function(e) {
  		// Go to the clicked marker
  		this.goToId(e.unique_id);
  		this.fire("change", {unique_id: e.unique_id});
  	},
  
  	_onMouseScroll: function(e) {
  
  		var delta		= 0,
  			scroll_to	= 0,
  			constraint 	= {
  				right: 	-(this.timescale.getPixelWidth() - (this.options.width/2)),
  				left: 	this.options.width/2
  			};
  		if (!e) {
  			e = window.event;
  		}
  		if (e.originalEvent) {
  			e = e.originalEvent;
  		}
  
  		// Webkit and browsers able to differntiate between up/down and left/right scrolling
  		if (typeof e.wheelDeltaX != 'undefined' ) {
  			delta = e.wheelDeltaY/6;
  			if (Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY)) {
  				delta = e.wheelDeltaX/6;
  			} else {
  				//delta = e.wheelDeltaY/6;
  				delta = 0;
  			}
  		}
  		if (delta) {
  			if (e.preventDefault) {
  				 e.preventDefault();
  			}
  			e.returnValue = false;
  		}
  		// Stop from scrolling too far
  		scroll_to = parseInt(this._el.slider.style.left.replace("px", "")) + delta;
  
  
  		if (scroll_to > constraint.left) {
  			scroll_to = constraint.left;
  		} else if (scroll_to < constraint.right) {
  			scroll_to = constraint.right;
  		}
  
  		if (this.animate_css) {
  			this._el.slider.className = "tl-timenav-slider";
  			this.animate_css = false;
  		}
  
  		this._el.slider.style.left = scroll_to + "px";
  
  	},
  
  	_onDragMove: function(e) {
  		if (this.animate_css) {
  			this._el.slider.className = "tl-timenav-slider";
  			this.animate_css = false;
  		}
  
  	},
  
  	/*	Private Methods
  	================================================== */
  	// Update Display
  	_updateDisplay: function(width, height, animate) {
  
  		if (width) {
  			this.options.width = width;
  		}
  		if (height && height != this.options.height) {
  			this.options.height = height;
  			this.timescale = this._getTimeScale();
  		}
  
  		// Size Markers
  		this._assignRowsToMarkers();
  
  		// Size swipable area
  		this._el.slider_background.style.width = this.timescale.getPixelWidth() + this.options.width + "px";
  		this._el.slider_background.style.left = -(this.options.width/2) + "px";
  		this._el.slider.style.width = this.timescale.getPixelWidth() + this.options.width + "px";
  
  		// Update Swipable constraint
  		this._swipable.updateConstraint({top: false,bottom: false,left: (this.options.width/2),right: -(this.timescale.getPixelWidth() - (this.options.width/2))});
  
  		// Go to the current slide
  		this.goToId(this.current_id, true);
  	},
  
  	_drawTimeline: function(fast) {
  		this.timescale = this._getTimeScale();
  		this.timeaxis.drawTicks(this.timescale, this.options.optimal_tick_width);
  		this._positionMarkers(fast);
  		this._assignRowsToMarkers();
  		this._createGroups();
  		this._positionGroups();
  
  		if (this.has_eras) {
  
  			this._positionEras(fast);
  		}
  	},
  
  	_updateDrawTimeline: function(check_update) {
  		var do_update = false;
  
  		// Check to see if redraw is needed
  		if (check_update) {
  			/* keep this aligned with _getTimeScale or reduce code duplication */
  			var temp_timescale = new TL.TimeScale(this.config, {
  	            display_width: this._el.container.offsetWidth,
  	            screen_multiplier: this.options.scale_factor,
  	            max_rows: this.max_rows
  
  			});
  
  			if (this.timescale.getMajorScale() == temp_timescale.getMajorScale()
  			 && this.timescale.getMinorScale() == temp_timescale.getMinorScale()) {
  				do_update = true;
  			}
  		} else {
  			do_update = true;
  		}
  
  		// Perform update or redraw
  		if (do_update) {
  			this.timescale = this._getTimeScale();
  			this.timeaxis.positionTicks(this.timescale, this.options.optimal_tick_width);
  			this._positionMarkers();
  			this._assignRowsToMarkers();
  			this._positionGroups();
  			if (this.has_eras) {
  				this._positionEras();
  			}
  			this._updateDisplay();
  		} else {
  			this._drawTimeline(true);
  		}
  
  		return do_update;
  
  	},
  
  
  	/*	Init
  	================================================== */
  	_initLayout: function () {
  		// Create Layout
  		this._el.attribution 				= TL.Dom.create('div', 'tl-attribution', this._el.container);
  		this._el.line						= TL.Dom.create('div', 'tl-timenav-line', this._el.container);
  		this._el.slider						= TL.Dom.create('div', 'tl-timenav-slider', this._el.container);
  		this._el.slider_background			= TL.Dom.create('div', 'tl-timenav-slider-background', this._el.slider);
  		this._el.marker_container_mask		= TL.Dom.create('div', 'tl-timenav-container-mask', this._el.slider);
  		this._el.marker_container			= TL.Dom.create('div', 'tl-timenav-container', this._el.marker_container_mask);
  		this._el.marker_item_container		= TL.Dom.create('div', 'tl-timenav-item-container', this._el.marker_container);
  		this._el.timeaxis 					= TL.Dom.create('div', 'tl-timeaxis', this._el.slider);
  		this._el.timeaxis_background 		= TL.Dom.create('div', 'tl-timeaxis-background', this._el.container);
  
  
  		// Knight Lab Logo
  		this._el.attribution.innerHTML = "<a href='http://timeline.knightlab.com' target='_blank'><span class='tl-knightlab-logo'></span>Timeline JS</a>"
  
  		// Time Axis
  		this.timeaxis = new TL.TimeAxis(this._el.timeaxis, this.options);
  
  		// Swipable
  		this._swipable = new TL.Swipable(this._el.slider_background, this._el.slider, {
  			enable: {x:true, y:false},
  			constraint: {top: false,bottom: false,left: (this.options.width/2),right: false},
  			snap: 	false
  		});
  		this._swipable.enable();
  
  	},
  
  	_initEvents: function () {
  		// Drag Events
  		this._swipable.on('dragmove', this._onDragMove, this);
  
  		// Scroll Events
  		TL.DomEvent.addListener(this._el.container, 'mousewheel', this._onMouseScroll, this);
  		TL.DomEvent.addListener(this._el.container, 'DOMMouseScroll', this._onMouseScroll, this);
  	},
  
  	_initData: function() {
  		// Create Markers and then add them
  		this._createMarkers(this.config.events);
  
  		if (this.config.eras) {
  			this.has_eras = true;
  			this._createEras(this.config.eras);
  		}
  
  		this._drawTimeline();
  
  	}
  
  
  });
  
  
  /* **********************************************
       Begin TL.TimeMarker.js
  ********************************************** */
  
  /*	TL.TimeMarker
  
  ================================================== */
  
  TL.TimeMarker = TL.Class.extend({
  
  	includes: [TL.Events, TL.DomMixins],
  
  	_el: {},
  
  	/*	Constructor
  	================================================== */
  	initialize: function(data, options) {
  
  		// DOM Elements
  		this._el = {
  			container: {},
  			content_container: {},
  			media_container: {},
  			timespan: {},
  			line_left: {},
  			line_right: {},
  			content: {},
  			text: {},
  			media: {},
  		};
  
  		// Components
  		this._text			= {};
  
  		// State
  		this._state = {
  			loaded: 		false
  		};
  
  
  		// Data
  		this.data = {
  			unique_id: 			"",
  			background: 		null,
  			date: {
  				year:			0,
  				month:			0,
  				day: 			0,
  				hour: 			0,
  				minute: 		0,
  				second: 		0,
  				millisecond: 	0,
  				thumbnail: 		"",
  				format: 		""
  			},
  			text: {
  				headline: 		"",
  				text: 			""
  			},
  			media: 				null
  		};
  
  		// Options
  		this.options = {
  			duration: 			1000,
  			ease: 				TL.Ease.easeInSpline,
  			width: 				600,
  			height: 			600,
  			marker_width_min: 	100 			// Minimum Marker Width
  		};
  
  		// Actively Displaying
  		this.active = false;
  
  		// Animation Object
  		this.animator = {};
  
  		// End date
  		this.has_end_date = false;
  
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  		TL.Util.mergeData(this.data, data);
  
  		this._initLayout();
  		this._initEvents();
  
  
  	},
  
  	/*	Adding, Hiding, Showing etc
  	================================================== */
  	show: function() {
  
  	},
  
  	hide: function() {
  
  	},
  
  	setActive: function(is_active) {
  		this.active = is_active;
  
  		if (this.active && this.has_end_date) {
  			this._el.container.className = 'tl-timemarker tl-timemarker-with-end tl-timemarker-active';
  		} else if (this.active){
  			this._el.container.className = 'tl-timemarker tl-timemarker-active';
  		} else if (this.has_end_date){
  			this._el.container.className = 'tl-timemarker tl-timemarker-with-end';
  		} else {
  			this._el.container.className = 'tl-timemarker';
  		}
  	},
  
  	addTo: function(container) {
  		container.appendChild(this._el.container);
  	},
  
  	removeFrom: function(container) {
  		container.removeChild(this._el.container);
  	},
  
  	updateDisplay: function(w, h) {
  		this._updateDisplay(w, h);
  	},
  
  	loadMedia: function() {
  
  		if (this._media && !this._state.loaded) {
  			this._media.loadMedia();
  			this._state.loaded = true;
  		}
  	},
  
  	stopMedia: function() {
  		if (this._media && this._state.loaded) {
  			this._media.stopMedia();
  		}
  	},
  
  	getLeft: function() {
  		return this._el.container.style.left.slice(0, -2);
  	},
  
  	getTime: function() { // TODO does this need to know about the end date?
  		return this.data.start_date.getTime();
  	},
  
  	getEndTime: function() {
  
  		if (this.data.end_date) {
  			return this.data.end_date.getTime();
  		} else {
  			return false;
  		}
  	},
  
  	setHeight: function(h) {
  		var text_line_height = 12,
  			text_lines = 1;
  
  		this._el.content_container.style.height = h  + "px";
  		this._el.timespan_content.style.height = h + "px";
  		// Handle Line height for better display of text
  		if (h <= 30) {
  			this._el.content.className = "tl-timemarker-content tl-timemarker-content-small";
  		} else {
  			this._el.content.className = "tl-timemarker-content";
  		}
  
  		if (h <= 56) {
  			TL.DomUtil.addClass(this._el.content_container, "tl-timemarker-content-container-small");
  		} else {
  			TL.DomUtil.removeClass(this._el.content_container, "tl-timemarker-content-container-small");
  		}
  
  		// Handle number of lines visible vertically
  
  		if (TL.Browser.webkit) {
  			text_lines = Math.floor(h / (text_line_height + 2));
  			if (text_lines < 1) {
  				text_lines = 1;
  			}
  			this._text.className = "tl-headline";
  			this._text.style.webkitLineClamp = text_lines;
  		} else {
  			text_lines = h / text_line_height;
  			if (text_lines > 1) {
  				this._text.className = "tl-headline tl-headline-fadeout";
  			} else {
  				this._text.className = "tl-headline";
  			}
  			this._text.style.height = (text_lines * text_line_height)  + "px";
  		}
  
  	},
  
  	setWidth: function(w) {
  		if (this.data.end_date) {
  			this._el.container.style.width = w + "px";
  
  			if (w > this.options.marker_width_min) {
  				this._el.content_container.style.width = w + "px";
  				this._el.content_container.className = "tl-timemarker-content-container tl-timemarker-content-container-long";
  			} else {
  				this._el.content_container.style.width = this.options.marker_width_min + "px";
  				this._el.content_container.className = "tl-timemarker-content-container";
  			}
  		}
  
  	},
  
  	setClass: function(n) {
  		this._el.container.className = n;
  	},
  
  	setRowPosition: function(n, remainder) {
  		this.setPosition({top:n});
  		this._el.timespan.style.height = remainder + "px";
  
  		if (remainder < 56) {
  			//TL.DomUtil.removeClass(this._el.content_container, "tl-timemarker-content-container-small");
  		}
  	},
  
  	/*	Events
  	================================================== */
  	_onMarkerClick: function(e) {
  		this.fire("markerclick", {unique_id:this.data.unique_id});
  	},
  
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  		//trace(this.data)
  		// Create Layout
  		this._el.container 				= TL.Dom.create("div", "tl-timemarker");
  		if (this.data.unique_id) {
  			this._el.container.id 		= this.data.unique_id + "-marker";
  		}
  
  		if (this.data.end_date) {
  			this.has_end_date = true;
  			this._el.container.className = 'tl-timemarker tl-timemarker-with-end';
  		}
  
  		this._el.timespan				= TL.Dom.create("div", "tl-timemarker-timespan", this._el.container);
  		this._el.timespan_content		= TL.Dom.create("div", "tl-timemarker-timespan-content", this._el.timespan);
  		this._el.content_container		= TL.Dom.create("div", "tl-timemarker-content-container", this._el.container);
  
  		this._el.content				= TL.Dom.create("div", "tl-timemarker-content", this._el.content_container);
  
  		this._el.line_left				= TL.Dom.create("div", "tl-timemarker-line-left", this._el.timespan);
  		this._el.line_right				= TL.Dom.create("div", "tl-timemarker-line-right", this._el.timespan);
  
  		// Thumbnail or Icon
  		if (this.data.media) {
  			this._el.media_container	= TL.Dom.create("div", "tl-timemarker-media-container", this._el.content);
  			// ugh. needs an overhaul
  			var mtd = {url: this.data.media.thumbnail};
  			var thumbnail_media_type = (this.data.media.thumbnail) ? TL.MediaType(mtd, true) : null;
  			if (thumbnail_media_type) {
  				var thumbnail_media = new thumbnail_media_type.cls(mtd);
  				thumbnail_media.on("loaded", function() {
  					this._el.media				= TL.Dom.create("img", "tl-timemarker-media", this._el.media_container);
  					this._el.media.src			= thumbnail_media.getImageURL();
  				}.bind(this));
  				thumbnail_media.loadMedia();
  			} else {
  				var media_type = TL.MediaType(this.data.media).type;
  				this._el.media				= TL.Dom.create("span", "tl-icon-" + media_type, this._el.media_container);
  
  			}
  
  		}
  
  
  		// Text
  		this._el.text					= TL.Dom.create("div", "tl-timemarker-text", this._el.content);
  		this._text						= TL.Dom.create("h2", "tl-headline", this._el.text);
  		if (this.data.text.headline && this.data.text.headline != "") {
  			this._text.innerHTML		= TL.Util.unlinkify(this.data.text.headline);
  		} else if (this.data.text.text && this.data.text.text != "") {
  			this._text.innerHTML		= TL.Util.unlinkify(this.data.text.text);
  		} else if (this.data.media && this.data.media.caption && this.data.media.caption != "") {
  			this._text.innerHTML		= TL.Util.unlinkify(this.data.media.caption);
  		}
  
  
  
  		// Fire event that the slide is loaded
  		this.onLoaded();
  
  	},
  
  	_initEvents: function() {
  		TL.DomEvent.addListener(this._el.container, 'click', this._onMarkerClick, this);
  	},
  
  	// Update Display
  	_updateDisplay: function(width, height, layout) {
  
  		if (width) {
  			this.options.width 					= width;
  		}
  
  		if (height) {
  			this.options.height = height;
  		}
  
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.TimeEra.js
  ********************************************** */
  
  /*	TL.TimeMarker
  
  ================================================== */
  
  TL.TimeEra = TL.Class.extend({
  
  	includes: [TL.Events, TL.DomMixins],
  
  	_el: {},
  
  	/*	Constructor
  	================================================== */
  	initialize: function(data, options) {
  
  		// DOM Elements
  		this._el = {
  			container: {},
  			background: {},
  			content_container: {},
  			content: {},
  			text: {}
  		};
  
  		// Components
  		this._text			= {};
  
  		// State
  		this._state = {
  			loaded: 		false
  		};
  
  
  		// Data
  		this.data = {
  			unique_id: 			"",
  			date: {
  				year:			0,
  				month:			0,
  				day: 			0,
  				hour: 			0,
  				minute: 		0,
  				second: 		0,
  				millisecond: 	0,
  				thumbnail: 		"",
  				format: 		""
  			},
  			text: {
  				headline: 		"",
  				text: 			""
  			}
  		};
  
  		// Options
  		this.options = {
  			duration: 			1000,
  			ease: 				TL.Ease.easeInSpline,
  			width: 				600,
  			height: 			600,
  			marker_width_min: 	100 			// Minimum Marker Width
  		};
  
  		// Actively Displaying
  		this.active = false;
  
  		// Animation Object
  		this.animator = {};
  
  		// End date
  		this.has_end_date = false;
  
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  		TL.Util.mergeData(this.data, data);
  
  		this._initLayout();
  		this._initEvents();
  
  
  	},
  
  	/*	Adding, Hiding, Showing etc
  	================================================== */
  	show: function() {
  
  	},
  
  	hide: function() {
  
  	},
  
  	setActive: function(is_active) {
  
  	},
  
  	addTo: function(container) {
  		container.appendChild(this._el.container);
  	},
  
  	removeFrom: function(container) {
  		container.removeChild(this._el.container);
  	},
  
  	updateDisplay: function(w, h) {
  		this._updateDisplay(w, h);
  	},
  
  	getLeft: function() {
  		return this._el.container.style.left.slice(0, -2);
  	},
  
  	getTime: function() { // TODO does this need to know about the end date?
  		return this.data.start_date.getTime();
  	},
  
  	getEndTime: function() {
  
  		if (this.data.end_date) {
  			return this.data.end_date.getTime();
  		} else {
  			return false;
  		}
  	},
  
  	setHeight: function(h) {
  		var text_line_height = 12,
  			text_lines = 1;
  
  		this._el.content_container.style.height = h  + "px";
  		this._el.content.className = "tl-timeera-content";
  
  		// Handle number of lines visible vertically
  
  		if (TL.Browser.webkit) {
  			text_lines = Math.floor(h / (text_line_height + 2));
  			if (text_lines < 1) {
  				text_lines = 1;
  			}
  			this._text.className = "tl-headline";
  			this._text.style.webkitLineClamp = text_lines;
  		} else {
  			text_lines = h / text_line_height;
  			if (text_lines > 1) {
  				this._text.className = "tl-headline tl-headline-fadeout";
  			} else {
  				this._text.className = "tl-headline";
  			}
  			this._text.style.height = (text_lines * text_line_height)  + "px";
  		}
  
  	},
  
  	setWidth: function(w) {
  		if (this.data.end_date) {
  			this._el.container.style.width = w + "px";
  
  			if (w > this.options.marker_width_min) {
  				this._el.content_container.style.width = w + "px";
  				this._el.content_container.className = "tl-timeera-content-container tl-timeera-content-container-long";
  			} else {
  				this._el.content_container.style.width = this.options.marker_width_min + "px";
  				this._el.content_container.className = "tl-timeera-content-container";
  			}
  		}
  
  	},
  
  	setClass: function(n) {
  		this._el.container.className = n;
  	},
  
  	setRowPosition: function(n, remainder) {
  		this.setPosition({top:n});
  
  		if (remainder < 56) {
  			//TL.DomUtil.removeClass(this._el.content_container, "tl-timeera-content-container-small");
  		}
  	},
  
  	setColor: function(color_num) {
  		this._el.container.className = 'tl-timeera tl-timeera-color' + color_num;
  	},
  
  	/*	Events
  	================================================== */
  
  
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  		//trace(this.data)
  		// Create Layout
  		this._el.container 				= TL.Dom.create("div", "tl-timeera");
  		if (this.data.unique_id) {
  			this._el.container.id 		= this.data.unique_id + "-era";
  		}
  
  		if (this.data.end_date) {
  			this.has_end_date = true;
  			this._el.container.className = 'tl-timeera tl-timeera-with-end';
  		}
  
  		this._el.content_container		= TL.Dom.create("div", "tl-timeera-content-container", this._el.container);
  
  		this._el.background 			= TL.Dom.create("div", "tl-timeera-background", this._el.content_container);
  
  		this._el.content				= TL.Dom.create("div", "tl-timeera-content", this._el.content_container);
  
  		
  
  		// Text
  		this._el.text					= TL.Dom.create("div", "tl-timeera-text", this._el.content);
  		this._text						= TL.Dom.create("h2", "tl-headline", this._el.text);
  		if (this.data.text.headline && this.data.text.headline != "") {
  			this._text.innerHTML		= TL.Util.unlinkify(this.data.text.headline);
  		} 
  
  
  
  		// Fire event that the slide is loaded
  		this.onLoaded();
  
  	},
  
  	_initEvents: function() {
  		
  	},
  
  	// Update Display
  	_updateDisplay: function(width, height, layout) {
  
  		if (width) {
  			this.options.width 					= width;
  		}
  
  		if (height) {
  			this.options.height = height;
  		}
  
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.TimeGroup.js
  ********************************************** */
  
  /*	TL.TimeGroup
  	
  ================================================== */
   
  TL.TimeGroup = TL.Class.extend({
  	
  	includes: [TL.Events, TL.DomMixins],
  	
  	_el: {},
  	
  	/*	Constructor
  	================================================== */
  	initialize: function(data) {
  		
  		// DOM ELEMENTS
  		this._el = {
  			parent: {},
  			container: {},
  			message: {}
  		};
  		
  		//Options
  		this.options = {
  			width: 					600,
  			height: 				600
  		};
  		
  		// Data
  		this.data = {
  			label: "",
  			rows: 1
  		};
  		
  		
  		this._el.container = TL.Dom.create("div", "tl-timegroup"); 
  		
  		// Merge Data
  		TL.Util.mergeData(this.data, data);
  		
  		// Animation
  		this.animator = {};
  		
  		
  		this._initLayout();
  		this._initEvents();
  	},
  	
  	/*	Public
  	================================================== */
  	
  	
  	
  	/*	Update Display
  	================================================== */
  	updateDisplay: function(w, h) {
  		
  	},
  	
  	setRowPosition: function(n, h) {
  		// trace(n);
  		// trace(this._el.container)
  		this.options.height = h * this.data.rows;
  		this.setPosition({top:n});
  		this._el.container.style.height = this.options.height + "px";
  		
  	},
  	
  	setAlternateRowColor: function(alternate, hide) {
  		var class_name = "tl-timegroup";
  		if (alternate) {
  			class_name += " tl-timegroup-alternate";
  		}
  		if (hide) {
  			class_name += " tl-timegroup-hidden";
  		}
  		this._el.container.className = class_name;
  	},
  	
  	/*	Events
  	================================================== */
  
  	
  	_onMouseClick: function() {
  		this.fire("clicked", this.options);
  	},
  
  	
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  		
  		// Create Layout
  		this._el.message = TL.Dom.create("div", "tl-timegroup-message", this._el.container);
  		this._el.message.innerHTML = this.data.label;
  		
  		
  	},
  	
  	_initEvents: function () {
  		TL.DomEvent.addListener(this._el.container, 'click', this._onMouseClick, this);
  	},
  	
  	// Update Display
  	_updateDisplay: function(width, height, animate) {
  		
  	}
  	
  });
  
  /* **********************************************
       Begin TL.TimeScale.js
  ********************************************** */
  
  /*  TL.TimeScale
      Strategies for laying out the timenav
      make a new one if the slides change
  
      TODOS: deal with clustering
  ================================================== */
  TL.TimeScale = TL.Class.extend({
  
      initialize: function (timeline_config, options) {
  
          var slides = timeline_config.events;
          this._scale = timeline_config.scale;
  
          options = TL.Util.mergeData({ // establish defaults
              display_width: 500,
              screen_multiplier: 3,
              max_rows: null
          }, options);
  
          this._display_width = options.display_width;
          this._screen_multiplier = options.screen_multiplier;
          this._pixel_width = this._screen_multiplier * this._display_width;
  
          this._group_labels = undefined;
          this._positions = [];
          this._pixels_per_milli = 0;
  
          this._earliest = timeline_config.getEarliestDate().getTime();
          this._latest = timeline_config.getLatestDate().getTime();
          this._span_in_millis = this._latest - this._earliest;
          if (this._span_in_millis <= 0) {
              this._span_in_millis = this._computeDefaultSpan(timeline_config);
          }
          this._average = (this._span_in_millis)/slides.length;
  
          this._pixels_per_milli = this.getPixelWidth() / this._span_in_millis;
  
          this._axis_helper = TL.AxisHelper.getBestHelper(this);
  
          this._scaled_padding = (1/this.getPixelsPerTick()) * (this._display_width/2)
          this._computePositionInfo(slides, options.max_rows);
      },
  
      _computeDefaultSpan: function(timeline_config) {
          // this gets called when all events are at the same instant,
          // or maybe when the span_in_millis is > 0 but still below a desired threshold
          // TODO: does this need smarts about eras?
          if (timeline_config.scale == 'human') {
              var formats = {}
              for (var i = 0; i < timeline_config.events.length; i++) {
                  var fmt = timeline_config.events[i].start_date.findBestFormat();
                  formats[fmt] = (formats[fmt]) ? formats[fmt] + 1 : 1;
              };
  
              for (var i = TL.Date.SCALES.length - 1; i >= 0; i--) {
                  if (formats.hasOwnProperty(TL.Date.SCALES[i][0])) {
                      var scale = TL.Date.SCALES[TL.Date.SCALES.length - 1]; // default
                      if (TL.Date.SCALES[i+1]) {
                          scale = TL.Date.SCALES[i+1]; // one larger than the largest in our data
                      }
                      return scale[1]
                  }
              };
              return 365 * 24 * 60 * 60 * 1000; // default to a year?
          }
  
          return 200000; // what is the right handling for cosmo dates?
      },
      getGroupLabels: function() { /*
          return an array of objects, one per group, in the order (top to bottom) that the groups are expected to appear. Each object will have two properties:
              * label (the string as specified in one or more 'group' properties of events in the configuration)
              * rows (the number of rows occupied by events associated with the label. )
          */
          return (this._group_labels || []);
      },
  
      getScale: function() {
          return this._scale;
      },
  
      getNumberOfRows: function() {
          return this._number_of_rows
      },
  
      getPixelWidth: function() {
          return this._pixel_width;
      },
  
      getPosition: function(time_in_millis) {
          // be careful using millis, as they won't scale to cosmological time.
          // however, we're moving to make the arg to this whatever value
          // comes from TL.Date.getTime() which could be made smart about that --
          // so it may just be about the naming.
          return ( time_in_millis - this._earliest ) * this._pixels_per_milli
      },
  
      getPositionInfo: function(idx) {
          return this._positions[idx];
      },
  
      getPixelsPerTick: function() {
          return this._axis_helper.getPixelsPerTick(this._pixels_per_milli);
      },
  
      getTicks: function() {
          return {
              major: this._axis_helper.getMajorTicks(this),
              minor: this._axis_helper.getMinorTicks(this) }
      },
  
      getDateFromTime: function(t) {
          if(this._scale == 'human') {
              return new TL.Date(t);
          } else if(this._scale == 'cosmological') {
              return new TL.BigDate(new TL.BigYear(t));
          }
          throw new TL.Error("time_scale_scale_err", this._scale);
      },
  
      getMajorScale: function() {
          return this._axis_helper.major.name;
      },
  
      getMinorScale: function() {
          return this._axis_helper.minor.name;
      },
  
      _assessGroups: function(slides) {
          var groups = [];
          var empty_group = false;
          for (var i = 0; i < slides.length; i++) {
              if(slides[i].group) {
                  if(groups.indexOf(slides[i].group) < 0) {
                      groups.push(slides[i].group);
                  } else {
                      empty_group = true;
                  }
              }
          };
          if (groups.length && empty_group) {
              groups.push('');
          }
          return groups;
      },
  
      /*  Compute the marker row positions, minimizing the number of
          overlaps.
  
          @positions = list of objects from this._positions
          @rows_left = number of rows available (assume > 0)
      */
      _computeRowInfo: function(positions, rows_left) {
          var lasts_in_row = [];
          var n_overlaps = 0;
  
          for (var i = 0; i < positions.length; i++) {
              var pos_info = positions[i];
              var overlaps = [];
  
              // See if we can add item to an existing row without
              // overlapping the previous item in that row
              delete pos_info.row;
  
              for (var j = 0; j < lasts_in_row.length; j++) {
                  overlaps.push(lasts_in_row[j].end - pos_info.start);
                  if(overlaps[j] <= 0) {
                      pos_info.row = j;
                      lasts_in_row[j] = pos_info;
                      break;
                  }
              }
  
              // If we couldn't add to an existing row without overlap...
              if (typeof(pos_info.row) == 'undefined') {
                  if (rows_left === null) {
                      // Make a new row
                      pos_info.row = lasts_in_row.length;
                      lasts_in_row.push(pos_info);
                  } else if (rows_left > 0) {
                      // Make a new row
                      pos_info.row = lasts_in_row.length;
                      lasts_in_row.push(pos_info);
                      rows_left--;
                  } else {
                      // Add to existing row with minimum overlap.
                      var min_overlap = Math.min.apply(null, overlaps);
                      var idx = overlaps.indexOf(min_overlap);
                      pos_info.row = idx;
                      if (pos_info.end > lasts_in_row[idx].end) {
                          lasts_in_row[idx] = pos_info;
                      }
                      n_overlaps++;
                  }
              }
          }
  
          return {n_rows: lasts_in_row.length, n_overlaps: n_overlaps};
      },
  
      /*  Compute marker positions.  If using groups, this._number_of_rows
          will never be less than the number of groups.
  
          @max_rows = total number of available rows
          @default_marker_width should be in pixels
      */
      _computePositionInfo: function(slides, max_rows, default_marker_width) {
          default_marker_width = default_marker_width || 100;
  
          var groups = [];
          var empty_group = false;
  
          // Set start/end/width; enumerate groups
          for (var i = 0; i < slides.length; i++) {
              var pos_info = {
                  start: this.getPosition(slides[i].start_date.getTime())
              };
              this._positions.push(pos_info);
  
              if (typeof(slides[i].end_date) != 'undefined') {
                  var end_pos = this.getPosition(slides[i].end_date.getTime());
                  pos_info.width = end_pos - pos_info.start;
                  if (pos_info.width > default_marker_width) {
                      pos_info.end = pos_info.start + pos_info.width;
                  } else {
                      pos_info.end = pos_info.start + default_marker_width;
                  }
              } else {
                  pos_info.width = default_marker_width;
                  pos_info.end = pos_info.start + default_marker_width;
              }
  
              if(slides[i].group) {
                  if(groups.indexOf(slides[i].group) < 0) {
                      groups.push(slides[i].group);
                  }
              } else {
                  empty_group = true;
              }
          }
  
          if(!(groups.length)) {
              var result = this._computeRowInfo(this._positions, max_rows);
              this._number_of_rows = result.n_rows;
          } else {
              if(empty_group) {
                  groups.push("");
              }
  
              // Init group info
              var group_info = [];
  
              for(var i = 0; i < groups.length; i++) {
                  group_info[i] = {
                      label: groups[i],
                      idx: i,
                      positions: [],
                      n_rows: 1,      // default
                      n_overlaps: 0
                  };
              }
  
              for(var i = 0; i < this._positions.length; i++) {
                  var pos_info = this._positions[i];
  
                  pos_info.group = groups.indexOf(slides[i].group || "");
                  pos_info.row = 0;
  
                  var gi = group_info[pos_info.group];
                  for(var j = gi.positions.length - 1; j >= 0; j--) {
                      if(gi.positions[j].end > pos_info.start) {
                          gi.n_overlaps++;
                      }
                  }
  
                  gi.positions.push(pos_info);
              }
  
              var n_rows = groups.length; // start with 1 row per group
  
              while(true) {
                  // Count free rows available
                  var rows_left = Math.max(0, max_rows - n_rows);
                  if(!rows_left) {
                      break;  // no free rows, nothing to do
                  }
  
                  // Sort by # overlaps, idx
                 group_info.sort(function(a, b) {
                      if(a.n_overlaps > b.n_overlaps) {
                          return -1;
                      } else if(a.n_overlaps < b.n_overlaps) {
                          return 1;
                      }
                      return a.idx - b.idx;
                  });
                  if(!group_info[0].n_overlaps) {
                      break; // no overlaps, nothing to do
                  }
  
                  // Distribute free rows among groups with overlaps
                  var n_rows = 0;
                  for(var i = 0; i < group_info.length; i++) {
                      var gi = group_info[i];
  
                      if(gi.n_overlaps && rows_left) {
                          var res = this._computeRowInfo(gi.positions,  gi.n_rows + 1);
                          gi.n_rows = res.n_rows;     // update group info
                          gi.n_overlaps = res.n_overlaps;
                          rows_left--;                // update rows left
                      }
  
                      n_rows += gi.n_rows;            // update rows used
                  }
              }
  
              // Set number of rows
              this._number_of_rows = n_rows;
  
              // Set group labels; offset row positions
              this._group_labels = [];
  
              group_info.sort(function(a, b) {return a.idx - b.idx; });
  
              for(var i = 0, row_offset = 0; i < group_info.length; i++) {
                  this._group_labels.push({
                      label: group_info[i].label,
                      rows: group_info[i].n_rows
                  });
  
                  for(var j = 0; j < group_info[i].positions.length; j++) {
                      var pos_info = group_info[i].positions[j];
                      pos_info.row += row_offset;
                  }
  
                  row_offset += group_info[i].n_rows;
              }
          }
  
      }
  });
  
  
  /* **********************************************
       Begin TL.TimeAxis.js
  ********************************************** */
  
  /*	TL.TimeAxis
  	Display element for showing timescale ticks
  ================================================== */
  
  TL.TimeAxis = TL.Class.extend({
  
  	includes: [TL.Events, TL.DomMixins, TL.I18NMixins],
  
  	_el: {},
  
  	/*	Constructor
  	================================================== */
  	initialize: function(elem, options) {
  		// DOM Elements
  		this._el = {
  			container: {},
  			content_container: {},
  			major: {},
  			minor: {},
  		};
  
  		// Components
  		this._text			= {};
  
  		// State
  		this._state = {
  			loaded: 		false
  		};
  
  
  		// Data
  		this.data = {};
  
  		// Options
  		this.options = {
  			duration: 				1000,
  			ease: 					TL.Ease.easeInSpline,
  			width: 					600,
  			height: 				600
  		};
  
  		// Actively Displaying
  		this.active = false;
  
  		// Animation Object
  		this.animator = {};
  
  		// Axis Helper
  		this.axis_helper = {};
  
  		// Minor tick dom element array
  		this.minor_ticks = [];
  
  		// Minor tick dom element array
  		this.major_ticks = [];
  
  		// Date Format Lookup, map TL.Date.SCALES names to...
  		this.dateformat_lookup = {
  	        millisecond: 'time_milliseconds',     // ...TL.Language.<code>.dateformats
  	        second: 'time_short',
  	        minute: 'time_no_seconds_short',
  	        hour: 'time_no_minutes_short',
  	        day: 'full_short',
  	        month: 'month_short',
  	        year: 'year',
  	        decade: 'year',
  	        century: 'year',
  	        millennium: 'year',
  	        age: 'compact',  // ...TL.Language.<code>.bigdateformats
  	        epoch: 'compact',
  	        era: 'compact',
  	        eon: 'compact',
  	        eon2: 'compact'
  	    }
  
  		// Main element
  		if (typeof elem === 'object') {
  			this._el.container = elem;
  		} else {
  			this._el.container = TL.Dom.get(elem);
  		}
  
  		// Merge Data and Options
  		TL.Util.mergeData(this.options, options);
  
  		this._initLayout();
  		this._initEvents();
  
  	},
  
  	/*	Adding, Hiding, Showing etc
  	================================================== */
  	show: function() {
  
  	},
  
  	hide: function() {
  
  	},
  
  	addTo: function(container) {
  		container.appendChild(this._el.container);
  	},
  
  	removeFrom: function(container) {
  		container.removeChild(this._el.container);
  	},
  
  	updateDisplay: function(w, h) {
  		this._updateDisplay(w, h);
  	},
  
  	getLeft: function() {
  		return this._el.container.style.left.slice(0, -2);
  	},
  
  	drawTicks: function(timescale, optimal_tick_width) {
  
  		var ticks = timescale.getTicks();
  
  		var controls = {
  			minor: {
  				el: this._el.minor,
  				dateformat: this.dateformat_lookup[ticks['minor'].name],
  				ts_ticks: ticks['minor'].ticks,
  				tick_elements: this.minor_ticks
  			},
  			major: {
  				el: this._el.major,
  				dateformat: this.dateformat_lookup[ticks['major'].name],
  				ts_ticks: ticks['major'].ticks,
  				tick_elements: this.major_ticks
  			}
  		}
  		// FADE OUT
  		this._el.major.className = "tl-timeaxis-major";
  		this._el.minor.className = "tl-timeaxis-minor";
  		this._el.major.style.opacity = 0;
  		this._el.minor.style.opacity = 0;
  
  		// CREATE MAJOR TICKS
  		this.major_ticks = this._createTickElements(
  			ticks['major'].ticks,
  			this._el.major,
  			this.dateformat_lookup[ticks['major'].name]
  		);
  
  		// CREATE MINOR TICKS
  		this.minor_ticks = this._createTickElements(
  			ticks['minor'].ticks,
  			this._el.minor,
  			this.dateformat_lookup[ticks['minor'].name],
  			ticks['major'].ticks
  		);
  
  		this.positionTicks(timescale, optimal_tick_width, true);
  
  		// FADE IN
  		this._el.major.className = "tl-timeaxis-major tl-animate-opacity tl-timeaxis-animate-opacity";
  		this._el.minor.className = "tl-timeaxis-minor tl-animate-opacity tl-timeaxis-animate-opacity";
  		this._el.major.style.opacity = 1;
  		this._el.minor.style.opacity = 1;
  	},
  
  	_createTickElements: function(ts_ticks,tick_element,dateformat,ticks_to_skip) {
  		tick_element.innerHTML = "";
  		var skip_times = {};
  
  		var yearZero = new Date(-1,13,-30);
  		skip_times[yearZero.getTime()] = true;
  
  		if (ticks_to_skip){
  			for (var i = 0; i < ticks_to_skip.length; i++) {
  				skip_times[ticks_to_skip[i].getTime()] = true;
  			}
  		}
  
  		var tick_elements = []
  		for (var i = 0; i < ts_ticks.length; i++) {
  			var ts_tick = ts_ticks[i];
  			if (!(ts_tick.getTime() in skip_times)) {
  				var tick = TL.Dom.create("div", "tl-timeaxis-tick", tick_element),
  					tick_text 	= TL.Dom.create("span", "tl-timeaxis-tick-text tl-animate-opacity", tick);
  
  				tick_text.innerHTML = ts_tick.getDisplayDate(this.getLanguage(), dateformat);
  
  				tick_elements.push({
  					tick:tick,
  					tick_text:tick_text,
  					display_date:ts_tick.getDisplayDate(this.getLanguage(), dateformat),
  					date:ts_tick
  				});
  			}
  		}
  		return tick_elements;
  	},
  
  	positionTicks: function(timescale, optimal_tick_width, no_animate) {
  
  		// Handle Animation
  		if (no_animate) {
  			this._el.major.className = "tl-timeaxis-major";
  			this._el.minor.className = "tl-timeaxis-minor";
  		} else {
  			this._el.major.className = "tl-timeaxis-major tl-timeaxis-animate";
  			this._el.minor.className = "tl-timeaxis-minor tl-timeaxis-animate";
  		}
  
  		this._positionTickArray(this.major_ticks, timescale, optimal_tick_width);
  		this._positionTickArray(this.minor_ticks, timescale, optimal_tick_width);
  
  	},
  
  	_positionTickArray: function(tick_array, timescale, optimal_tick_width) {
  		// Poition Ticks & Handle density of ticks
  		if (tick_array[1] && tick_array[0]) {
  			var distance = ( timescale.getPosition(tick_array[1].date.getMillisecond()) - timescale.getPosition(tick_array[0].date.getMillisecond()) ),
  				fraction_of_array = 1;
  
  
  			if (distance < optimal_tick_width) {
  				fraction_of_array = Math.round(optimal_tick_width/timescale.getPixelsPerTick());
  			}
  
  			var show = 1;
  
  			for (var i = 0; i < tick_array.length; i++) {
  
  				var tick = tick_array[i];
  
  				// Poition Ticks
  				tick.tick.style.left = timescale.getPosition(tick.date.getMillisecond()) + "px";
  				tick.tick_text.innerHTML = tick.display_date;
  
  				// Handle density of ticks
  				if (fraction_of_array > 1) {
  					if (show >= fraction_of_array) {
  						show = 1;
  						tick.tick_text.style.opacity = 1;
  						tick.tick.className = "tl-timeaxis-tick";
  					} else {
  						show++;
  						tick.tick_text.style.opacity = 0;
  						tick.tick.className = "tl-timeaxis-tick tl-timeaxis-tick-hidden";
  					}
  				} else {
  					tick.tick_text.style.opacity = 1;
  					tick.tick.className = "tl-timeaxis-tick";
  				}
  
  			};
  		}
  	},
  
  	/*	Events
  	================================================== */
  
  
  	/*	Private Methods
  	================================================== */
  	_initLayout: function () {
  		this._el.content_container		= TL.Dom.create("div", "tl-timeaxis-content-container", this._el.container);
  		this._el.major					= TL.Dom.create("div", "tl-timeaxis-major", this._el.content_container);
  		this._el.minor					= TL.Dom.create("div", "tl-timeaxis-minor", this._el.content_container);
  
  		// Fire event that the slide is loaded
  		this.onLoaded();
  	},
  
  	_initEvents: function() {
  
  	},
  
  	// Update Display
  	_updateDisplay: function(width, height, layout) {
  
  		if (width) {
  			this.options.width 					= width;
  		}
  
  		if (height) {
  			this.options.height = height;
  		}
  
  	}
  
  });
  
  
  /* **********************************************
       Begin TL.AxisHelper.js
  ********************************************** */
  
  /*  TL.AxisHelper
      Strategies for laying out the timenav
      markers and time axis
      Intended as a private class -- probably only known to TimeScale
  ================================================== */
  TL.AxisHelper = TL.Class.extend({
      initialize: function (options) {
  		if (options) {
              this.scale = options.scale;
  	        this.minor = options.minor;
  	        this.major = options.major;
  		} else {
              throw new TL.Error("axis_helper_no_options_err")
          }
         
      },
      
      getPixelsPerTick: function(pixels_per_milli) {
          return pixels_per_milli * this.minor.factor;
      },
  
      getMajorTicks: function(timescale) {
  		return this._getTicks(timescale, this.major)
      },
  
      getMinorTicks: function(timescale) {
          return this._getTicks(timescale, this.minor)
      },
  
      _getTicks: function(timescale, option) {
  
          var factor_scale = timescale._scaled_padding * option.factor;
          var first_tick_time = timescale._earliest - factor_scale;
          var last_tick_time = timescale._latest + factor_scale;
          var ticks = []
          for (var i = first_tick_time; i < last_tick_time; i += option.factor) {
              ticks.push(timescale.getDateFromTime(i).floor(option.name));
          }
  
          return {
              name: option.name,
              ticks: ticks
          }
  
      }
  
  });
  
  (function(cls){ // add some class-level behavior
  
      var HELPERS = {};
      
      var setHelpers = function(scale_type, scales) {
          HELPERS[scale_type] = [];
          
          for (var idx = 0; idx < scales.length - 1; idx++) {
              var minor = scales[idx];
              var major = scales[idx+1];
              HELPERS[scale_type].push(new cls({
                  scale: minor[3],
                  minor: { name: minor[0], factor: minor[1]},
                  major: { name: major[0], factor: major[1]}
              }));
          }
      };
      
      setHelpers('human', TL.Date.SCALES);
      setHelpers('cosmological', TL.BigDate.SCALES);
      
      cls.HELPERS = HELPERS;
      
      cls.getBestHelper = function(ts,optimal_tick_width) {
          if (typeof(optimal_tick_width) != 'number' ) {
              optimal_tick_width = 100;
          }
          var ts_scale = ts.getScale();
          var helpers = HELPERS[ts_scale];
          
          if (!helpers) {
              throw new TL.Error("axis_helper_scale_err", ts_scale);
          }
          
          var prev = null;
          for (var idx = 0; idx < helpers.length; idx++) {
              var curr = helpers[idx];
              var pixels_per_tick = curr.getPixelsPerTick(ts._pixels_per_milli);
              if (pixels_per_tick > optimal_tick_width)  {
                  if (prev == null) return curr;
                  var curr_dist = Math.abs(optimal_tick_width - pixels_per_tick);
                  var prev_dist = Math.abs(optimal_tick_width - pixels_per_tick);
                  if (curr_dist < prev_dist) {
                      return curr;
                  } else {
                      return prev;
                  }
              }
              prev = curr;
          }
          return helpers[helpers.length - 1]; // last resort           
      }
  })(TL.AxisHelper);
  
  
  /* **********************************************
       Begin TL.Timeline.js
  ********************************************** */
  
  /*  TimelineJS
  Designed and built by Zach Wise at KnightLab
  
  This Source Code Form is subject to the terms of the Mozilla Public
  License, v. 2.0. If a copy of the MPL was not distributed with this
  file, You can obtain one at https://mozilla.org/MPL/2.0/.
  
  ================================================== */
  /*
  TODO
  
  */
  
  /*  Required Files
  CodeKit Import
  https://incident57.com/codekit/
  ================================================== */
  
  // CORE
  	// @codekit-prepend "core/TL.js";
  	// @codekit-prepend "core/TL.Error.js";
  	// @codekit-prepend "core/TL.Util.js";
  	// @codekit-prepend "data/TL.Data.js";
  	// @codekit-prepend "core/TL.Class.js";
  	// @codekit-prepend "core/TL.Events.js";
  	// @codekit-prepend "core/TL.Browser.js";
  	// @codekit-prepend "core/TL.Load.js";
  	// @codekit-prepend "core/TL.TimelineConfig.js";
  	// @codekit-prepend "core/TL.ConfigFactory.js";
  
  
  // LANGUAGE
  	// @codekit-prepend "language/TL.Language.js";
  	// @codekit-prepend "language/TL.I18NMixins.js";
  
  // ANIMATION
  	// @codekit-prepend "animation/TL.Ease.js";
  	// @codekit-prepend "animation/TL.Animate.js";
  
  // DOM
  	// @codekit-prepend "dom/TL.Point.js";
  	// @codekit-prepend "dom/TL.DomMixins.js";
  	// @codekit-prepend "dom/TL.Dom.js";
  	// @codekit-prepend "dom/TL.DomUtil.js";
  	// @codekit-prepend "dom/TL.DomEvent.js";
  	// @codekit-prepend "dom/TL.StyleSheet.js";
  
  // Date
  	// @codekit-prepend "date/TL.Date.js";
  	// @codekit-prepend "date/TL.DateUtil.js";
  
  // UI
  	// @codekit-prepend "ui/TL.Draggable.js";
  	// @codekit-prepend "ui/TL.Swipable.js";
  	// @codekit-prepend "ui/TL.MenuBar.js";
  	// @codekit-prepend "ui/TL.Message.js";
  
  // MEDIA
  	// @codekit-prepend "media/TL.MediaType.js";
  	// @codekit-prepend "media/TL.Media.js";
  
  // MEDIA TYPES
  	// @codekit-prepend "media/types/TL.Media.Blockquote.js";
  	// @codekit-prepend "media/types/TL.Media.DailyMotion.js";
  	// @codekit-prepend "media/types/TL.Media.DocumentCloud.js";
  	// @codekit-prepend "media/types/TL.Media.Flickr.js";
  	// @codekit-prepend "media/types/TL.Media.GoogleDoc.js";
  	// @codekit-prepend "media/types/TL.Media.GooglePlus.js";
  	// @codekit-prepend "media/types/TL.Media.IFrame.js";
  	// @codekit-prepend "media/types/TL.Media.Image.js";
  	// @codekit-prepend "media/types/TL.Media.Imgur.js";
  	// @codekit-prepend "media/types/TL.Media.Instagram.js";
  	// @codekit-prepend "media/types/TL.Media.GoogleMap.js";
  	// @codekit-prepend "media/types/TL.Media.PDF.js";
  	// @codekit-prepend "media/types/TL.Media.Profile.js";
  	// @codekit-prepend "media/types/TL.Media.Slider.js";
  	// @codekit-prepend "media/types/TL.Media.SoundCloud.js";
  	// @codekit-prepend "media/types/TL.Media.Spotify.js";
  	// @codekit-prepend "media/types/TL.Media.Storify.js";
  	// @codekit-prepend "media/types/TL.Media.Text.js";
  	// @codekit-prepend "media/types/TL.Media.Twitter.js";
  	// @codekit-prepend "media/types/TL.Media.TwitterEmbed.js";
  	// @codekit-prepend "media/types/TL.Media.Vimeo.js";
  	// @codekit-prepend "media/types/TL.Media.Vine.js";
  	// @codekit-prepend "media/types/TL.Media.Website.js";
  	// @codekit-prepend "media/types/TL.Media.Wikipedia.js";
  	// @codekit-prepend "media/types/TL.Media.Wistia.js";
  	// @codekit-prepend "media/types/TL.Media.YouTube.js";
  	// @codekit-prepend "media/types/TL.Media.Audio.js";
  	// @codekit-prepend "media/types/TL.Media.Video.js";
  
  // STORYSLIDER
  	// @codekit-prepend "slider/TL.Slide.js";
  	// @codekit-prepend "slider/TL.SlideNav.js";
  	// @codekit-prepend "slider/TL.StorySlider.js";
  
  // TIMENAV
  	// @codekit-prepend "timenav/TL.TimeNav.js";
  	// @codekit-prepend "timenav/TL.TimeMarker.js";
  	// @codekit-prepend "timenav/TL.TimeEra.js";
  	// @codekit-prepend "timenav/TL.TimeGroup.js";
  	// @codekit-prepend "timenav/TL.TimeScale.js";
  	// @codekit-prepend "timenav/TL.TimeAxis.js";
  	// @codekit-prepend "timenav/TL.AxisHelper.js";
  
  
  TL.Timeline = TL.Class.extend({
  	includes: [TL.Events, TL.I18NMixins],
  
  	/*  Private Methods
  	================================================== */
  	initialize: function (elem, data, options) {
  		var self = this;
  		if (!options) { options = {}};
  		// Version
  		this.version = "3.2.6";
  
  		// Ready
  		this.ready = false;
  
  		// DOM ELEMENTS
  		this._el = {
  			container: {},
  			storyslider: {},
  			timenav: {},
  			menubar: {}
  		};
  
  		// Determine Container Element
  		if (typeof elem === 'object') {
  			this._el.container = elem;
  		} else {
  			this._el.container = TL.Dom.get(elem);
  		}
  
  		// Slider
  		this._storyslider = {};
  
  		// Style Sheet
  		this._style_sheet = new TL.StyleSheet();
  
  		// TimeNav
  		this._timenav = {};
  
  		// Menu Bar
  		this._menubar = {};
  
  		// Loaded State
  		this._loaded = {storyslider:false, timenav:false};
  
  		// Data Object
  		this.config = null;
  
  		this.options = {
  			script_path: 				"",
  			height: 					this._el.container.offsetHeight,
  			width: 						this._el.container.offsetWidth,
  			debug: 						false,
  			is_embed: 					false,
  			is_full_embed: 				false,
  			hash_bookmark: false,
  			default_bg_color: 			{r:255, g:255, b:255},
  			scale_factor: 				2,						// How many screen widths wide should the timeline be
  			layout: 					"landscape",			// portrait or landscape
  			timenav_position: 			"bottom",				// timeline on top or bottom
  			optimal_tick_width: 		60,						// optimal distance (in pixels) between ticks on axis
  			base_class: 				"tl-timeline", 		// removing tl-timeline will break all default stylesheets...
  			timenav_height: 			null,
  			timenav_height_percentage: 	25,						// Overrides timenav height as a percentage of the screen
  			timenav_mobile_height_percentage: 40, 				// timenav height as a percentage on mobile devices
  			timenav_height_min: 		175,					// Minimum timenav height
  			marker_height_min: 			30,						// Minimum Marker Height
  			marker_width_min: 			100,					// Minimum Marker Width
  			marker_padding: 			5,						// Top Bottom Marker Padding
  			start_at_slide: 			0,
  			start_at_end: 				false,
  			menubar_height: 			0,
  			skinny_size: 				650,
  			medium_size: 				800,
  			relative_date: 				false,					// Use momentjs to show a relative date from the slide.text.date.created_time field
  			use_bc: 					false,					// Use declared suffix on dates earlier than 0
  			// animation
  			duration: 					1000,
  			ease: 						TL.Ease.easeInOutQuint,
  			// interaction
  			dragging: 					true,
  			trackResize: 				true,
  			map_type: 					"stamen:toner-lite",
  			slide_padding_lr: 			100,					// padding on slide of slide
  			slide_default_fade: 		"0%",					// landscape fade
  			zoom_sequence: 				[0.5, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89], // Array of Fibonacci numbers for TimeNav zoom levels
  			language: 					"en",
  			ga_property_id: 			null,
  			track_events: 				['back_to_start','nav_next','nav_previous','zoom_in','zoom_out' ]
  		};
  
  		// Animation Objects
  		this.animator_timenav = null;
  		this.animator_storyslider = null;
  		this.animator_menubar = null;
  
  		// Add message to DOM
  		this.message = new TL.Message({}, {message_class: "tl-message-full"}, this._el.container);
  
  		// Merge Options
  		if (typeof(options.default_bg_color) == "string") {
  			var parsed = TL.Util.hexToRgb(options.default_bg_color); // will clear it out if its invalid
  			if (parsed) {
  				options.default_bg_color = parsed;
  			} else {
  				delete options.default_bg_color
  				trace("Invalid default background color. Ignoring.");
  			}
  		}
  		TL.Util.mergeData(this.options, options);
  
  		window.addEventListener("resize", function(e){
  			self.updateDisplay();
  		});
  
  		// Set Debug Mode
  		TL.debug = this.options.debug;
  
  		// Apply base class to container
  		TL.DomUtil.addClass(this._el.container, 'tl-timeline');
  
  		if (this.options.is_embed) {
  			TL.DomUtil.addClass(this._el.container, 'tl-timeline-embed');
  		}
  
  		if (this.options.is_full_embed) {
  			TL.DomUtil.addClass(this._el.container, 'tl-timeline-full-embed');
  		}
  
  		document.addEventListener("keydown", function(event) {
  			var keyName = event.key;
  			var currentSlide = self._getSlideIndex(self.current_id);
  			var _n = self.config.events.length - 1;
  			var lastSlide = self.config.title ? _n + 1 : _n;
  			var firstSlide = 0;
  
  			if (keyName == 'ArrowLeft'){
  				if (currentSlide!=firstSlide){
  					self.goToPrev();
  				}
  			}
  			else if (keyName == 'ArrowRight'){
  				if (currentSlide!=lastSlide){
  					self.goToNext();
  				}
  			}
  		});
  
  		// Use Relative Date Calculations
  		// NOT YET IMPLEMENTED
  		if(this.options.relative_date) {
  			if (typeof(moment) !== 'undefined') {
  				self._loadLanguage(data);
  			} else {
  				TL.Load.js(this.options.script_path + "/library/moment.js", function() {
  					self._loadLanguage(data);
  					trace("LOAD MOMENTJS")
  				});
  			}
  		} else {
  			self._loadLanguage(data);
  		}
  
  	},
  	_translateError: function(e) {
  	    if(e.hasOwnProperty('stack')) {
  	        trace(e.stack);
  	    }
  	    if(e.message_key) {
  	        return this._(e.message_key) + (e.detail ? ' [' + e.detail +']' : '')
  	    }
  	    return e;
  	},
  
  	/*  Load Language
  	================================================== */
  	_loadLanguage: function(data) {
  		try {
  		    this.options.language = new TL.Language(this.options);
  
  		    this._initData(data);
  		} catch(e) {
  		    this.showMessage(this._translateError(e));
  		}
  	},
  
  
  	/*  Navigation
  	================================================== */
  
  	// Goto slide with id
  	goToId: function(id) {
  		if (this.current_id != id) {
  			this.current_id = id;
  			this._timenav.goToId(this.current_id);
  			this._storyslider.goToId(this.current_id, false, true);
  			this.fire("change", {unique_id: this.current_id}, this);
  		}
  	},
  
  	// Goto slide n
  	goTo: function(n) {
  		if(this.config.title) {
  			if(n == 0) {
  				this.goToId(this.config.title.unique_id);
  			} else {
  				this.goToId(this.config.events[n - 1].unique_id);
  			}
  		} else {
  			this.goToId(this.config.events[n].unique_id);
  		}
  	},
  
  	// Goto first slide
  	goToStart: function() {
  		this.goTo(0);
  	},
  
  	// Goto last slide
  	goToEnd: function() {
  		var _n = this.config.events.length - 1;
  		this.goTo(this.config.title ? _n + 1 : _n);
  	},
  
  	// Goto previous slide
  	goToPrev: function() {
  		this.goTo(this._getSlideIndex(this.current_id) - 1);
  	},
  
  	// Goto next slide
  	goToNext: function() {
  		this.goTo(this._getSlideIndex(this.current_id) + 1);
  	},
  
  	/* Event maniupluation
  	================================================== */
  
  	// Add an event
  	add: function(data) {
  		var unique_id = this.config.addEvent(data);
  
  		var n = this._getEventIndex(unique_id);
  		var d = this.config.events[n];
  
  		this._storyslider.createSlide(d, this.config.title ? n+1 : n);
  		this._storyslider._updateDrawSlides();
  
  		this._timenav.createMarker(d, n);
  		this._timenav._updateDrawTimeline(false);
  
  		this.fire("added", {unique_id: unique_id});
  	},
  
  	// Remove an event
  	remove: function(n) {
  		if(n >= 0  && n < this.config.events.length) {
  			// If removing the current, nav to new one first
  			if(this.config.events[n].unique_id == this.current_id) {
  				if(n < this.config.events.length - 1) {
  					this.goTo(n + 1);
  				} else {
  					this.goTo(n - 1);
  				}
  			}
  
  			var event = this.config.events.splice(n, 1);
  			delete this.config.event_dict[event[0].unique_id];
  			this._storyslider.destroySlide(this.config.title ? n+1 : n);
  			this._storyslider._updateDrawSlides();
  
  			this._timenav.destroyMarker(n);
  			this._timenav._updateDrawTimeline(false);
  
  			this.fire("removed", {unique_id: event[0].unique_id});
  		}
  	},
  
  	removeId: function(id) {
  		this.remove(this._getEventIndex(id));
  	},
  
  	/* Get slide data
  	================================================== */
  
  	getData: function(n) {
  		if(this.config.title) {
  			if(n == 0) {
  				return this.config.title;
  			} else if(n > 0 && n <= this.config.events.length) {
  				return this.config.events[n - 1];
  			}
  		} else if(n >= 0 && n < this.config.events.length) {
  			return this.config.events[n];
  		}
  		return null;
  	},
  
  	getDataById: function(id) {
  		return this.getData(this._getSlideIndex(id));
  	},
  
  	/* Get slide object
  	================================================== */
  
  	getSlide: function(n) {
  		if(n >= 0 && n < this._storyslider._slides.length) {
  			return this._storyslider._slides[n];
  		}
  		return null;
  	},
  
  	getSlideById: function(id) {
  		return this.getSlide(this._getSlideIndex(id));
  	},
  
  	getCurrentSlide: function() {
  		return this.getSlideById(this.current_id);
  	},
  
  
  	/*  Display
  	================================================== */
  	updateDisplay: function() {
  		if (this.ready) {
  			this._updateDisplay();
  		}
  	},
  
    	/*
    		Compute the height of the navigation section of the Timeline, taking into account
    		the possibility of an explicit height or height percentage, but also honoring the
    		`timenav_height_min` option value. If `timenav_height` is specified it takes precedence over `timenav_height_percentage` but in either case, if the resultant pixel height is less than `options.timenav_height_min` then the value of `options.timenav_height_min` will be returned. (A minor adjustment is made to the returned value to account for marker padding.)
  
    		Arguments:
    		@timenav_height (optional): an integer value for the desired height in pixels
    		@timenav_height_percentage (optional): an integer between 1 and 100
  
    	 */
  	_calculateTimeNavHeight: function(timenav_height, timenav_height_percentage) {
  
  		var height = 0;
  
  		if (timenav_height) {
  			height = timenav_height;
  		} else {
  			if (this.options.timenav_height_percentage || timenav_height_percentage) {
  				if (timenav_height_percentage) {
  					height = Math.round((this.options.height/100)*timenav_height_percentage);
  				} else {
  					height = Math.round((this.options.height/100)*this.options.timenav_height_percentage);
  				}
  
  			}
  		}
  
  		// Set new minimum based on how many rows needed
  		if (this._timenav.ready) {
  			if (this.options.timenav_height_min < this._timenav.getMinimumHeight()) {
  				this.options.timenav_height_min = this._timenav.getMinimumHeight();
  			}
  		}
  
  		// If height is less than minimum set it to minimum
  		if (height < this.options.timenav_height_min) {
  			height = this.options.timenav_height_min;
  		}
  
  		height = height - (this.options.marker_padding * 2);
  
  		return height;
  	},
  
  	/*  Private Methods
  	================================================== */
  
  	// Update View
  	_updateDisplay: function(timenav_height, animate, d) {
  		var duration    = this.options.duration,
  		display_class   = this.options.base_class,
  		menu_position   = 0,
  		self      = this;
  
  		if (d) {
  			duration = d;
  		}
  
  		// Update width and height
  		this.options.width = this._el.container.offsetWidth;
  		this.options.height = this._el.container.offsetHeight;
  
  		// Check if skinny
  		if (this.options.width <= this.options.skinny_size) {
  			display_class += " tl-skinny";
  			this.options.layout = "portrait";
  		} else if (this.options.width <= this.options.medium_size) {
  			display_class += " tl-medium";
  			this.options.layout = "landscape";
  		} else {
  			this.options.layout = "landscape";
  		}
  
  		// Detect Mobile and Update Orientation on Touch devices
  		if (TL.Browser.touch) {
  			this.options.layout = TL.Browser.orientation();
  		}
  
  		if (TL.Browser.mobile) {
  			display_class += " tl-mobile";
  			// Set TimeNav Height
  			this.options.timenav_height = this._calculateTimeNavHeight(timenav_height, this.options.timenav_mobile_height_percentage);
  		} else {
  			// Set TimeNav Height
  			this.options.timenav_height = this._calculateTimeNavHeight(timenav_height);
  		}
  
  		// LAYOUT
  		if (this.options.layout == "portrait") {
  			// Portrait
  			display_class += " tl-layout-portrait";
  
  		} else {
  			// Landscape
  			display_class += " tl-layout-landscape";
  
  		}
  
  		// Set StorySlider Height
  		this.options.storyslider_height = (this.options.height - this.options.timenav_height);
  
  		// Positon Menu
  		if (this.options.timenav_position == "top") {
  			menu_position = ( Math.ceil(this.options.timenav_height)/2 ) - (this._el.menubar.offsetHeight/2) - (39/2) ;
  		} else {
  			menu_position = Math.round(this.options.storyslider_height + 1 + ( Math.ceil(this.options.timenav_height)/2 ) - (this._el.menubar.offsetHeight/2) - (35/2));
  		}
  
  
  		if (animate) {
  
  			// Animate TimeNav
  
  			/*
  			if (this.animator_timenav) {
  			this.animator_timenav.stop();
  			}
  
  			this.animator_timenav = TL.Animate(this._el.timenav, {
  			height:   (this.options.timenav_height) + "px",
  			duration:   duration/4,
  			easing:   TL.Ease.easeOutStrong,
  			complete: function () {
  			//self._map.updateDisplay(self.options.width, self.options.timenav_height, animate, d, self.options.menubar_height);
  			}
  			});
  			*/
  
  			this._el.timenav.style.height = Math.ceil(this.options.timenav_height) + "px";
  
  			// Animate StorySlider
  			if (this.animator_storyslider) {
  				this.animator_storyslider.stop();
  			}
  			this.animator_storyslider = TL.Animate(this._el.storyslider, {
  				height:   this.options.storyslider_height + "px",
  				duration:   duration/2,
  				easing:   TL.Ease.easeOutStrong
  			});
  
  			// Animate Menubar
  			if (this.animator_menubar) {
  				this.animator_menubar.stop();
  			}
  
  			this.animator_menubar = TL.Animate(this._el.menubar, {
  				top:  menu_position + "px",
  				duration:   duration/2,
  				easing:   TL.Ease.easeOutStrong
  			});
  
  		} else {
  			// TimeNav
  			this._el.timenav.style.height = Math.ceil(this.options.timenav_height) + "px";
  
  			// StorySlider
  			this._el.storyslider.style.height = this.options.storyslider_height + "px";
  
  			// Menubar
  			this._el.menubar.style.top = menu_position + "px";
  		}
  
  		if (this.message) {
  			this.message.updateDisplay(this.options.width, this.options.height);
  		}
  		// Update Component Displays
  		this._timenav.updateDisplay(this.options.width, this.options.timenav_height, animate);
  		this._storyslider.updateDisplay(this.options.width, this.options.storyslider_height, animate, this.options.layout);
  
  		if (this.options.language.direction == 'rtl') {
  			display_class += ' tl-rtl';
  		}
  
  
  		// Apply class
  		this._el.container.className = display_class;
  
  	},
  
  	// Update hashbookmark in the url bar
  	_updateHashBookmark: function(id) {
  		var hash = "#" + "event-" + id.toString();
  		if (window.location.protocol != 'file:') {
  			window.history.replaceState(null, "Browsing TimelineJS", hash);
  		}
  		this.fire("hash_updated", {unique_id:this.current_id, hashbookmark:"#" + "event-" + id.toString()}, this);
  	},
  
  	/*  Init
  	================================================== */
  	// Initialize the data
  	_initData: function(data) {
  		var self = this;
  
  		if (typeof data == 'string') {
  			var self = this;
  			TL.ConfigFactory.makeConfig(data, function(config) {
  				self.setConfig(config);
  			});
  		} else if (TL.TimelineConfig == data.constructor) {
  			this.setConfig(data);
  		} else {
  			this.setConfig(new TL.TimelineConfig(data));
  		}
  	},
  
  	setConfig: function(config) {
  		this.config = config;
  		this.config.validate();
  		this._validateOptions();
  		if (this.config.isValid()) {
  		    try {
  			    this._onDataLoaded();
  			} catch(e) {
  			    this.showMessage("<strong>"+ this._('error') +":</strong> " + this._translateError(e));
  			}
  		} else {
  		    var translated_errs = [];
  
  		    for(var i = 0, errs = this.config.getErrors(); i < errs.length; i++) {
  		        translated_errs.push(this._translateError(errs[i]));
  		    }
  
  			this.showMessage("<strong>"+ this._('error') +":</strong> " + translated_errs.join('<br>'));
  			// should we set 'self.ready'? if not, it won't resize,
  			// but most resizing would only work
  			// if more setup happens
  		}
  	},
  	_validateOptions: function() {
  		// assumes that this.options and this.config have been set.
  		var INTEGER_PROPERTIES = ['timenav_height', 'timenav_height_min', 'marker_height_min', 'marker_width_min', 'marker_padding', 'start_at_slide', 'slide_padding_lr'  ];
  
  		for (var i = 0; i < INTEGER_PROPERTIES.length; i++) {
  				var opt = INTEGER_PROPERTIES[i];
  				var value = this.options[opt];
  				valid = true;
  				if (typeof(value) == 'number') {
  					valid = (value == parseInt(value))
  				} else if (typeof(value) == "string") {
  					valid = (value.match(/^\s*(\-?\d+)?\s*$/));
  				}
  				if (!valid) {
  					this.config.logError({ message_key: 'invalid_integer_option', detail: opt });
  				}
  		}
  	},
  	// Initialize the layout
  	_initLayout: function () {
  		var self = this;
  
          this.message.removeFrom(this._el.container);
  		this._el.container.innerHTML = "";
  
  		// Create Layout
  		if (this.options.timenav_position == "top") {
  			this._el.timenav		= TL.Dom.create('div', 'tl-timenav', this._el.container);
  			this._el.storyslider	= TL.Dom.create('div', 'tl-storyslider', this._el.container);
  		} else {
  			this._el.storyslider  	= TL.Dom.create('div', 'tl-storyslider', this._el.container);
  			this._el.timenav		= TL.Dom.create('div', 'tl-timenav', this._el.container);
  		}
  
  		this._el.menubar			= TL.Dom.create('div', 'tl-menubar', this._el.container);
  
  
  		// Initial Default Layout
  		this.options.width        = this._el.container.offsetWidth;
  		this.options.height       = this._el.container.offsetHeight;
  		// this._el.storyslider.style.top  = "1px";
  
  		// Set TimeNav Height
  		this.options.timenav_height = this._calculateTimeNavHeight(this.options.timenav_height);
  
  		// Create TimeNav
  		this._timenav = new TL.TimeNav(this._el.timenav, this.config, this.options);
  		this._timenav.on('loaded', this._onTimeNavLoaded, this);
  		this._timenav.on('update_timenav_min', this._updateTimeNavHeightMin, this);
  		this._timenav.options.height = this.options.timenav_height;
  		this._timenav.init();
  
          // intial_zoom cannot be applied before the timenav has been created
          if (this.options.initial_zoom) {
              // at this point, this.options refers to the merged set of options
              this.setZoom(this.options.initial_zoom);
          }
  
  		// Create StorySlider
  		this._storyslider = new TL.StorySlider(this._el.storyslider, this.config, this.options);
  		this._storyslider.on('loaded', this._onStorySliderLoaded, this);
  		this._storyslider.init();
  
  		// Create Menu Bar
  		this._menubar = new TL.MenuBar(this._el.menubar, this._el.container, this.options);
  
  		// LAYOUT
  		if (this.options.layout == "portrait") {
  			this.options.storyslider_height = (this.options.height - this.options.timenav_height - 1);
  		} else {
  			this.options.storyslider_height = (this.options.height - 1);
  		}
  
  
  		// Update Display
  		this._updateDisplay(this._timenav.options.height, true, 2000);
  
  	},
  
    /* Depends upon _initLayout because these events are on things the layout initializes */
  	_initEvents: function () {
  		// TimeNav Events
  		this._timenav.on('change', this._onTimeNavChange, this);
  		this._timenav.on('zoomtoggle', this._onZoomToggle, this);
  
  		// StorySlider Events
  		this._storyslider.on('change', this._onSlideChange, this);
  		this._storyslider.on('colorchange', this._onColorChange, this);
  		this._storyslider.on('nav_next', this._onStorySliderNext, this);
  		this._storyslider.on('nav_previous', this._onStorySliderPrevious, this);
  
  		// Menubar Events
  		this._menubar.on('zoom_in', this._onZoomIn, this);
  		this._menubar.on('zoom_out', this._onZoomOut, this);
  		this._menubar.on('back_to_start', this._onBackToStart, this);
  
  	},
  
  	/* Analytics
  	================================================== */
  	_initGoogleAnalytics: function() {
  		(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
  
  		ga('create', this.options.ga_property_id, 'auto');
  		ga('set', 'anonymizeIp', true);
  	},
  
  	_initAnalytics: function() {
  		if (this.options.ga_property_id === null) { return; }
  		this._initGoogleAnalytics();
          ga('send', 'pageview');
  		var events = this.options.track_events;
  		for (i=0; i < events.length; i++) {
  			var event_ = events[i];
  			this.addEventListener(event_, function(e) {
  				ga('send', 'event', e.type, 'clicked');
  			});
  		}
  	},
  
  	_onZoomToggle: function(e) {
  		if (e.zoom == "in") {
  			this._menubar.toogleZoomIn(e.show);
  		} else if (e.zoom == "out") {
  			this._menubar.toogleZoomOut(e.show);
  		}
  
  	},
  
  	/* Get index of event by id
  	================================================== */
  	_getEventIndex: function(id) {
  		for(var i = 0; i < this.config.events.length; i++) {
  			if(id == this.config.events[i].unique_id) {
  				return i;
  			}
  		}
  		return -1;
  	},
  
  	/*  Get index of slide by id
  	================================================== */
  	_getSlideIndex: function(id) {
  		if(this.config.title && this.config.title.unique_id == id) {
  			return 0;
  		}
  		for(var i = 0; i < this.config.events.length; i++) {
  			if(id == this.config.events[i].unique_id) {
  				return this.config.title ? i+1 : i;
  			}
  		}
  		return -1;
  	},
  
  	/*  Events
  	================================================== */
  
  	_onDataLoaded: function(e) {
  		this.fire("dataloaded");
  		this._initLayout();
  		this._initEvents();
  		this._initAnalytics();
  		if (this.message) {
  			this.message.hide();
  		}
  
  		this.ready = true;
  
  	},
  
  	showMessage: function(msg) {
  		if (this.message) {
  			this.message.updateMessage(msg);
  		} else {
  			trace("No message display available.")
  			trace(msg);
  		}
  	},
  
  	_onColorChange: function(e) {
  		this.fire("color_change", {unique_id:this.current_id}, this);
  		if (e.color || e.image) {
  
  		} else {
  
  		}
  	},
  
  	_onSlideChange: function(e) {
  		if (this.current_id != e.unique_id) {
  			this.current_id = e.unique_id;
  			this._timenav.goToId(this.current_id);
  			this._onChange(e);
  		}
  	},
  
  	_onTimeNavChange: function(e) {
  		if (this.current_id != e.unique_id) {
  			this.current_id = e.unique_id;
  			this._storyslider.goToId(this.current_id);
  			this._onChange(e);
  		}
  	},
  
  	_onChange: function(e) {
  		this.fire("change", {unique_id:this.current_id}, this);
  		if (this.options.hash_bookmark && this.current_id) {
  			this._updateHashBookmark(this.current_id);
  		}
  	},
  
  	_onBackToStart: function(e) {
  		this._storyslider.goTo(0);
  		this.fire("back_to_start", {unique_id:this.current_id}, this);
  	},
  
  	/**
  	 * Zoom in and zoom out should be part of the public API.
  	 */
  	zoomIn: function() {
  	    this._timenav.zoomIn();
  	},
  	zoomOut: function() {
  	    this._timenav.zoomOut();
  	},
  
  	setZoom: function(level) {
  	    this._timenav.setZoom(level);
  	},
  
  	_onZoomIn: function(e) {
  		this._timenav.zoomIn();
  		this.fire("zoom_in", {zoom_level:this._timenav.options.scale_factor}, this);
  	},
  
  	_onZoomOut: function(e) {
  		this._timenav.zoomOut();
  		this.fire("zoom_out", {zoom_level:this._timenav.options.scale_factor}, this);
  	},
  
  	_onTimeNavLoaded: function() {
  		this._loaded.timenav = true;
  		this._onLoaded();
  	},
  
  	_onStorySliderLoaded: function() {
  		this._loaded.storyslider = true;
  		this._onLoaded();
  	},
  
  	_onStorySliderNext: function(e) {
  		this.fire("nav_next", e);
  	},
  
  	_onStorySliderPrevious: function(e) {
  		this.fire("nav_previous", e);
  	},
  
  	_onLoaded: function() {
  		if (this._loaded.storyslider && this._loaded.timenav) {
  			this.fire("loaded", this.config);
  			// Go to proper slide
  			if (this.options.hash_bookmark && window.location.hash != "") {
  				this.goToId(window.location.hash.replace("#event-", ""));
  			} else {
  				if( TL.Util.isTrue(this.options.start_at_end) || this.options.start_at_slide > this.config.events.length ) {
  					this.goToEnd();
  				} else {
  					this.goTo(this.options.start_at_slide);
  				}
  				if (this.options.hash_bookmark ) {
  					this._updateHashBookmark(this.current_id);
  				}
  			}
  
  		}
  	}
  
  });
  
  TL.Timeline.source_path = (function() {
  	var script_tags = document.getElementsByTagName('script');
  	var src = script_tags[script_tags.length-1].src;
  	return src.substr(0,src.lastIndexOf('/'));
  })();
  
  