text: /*!
   * sly 1.4.4 - 30th Mar 2015
   * https://github.com/darsain/sly
   *
   * Licensed under the MIT license.
   * http://opensource.org/licenses/MIT
   */
  ;(function ($, w, undefined) {
  	'use strict';
  
  	var pluginName = 'sly';
  	var className  = 'Sly';
  	var namespace  = pluginName;
  
  	// Local WindowAnimationTiming interface
  	var cAF = w.cancelAnimationFrame || w.cancelRequestAnimationFrame;
  	var rAF = w.requestAnimationFrame;
  
  	// Support indicators
  	var transform, gpuAcceleration;
  
  	// Other global values
  	var $doc = $(document);
  	var dragInitEvents = 'touchstart.' + namespace + ' mousedown.' + namespace;
  	var dragMouseEvents = 'mousemove.' + namespace + ' mouseup.' + namespace;
  	var dragTouchEvents = 'touchmove.' + namespace + ' touchend.' + namespace;
  	var wheelEvent = (document.implementation.hasFeature('Event.wheel', '3.0') ? 'wheel.' : 'mousewheel.') + namespace;
  	var clickEvent = 'click.' + namespace;
  	var mouseDownEvent = 'mousedown.' + namespace;
  	var interactiveElements = ['INPUT', 'SELECT', 'BUTTON', 'TEXTAREA'];
  	var tmpArray = [];
  	var time;
  
  	// Math shorthands
  	var abs = Math.abs;
  	var sqrt = Math.sqrt;
  	var pow = Math.pow;
  	var round = Math.round;
  	var max = Math.max;
  	var min = Math.min;
  
  	// Keep track of last fired global wheel event
  	var lastGlobalWheel = 0;
  	$doc.on(wheelEvent, function (event) {
  		var sly = event.originalEvent[namespace];
  		var time = +new Date();
  		// Update last global wheel time, but only when event didn't originate
  		// in Sly frame, or the origin was less than scrollHijack time ago
  		if (!sly || sly.options.scrollHijack < time - lastGlobalWheel) lastGlobalWheel = time;
  	});
  
  	/**
  	 * Sly.
  	 *
  	 * @class
  	 *
  	 * @param {Element} frame       DOM element of sly container.
  	 * @param {Object}  options     Object with options.
  	 * @param {Object}  callbackMap Callbacks map.
  	 */
  	function Sly(frame, options, callbackMap) {
  		// Extend options
  		var o = $.extend({}, Sly.defaults, options);
  
  		// Private variables
  		var self = this;
  		var parallax = isNumber(frame);
  
  		// Frame
  		var $frame = $(frame);
  		var $slidee = $frame.children().eq(0);
  		var frameSize = 0;
  		var slideeSize = 0;
  		var pos = {
  			start: 0,
  			center: 0,
  			end: 0,
  			cur: 0,
  			dest: 0
  		};
  
  		// Scrollbar
  		var $sb = $(o.scrollBar).eq(0);
  		var $handle = $sb.children().eq(0);
  		var sbSize = 0;
  		var handleSize = 0;
  		var hPos = {
  			start: 0,
  			end: 0,
  			cur: 0
  		};
  
  		// Pagesbar
  		var $pb = $(o.pagesBar);
  		var $pages = 0;
  		var pages = [];
  
  		// Items
  		var $items = 0;
  		var items = [];
  		var rel = {
  			firstItem: 0,
  			lastItem: 0,
  			centerItem: 0,
  			activeItem: null,
  			activePage: 0
  		};
  
  		// Styles
  		var frameStyles = new StyleRestorer($frame[0]);
  		var slideeStyles = new StyleRestorer($slidee[0]);
  		var sbStyles = new StyleRestorer($sb[0]);
  		var handleStyles = new StyleRestorer($handle[0]);
  
  		// Navigation type booleans
  		var basicNav = o.itemNav === 'basic';
  		var forceCenteredNav = o.itemNav === 'forceCentered';
  		var centeredNav = o.itemNav === 'centered' || forceCenteredNav;
  		var itemNav = !parallax && (basicNav || centeredNav || forceCenteredNav);
  
  		// Miscellaneous
  		var $scrollSource = o.scrollSource ? $(o.scrollSource) : $frame;
  		var $dragSource = o.dragSource ? $(o.dragSource) : $frame;
  		var $forwardButton = $(o.forward);
  		var $backwardButton = $(o.backward);
  		var $prevButton = $(o.prev);
  		var $nextButton = $(o.next);
  		var $prevPageButton = $(o.prevPage);
  		var $nextPageButton = $(o.nextPage);
  		var callbacks = {};
  		var last = {};
  		var animation = {};
  		var move = {};
  		var dragging = {
  			released: 1
  		};
  		var scrolling = {
  			last: 0,
  			delta: 0,
  			resetTime: 200
  		};
  		var renderID = 0;
  		var historyID = 0;
  		var cycleID = 0;
  		var continuousID = 0;
  		var i, l;
  
  		// Normalizing frame
  		if (!parallax) {
  			frame = $frame[0];
  		}
  
  		// Expose properties
  		self.initialized = 0;
  		self.frame = frame;
  		self.slidee = $slidee[0];
  		self.pos = pos;
  		self.rel = rel;
  		self.items = items;
  		self.pages = pages;
  		self.isPaused = 0;
  		self.options = o;
  		self.dragging = dragging;
  
  		/**
  		 * Loading function.
  		 *
  		 * Populate arrays, set sizes, bind events, ...
  		 *
  		 * @param {Boolean} [isInit] Whether load is called from within self.init().
  		 * @return {Void}
  		 */
  		function load(isInit) {
  			// Local variables
  			var lastItemsCount = 0;
  			var lastPagesCount = pages.length;
  
  			// Save old position
  			pos.old = $.extend({}, pos);
  
  			// Reset global variables
  			frameSize = parallax ? 0 : $frame[o.horizontal ? 'width' : 'height']();
  			sbSize = $sb[o.horizontal ? 'width' : 'height']();
  			slideeSize = parallax ? frame : $slidee[o.horizontal ? 'outerWidth' : 'outerHeight']();
  			pages.length = 0;
  
  			// Set position limits & relatives
  			pos.start = 0;
  			pos.end = max(slideeSize - frameSize, 0);
  
  			// Sizes & offsets for item based navigations
  			if (itemNav) {
  				// Save the number of current items
  				lastItemsCount = items.length;
  
  				// Reset itemNav related variables
  				$items = $slidee.children(o.itemSelector);
  				items.length = 0;
  
  				// Needed variables
  				var paddingStart = getPx($slidee, o.horizontal ? 'paddingLeft' : 'paddingTop');
  				var paddingEnd = getPx($slidee, o.horizontal ? 'paddingRight' : 'paddingBottom');
  				var borderBox = $($items).css('boxSizing') === 'border-box';
  				var areFloated = $items.css('float') !== 'none';
  				var ignoredMargin = 0;
  				var lastItemIndex = $items.length - 1;
  				var lastItem;
  
  				// Reset slideeSize
  				slideeSize = 0;
  
  				// Iterate through items
  				$items.each(function (i, element) {
  					// Item
  					var $item = $(element);
  					var rect = element.getBoundingClientRect();
  					var itemSize = round(o.horizontal ? rect.width || rect.right - rect.left : rect.height || rect.bottom - rect.top);
  					var itemMarginStart = getPx($item, o.horizontal ? 'marginLeft' : 'marginTop');
  					var itemMarginEnd = getPx($item, o.horizontal ? 'marginRight' : 'marginBottom');
  					var itemSizeFull = itemSize + itemMarginStart + itemMarginEnd;
  					var singleSpaced = !itemMarginStart || !itemMarginEnd;
  					var item = {};
  					item.el = element;
  					item.size = singleSpaced ? itemSize : itemSizeFull;
  					item.half = item.size / 2;
  					item.start = slideeSize + (singleSpaced ? itemMarginStart : 0);
  					item.center = item.start - round(frameSize / 2 - item.size / 2);
  					item.end = item.start - frameSize + item.size;
  
  					// Account for slidee padding
  					if (!i) {
  						slideeSize += paddingStart;
  					}
  
  					// Increment slidee size for size of the active element
  					slideeSize += itemSizeFull;
  
  					// Try to account for vertical margin collapsing in vertical mode
  					// It's not bulletproof, but should work in 99% of cases
  					if (!o.horizontal && !areFloated) {
  						// Subtract smaller margin, but only when top margin is not 0, and this is not the first element
  						if (itemMarginEnd && itemMarginStart && i > 0) {
  							slideeSize -= min(itemMarginStart, itemMarginEnd);
  						}
  					}
  
  					// Things to be done on last item
  					if (i === lastItemIndex) {
  						item.end += paddingEnd;
  						slideeSize += paddingEnd;
  						ignoredMargin = singleSpaced ? itemMarginEnd : 0;
  					}
  
  					// Add item object to items array
  					items.push(item);
  					lastItem = item;
  				});
  
  				// Resize SLIDEE to fit all items
  				$slidee[0].style[o.horizontal ? 'width' : 'height'] = (borderBox ? slideeSize: slideeSize - paddingStart - paddingEnd) + 'px';
  
  				// Adjust internal SLIDEE size for last margin
  				slideeSize -= ignoredMargin;
  
  				// Set limits
  				if (items.length) {
  					pos.start =  items[0][forceCenteredNav ? 'center' : 'start'];
  					pos.end = forceCenteredNav ? lastItem.center : frameSize < slideeSize ? lastItem.end : pos.start;
  				} else {
  					pos.start = pos.end = 0;
  				}
  			}
  
  			// Calculate SLIDEE center position
  			pos.center = round(pos.end / 2 + pos.start / 2);
  
  			// Update relative positions
  			updateRelatives();
  
  			// Scrollbar
  			if ($handle.length && sbSize > 0) {
  				// Stretch scrollbar handle to represent the visible area
  				if (o.dynamicHandle) {
  					handleSize = pos.start === pos.end ? sbSize : round(sbSize * frameSize / slideeSize);
  					handleSize = within(handleSize, o.minHandleSize, sbSize);
  					$handle[0].style[o.horizontal ? 'width' : 'height'] = handleSize + 'px';
  				} else {
  					handleSize = $handle[o.horizontal ? 'outerWidth' : 'outerHeight']();
  				}
  
  				hPos.end = sbSize - handleSize;
  
  				if (!renderID) {
  					syncScrollbar();
  				}
  			}
  
  			// Pages
  			if (!parallax && frameSize > 0) {
  				var tempPagePos = pos.start;
  				var pagesHtml = '';
  
  				// Populate pages array
  				if (itemNav) {
  					$.each(items, function (i, item) {
  						if (forceCenteredNav) {
  							pages.push(item.center);
  						} else if (item.start + item.size > tempPagePos && tempPagePos <= pos.end) {
  							tempPagePos = item.start;
  							pages.push(tempPagePos);
  							tempPagePos += frameSize;
  							if (tempPagePos > pos.end && tempPagePos < pos.end + frameSize) {
  								pages.push(pos.end);
  							}
  						}
  					});
  				} else {
  					while (tempPagePos - frameSize < pos.end) {
  						pages.push(tempPagePos);
  						tempPagePos += frameSize;
  					}
  				}
  
  				// Pages bar
  				if ($pb[0] && lastPagesCount !== pages.length) {
  					for (var i = 0; i < pages.length; i++) {
  						pagesHtml += o.pageBuilder.call(self, i);
  					}
  					$pages = $pb.html(pagesHtml).children();
  					$pages.eq(rel.activePage).addClass(o.activeClass);
  				}
  			}
  
  			// Extend relative variables object with some useful info
  			rel.slideeSize = slideeSize;
  			rel.frameSize = frameSize;
  			rel.sbSize = sbSize;
  			rel.handleSize = handleSize;
  
  			// Activate requested position
  			if (itemNav) {
  				if (isInit && o.startAt != null) {
  					activate(o.startAt);
  					self[centeredNav ? 'toCenter' : 'toStart'](o.startAt);
  				}
  				// Fix possible overflowing
  				var activeItem = items[rel.activeItem];
  				slideTo(centeredNav && activeItem ? activeItem.center : within(pos.dest, pos.start, pos.end));
  			} else {
  				if (isInit) {
  					if (o.startAt != null) slideTo(o.startAt, 1);
  				} else {
  					// Fix possible overflowing
  					slideTo(within(pos.dest, pos.start, pos.end));
  				}
  			}
  
  			// Trigger load event
  			trigger('load');
  		}
  		self.reload = function () { load(); };
  
  		/**
  		 * Animate to a position.
  		 *
  		 * @param {Int}  newPos    New position.
  		 * @param {Bool} immediate Reposition immediately without an animation.
  		 * @param {Bool} dontAlign Do not align items, use the raw position passed in first argument.
  		 *
  		 * @return {Void}
  		 */
  		function slideTo(newPos, immediate, dontAlign) {
  			// Align items
  			if (itemNav && dragging.released && !dontAlign) {
  				var tempRel = getRelatives(newPos);
  				var isNotBordering = newPos > pos.start && newPos < pos.end;
  
  				if (centeredNav) {
  					if (isNotBordering) {
  						newPos = items[tempRel.centerItem].center;
  					}
  					if (forceCenteredNav && o.activateMiddle) {
  						activate(tempRel.centerItem);
  					}
  				} else if (isNotBordering) {
  					newPos = items[tempRel.firstItem].start;
  				}
  			}
  
  			// Handle overflowing position limits
  			if (dragging.init && dragging.slidee && o.elasticBounds) {
  				if (newPos > pos.end) {
  					newPos = pos.end + (newPos - pos.end) / 6;
  				} else if (newPos < pos.start) {
  					newPos = pos.start + (newPos - pos.start) / 6;
  				}
  			} else {
  				newPos = within(newPos, pos.start, pos.end);
  			}
  
  			// Update the animation object
  			animation.start = +new Date();
  			animation.time = 0;
  			animation.from = pos.cur;
  			animation.to = newPos;
  			animation.delta = newPos - pos.cur;
  			animation.tweesing = dragging.tweese || dragging.init && !dragging.slidee;
  			animation.immediate = !animation.tweesing && (immediate || dragging.init && dragging.slidee || !o.speed);
  
  			// Reset dragging tweesing request
  			dragging.tweese = 0;
  
  			// Start animation rendering
  			if (newPos !== pos.dest) {
  				pos.dest = newPos;
  				trigger('change');
  				if (!renderID) {
  					render();
  				}
  			}
  
  			// Reset next cycle timeout
  			resetCycle();
  
  			// Synchronize states
  			updateRelatives();
  			updateButtonsState();
  			syncPagesbar();
  		}
  
  		/**
  		 * Render animation frame.
  		 *
  		 * @return {Void}
  		 */
  		function render() {
  			if (!self.initialized) {
  				return;
  			}
  
  			// If first render call, wait for next animationFrame
  			if (!renderID) {
  				renderID = rAF(render);
  				if (dragging.released) {
  					trigger('moveStart');
  				}
  				return;
  			}
  
  			// If immediate repositioning is requested, don't animate.
  			if (animation.immediate) {
  				pos.cur = animation.to;
  			}
  			// Use tweesing for animations without known end point
  			else if (animation.tweesing) {
  				animation.tweeseDelta = animation.to - pos.cur;
  				// Fuck Zeno's paradox
  				if (abs(animation.tweeseDelta) < 0.1) {
  					pos.cur = animation.to;
  				} else {
  					pos.cur += animation.tweeseDelta * (dragging.released ? o.swingSpeed : o.syncSpeed);
  				}
  			}
  			// Use tweening for basic animations with known end point
  			else {
  				animation.time = min(+new Date() - animation.start, o.speed);
  				pos.cur = animation.from + animation.delta * $.easing[o.easing](animation.time/o.speed, animation.time, 0, 1, o.speed);
  			}
  
  			// If there is nothing more to render break the rendering loop, otherwise request new animation frame.
  			if (animation.to === pos.cur) {
  				pos.cur = animation.to;
  				dragging.tweese = renderID = 0;
  			} else {
  				renderID = rAF(render);
  			}
  
  			trigger('move');
  
  			// Update SLIDEE position
  			if (!parallax) {
  				if (transform) {
  					$slidee[0].style[transform] = gpuAcceleration + (o.horizontal ? 'translateX' : 'translateY') + '(' + (-pos.cur) + 'px)';
  				} else {
  					$slidee[0].style[o.horizontal ? 'left' : 'top'] = -round(pos.cur) + 'px';
  				}
  			}
  
  			// When animation reached the end, and dragging is not active, trigger moveEnd
  			if (!renderID && dragging.released) {
  				trigger('moveEnd');
  			}
  
  			syncScrollbar();
  		}
  
  		/**
  		 * Synchronizes scrollbar with the SLIDEE.
  		 *
  		 * @return {Void}
  		 */
  		function syncScrollbar() {
  			if ($handle.length) {
  				hPos.cur = pos.start === pos.end ? 0 : (((dragging.init && !dragging.slidee) ? pos.dest : pos.cur) - pos.start) / (pos.end - pos.start) * hPos.end;
  				hPos.cur = within(round(hPos.cur), hPos.start, hPos.end);
  				if (last.hPos !== hPos.cur) {
  					last.hPos = hPos.cur;
  					if (transform) {
  						$handle[0].style[transform] = gpuAcceleration + (o.horizontal ? 'translateX' : 'translateY') + '(' + hPos.cur + 'px)';
  					} else {
  						$handle[0].style[o.horizontal ? 'left' : 'top'] = hPos.cur + 'px';
  					}
  				}
  			}
  		}
  
  		/**
  		 * Synchronizes pagesbar with SLIDEE.
  		 *
  		 * @return {Void}
  		 */
  		function syncPagesbar() {
  			if ($pages[0] && last.page !== rel.activePage) {
  				last.page = rel.activePage;
  				$pages.removeClass(o.activeClass).eq(rel.activePage).addClass(o.activeClass);
  				trigger('activePage', last.page);
  			}
  		}
  
  		/**
  		 * Returns the position object.
  		 *
  		 * @param {Mixed} item
  		 *
  		 * @return {Object}
  		 */
  		self.getPos = function (item) {
  			if (itemNav) {
  				var index = getIndex(item);
  				return index !== -1 ? items[index] : false;
  			} else {
  				var $item = $slidee.find(item).eq(0);
  
  				if ($item[0]) {
  					var offset = o.horizontal ? $item.offset().left - $slidee.offset().left : $item.offset().top - $slidee.offset().top;
  					var size = $item[o.horizontal ? 'outerWidth' : 'outerHeight']();
  
  					return {
  						start: offset,
  						center: offset - frameSize / 2 + size / 2,
  						end: offset - frameSize + size,
  						size: size
  					};
  				} else {
  					return false;
  				}
  			}
  		};
  
  		/**
  		 * Continuous move in a specified direction.
  		 *
  		 * @param  {Bool} forward True for forward movement, otherwise it'll go backwards.
  		 * @param  {Int}  speed   Movement speed in pixels per frame. Overrides options.moveBy value.
  		 *
  		 * @return {Void}
  		 */
  		self.moveBy = function (speed) {
  			move.speed = speed;
  			// If already initiated, or there is nowhere to move, abort
  			if (dragging.init || !move.speed || pos.cur === (move.speed > 0 ? pos.end : pos.start)) {
  				return;
  			}
  			// Initiate move object
  			move.lastTime = +new Date();
  			move.startPos = pos.cur;
  			// Set dragging as initiated
  			continuousInit('button');
  			dragging.init = 1;
  			// Start movement
  			trigger('moveStart');
  			cAF(continuousID);
  			moveLoop();
  		};
  
  		/**
  		 * Continuous movement loop.
  		 *
  		 * @return {Void}
  		 */
  		function moveLoop() {
  			// If there is nowhere to move anymore, stop
  			if (!move.speed || pos.cur === (move.speed > 0 ? pos.end : pos.start)) {
  				self.stop();
  			}
  			// Request new move loop if it hasn't been stopped
  			continuousID = dragging.init ? rAF(moveLoop) : 0;
  			// Update move object
  			move.now = +new Date();
  			move.pos = pos.cur + (move.now - move.lastTime) / 1000 * move.speed;
  			// Slide
  			slideTo(dragging.init ? move.pos : round(move.pos));
  			// Normally, this is triggered in render(), but if there
  			// is nothing to render, we have to do it manually here.
  			if (!dragging.init && pos.cur === pos.dest) {
  				trigger('moveEnd');
  			}
  			// Update times for future iteration
  			move.lastTime = move.now;
  		}
  
  		/**
  		 * Stops continuous movement.
  		 *
  		 * @return {Void}
  		 */
  		self.stop = function () {
  			if (dragging.source === 'button') {
  				dragging.init = 0;
  				dragging.released = 1;
  			}
  		};
  
  		/**
  		 * Activate previous item.
  		 *
  		 * @return {Void}
  		 */
  		self.prev = function () {
  			self.activate(rel.activeItem == null ? 0 : rel.activeItem - 1);
  		};
  
  		/**
  		 * Activate next item.
  		 *
  		 * @return {Void}
  		 */
  		self.next = function () {
  			self.activate(rel.activeItem == null ? 0 : rel.activeItem + 1);
  		};
  
  		/**
  		 * Activate previous page.
  		 *
  		 * @return {Void}
  		 */
  		self.prevPage = function () {
  			self.activatePage(rel.activePage - 1);
  		};
  
  		/**
  		 * Activate next page.
  		 *
  		 * @return {Void}
  		 */
  		self.nextPage = function () {
  			self.activatePage(rel.activePage + 1);
  		};
  
  		/**
  		 * Slide SLIDEE by amount of pixels.
  		 *
  		 * @param {Int}  delta     Pixels/Items. Positive means forward, negative means backward.
  		 * @param {Bool} immediate Reposition immediately without an animation.
  		 *
  		 * @return {Void}
  		 */
  		self.slideBy = function (delta, immediate) {
  			if (!delta) {
  				return;
  			}
  			if (itemNav) {
  				self[centeredNav ? 'toCenter' : 'toStart'](
  					within((centeredNav ? rel.centerItem : rel.firstItem) + o.scrollBy * delta, 0, items.length)
  				);
  			} else {
  				slideTo(pos.dest + delta, immediate);
  			}
  		};
  
  		/**
  		 * Animate SLIDEE to a specific position.
  		 *
  		 * @param {Int}  pos       New position.
  		 * @param {Bool} immediate Reposition immediately without an animation.
  		 *
  		 * @return {Void}
  		 */
  		self.slideTo = function (pos, immediate) {
  			slideTo(pos, immediate);
  		};
  
  		/**
  		 * Core method for handling `toLocation` methods.
  		 *
  		 * @param  {String} location
  		 * @param  {Mixed}  item
  		 * @param  {Bool}   immediate
  		 *
  		 * @return {Void}
  		 */
  		function to(location, item, immediate) {
  			// Optional arguments logic
  			if (type(item) === 'boolean') {
  				immediate = item;
  				item = undefined;
  			}
  
  			if (item === undefined) {
  				slideTo(pos[location], immediate);
  			} else {
  				// You can't align items to sides of the frame
  				// when centered navigation type is enabled
  				if (centeredNav && location !== 'center') {
  					return;
  				}
  
  				var itemPos = self.getPos(item);
  				if (itemPos) {
  					slideTo(itemPos[location], immediate, !centeredNav);
  				}
  			}
  		}
  
  		/**
  		 * Animate element or the whole SLIDEE to the start of the frame.
  		 *
  		 * @param {Mixed} item      Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
  		 * @param {Bool}  immediate Reposition immediately without an animation.
  		 *
  		 * @return {Void}
  		 */
  		self.toStart = function (item, immediate) {
  			to('start', item, immediate);
  		};
  
  		/**
  		 * Animate element or the whole SLIDEE to the end of the frame.
  		 *
  		 * @param {Mixed} item      Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
  		 * @param {Bool}  immediate Reposition immediately without an animation.
  		 *
  		 * @return {Void}
  		 */
  		self.toEnd = function (item, immediate) {
  			to('end', item, immediate);
  		};
  
  		/**
  		 * Animate element or the whole SLIDEE to the center of the frame.
  		 *
  		 * @param {Mixed} item      Item DOM element, or index starting at 0. Omitting will animate SLIDEE.
  		 * @param {Bool}  immediate Reposition immediately without an animation.
  		 *
  		 * @return {Void}
  		 */
  		self.toCenter = function (item, immediate) {
  			to('center', item, immediate);
  		};
  
  		/**
  		 * Get the index of an item in SLIDEE.
  		 *
  		 * @param {Mixed} item     Item DOM element.
  		 *
  		 * @return {Int}  Item index, or -1 if not found.
  		 */
  		function getIndex(item) {
  			return item != null ?
  					isNumber(item) ?
  						item >= 0 && item < items.length ? item : -1 :
  						$items.index(item) :
  					-1;
  		}
  		// Expose getIndex without lowering the compressibility of it,
  		// as it is used quite often throughout Sly.
  		self.getIndex = getIndex;
  
  		/**
  		 * Get index of an item in SLIDEE based on a variety of input types.
  		 *
  		 * @param  {Mixed} item DOM element, positive or negative integer.
  		 *
  		 * @return {Int}   Item index, or -1 if not found.
  		 */
  		function getRelativeIndex(item) {
  			return getIndex(isNumber(item) && item < 0 ? item + items.length : item);
  		}
  
  		/**
  		 * Activates an item.
  		 *
  		 * @param  {Mixed} item Item DOM element, or index starting at 0.
  		 *
  		 * @return {Mixed} Activated item index or false on fail.
  		 */
  		function activate(item, force) {
  			var index = getIndex(item);
  
  			if (!itemNav || index < 0) {
  				return false;
  			}
  
  			// Update classes, last active index, and trigger active event only when there
  			// has been a change. Otherwise just return the current active index.
  			if (last.active !== index || force) {
  				// Update classes
  				$items.eq(rel.activeItem).removeClass(o.activeClass);
  				$items.eq(index).addClass(o.activeClass);
  
  				last.active = rel.activeItem = index;
  
  				updateButtonsState();
  				trigger('active', index);
  			}
  
  			return index;
  		}
  
  		/**
  		 * Activates an item and helps with further navigation when o.smart is enabled.
  		 *
  		 * @param {Mixed} item      Item DOM element, or index starting at 0.
  		 * @param {Bool}  immediate Whether to reposition immediately in smart navigation.
  		 *
  		 * @return {Void}
  		 */
  		self.activate = function (item, immediate) {
  			var index = activate(item);
  
  			// Smart navigation
  			if (o.smart && index !== false) {
  				// When centeredNav is enabled, center the element.
  				// Otherwise, determine where to position the element based on its current position.
  				// If the element is currently on the far end side of the frame, assume that user is
  				// moving forward and animate it to the start of the visible frame, and vice versa.
  				if (centeredNav) {
  					self.toCenter(index, immediate);
  				} else if (index >= rel.lastItem) {
  					self.toStart(index, immediate);
  				} else if (index <= rel.firstItem) {
  					self.toEnd(index, immediate);
  				} else {
  					resetCycle();
  				}
  			}
  		};
  
  		/**
  		 * Activates a page.
  		 *
  		 * @param {Int}  index     Page index, starting from 0.
  		 * @param {Bool} immediate Whether to reposition immediately without animation.
  		 *
  		 * @return {Void}
  		 */
  		self.activatePage = function (index, immediate) {
  			if (isNumber(index)) {
  				slideTo(pages[within(index, 0, pages.length - 1)], immediate);
  			}
  		};
  
  		/**
  		 * Return relative positions of items based on their visibility within FRAME.
  		 *
  		 * @param {Int} slideePos Position of SLIDEE.
  		 *
  		 * @return {Void}
  		 */
  		function getRelatives(slideePos) {
  			slideePos = within(isNumber(slideePos) ? slideePos : pos.dest, pos.start, pos.end);
  
  			var relatives = {};
  			var centerOffset = forceCenteredNav ? 0 : frameSize / 2;
  
  			// Determine active page
  			if (!parallax) {
  				for (var p = 0, pl = pages.length; p < pl; p++) {
  					if (slideePos >= pos.end || p === pages.length - 1) {
  						relatives.activePage = pages.length - 1;
  						break;
  					}
  
  					if (slideePos <= pages[p] + centerOffset) {
  						relatives.activePage = p;
  						break;
  					}
  				}
  			}
  
  			// Relative item indexes
  			if (itemNav) {
  				var first = false;
  				var last = false;
  				var center = false;
  
  				// From start
  				for (var i = 0, il = items.length; i < il; i++) {
  					// First item
  					if (first === false && slideePos <= items[i].start + items[i].half) {
  						first = i;
  					}
  
  					// Center item
  					if (center === false && slideePos <= items[i].center + items[i].half) {
  						center = i;
  					}
  
  					// Last item
  					if (i === il - 1 || slideePos <= items[i].end + items[i].half) {
  						last = i;
  						break;
  					}
  				}
  
  				// Safe assignment, just to be sure the false won't be returned
  				relatives.firstItem = isNumber(first) ? first : 0;
  				relatives.centerItem = isNumber(center) ? center : relatives.firstItem;
  				relatives.lastItem = isNumber(last) ? last : relatives.centerItem;
  			}
  
  			return relatives;
  		}
  
  		/**
  		 * Update object with relative positions.
  		 *
  		 * @param {Int} newPos
  		 *
  		 * @return {Void}
  		 */
  		function updateRelatives(newPos) {
  			$.extend(rel, getRelatives(newPos));
  		}
  
  		/**
  		 * Disable navigation buttons when needed.
  		 *
  		 * Adds disabledClass, and when the button is <button> or <input>, activates :disabled state.
  		 *
  		 * @return {Void}
  		 */
  		function updateButtonsState() {
  			var isStart = pos.dest <= pos.start;
  			var isEnd = pos.dest >= pos.end;
  			var slideePosState = (isStart ? 1 : 0) | (isEnd ? 2 : 0);
  
  			// Update paging buttons only if there has been a change in SLIDEE position
  			if (last.slideePosState !== slideePosState) {
  				last.slideePosState = slideePosState;
  
  				if ($prevPageButton.is('button,input')) {
  					$prevPageButton.prop('disabled', isStart);
  				}
  
  				if ($nextPageButton.is('button,input')) {
  					$nextPageButton.prop('disabled', isEnd);
  				}
  
  				$prevPageButton.add($backwardButton)[isStart ? 'addClass' : 'removeClass'](o.disabledClass);
  				$nextPageButton.add($forwardButton)[isEnd ? 'addClass' : 'removeClass'](o.disabledClass);
  			}
  
  			// Forward & Backward buttons need a separate state caching because we cannot "property disable"
  			// them while they are being used, as disabled buttons stop emitting mouse events.
  			if (last.fwdbwdState !== slideePosState && dragging.released) {
  				last.fwdbwdState = slideePosState;
  
  				if ($backwardButton.is('button,input')) {
  					$backwardButton.prop('disabled', isStart);
  				}
  
  				if ($forwardButton.is('button,input')) {
  					$forwardButton.prop('disabled', isEnd);
  				}
  			}
  
  			// Item navigation
  			if (itemNav && rel.activeItem != null) {
  				var isFirst = rel.activeItem === 0;
  				var isLast = rel.activeItem >= items.length - 1;
  				var itemsButtonState = (isFirst ? 1 : 0) | (isLast ? 2 : 0);
  
  				if (last.itemsButtonState !== itemsButtonState) {
  					last.itemsButtonState = itemsButtonState;
  
  					if ($prevButton.is('button,input')) {
  						$prevButton.prop('disabled', isFirst);
  					}
  
  					if ($nextButton.is('button,input')) {
  						$nextButton.prop('disabled', isLast);
  					}
  
  					$prevButton[isFirst ? 'addClass' : 'removeClass'](o.disabledClass);
  					$nextButton[isLast ? 'addClass' : 'removeClass'](o.disabledClass);
  				}
  			}
  		}
  
  		/**
  		 * Resume cycling.
  		 *
  		 * @param {Int} priority Resume pause with priority lower or equal than this. Used internally for pauseOnHover.
  		 *
  		 * @return {Void}
  		 */
  		self.resume = function (priority) {
  			if (!o.cycleBy || !o.cycleInterval || o.cycleBy === 'items' && (!items[0] || rel.activeItem == null) || priority < self.isPaused) {
  				return;
  			}
  
  			self.isPaused = 0;
  
  			if (cycleID) {
  				cycleID = clearTimeout(cycleID);
  			} else {
  				trigger('resume');
  			}
  
  			cycleID = setTimeout(function () {
  				trigger('cycle');
  				switch (o.cycleBy) {
  					case 'items':
  						self.activate(rel.activeItem >= items.length - 1 ? 0 : rel.activeItem + 1);
  						break;
  
  					case 'pages':
  						self.activatePage(rel.activePage >= pages.length - 1 ? 0 : rel.activePage + 1);
  						break;
  				}
  			}, o.cycleInterval);
  		};
  
  		/**
  		 * Pause cycling.
  		 *
  		 * @param {Int} priority Pause priority. 100 is default. Used internally for pauseOnHover.
  		 *
  		 * @return {Void}
  		 */
  		self.pause = function (priority) {
  			if (priority < self.isPaused) {
  				return;
  			}
  
  			self.isPaused = priority || 100;
  
  			if (cycleID) {
  				cycleID = clearTimeout(cycleID);
  				trigger('pause');
  			}
  		};
  
  		/**
  		 * Toggle cycling.
  		 *
  		 * @return {Void}
  		 */
  		self.toggle = function () {
  			self[cycleID ? 'pause' : 'resume']();
  		};
  
  		/**
  		 * Updates a signle or multiple option values.
  		 *
  		 * @param {Mixed} name  Name of the option that should be updated, or object that will extend the options.
  		 * @param {Mixed} value New option value.
  		 *
  		 * @return {Void}
  		 */
  		self.set = function (name, value) {
  			if ($.isPlainObject(name)) {
  				$.extend(o, name);
  			} else if (o.hasOwnProperty(name)) {
  				o[name] = value;
  			}
  		};
  
  		/**
  		 * Add one or multiple items to the SLIDEE end, or a specified position index.
  		 *
  		 * @param {Mixed} element Node element, or HTML string.
  		 * @param {Int}   index   Index of a new item position. By default item is appended at the end.
  		 *
  		 * @return {Void}
  		 */
  		self.add = function (element, index) {
  			var $element = $(element);
  
  			if (itemNav) {
  				// Insert the element(s)
  				if (index == null || !items[0] || index >= items.length) {
  					$element.appendTo($slidee);
  				} else if (items.length) {
  					$element.insertBefore(items[index].el);
  				}
  
  				// Adjust the activeItem index
  				if (rel.activeItem != null && index <= rel.activeItem) {
  					last.active = rel.activeItem += $element.length;
  				}
  			} else {
  				$slidee.append($element);
  			}
  
  			// Reload
  			load();
  		};
  
  		/**
  		 * Remove an item from SLIDEE.
  		 *
  		 * @param {Mixed} element Item index, or DOM element.
  		 * @param {Int}   index   Index of a new item position. By default item is appended at the end.
  		 *
  		 * @return {Void}
  		 */
  		self.remove = function (element) {
  			if (itemNav) {
  				var index = getRelativeIndex(element);
  
  				if (index > -1) {
  					// Remove the element
  					$items.eq(index).remove();
  
  					// If the current item is being removed, activate new one after reload
  					var reactivate = index === rel.activeItem;
  
  					// Adjust the activeItem index
  					if (rel.activeItem != null && index < rel.activeItem) {
  						last.active = --rel.activeItem;
  					}
  
  					// Reload
  					load();
  
  					// Activate new item at the removed position
  					if (reactivate) {
  						last.active = null;
  						self.activate(rel.activeItem);
  					}
  				}
  			} else {
  				$(element).remove();
  				load();
  			}
  		};
  
  		/**
  		 * Helps re-arranging items.
  		 *
  		 * @param  {Mixed} item     Item DOM element, or index starting at 0. Use negative numbers to select items from the end.
  		 * @param  {Mixed} position Item insertion anchor. Accepts same input types as item argument.
  		 * @param  {Bool}  after    Insert after instead of before the anchor.
  		 *
  		 * @return {Void}
  		 */
  		function moveItem(item, position, after) {
  			item = getRelativeIndex(item);
  			position = getRelativeIndex(position);
  
  			// Move only if there is an actual change requested
  			if (item > -1 && position > -1 && item !== position && (!after || position !== item - 1) && (after || position !== item + 1)) {
  				$items.eq(item)[after ? 'insertAfter' : 'insertBefore'](items[position].el);
  
  				var shiftStart = item < position ? item : (after ? position : position - 1);
  				var shiftEnd = item > position ? item : (after ? position + 1 : position);
  				var shiftsUp = item > position;
  
  				// Update activeItem index
  				if (rel.activeItem != null) {
  					if (item === rel.activeItem) {
  						last.active = rel.activeItem = after ? (shiftsUp ? position + 1 : position) : (shiftsUp ? position : position - 1);
  					} else if (rel.activeItem > shiftStart && rel.activeItem < shiftEnd) {
  						last.active = rel.activeItem += shiftsUp ? 1 : -1;
  					}
  				}
  
  				// Reload
  				load();
  			}
  		}
  
  		/**
  		 * Move item after the target anchor.
  		 *
  		 * @param  {Mixed} item     Item to be moved. Can be DOM element or item index.
  		 * @param  {Mixed} position Target position anchor. Can be DOM element or item index.
  		 *
  		 * @return {Void}
  		 */
  		self.moveAfter = function (item, position) {
  			moveItem(item, position, 1);
  		};
  
  		/**
  		 * Move item before the target anchor.
  		 *
  		 * @param  {Mixed} item     Item to be moved. Can be DOM element or item index.
  		 * @param  {Mixed} position Target position anchor. Can be DOM element or item index.
  		 *
  		 * @return {Void}
  		 */
  		self.moveBefore = function (item, position) {
  			moveItem(item, position);
  		};
  
  		/**
  		 * Registers callbacks.
  		 *
  		 * @param  {Mixed} name  Event name, or callbacks map.
  		 * @param  {Mixed} fn    Callback, or an array of callback functions.
  		 *
  		 * @return {Void}
  		 */
  		self.on = function (name, fn) {
  			// Callbacks map
  			if (type(name) === 'object') {
  				for (var key in name) {
  					if (name.hasOwnProperty(key)) {
  						self.on(key, name[key]);
  					}
  				}
  			// Callback
  			} else if (type(fn) === 'function') {
  				var names = name.split(' ');
  				for (var n = 0, nl = names.length; n < nl; n++) {
  					callbacks[names[n]] = callbacks[names[n]] || [];
  					if (callbackIndex(names[n], fn) === -1) {
  						callbacks[names[n]].push(fn);
  					}
  				}
  			// Callbacks array
  			} else if (type(fn) === 'array') {
  				for (var f = 0, fl = fn.length; f < fl; f++) {
  					self.on(name, fn[f]);
  				}
  			}
  		};
  
  		/**
  		 * Registers callbacks to be executed only once.
  		 *
  		 * @param  {Mixed} name  Event name, or callbacks map.
  		 * @param  {Mixed} fn    Callback, or an array of callback functions.
  		 *
  		 * @return {Void}
  		 */
  		self.one = function (name, fn) {
  			function proxy() {
  				fn.apply(self, arguments);
  				self.off(name, proxy);
  			}
  			self.on(name, proxy);
  		};
  
  		/**
  		 * Remove one or all callbacks.
  		 *
  		 * @param  {String} name Event name.
  		 * @param  {Mixed}  fn   Callback, or an array of callback functions. Omit to remove all callbacks.
  		 *
  		 * @return {Void}
  		 */
  		self.off = function (name, fn) {
  			if (fn instanceof Array) {
  				for (var f = 0, fl = fn.length; f < fl; f++) {
  					self.off(name, fn[f]);
  				}
  			} else {
  				var names = name.split(' ');
  				for (var n = 0, nl = names.length; n < nl; n++) {
  					callbacks[names[n]] = callbacks[names[n]] || [];
  					if (fn == null) {
  						callbacks[names[n]].length = 0;
  					} else {
  						var index = callbackIndex(names[n], fn);
  						if (index !== -1) {
  							callbacks[names[n]].splice(index, 1);
  						}
  					}
  				}
  			}
  		};
  
  		/**
  		 * Returns callback array index.
  		 *
  		 * @param  {String}   name Event name.
  		 * @param  {Function} fn   Function
  		 *
  		 * @return {Int} Callback array index, or -1 if isn't registered.
  		 */
  		function callbackIndex(name, fn) {
  			for (var i = 0, l = callbacks[name].length; i < l; i++) {
  				if (callbacks[name][i] === fn) {
  					return i;
  				}
  			}
  			return -1;
  		}
  
  		/**
  		 * Reset next cycle timeout.
  		 *
  		 * @return {Void}
  		 */
  		function resetCycle() {
  			if (dragging.released && !self.isPaused) {
  				self.resume();
  			}
  		}
  
  		/**
  		 * Calculate SLIDEE representation of handle position.
  		 *
  		 * @param  {Int} handlePos
  		 *
  		 * @return {Int}
  		 */
  		function handleToSlidee(handlePos) {
  			return round(within(handlePos, hPos.start, hPos.end) / hPos.end * (pos.end - pos.start)) + pos.start;
  		}
  
  		/**
  		 * Keeps track of a dragging delta history.
  		 *
  		 * @return {Void}
  		 */
  		function draggingHistoryTick() {
  			// Looking at this, I know what you're thinking :) But as we need only 4 history states, doing it this way
  			// as opposed to a proper loop is ~25 bytes smaller (when minified with GCC), a lot faster, and doesn't
  			// generate garbage. The loop version would create 2 new variables on every tick. Unexaptable!
  			dragging.history[0] = dragging.history[1];
  			dragging.history[1] = dragging.history[2];
  			dragging.history[2] = dragging.history[3];
  			dragging.history[3] = dragging.delta;
  		}
  
  		/**
  		 * Initialize continuous movement.
  		 *
  		 * @return {Void}
  		 */
  		function continuousInit(source) {
  			dragging.released = 0;
  			dragging.source = source;
  			dragging.slidee = source === 'slidee';
  		}
  
  		/**
  		 * Dragging initiator.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function dragInit(event) {
  			var isTouch = event.type === 'touchstart';
  			var source = event.data.source;
  			var isSlidee = source === 'slidee';
  
  			// Ignore when already in progress, or interactive element in non-touch navivagion
  			if (dragging.init || !isTouch && isInteractive(event.target)) {
  				return;
  			}
  
  			// Handle dragging conditions
  			if (source === 'handle' && (!o.dragHandle || hPos.start === hPos.end)) {
  				return;
  			}
  
  			// SLIDEE dragging conditions
  			if (isSlidee && !(isTouch ? o.touchDragging : o.mouseDragging && event.which < 2)) {
  				return;
  			}
  
  			if (!isTouch) {
  				// prevents native image dragging in Firefox
  				stopDefault(event);
  			}
  
  			// Reset dragging object
  			continuousInit(source);
  
  			// Properties used in dragHandler
  			dragging.init = 0;
  			dragging.$source = $(event.target);
  			dragging.touch = isTouch;
  			dragging.pointer = isTouch ? event.originalEvent.touches[0] : event;
  			dragging.initX = dragging.pointer.pageX;
  			dragging.initY = dragging.pointer.pageY;
  			dragging.initPos = isSlidee ? pos.cur : hPos.cur;
  			dragging.start = +new Date();
  			dragging.time = 0;
  			dragging.path = 0;
  			dragging.delta = 0;
  			dragging.locked = 0;
  			dragging.history = [0, 0, 0, 0];
  			dragging.pathToLock = isSlidee ? isTouch ? 30 : 10 : 0;
  
  			// Bind dragging events
  			$doc.on(isTouch ? dragTouchEvents : dragMouseEvents, dragHandler);
  
  			// Pause ongoing cycle
  			self.pause(1);
  
  			// Add dragging class
  			(isSlidee ? $slidee : $handle).addClass(o.draggedClass);
  
  			// Trigger moveStart event
  			trigger('moveStart');
  
  			// Keep track of a dragging path history. This is later used in the
  			// dragging release swing calculation when dragging SLIDEE.
  			if (isSlidee) {
  				historyID = setInterval(draggingHistoryTick, 10);
  			}
  		}
  
  		/**
  		 * Handler for dragging scrollbar handle or SLIDEE.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function dragHandler(event) {
  			dragging.released = event.type === 'mouseup' || event.type === 'touchend';
  			dragging.pointer = dragging.touch ? event.originalEvent[dragging.released ? 'changedTouches' : 'touches'][0] : event;
  			dragging.pathX = dragging.pointer.pageX - dragging.initX;
  			dragging.pathY = dragging.pointer.pageY - dragging.initY;
  			dragging.path = sqrt(pow(dragging.pathX, 2) + pow(dragging.pathY, 2));
  			dragging.delta = o.horizontal ? dragging.pathX : dragging.pathY;
  
  			if (!dragging.released && dragging.path < 1) return;
  
  			if (!dragging.init) {
  				if (o.horizontal ? abs(dragging.pathX) > abs(dragging.pathY) : abs(dragging.pathX) < abs(dragging.pathY)) {
  					dragging.init = 1;
  				} else {
  					return dragEnd();
  				}
  			}
  
  			stopDefault(event);
  
  			// Disable click on a source element, as it is unwelcome when dragging
  			if (!dragging.locked && dragging.path > dragging.pathToLock && dragging.slidee) {
  				dragging.locked = 1;
  				dragging.$source.on(clickEvent, disableOneEvent);
  			}
  
  			// Cancel dragging on release
  			if (dragging.released) {
  				dragEnd();
  
  				// Adjust path with a swing on mouse release
  				if (o.releaseSwing && dragging.slidee) {
  					dragging.swing = (dragging.delta - dragging.history[0]) / 40 * 300;
  					dragging.delta += dragging.swing;
  					dragging.tweese = abs(dragging.swing) > 10;
  				}
  			}
  
  			slideTo(dragging.slidee ? round(dragging.initPos - dragging.delta) : handleToSlidee(dragging.initPos + dragging.delta));
  		}
  
  		/**
  		 * Stops dragging and cleans up after it.
  		 *
  		 * @return {Void}
  		 */
  		function dragEnd() {
  			clearInterval(historyID);
  			dragging.released = true;
  			$doc.off(dragging.touch ? dragTouchEvents : dragMouseEvents, dragHandler);
  			(dragging.slidee ? $slidee : $handle).removeClass(o.draggedClass);
  
  			// Make sure that disableOneEvent is not active in next tick.
  			setTimeout(function () {
  				dragging.$source.off(clickEvent, disableOneEvent);
  			});
  
  			// Normally, this is triggered in render(), but if there
  			// is nothing to render, we have to do it manually here.
  			if (pos.cur === pos.dest && dragging.init) {
  				trigger('moveEnd');
  			}
  
  			// Resume ongoing cycle
  			self.resume(1);
  
  			dragging.init = 0;
  		}
  
  		/**
  		 * Check whether element is interactive.
  		 *
  		 * @return {Boolean}
  		 */
  		function isInteractive(element) {
  			return ~$.inArray(element.nodeName, interactiveElements) || $(element).is(o.interactive);
  		}
  
  		/**
  		 * Continuous movement cleanup on mouseup.
  		 *
  		 * @return {Void}
  		 */
  		function movementReleaseHandler() {
  			self.stop();
  			$doc.off('mouseup', movementReleaseHandler);
  		}
  
  		/**
  		 * Buttons navigation handler.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function buttonsHandler(event) {
  			/*jshint validthis:true */
  			stopDefault(event);
  			switch (this) {
  				case $forwardButton[0]:
  				case $backwardButton[0]:
  					self.moveBy($forwardButton.is(this) ? o.moveBy : -o.moveBy);
  					$doc.on('mouseup', movementReleaseHandler);
  					break;
  
  				case $prevButton[0]:
  					self.prev();
  					break;
  
  				case $nextButton[0]:
  					self.next();
  					break;
  
  				case $prevPageButton[0]:
  					self.prevPage();
  					break;
  
  				case $nextPageButton[0]:
  					self.nextPage();
  					break;
  			}
  		}
  
  		/**
  		 * Mouse wheel delta normalization.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Int}
  		 */
  		function normalizeWheelDelta(event) {
  			// wheelDelta needed only for IE8-
  			scrolling.curDelta = ((o.horizontal ? event.deltaY || event.deltaX : event.deltaY) || -event.wheelDelta);
  			scrolling.curDelta /= event.deltaMode === 1 ? 3 : 100;
  			if (!itemNav) {
  				return scrolling.curDelta;
  			}
  			time = +new Date();
  			if (scrolling.last < time - scrolling.resetTime) {
  				scrolling.delta = 0;
  			}
  			scrolling.last = time;
  			scrolling.delta += scrolling.curDelta;
  			if (abs(scrolling.delta) < 1) {
  				scrolling.finalDelta = 0;
  			} else {
  				scrolling.finalDelta = round(scrolling.delta / 1);
  				scrolling.delta %= 1;
  			}
  			return scrolling.finalDelta;
  		}
  
  		/**
  		 * Mouse scrolling handler.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function scrollHandler(event) {
  			// Mark event as originating in a Sly instance
  			event.originalEvent[namespace] = self;
  			// Don't hijack global scrolling
  			var time = +new Date();
  			if (lastGlobalWheel + o.scrollHijack > time) {
  				lastGlobalWheel = time;
  				return;
  			}
  			// Ignore if there is no scrolling to be done
  			if (!o.scrollBy || pos.start === pos.end) {
  				return;
  			}
  			var delta = normalizeWheelDelta(event.originalEvent);
  			// Trap scrolling only when necessary and/or requested
  			if (o.scrollTrap || delta > 0 && pos.dest < pos.end || delta < 0 && pos.dest > pos.start) {
  				stopDefault(event, 1);
  			}
  			self.slideBy(o.scrollBy * delta);
  		}
  
  		/**
  		 * Scrollbar click handler.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function scrollbarHandler(event) {
  			// Only clicks on scroll bar. Ignore the handle.
  			if (o.clickBar && event.target === $sb[0]) {
  				stopDefault(event);
  				// Calculate new handle position and sync SLIDEE to it
  				slideTo(handleToSlidee((o.horizontal ? event.pageX - $sb.offset().left : event.pageY - $sb.offset().top) - handleSize / 2));
  			}
  		}
  
  		/**
  		 * Keyboard input handler.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function keyboardHandler(event) {
  			if (!o.keyboardNavBy) {
  				return;
  			}
  
  			switch (event.which) {
  				// Left or Up
  				case o.horizontal ? 37 : 38:
  					stopDefault(event);
  					self[o.keyboardNavBy === 'pages' ? 'prevPage' : 'prev']();
  					break;
  
  				// Right or Down
  				case o.horizontal ? 39 : 40:
  					stopDefault(event);
  					self[o.keyboardNavBy === 'pages' ? 'nextPage' : 'next']();
  					break;
  			}
  		}
  
  		/**
  		 * Click on item activation handler.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function activateHandler(event) {
  			/*jshint validthis:true */
  
  			// Ignore clicks on interactive elements.
  			if (isInteractive(this)) {
  				event.originalEvent[namespace + 'ignore'] = true;
  				return;
  			}
  
  			// Ignore events that:
  			// - are not originating from direct SLIDEE children
  			// - originated from interactive elements
  			if (this.parentNode !== $slidee[0] || event.originalEvent[namespace + 'ignore']) return;
  
  			self.activate(this);
  		}
  
  		/**
  		 * Click on page button handler.
  		 *
  		 * @param {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function activatePageHandler() {
  			/*jshint validthis:true */
  			// Accept only events from direct pages bar children.
  			if (this.parentNode === $pb[0]) {
  				self.activatePage($pages.index(this));
  			}
  		}
  
  		/**
  		 * Pause on hover handler.
  		 *
  		 * @param  {Event} event
  		 *
  		 * @return {Void}
  		 */
  		function pauseOnHoverHandler(event) {
  			if (o.pauseOnHover) {
  				self[event.type === 'mouseenter' ? 'pause' : 'resume'](2);
  			}
  		}
  
  		/**
  		 * Trigger callbacks for event.
  		 *
  		 * @param  {String} name Event name.
  		 * @param  {Mixed}  argX Arguments passed to callbacks.
  		 *
  		 * @return {Void}
  		 */
  		function trigger(name, arg1) {
  			if (callbacks[name]) {
  				l = callbacks[name].length;
  				// Callbacks will be stored and executed from a temporary array to not
  				// break the execution queue when one of the callbacks unbinds itself.
  				tmpArray.length = 0;
  				for (i = 0; i < l; i++) {
  					tmpArray.push(callbacks[name][i]);
  				}
  				// Execute the callbacks
  				for (i = 0; i < l; i++) {
  					tmpArray[i].call(self, name, arg1);
  				}
  			}
  		}
  
  		/**
  		 * Destroys instance and everything it created.
  		 *
  		 * @return {Void}
  		 */
  		self.destroy = function () {
  			// Unbind all events
  			$scrollSource
  				.add($handle)
  				.add($sb)
  				.add($pb)
  				.add($forwardButton)
  				.add($backwardButton)
  				.add($prevButton)
  				.add($nextButton)
  				.add($prevPageButton)
  				.add($nextPageButton)
  				.off('.' + namespace);
  
  			// Unbinding specifically as to not nuke out other instances
  			$doc.off('keydown', keyboardHandler);
  
  			// Remove classes
  			$prevButton
  				.add($nextButton)
  				.add($prevPageButton)
  				.add($nextPageButton)
  				.removeClass(o.disabledClass);
  
  			if ($items && rel.activeItem != null) {
  				$items.eq(rel.activeItem).removeClass(o.activeClass);
  			}
  
  			// Remove page items
  			$pb.empty();
  
  			if (!parallax) {
  				// Unbind events from frame
  				$frame.off('.' + namespace);
  				// Restore original styles
  				frameStyles.restore();
  				slideeStyles.restore();
  				sbStyles.restore();
  				handleStyles.restore();
  				// Remove the instance from element data storage
  				$.removeData(frame, namespace);
  			}
  
  			// Clean up collections
  			items.length = pages.length = 0;
  			last = {};
  
  			// Reset initialized status and return the instance
  			self.initialized = 0;
  			return self;
  		};
  
  		/**
  		 * Initialize.
  		 *
  		 * @return {Object}
  		 */
  		self.init = function () {
  			if (self.initialized) {
  				return;
  			}
  
  			// Register callbacks map
  			self.on(callbackMap);
  
  			// Save styles
  			var holderProps = ['overflow', 'position'];
  			var movableProps = ['position', 'webkitTransform', 'msTransform', 'transform', 'left', 'top', 'width', 'height'];
  			frameStyles.save.apply(frameStyles, holderProps);
  			sbStyles.save.apply(sbStyles, holderProps);
  			slideeStyles.save.apply(slideeStyles, movableProps);
  			handleStyles.save.apply(handleStyles, movableProps);
  
  			// Set required styles
  			var $movables = $handle;
  			if (!parallax) {
  				$movables = $movables.add($slidee);
  				$frame.css('overflow', 'hidden');
  				if (!transform && $frame.css('position') === 'static') {
  					$frame.css('position', 'relative');
  				}
  			}
  			if (transform) {
  				if (gpuAcceleration) {
  					$movables.css(transform, gpuAcceleration);
  				}
  			} else {
  				if ($sb.css('position') === 'static') {
  					$sb.css('position', 'relative');
  				}
  				$movables.css({ position: 'absolute' });
  			}
  
  			// Navigation buttons
  			if (o.forward) {
  				$forwardButton.on(mouseDownEvent, buttonsHandler);
  			}
  			if (o.backward) {
  				$backwardButton.on(mouseDownEvent, buttonsHandler);
  			}
  			if (o.prev) {
  				$prevButton.on(clickEvent, buttonsHandler);
  			}
  			if (o.next) {
  				$nextButton.on(clickEvent, buttonsHandler);
  			}
  			if (o.prevPage) {
  				$prevPageButton.on(clickEvent, buttonsHandler);
  			}
  			if (o.nextPage) {
  				$nextPageButton.on(clickEvent, buttonsHandler);
  			}
  
  			// Scrolling navigation
  			$scrollSource.on(wheelEvent, scrollHandler);
  
  			// Clicking on scrollbar navigation
  			if ($sb[0]) {
  				$sb.on(clickEvent, scrollbarHandler);
  			}
  
  			// Click on items navigation
  			if (itemNav && o.activateOn) {
  				$frame.on(o.activateOn + '.' + namespace, '*', activateHandler);
  			}
  
  			// Pages navigation
  			if ($pb[0] && o.activatePageOn) {
  				$pb.on(o.activatePageOn + '.' + namespace, '*', activatePageHandler);
  			}
  
  			// Dragging navigation
  			$dragSource.on(dragInitEvents, { source: 'slidee' }, dragInit);
  
  			// Scrollbar dragging navigation
  			if ($handle) {
  				$handle.on(dragInitEvents, { source: 'handle' }, dragInit);
  			}
  
  			// Keyboard navigation
  			$doc.on('keydown', keyboardHandler);
  
  			if (!parallax) {
  				// Pause on hover
  				$frame.on('mouseenter.' + namespace + ' mouseleave.' + namespace, pauseOnHoverHandler);
  				// Reset native FRAME element scroll
  				$frame.on('scroll.' + namespace, resetScroll);
  			}
  
  			// Mark instance as initialized
  			self.initialized = 1;
  
  			// Load
  			load(true);
  
  			// Initiate automatic cycling
  			if (o.cycleBy && !parallax) {
  				self[o.startPaused ? 'pause' : 'resume']();
  			}
  
  			// Return instance
  			return self;
  		};
  	}
  
  	/**
  	 * Return type of the value.
  	 *
  	 * @param  {Mixed} value
  	 *
  	 * @return {String}
  	 */
  	function type(value) {
  		if (value == null) {
  			return String(value);
  		}
  
  		if (typeof value === 'object' || typeof value === 'function') {
  			return Object.prototype.toString.call(value).match(/\s([a-z]+)/i)[1].toLowerCase() || 'object';
  		}
  
  		return typeof value;
  	}
  
  	/**
  	 * Event preventDefault & stopPropagation helper.
  	 *
  	 * @param {Event} event     Event object.
  	 * @param {Bool}  noBubbles Cancel event bubbling.
  	 *
  	 * @return {Void}
  	 */
  	function stopDefault(event, noBubbles) {
  		event.preventDefault();
  		if (noBubbles) {
  			event.stopPropagation();
  		}
  	}
  
  	/**
  	 * Disables an event it was triggered on and unbinds itself.
  	 *
  	 * @param  {Event} event
  	 *
  	 * @return {Void}
  	 */
  	function disableOneEvent(event) {
  		/*jshint validthis:true */
  		stopDefault(event, 1);
  		$(this).off(event.type, disableOneEvent);
  	}
  
  	/**
  	 * Resets native element scroll values to 0.
  	 *
  	 * @return {Void}
  	 */
  	function resetScroll() {
  		/*jshint validthis:true */
  		this.scrollLeft = 0;
  		this.scrollTop = 0;
  	}
  
  	/**
  	 * Check if variable is a number.
  	 *
  	 * @param {Mixed} value
  	 *
  	 * @return {Boolean}
  	 */
  	function isNumber(value) {
  		return !isNaN(parseFloat(value)) && isFinite(value);
  	}
  
  	/**
  	 * Parse style to pixels.
  	 *
  	 * @param {Object}   $item    jQuery object with element.
  	 * @param {Property} property CSS property to get the pixels from.
  	 *
  	 * @return {Int}
  	 */
  	function getPx($item, property) {
  		return 0 | round(String($item.css(property)).replace(/[^\-0-9.]/g, ''));
  	}
  
  	/**
  	 * Make sure that number is within the limits.
  	 *
  	 * @param {Number} number
  	 * @param {Number} min
  	 * @param {Number} max
  	 *
  	 * @return {Number}
  	 */
  	function within(number, min, max) {
  		return number < min ? min : number > max ? max : number;
  	}
  
  	/**
  	 * Saves element styles for later restoration.
  	 *
  	 * Example:
  	 *   var styles = new StyleRestorer(frame);
  	 *   styles.save('position');
  	 *   element.style.position = 'absolute';
  	 *   styles.restore(); // restores to state before the assignment above
  	 *
  	 * @param {Element} element
  	 */
  	function StyleRestorer(element) {
  		var self = {};
  		self.style = {};
  		self.save = function () {
  			if (!element) return;
  			for (var i = 0; i < arguments.length; i++) {
  				self.style[arguments[i]] = element.style[arguments[i]];
  			}
  			return self;
  		};
  		self.restore = function () {
  			if (!element) return;
  			for (var prop in self.style) {
  				if (self.style.hasOwnProperty(prop)) element.style[prop] = self.style[prop];
  			}
  			return self;
  		};
  		return self;
  	}
  
  	// Local WindowAnimationTiming interface polyfill
  	(function (w) {
  		rAF = w.requestAnimationFrame
  			|| w.webkitRequestAnimationFrame
  			|| fallback;
  
  		/**
  		* Fallback implementation.
  		*/
  		var prev = new Date().getTime();
  		function fallback(fn) {
  			var curr = new Date().getTime();
  			var ms = Math.max(0, 16 - (curr - prev));
  			var req = setTimeout(fn, ms);
  			prev = curr;
  			return req;
  		}
  
  		/**
  		* Cancel.
  		*/
  		var cancel = w.cancelAnimationFrame
  			|| w.webkitCancelAnimationFrame
  			|| w.clearTimeout;
  
  		cAF = function(id){
  			cancel.call(w, id);
  		};
  	}(window));
  
  	// Feature detects
  	(function () {
  		var prefixes = ['', 'webkit', 'moz', 'ms', 'o'];
  		var el = document.createElement('div');
  
  		function testProp(prop) {
  			for (var p = 0, pl = prefixes.length; p < pl; p++) {
  				var prefixedProp = prefixes[p] ? prefixes[p] + prop.charAt(0).toUpperCase() + prop.slice(1) : prop;
  				if (el.style[prefixedProp] != null) {
  					return prefixedProp;
  				}
  			}
  		}
  
  		// Global support indicators
  		transform = testProp('transform');
  		gpuAcceleration = testProp('perspective') ? 'translateZ(0) ' : '';
  	}());
  
  	// Expose class globally
  	w[className] = Sly;
  
  	// jQuery proxy
  	$.fn[pluginName] = function (options, callbackMap) {
  		var method, methodArgs;
  
  		// Attributes logic
  		if (!$.isPlainObject(options)) {
  			if (type(options) === 'string' || options === false) {
  				method = options === false ? 'destroy' : options;
  				methodArgs = Array.prototype.slice.call(arguments, 1);
  			}
  			options = {};
  		}
  
  		// Apply to all elements
  		return this.each(function (i, element) {
  			// Call with prevention against multiple instantiations
  			var plugin = $.data(element, namespace);
  
  			if (!plugin && !method) {
  				// Create a new object if it doesn't exist yet
  				plugin = $.data(element, namespace, new Sly(element, options, callbackMap).init());
  			} else if (plugin && method) {
  				// Call method
  				if (plugin[method]) {
  					plugin[method].apply(plugin, methodArgs);
  				}
  			}
  		});
  	};
  
  	// Default options
  	Sly.defaults = {
  		horizontal: false, // Switch to horizontal mode.
  
  		// Item based navigation
  		itemNav:        null,  // Item navigation type. Can be: 'basic', 'centered', 'forceCentered'.
  		itemSelector:   null,  // Select only items that match this selector.
  		smart:          false, // Repositions the activated item to help with further navigation.
  		activateOn:     null,  // Activate an item on this event. Can be: 'click', 'mouseenter', ...
  		activateMiddle: false, // Always activate the item in the middle of the FRAME. forceCentered only.
  
  		// Scrolling
  		scrollSource: null,  // Element for catching the mouse wheel scrolling. Default is FRAME.
  		scrollBy:     0,     // Pixels or items to move per one mouse scroll. 0 to disable scrolling.
  		scrollHijack: 300,   // Milliseconds since last wheel event after which it is acceptable to hijack global scroll.
  		scrollTrap:   false, // Don't bubble scrolling when hitting scrolling limits.
  
  		// Dragging
  		dragSource:    null,  // Selector or DOM element for catching dragging events. Default is FRAME.
  		mouseDragging: false, // Enable navigation by dragging the SLIDEE with mouse cursor.
  		touchDragging: false, // Enable navigation by dragging the SLIDEE with touch events.
  		releaseSwing:  false, // Ease out on dragging swing release.
  		swingSpeed:    0.2,   // Swing synchronization speed, where: 1 = instant, 0 = infinite.
  		elasticBounds: false, // Stretch SLIDEE position limits when dragging past FRAME boundaries.
  		interactive:   null,  // Selector for special interactive elements.
  
  		// Scrollbar
  		scrollBar:     null,  // Selector or DOM element for scrollbar container.
  		dragHandle:    false, // Whether the scrollbar handle should be draggable.
  		dynamicHandle: false, // Scrollbar handle represents the ratio between hidden and visible content.
  		minHandleSize: 50,    // Minimal height or width (depends on sly direction) of a handle in pixels.
  		clickBar:      false, // Enable navigation by clicking on scrollbar.
  		syncSpeed:     0.5,   // Handle => SLIDEE synchronization speed, where: 1 = instant, 0 = infinite.
  
  		// Pagesbar
  		pagesBar:       null, // Selector or DOM element for pages bar container.
  		activatePageOn: null, // Event used to activate page. Can be: click, mouseenter, ...
  		pageBuilder:          // Page item generator.
  			function (index) {
  				return '<li>' + (index + 1) + '</li>';
  			},
  
  		// Navigation buttons
  		forward:  null, // Selector or DOM element for "forward movement" button.
  		backward: null, // Selector or DOM element for "backward movement" button.
  		prev:     null, // Selector or DOM element for "previous item" button.
  		next:     null, // Selector or DOM element for "next item" button.
  		prevPage: null, // Selector or DOM element for "previous page" button.
  		nextPage: null, // Selector or DOM element for "next page" button.
  
  		// Automated cycling
  		cycleBy:       null,  // Enable automatic cycling by 'items' or 'pages'.
  		cycleInterval: 5000,  // Delay between cycles in milliseconds.
  		pauseOnHover:  false, // Pause cycling when mouse hovers over the FRAME.
  		startPaused:   false, // Whether to start in paused sate.
  
  		// Mixed options
  		moveBy:        300,     // Speed in pixels per second used by forward and backward buttons.
  		speed:         0,       // Animations speed in milliseconds. 0 to disable animations.
  		easing:        'swing', // Easing for duration based (tweening) animations.
  		startAt:       null,    // Starting offset in pixels or items.
  		keyboardNavBy: null,    // Enable keyboard navigation by 'items' or 'pages'.
  
  		// Classes
  		draggedClass:  'dragged', // Class for dragged elements (like SLIDEE or scrollbar handle).
  		activeClass:   'active',  // Class for active items and pages.
  		disabledClass: 'disabled' // Class for disabled navigation elements.
  	};
  }(jQuery, window));