text: (function($) {
  
  	/**
  	 * Generate an indented list of links from a nav. Meant for use with panel().
  	 * @return {jQuery} jQuery object.
  	 */
  	$.fn.navList = function() {
  
  		var	$this = $(this);
  			$a = $this.find('a'),
  			b = [];
  
  		$a.each(function() {
  
  			var	$this = $(this),
  				indent = Math.max(0, $this.parents('li').length - 1),
  				href = $this.attr('href'),
  				target = $this.attr('target');
  
  			b.push(
  				'<a ' +
  					'class="link depth-' + indent + '"' +
  					( (typeof target !== 'undefined' && target != '') ? ' target="' + target + '"' : '') +
  					( (typeof href !== 'undefined' && href != '') ? ' href="' + href + '"' : '') +
  				'>' +
  					'<span class="indent-' + indent + '"></span>' +
  					$this.text() +
  				'</a>'
  			);
  
  		});
  
  		return b.join('');
  
  	};
  
  	/**
  	 * Panel-ify an element.
  	 * @param {object} userConfig User config.
  	 * @return {jQuery} jQuery object.
  	 */
  	$.fn.panel = function(userConfig) {
  
  		// No elements?
  			if (this.length == 0)
  				return $this;
  
  		// Multiple elements?
  			if (this.length > 1) {
  
  				for (var i=0; i < this.length; i++)
  					$(this[i]).panel(userConfig);
  
  				return $this;
  
  			}
  
  		// Vars.
  			var	$this = $(this),
  				$body = $('body'),
  				$window = $(window),
  				id = $this.attr('id'),
  				config;
  
  		// Config.
  			config = $.extend({
  
  				// Delay.
  					delay: 0,
  
  				// Hide panel on link click.
  					hideOnClick: false,
  
  				// Hide panel on escape keypress.
  					hideOnEscape: false,
  
  				// Hide panel on swipe.
  					hideOnSwipe: false,
  
  				// Reset scroll position on hide.
  					resetScroll: false,
  
  				// Reset forms on hide.
  					resetForms: false,
  
  				// Side of viewport the panel will appear.
  					side: null,
  
  				// Target element for "class".
  					target: $this,
  
  				// Class to toggle.
  					visibleClass: 'visible'
  
  			}, userConfig);
  
  			// Expand "target" if it's not a jQuery object already.
  				if (typeof config.target != 'jQuery')
  					config.target = $(config.target);
  
  		// Panel.
  
  			// Methods.
  				$this._hide = function(event) {
  
  					// Already hidden? Bail.
  						if (!config.target.hasClass(config.visibleClass))
  							return;
  
  					// If an event was provided, cancel it.
  						if (event) {
  
  							event.preventDefault();
  							event.stopPropagation();
  
  						}
  
  					// Hide.
  						config.target.removeClass(config.visibleClass);
  
  					// Post-hide stuff.
  						window.setTimeout(function() {
  
  							// Reset scroll position.
  								if (config.resetScroll)
  									$this.scrollTop(0);
  
  							// Reset forms.
  								if (config.resetForms)
  									$this.find('form').each(function() {
  										this.reset();
  									});
  
  						}, config.delay);
  
  				};
  
  			// Vendor fixes.
  				$this
  					.css('-ms-overflow-style', '-ms-autohiding-scrollbar')
  					.css('-webkit-overflow-scrolling', 'touch');
  
  			// Hide on click.
  				if (config.hideOnClick) {
  
  					$this.find('a')
  						.css('-webkit-tap-highlight-color', 'rgba(0,0,0,0)');
  
  					$this
  						.on('click', 'a', function(event) {
  
  							var $a = $(this),
  								href = $a.attr('href'),
  								target = $a.attr('target');
  
  							if (!href || href == '#' || href == '' || href == '#' + id)
  								return;
  
  							// Cancel original event.
  								event.preventDefault();
  								event.stopPropagation();
  
  							// Hide panel.
  								$this._hide();
  
  							// Redirect to href.
  								window.setTimeout(function() {
  
  									if (target == '_blank')
  										window.open(href);
  									else
  										window.location.href = href;
  
  								}, config.delay + 10);
  
  						});
  
  				}
  
  			// Event: Touch stuff.
  				$this.on('touchstart', function(event) {
  
  					$this.touchPosX = event.originalEvent.touches[0].pageX;
  					$this.touchPosY = event.originalEvent.touches[0].pageY;
  
  				})
  
  				$this.on('touchmove', function(event) {
  
  					if ($this.touchPosX === null
  					||	$this.touchPosY === null)
  						return;
  
  					var	diffX = $this.touchPosX - event.originalEvent.touches[0].pageX,
  						diffY = $this.touchPosY - event.originalEvent.touches[0].pageY,
  						th = $this.outerHeight(),
  						ts = ($this.get(0).scrollHeight - $this.scrollTop());
  
  					// Hide on swipe?
  						if (config.hideOnSwipe) {
  
  							var result = false,
  								boundary = 20,
  								delta = 50;
  
  							switch (config.side) {
  
  								case 'left':
  									result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX > delta);
  									break;
  
  								case 'right':
  									result = (diffY < boundary && diffY > (-1 * boundary)) && (diffX < (-1 * delta));
  									break;
  
  								case 'top':
  									result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY > delta);
  									break;
  
  								case 'bottom':
  									result = (diffX < boundary && diffX > (-1 * boundary)) && (diffY < (-1 * delta));
  									break;
  
  								default:
  									break;
  
  							}
  
  							if (result) {
  
  								$this.touchPosX = null;
  								$this.touchPosY = null;
  								$this._hide();
  
  								return false;
  
  							}
  
  						}
  
  					// Prevent vertical scrolling past the top or bottom.
  						if (($this.scrollTop() < 0 && diffY < 0)
  						|| (ts > (th - 2) && ts < (th + 2) && diffY > 0)) {
  
  							event.preventDefault();
  							event.stopPropagation();
  
  						}
  
  				});
  
  			// Event: Prevent certain events inside the panel from bubbling.
  				$this.on('click touchend touchstart touchmove', function(event) {
  					event.stopPropagation();
  				});
  
  			// Event: Hide panel if a child anchor tag pointing to its ID is clicked.
  				$this.on('click', 'a[href="#' + id + '"]', function(event) {
  
  					event.preventDefault();
  					event.stopPropagation();
  
  					config.target.removeClass(config.visibleClass);
  
  				});
  
  		// Body.
  
  			// Event: Hide panel on body click/tap.
  				$body.on('click touchend', function(event) {
  					$this._hide(event);
  				});
  
  			// Event: Toggle.
  				$body.on('click', 'a[href="#' + id + '"]', function(event) {
  
  					event.preventDefault();
  					event.stopPropagation();
  
  					config.target.toggleClass(config.visibleClass);
  
  				});
  
  		// Window.
  
  			// Event: Hide on ESC.
  				if (config.hideOnEscape)
  					$window.on('keydown', function(event) {
  
  						if (event.keyCode == 27)
  							$this._hide(event);
  
  					});
  
  		return $this;
  
  	};
  
  	/**
  	 * Apply "placeholder" attribute polyfill to one or more forms.
  	 * @return {jQuery} jQuery object.
  	 */
  	$.fn.placeholder = function() {
  
  		// Browser natively supports placeholders? Bail.
  			if (typeof (document.createElement('input')).placeholder != 'undefined')
  				return $(this);
  
  		// No elements?
  			if (this.length == 0)
  				return $this;
  
  		// Multiple elements?
  			if (this.length > 1) {
  
  				for (var i=0; i < this.length; i++)
  					$(this[i]).placeholder();
  
  				return $this;
  
  			}
  
  		// Vars.
  			var $this = $(this);
  
  		// Text, TextArea.
  			$this.find('input[type=text],textarea')
  				.each(function() {
  
  					var i = $(this);
  
  					if (i.val() == ''
  					||  i.val() == i.attr('placeholder'))
  						i
  							.addClass('polyfill-placeholder')
  							.val(i.attr('placeholder'));
  
  				})
  				.on('blur', function() {
  
  					var i = $(this);
  
  					if (i.attr('name').match(/-polyfill-field$/))
  						return;
  
  					if (i.val() == '')
  						i
  							.addClass('polyfill-placeholder')
  							.val(i.attr('placeholder'));
  
  				})
  				.on('focus', function() {
  
  					var i = $(this);
  
  					if (i.attr('name').match(/-polyfill-field$/))
  						return;
  
  					if (i.val() == i.attr('placeholder'))
  						i
  							.removeClass('polyfill-placeholder')
  							.val('');
  
  				});
  
  		// Password.
  			$this.find('input[type=password]')
  				.each(function() {
  
  					var i = $(this);
  					var x = $(
  								$('<div>')
  									.append(i.clone())
  									.remove()
  									.html()
  									.replace(/type="password"/i, 'type="text"')
  									.replace(/type=password/i, 'type=text')
  					);
  
  					if (i.attr('id') != '')
  						x.attr('id', i.attr('id') + '-polyfill-field');
  
  					if (i.attr('name') != '')
  						x.attr('name', i.attr('name') + '-polyfill-field');
  
  					x.addClass('polyfill-placeholder')
  						.val(x.attr('placeholder')).insertAfter(i);
  
  					if (i.val() == '')
  						i.hide();
  					else
  						x.hide();
  
  					i
  						.on('blur', function(event) {
  
  							event.preventDefault();
  
  							var x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
  
  							if (i.val() == '') {
  
  								i.hide();
  								x.show();
  
  							}
  
  						});
  
  					x
  						.on('focus', function(event) {
  
  							event.preventDefault();
  
  							var i = x.parent().find('input[name=' + x.attr('name').replace('-polyfill-field', '') + ']');
  
  							x.hide();
  
  							i
  								.show()
  								.focus();
  
  						})
  						.on('keypress', function(event) {
  
  							event.preventDefault();
  							x.val('');
  
  						});
  
  				});
  
  		// Events.
  			$this
  				.on('submit', function(event) {
  
  					$this.find('input[type=text],input[type=password],textarea')
  						.each(function(event) {
  
  							var i = $(this);
  
  							if (i.attr('name').match(/-polyfill-field$/))
  								i.attr('name', '');
  
  							if (i.val() == i.attr('placeholder')) {
  
  								i.removeClass('polyfill-placeholder');
  								i.val('');
  
  							}
  
  						});
  
  				})
  				.on('reset', function(event) {
  
  					event.preventDefault();
  
  					$this.find('select')
  						.val($('option:first').val());
  
  					$this.find('input,textarea')
  						.each(function() {
  
  							var i = $(this),
  								x;
  
  							i.removeClass('polyfill-placeholder');
  
  							switch (this.type) {
  
  								case 'submit':
  								case 'reset':
  									break;
  
  								case 'password':
  									i.val(i.attr('defaultValue'));
  
  									x = i.parent().find('input[name=' + i.attr('name') + '-polyfill-field]');
  
  									if (i.val() == '') {
  										i.hide();
  										x.show();
  									}
  									else {
  										i.show();
  										x.hide();
  									}
  
  									break;
  
  								case 'checkbox':
  								case 'radio':
  									i.attr('checked', i.attr('defaultValue'));
  									break;
  
  								case 'text':
  								case 'textarea':
  									i.val(i.attr('defaultValue'));
  
  									if (i.val() == '') {
  										i.addClass('polyfill-placeholder');
  										i.val(i.attr('placeholder'));
  									}
  
  									break;
  
  								default:
  									i.val(i.attr('defaultValue'));
  									break;
  
  							}
  						});
  
  				});
  
  		return $this;
  
  	};
  
  	/**
  	 * Moves elements to/from the first positions of their respective parents.
  	 * @param {jQuery} $elements Elements (or selector) to move.
  	 * @param {bool} condition If true, moves elements to the top. Otherwise, moves elements back to their original locations.
  	 */
  	$.prioritize = function($elements, condition) {
  
  		var key = '__prioritize';
  
  		// Expand $elements if it's not already a jQuery object.
  			if (typeof $elements != 'jQuery')
  				$elements = $($elements);
  
  		// Step through elements.
  			$elements.each(function() {
  
  				var	$e = $(this), $p,
  					$parent = $e.parent();
  
  				// No parent? Bail.
  					if ($parent.length == 0)
  						return;
  
  				// Not moved? Move it.
  					if (!$e.data(key)) {
  
  						// Condition is false? Bail.
  							if (!condition)
  								return;
  
  						// Get placeholder (which will serve as our point of reference for when this element needs to move back).
  							$p = $e.prev();
  
  							// Couldn't find anything? Means this element's already at the top, so bail.
  								if ($p.length == 0)
  									return;
  
  						// Move element to top of parent.
  							$e.prependTo($parent);
  
  						// Mark element as moved.
  							$e.data(key, $p);
  
  					}
  
  				// Moved already?
  					else {
  
  						// Condition is true? Bail.
  							if (condition)
  								return;
  
  						$p = $e.data(key);
  
  						// Move element back to its original location (using our placeholder).
  							$e.insertAfter($p);
  
  						// Unmark element as moved.
  							$e.removeData(key);
  
  					}
  
  			});
  
  	};
  
  })(jQuery);
  