/**
 * Copyright (c) 2008-current HB Stone
 * 
 * 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.
 * 
 * For more information:
 *     http://opensource.org/licenses/mit-license.php
 *     http://jelo.callee.info/
 */

/**
 * @namespace Core utilities and functions.
 */
var Jelo = window.Jelo || {
    version : 1.21
};

/**
 * Can be used to create unique IDs or global counters. Every time this function is called, a number will be returned
 * which is 1 greater than the number previously returned.
 * 
 * @function
 * @return {Number} A unique (to this function), autoincrementing positive integer. The first number returned is 1.
 */
Jelo.uID = function() {
    var id = 1; // initial value
    return function() {
        return id++;
    };
}();

/**
 * A reusable no-operation function. Useful for placeholders.
 */
Jelo.emptyFn = function() {};

/**
 * Performs a function on each item in a collection. If the first argument is not an array or object, the function is
 * called once with it. If the function ever returns false, execution halts immediately and the "failed" index is
 * returned. Normally, Jelo.each will return null.
 * 
 * @param {Array|NodeList|Object} collection The object over which to iterate.
 * @param {Function} fn The function to execute for each item in the collection. Arguments passed to the function will
 *        be the item, its index, and the complete array. For example, myFunc(collection[index], index, collection)
 * @param {Object} [scope] The scope ("this") in which to execute the function. By default, the scope will be the
 *        current item being iterated across.
 */
Jelo.each = function(a, f, s) {
    var i, ai;
    if (typeof a.length == "number") {
        for (i = 0, l = a.length; i < l; i++) {
            ai = a[i];
            if (typeof a[i] != 'undefined') {
                if (f.call(s || ai, ai, i, a) === false) {
                    return i;
                }
            }
        }
    } else if (typeof a == "object") {
        for (i in a) {
            ai = a[i];
            if (a.hasOwnProperty(i) && typeof ai != 'undefined') {
                if (f.call(s || ai, ai, i, a) === false) {
                    return i;
                }
            }
        }
    }
};

/**
 * Binds arguments to a function and optional scope.
 * 
 * @param {Function} fn The function to execute when the delegate is called.
 * @param {Object} [scope=window] The scope in which to execute the chosen function.
 * @param {Mixed} [...] Additional parameters to pass to the function
 * @return {Function} The new function reference.
 */
Jelo.delegate = function(fn, scope) {
    var s = scope || window;
    var a = [].slice.call(arguments, 2);
    return function() {
        fn.apply(s, a);
    };
};

// Normalizes window.console to prevent debug errors when there is no console.
(function() {
    if (!('console' in window) || !('firebug' in console)) {
        var names = ["log", "debug", "info", "warn", "error", "assert", "dir", "dirxml", "group", "groupEnd", "time",
            "timeEnd", "count", "trace", "profile", "profileEnd"];
        window.console = {};
        for (var i = 0; i < names.length; ++i) {
            // TODO: write to a hidden div, provide a method to toggle
            // visibility
            window.console[names[i]] = Jelo.emptyFn;
        }
    }
    /**
     * @namespace
     * @name Console
     * @memberOf Jelo
     */
    Jelo.Console = {
        firebug : window.console
    }; // Google likes to eat the console :(
    /**
     * @function
     * @param {Mixed} Information to log in the debug console.
     */
    Jelo.Console.log = window.console.log;
    /**
     * @function
     * @returns {Array} The current call stack.
     */
    Jelo.Console.getStackTrace = function() {
        var /* counter */i, /* length */len,
            /* stack */s = [],
            /* isStackPopulated */isPop = false, /* lines */l;
        try {
            hb.stone.javascript += 0; // raise an error
        } catch (e) {
            if (e.stack) {
                // Firefox
                l = e.stack.split("\n");
                for (i = 0, len = l.length; i < len; i++) {
                    if (l[i].match(/^\s*[a-z0-9\-_\$]+\(/i)) {
                        s.push(l[i]);
                    }
                }
                s.shift(); // remove call to getStackTrace
                isPop = true;
            } else if (window.opera && e.message) { // Opera
                l = e.message.split("\n");
                for (i = 0, len = l.length; i < len; i++) {
                    if (l[i].match(/^\s*[A-Za-z0-9\-_\$]+\(/)) {
                        var entry = l[i];
                        if (l[i + 1]) {
                            entry += " at " + l[i + 1]; // file info
                            i++;
                        }
                        s.push(entry);
                    }
                }
                s.shift(); // remove call to getStackTrace
                isPop = true;
            }
        }
        if (!isPop) {
            // IE and Safari
            var curr = arguments.callee;
            while ((curr = curr.caller)) {
                var fn = curr.toString();
                var fname = fn.substring(fn.indexOf("function") + 8, fn.indexOf("(")) || "anonymous";
                s.push(fname);
            }
        }
        return s;
        
    };
    
})();

/*
 * Normalizes setTimeout and setInterval behavior across all browsers. Typically, IE does not treat additional arguments
 * correctly, and Firefox adds a "lateness" argument to the supplied function call.
 */
(function(f) {
    /**
     * @function
     * @name setTimeout
     * @param {Function} fn Method to invoke.
     * @param {Number} ms Milliseconds to delay before invoking fn.
     * @param {Mixed} [...] Additional arguments to be passed to fn when it is called.
     * @returns {Number} Resource id, can be cancelled using clearTimeout(id)
     */
    window.setTimeout = f(window.setTimeout);
    /**
     * @function
     * @name setInterval
     * @param {Function} fn Method to invoke.
     * @param {Number} ms Milliseconds to delay between intervals
     * @param {Mixed} [...] Additional arguments to be passed to fn when it is called.
     * @returns {Number} Resource id, can be cancelled using clearInterval(id)
     */
    window.setInterval = f(window.setInterval);
})(function(f) {
    return function(c, t) {
        var a = [].slice.call(arguments, 2);
        return ((typeof c == 'function')
            ? f(function() {
                c.apply(this, a);
            }, t)
            : f.call(this, c, t));
    };
});

// Normalizes Internet Explorer's behavior to match modern browsers.
(function() {
    if (typeof Array.prototype.indexOf == 'undefined') {
        /**
         * @memberOf Array
         * @param {Mixed} x The item to attempt to find.
         * @returns {Number} The item's index if found, -1 otherwise.
         */
        Array.prototype.indexOf = function(k) {
            var len = this.length;
            for (var i = 0; i < len; i++) {
                if (this[i] == k) {
                    return i;
                }
            }
            return -1;
        };
        Array.indexOf = Array.prototype.indexOf;
    }
    if (typeof Array.prototype.lastIndexOf == 'undefined') {
        /**
         * @memberOf Array
         * @param {Mixed} x The item to attempt to find.
         * @returns {Number} The index of the item's last occurrence if found, -1 otherwise.
         */
        Array.prototype.lastIndexOf = function(k) {
            var len = this.length;
            for (var i = len - 1; i > -1; i--) {
                if (this[i] == k) {
                    return i;
                }
            }
            return -1;
        };
        Array.lastIndexOf = Array.prototype.lastIndexOf;
    }
    if (typeof Array.prototype.find == 'undefined') {
        /**
         * @memberOf Array
         * @param {Mixed} x The item to attempt to find, or a RegExp to match.
         * @returns {Array|Boolean} An array of indeces at which the item was found, or at which the RegExp tested
         *          positive. Boolean false if no element matched.
         */
        Array.prototype.find = function(k) {
            var res = [];
            var len = this.length;
            for (var i = 0; i < len; i++) {
                if ((k.test && k.test(this[i])) || k === this[i]) {
                    res.push(i);
                }
            }
            return !!res.length && res;
        };
        Array.find = Array.prototype.find;
    }
    if (typeof Array.prototype.shuffle == 'undefined') {
        /**
         * @memberOf Array
         * @returns {Array} The array, randomized.
         */
        Array.prototype.shuffle = function() {
            for (var j, x, i = this.length; i; j = parseInt(Math.random() * i, 10), x = this[--i], this[i] = this[j], this[j] = x) {}
            return this;
        };
        Array.shuffle = Array.prototype.shuffle;
    }
})();

/**
 * Allows you to check or uncheck a group of checkboxes by clicking and dragging across them (or their labels).
 * 
 * @function
 * @name dragCheckbox
 * @param {HTMLElement} [root=document] The element within which to apply "drag toggle" functionality.
 * @memberOf Jelo
 */
Jelo.dragCheckbox = function(root) {
    root = root || document;
    var dragging = false;
    var current = false;
    var undrag = function() {
        dragging = false;
    };
    Jelo.un(document, "mouseup", undrag);
    Jelo.on(document, "mouseup", undrag);
    var getTarget = function(element) {
        switch (element.tagName.toLowerCase()) {
            case 'input' :
                return element;
            case 'label' :
                Jelo.css(element, '-moz-user-select', 'none');
                Jelo.css(element, '-webkit-user-select', 'ignore');
                element.onselectstart = function() {
                    return false;
                };
                var el = element.getAttribute('for') || element.getAttribute('htmlFor');
                return $('input#' + el);
            default :
                return null; // invalid element
        }
    };
    Jelo.each($$('[type=checkbox]', root), function() {
        var down = function() {
            var box = getTarget(this);
            if (box) {
                dragging = true;
                box.checked = !box.checked;
                current = box.checked;
            }
        };
        var over = function() {
            var box = getTarget(this);
            if (box && dragging) {
                box.checked = current;
            }
        };
        var click = function(target, event) {
            var box = getTarget(this);
            if (box) {
                box.checked = current;
            }
        };
        Jelo.un(this, "mousedown", down);
        Jelo.un(this, "mouseover", over);
        Jelo.un(this, "click", click);
        Jelo.on(this, "mousedown", down);
        Jelo.on(this, "mouseover", over);
        Jelo.on(this, "click", click);
        var label = $('label[for=' + this.id + ']', root);
        if (label) {
            Jelo.un(label, "mousedown", down);
            Jelo.un(label, "mouseover", over);
            Jelo.un(label, "click", click);
            Jelo.on(label, "mousedown", down);
            Jelo.on(label, "mouseover", over);
            Jelo.on(label, "click", click);
        }
    });
};
/**
 * @namespace Information about the current browsing environment (browser type
 * and capabilities, screen size, browser viewport size, etc.)
 */
Jelo.Environment = function() {
    /** @private convenience */
    var D = window.document, DB = D.body;
    
    // many of these are verbatim or adapted from Ext.JS
    var ua = navigator.userAgent.toLowerCase();
    var isStrict = D.compatMode == "CSS1Compat";
    var isWindows = (ua.indexOf("windows") != -1 || ua.indexOf("win32") != -1);
    var isMac = (ua.indexOf("macintosh") != -1 || ua.indexOf("mac os x") != -1);
    var isLinux = (ua.indexOf("linux") != -1);
    var isAir = (ua.indexOf("adobeair") != -1);
    var isWebkit = (/webkit|khtml/).test(ua);
    var isSafari3 = (/webkit\/5/).test(ua);
    var isGecko = !isWebkit && (/gecko/).test(ua);
    var isFirefox = isGecko && (/firefox\/\d/).test(ua);
    var isFirefoxOld = isGecko && (/firefox\/[0-2]/).test(ua);
    var isOpera = (/opera/).test(ua);
    var isIE = !!(window.attachEvent && !isOpera);
    var isIE7 = isIE && (/msie 7/).test(ua);
    var isIE8 = isIE && (/msie 8/).test(ua);
    var isIEOld = isIE && (/msie [0-6]/).test(ua);
    var isGoogle = (/google/).test(ua);
    var isGoogleChrome = (/chrome\/[0-1]/).test(ua);
    var isYahoo = (/yahoo/).test(ua);
    var isBot = (/bot|crawler|http/).test(ua);
    var isSecure = window.location.href.toLowerCase().indexOf("https") === 0;
    var isModern = (typeof XMLHttpRequest != "undefined");
    
    // fix css image flicker
    if (isIEOld) {
        try {
            D.execCommand("BackgroundImageCache", false, true);
        } catch (e) {}
    }
    
    return {
        /**
         * When "full" is true, returns the width of the physical display screen.
         * When "full" is false, returns the screen width minus the taskbar (if applicable).
         * @param {Boolean} [full]
         * @returns {Number}
         */
        getScreenWidth    : function(full) {
            return full ? screen.width : screen.availWidth;
        },
        /**
         * When "full" is true, returns the height of the physical display screen.
         * When "full" is false, returns the screen height minus the taskbar (if applicable).
         * @param {Boolean} [full]
         * @returns {Number}
         */
        getScreenHeight   : function(full) {
            return full ? screen.height : screen.availHeight;
        },
        /**
         * When "full" is true, shorthand for getDocumentWidth.
         * When "full" is false, shorthand for getViewportWidth.
         * @param {Boolean} [full]
         * @returns {Number}
         */
        getViewWidth      : function(full) {
            return full ? this.getDocumentWidth() : this.getViewportWidth();
        },
        /**
         * When "full" is true, shorthand for getDocumentHeight.
         * When "full" is false, shorthand for getViewportHeight.
         * @param {Boolean} [full]
         * @returns {Number} The Viewport or Document height.
         */
        getViewHeight     : function(full) {
            return full ? this.getDocumentHeight() : this.getViewportHeight();
        },
        /**
         * @returns {Number} The complete horizontal size of the document, including scrollable content.
         */
        getDocumentWidth  : function() {
            var scrollWidth = (D.compatMode != "CSS1Compat")
                ? DB.scrollWidth
                : D.documentElement.scrollWidth;
            return Math.max(scrollWidth, this.getViewportWidth());
        },
        /**
         * @returns {Number} The complete vertical size of the document, including scrollable content.
         */
        getDocumentHeight : function() {
            var scrollHeight = (D.compatMode != "CSS1Compat")
                ? DB.scrollHeight
                : D.documentElement.scrollHeight;
            return Math.max(scrollHeight, this.getViewportHeight());
        },
        /**
         * @returns {Number} The current width of the browser's visible area.
         */
        getViewportWidth  : function() {
            if (this.isIE) {
                return this.isStrict ? D.documentElement.clientWidth : DB.clientWidth;
            } else {
                return window.innerWidth;
            }
        },
        /**
         * @returns {Number} The current height of the browser's visible area.
         */
        getViewportHeight : function() {
            if (this.isIE) {
                return this.isStrict ? D.documentElement.clientHeight : DB.clientHeight;
            } else {
                return window.innerHeight;
            }
        },
        /**
         * @returns {String} The browser's User Agent.
         */
        getUA             : function() {
            return ua;
        },
        /**
         * True if the browser is in strict mode.
         *
         * @type Boolean
         */
        isStrict          : isStrict,
        /**
         * True in a Windows environment.
         *
         * @type Boolean
         */
        isWindows         : isWindows,
        /**
         * True in an Apple Macintosh environment.
         *
         * @type Boolean
         */
        isMac             : isMac,
        /**
         * True in a Linux environment.
         *
         * @type Boolean
         */
        isLinux           : isLinux,
        /**
         * True in an Adobe AIR environment.
         *
         * @type Boolean
         */
        isAir             : isAir,
        /**
         * True if the browser identifies itself as using the Webkit rendering
         * engine (includes Safari and Google Chrome).
         *
         * @type Boolean
         */
        isWebkit          : isWebkit,
        /**
         * True if the browser appears to be Safari 3.x.
         *
         * @type Boolean
         */
        isSafari3         : isSafari3,
        /**
         * True if the browser identifies itself as using the Gecko rendering
         * engine (includes Firefox and several "microbrew" browsers).
         *
         * @type Boolean
         */
        isGecko           : isGecko,
        /**
         * True if the browser identifies itself as Mozilla Firefox.
         *
         * @type Boolean
         */
        isFirefox         : isFirefox,
        /**
         * True if the browser identifies itself as Opera.
         *
         * @type Boolean
         */
        isOpera           : isOpera,
        /**
         * True if the browser identifies itself as Microsoft Internet Explorer.
         *
         * @type Boolean
         */
        isIE              : isIE,
        /**
         * True if the browser identifies itself as Microsoft Internet Explorer
         * 7.
         *
         * @type Boolean
         */
        isIE7             : isIE7,
        /**
         * True if the browser identifies itself as Microsoft Internet Explorer
         * 8.
         *
         * @type Boolean
         */
        isIE8             : isIE8,
        /**
         * True if the browser identifies itself as Microsoft Internet Explorer
         * 6 or older.
         *
         * @type Boolean
         */
        isIEOld           : isIEOld,
        /**
         * True if the visitor identifies itself as Google, including the
         * Googlebot search engine and various other Google robots (which may or
         * may not support JavaScript anyway...).
         *
         * @type Boolean
         */
        isGoogle          : isGoogle,
        /**
         * True if the visitor identifies itself as Yahoo, including the Yahoo
         * Slurp search engine (which may or may not support JavaScript
         * anyway...)
         *
         * @type Boolean
         */
        isYahoo           : isYahoo,
        /**
         * True if the visitor appears to be a robot. Not necessarily a good or
         * bad thing, nor does this imply support (or lack thereof) for any
         * particular JS or CSS feature.
         *
         * @type Boolean
         */
        isBot             : isBot,
        /**
         * True if the browser is in secure mode (HTTPS).
         *
         * @type Boolean
         */
        isSecure          : isSecure,
        /**
         * True if the browser is standards-compliant. Note that actual
         * compliance depends on the page being viewed, this just determines
         * whether the browser can understand standards-compliant code.
         *
         * @type Boolean
         * @deprecated Replaced with {@link Jelo.Environment.isModern}
         * in v1.05, you should use that instead.
         */
        isStandard        : isModern, // deprecated
        /**
         * True in all modern browsers, indicates native AJAX support via
         * XMLHttpRequest. IE version 6 and older are known to be false,
         * nearly every other browser supports XMLHttpRequest natively.
         * 
         * @type Boolean
         */
        isModern          : isModern
    };
}();

/**
 * Shorthand for {@link Jelo.Environment}
 *
 * @memberOf Jelo
 * @namespace
 */
Jelo.Env = Jelo.Environment;
/**
 * @namespace Simple methods to assert that various objects exist or are of a
 * given type.
 */
Jelo.Valid = function() {
    
    // the easy way
    Array.prototype.isArray = true;
    Function.prototype.isFunction = true;
    Number.prototype.isNumber = true;
    String.prototype.isString = true;
    
    /** @scope Jelo.Valid */
    return {
        
        /**
         * Checks whether an object exists (not necessarily truthy).
         * 
         * @param {Mixed} item The item to investiate.
         * @return {Boolean} False only for null and undefined values
         */
        is         : function(i) {
            return (typeof i === "undefined") || (i === null);
        },
        
        /**
         * Checks whether an object is an array or array subclass.
         * 
         * @param {Mixed} item The item to investiate.
         * @return {Boolean} True if item is an array or array subclass.
         */
        isArray    : function(a) {
            return a && (a.constructor == Array);
        },
        
        /**
         * Checks whether an object is "Array-like" and can probably 
         * be iterated over, looped through, etc.
         * 
         * @param {Mixed} item The item to investiate.
         * @return {Boolean} True if item is iterable.
         */
        isIterable : function(a) {
            return (typeof a == 'object' && typeof a.length == 'number');
        },
        
        /**
         * Checks whether an object is an HTML/DOM element.
         * 
         * @param {Mixed} item The item to investiate.
         * @return {Boolean} True if item is an HTMLElement-like object.
         */
        isElement  : function(e) {
            return (typeof e == "object") && (e !== null) &&
                (typeof e.tagName == "string") &&
                (typeof e.className == "string");
        },
        
        /**
         * Checks whether an object is a Function.
         * 
         * @param {Mixed} item The item to investiate.
         * @return {Boolean} True if item is a Function.
         */
        isFunction : function(f) {
            return !!f.isFunction || (f instanceof Function) ||
                (typeof f == "function");
        },
        
        /**
         * Check whether an object is a Number.
         * 
         * @param {Mixed} item The item to investiate.
         * @return {Boolean} True if item is a Number.
         */
        isNumber   : function(n) {
            return n && (!!n.isNumber || !isNaN(n));
        },
        
        /**
         * Checks whether an e-mail address is formatted correctly.
         * 
         * @param {Mixed} item The item to investiate.
         * @return {Boolean} True if item is formatted like a valid e-mail
         * address. The actual account may or may not exist. Future versions of
         * Jelo will include a check for actual e-mail accounts.
         */
        isEmail    : function(e) {
            var regex = /^[a-z0-9_\-]+(\.[_a-z0-9\-]+)*@([_a-z0-9\-]+\.)+([a-z]{2}|aero|arpa|biz|com|coop|edu|gov|info|int|jobs|mil|museum|name|nato|net|org|pro|travel)$/;
            return regex.test(e);
        }
    };
}();
/**
 * @namespace Formatting and conversion utilities.
 */
Jelo.Format = function() {
    /** @scope Jelo.Format */
    return {
        /**
         * Converts a string from CSS hyphenated to Javascript camelCase.
         * 
         * <pre><code>
        // example:
        var property = Jelo.Format.toCamel("margin-left");
        alert(property); // alerts "marginLeft"
        </code></pre>
         */
        toCamel               : function(str) {
            return str.replace(/-(.)/g, function(m, l) {
                return l.toUpperCase();
            });
        },
        
        /**
         * @deprecated v1.02: Too long a name, replaced by
         * {@link Jelo.Format.toCamel}
         */
        hyphenatedToCamelCase : function(str) {
            return Jelo.Format.toCamel(str);
        },
        
        /**
         * Mainly used by internal Jelo functions. If a hash 
         * mark (#) appears at the beginning of the string, it 
         * will be stripped.
         * 
         * @param {String} hex A value such as "#0080FF"
         * @returns {String} A value such as "0080FF"
         */
        cutHexHash            : function(h) {
            return (h.charAt(0) == "#") ? h.substring(1) : h;
        },
        /**
         * Returns the "red" value from a CSS-style hex string.
         * 
         * @param {String} hex A value such as "#9966CC"
         * @returns {String} For the above example, "99"
         */
        hexToR                : function(h) {
            return parseInt(Jelo.Format.cutHexHash(h).substring(0, 2), 16);
        },
        /**
         * Returns the "green" value from a CSS-style hex string.
         * 
         * @param {String} hex A value such as "#9966CC"
         * @returns {String} For the above example, "66"
         */
        hexToG                : function(h) {
            return parseInt(Jelo.Format.cutHexHash(h).substring(2, 4), 16);
        },
        /**
         * Returns the "blue" value from a CSS-style hex string.
         * 
         * @param {String} hex A value such as "#9966CC"
         * @returns {String} For the above example, "CC"
         */
        hexToB                : function(h) {
            return parseInt(Jelo.Format.cutHexHash(h).substring(4, 6), 16);
        },
        /**
         * Splits a CSS-style hex value into a array of Red, Green, and Blue values.
         * 
         * @param {String} hex A value such as "#9966CC"
         * @returns {Array} For the above example, ["99", "66", "CC"]
         */
        hexToRGB              : function(h) {
            var r = this.hexToR(h);
            var g = this.hexToG(h);
            var b = this.hexToB(h);
            return [r, g, b];
        },
        
        /**
         * Converts a CSS RGB string to an array of RGB values. Mainly useful to
         * internal Jelo functions.
         * 
         * @param {String} hex A value such as "rgb(0, 128, 255)"
         * @returns {Array} For the above example, [0, 128, 255]
         */
        rgbStringToArray      : function(s) {
            if (s.isString) {
                try {
                    var sub = s.split(/\D/g);
                    var sub2 = [];
                    for (var i = 0; i < sub.length; i++) {
                        if (sub[i]) {
                            sub2[sub2.length] = parseInt(sub[i], 10);
                        }
                    }
                    return sub2;
                } catch (e) {
                    throw new Error("Jelo.Format.rgbStringToArray: Invalid input " + s);
                }
            } else {
                return [];
            }
        },
        
        /**
         * Converts a CSS RGB string to a CSS hex string. Mainly useful to
         * internal Jelo functions.
         * 
         * @param {String} hex A value such as "rgb(0, 128, 255)"
         * @returns {Array} For the above example, "#0080FF"
         */
        rgbToHex              : function(s) {
            if (s.isString) {
                try {
                    function toHex(n) {
                        var chr = "0123456789ABCDEF";
                        if (n === null) {
                            return "00";
                        }
                        n = parseInt(n, 10);
                        if (isNaN(n) || !n) {
                            return "00";
                        }
                        n = Math.max(0, n);
                        n = Math.min(n, 255);
                        n = Math.round(n);
                        return chr.charAt((n - n % 16) / 16) + chr.charAt(n % 16);
                    }
                    var a = Jelo.Format.rgbStringToArray(s);
                    return "#" + toHex(a[0]) + toHex(a[1]) + toHex(a[2]);
                } catch (e) {
                    throw new Error("Jelo.Format.rgbStringToHex: Invalid input " + s);
                }
            }
        },
        
        /**
         * Port of http://php.net/urldecode by http://kevin.vanzonneveld.net
         * 
         * @see <a href="http://php.net/urldecode">PHP documentation</a>
         * @see <a href="http://kevin.vanzonneveld.net">Original port</a>
         * @param {String} str
         * @returns {String}
         */
        urldecode             : function(str) {
            var /* histogram */h = {}, ret = (str || '').toString();
            function rep(s, r, str) {
                var tmp_arr = [];
                tmp_arr = str.split(s);
                return tmp_arr.join(r);
            }
            h["'"] = '%27';
            h['('] = '%28';
            h[')'] = '%29';
            h['*'] = '%2A';
            h['~'] = '%7E';
            h['!'] = '%21';
            h['%20'] = '+';
            for (var r in h) {
                if (h.hasOwnProperty(s)) {
                    s = h[r]; // Switch order when decoding
                    ret = rep(s, r, ret); // Custom replace. No regexing   
                }
            }
            return decodeURIComponent(ret);
        },
        
        /**
         * Port of http://php.net/urlencode by http://kevin.vanzonneveld.net
         * 
         * @see <a href="http://php.net/urldecode">PHP documentation</a>
         * @see <a href="http://kevin.vanzonneveld.net">Original port</a>
         * @param {String} str
         * @returns {String}
         */
        urlencode             : function(str) {
            var /* histogram */h = {}, tmp_arr = [], ret = (str || '').toString();
            function rep(s, r, str) {
                var tmp_arr = [];
                tmp_arr = str.split(s);
                return tmp_arr.join(r);
            }
            h["'"] = '%27';
            h['('] = '%28';
            h[')'] = '%29';
            h['*'] = '%2A';
            h['~'] = '%7E';
            h['!'] = '%21';
            h['%20'] = '+';
            ret = encodeURIComponent(ret);
            for (var s in h) {
                if (h.hasOwnProperty(s)) {
                    r = h[s]; // Switch order when encoding
                    ret = rep(s, r, ret); // Custom replace. No regexing
                }
            }
            return ret.replace(/(\%([a-z0-9]{2}))/g, function(full, m1, m2) {
                return "%" + m2.toUpperCase();
            });
        },
        
        /**
         * Alias for {@link Jelo.Format.urldecode}
         */
        urlDecode             : function(str) {
            return Jelo.Format.urldecode(str);
        },
        
        /**
         * Alias for {@link Jelo.Format.urlencode}
         */
        urlEncode             : function(str) {
            return Jelo.Format.urlencode(str);
        },
        
        /**
         * Converts a valid JSON string to a native Object.
         * 
         * @param {String} json A JSON-encoded string, e.g. '{ name : "John", age: "28" }'
         * @returns {Object} The object represented by valid input, or {} for invalid input.
         */
        fromJSON              : function(str) {
            try {
                var crockford = /[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/;
                var replace = /"(\\.|[^"\\])*"/g;
                return !(crockford.test(o.replace(replace, ''))) && eval('(' + o + ')');
            } catch (e) {
                return {};
            }
        }
    };
}();
/**
 * @namespace CSS stuff
 */
Jelo.CSS = function() {
    /**
     * @private Resets the opacity style of a DOM element. Required for IE.
     */
    var clearOpacity = function(el) {
        if (Jelo.Env.isIE) {
            if (typeof el.style.filter === "string" && (/alpha/i).test(el.style.filter)) {
                el.style.filter = "";
            }
        } else {
            el.style.opacity = "";
            el.style["-moz-opacity"] = "";
            el.style["-khtml-opacity"] = "";
        }
    };
    
    /** @private alias */
    var toCamel = Jelo.Format.toCamel;
    
    /** @scope Jelo.CSS */
    return {
        /**
         * Mainly used by internal functions, clearOpacity fixes IE behavior.
         * 
         * @function
         * @param {HTMLElement} element
         */
        clearOpacity : clearOpacity,
        
        /**
         * @function
         * @param {HTMLElement} element The item to which the class should be
         *        assigned.
         * @param {String} class The class name to assign. Duplicates will be
         *        filtered out automatically.
         */
        addClass     : function(e, c) {
        	if (Jelo.Valid.isArray(e)) {
        		var fn = arguments.callee;
        		Jelo.each(e, function() {
        			fn(this, c);
        		});
        		return;
        	}
            var curr = e.className;
            if (!Jelo.CSS.hasClass(e, c)) {
                e.className = curr + (curr.length ? " " : "") + c;
            }
        },
        
        /**
         * @function
         * @param {HTMLElement} element The item to investigate.
         * @param {String} class The class name to search for.
         * @returns {Boolean} True if the item's className property contains the
         *          supplied class name.
         */
        hasClass     : function(e, c) {
            return c && (' ' + e.className + ' ').indexOf(' ' + c + ' ') != -1;
        },
        
        /**
         * @function
         * @param {HTMLElement} element The item to affect.
         * @param {String} class The class name to remove.
         */
        removeClass  : function(e, c) {
            if (Jelo.Valid.isArray(e)) {
                var fn = arguments.callee;
                Jelo.each(e, function() {
                    fn(this, c);
                });
                return;
            }
            e.className = e.className.replace(new RegExp('\\b' + c + '\\b'), '').replace(/^\s\s*/, '').replace(/\s\s*$/, '');
        },
        
        /**
         * Gets the current or computed value for an element's CSS property.
         * 
         * @function
         * @param {HTMLElement|HTMLElement[]|String} element One or more items
         *        to investigate. If a string is supplied, it is considered a
         *        CSS selector and will match elements accordingly.
         * @param {String} property The CSS property to retrieve
         * @return {String} CSS property value
         */
        getStyle     : function() {
            var view = document.defaultView;
            return (view && view.getComputedStyle) ? function(el, p, toInt) {
                var /* counter */i, /* value */v, /* return value */ret, /* camelCase property */cp, styles = [];
                if (!el || !p) {
                    return null;
                }
                if (typeof el == "string") {
                    el = Jelo.Dom.select(el);
                }
                if (typeof p == "string") {
                    p = p.toLowerCase();
                }
                if (Jelo.Valid.isArray(el)) {
                    for (i = 0; i < el.length; i++) {
                        styles.push(Jelo.CSS.getStyle(el[i], p, toInt));
                    }
                    return styles;
                }
                if (Jelo.Valid.isArray(p)) {
                    for (i = 0; i < p.length; i++) {
                        styles.push(Jelo.CSS.getStyle(el, p[i], toInt));
                    }
                    return styles;
                }
                if (p == "float") {
                    p = "cssFloat";
                }
                cp = toCamel(p);
                switch (cp) {
                    case "backgroundPositionX" :
                        try {
                            ret = Jelo.CSS.getStyle(el, "background-position").split(" ")[0];
                        } catch (e1) {
                            return null;
                        }
                        break;
                    case "backgroundPositionY" :
                        try {
                            ret = Jelo.CSS.getStyle(el, "background-position").split(" ")[1];
                        } catch (e2) {
                            return null;
                        }
                        break;
                    default :
                        if ((v = el.style[p])) {
                            ret = (/color/).test(p)
                                ? Jelo.Format.rgbToHex(v)
                                : v.toString().toLowerCase();
                        } else if ((v = view.getComputedStyle(el, "")[cp])) {
                            ret = (/color/).test(p) ? Jelo.Format.rgbToHex(v) : v.toString();
                        }
                }
                return toInt ? parseInt(ret, 10) : ret;
            }
                : function(el, p, toInt) {
                    var /* counter */i, /* value */v, /* return value */ret, /* camelCase property */cp, styles = [];
                    if (!el || !p) {
                        return null;
                    }
                    if (typeof el == "string") {
                        el = Jelo.Dom.select(el);
                    }
                    if (typeof p == "string") {
                        p = p.toLowerCase();
                    }
                    if (Jelo.Valid.isArray(el)) {
                        for (i = 0; i < el.length; i++) {
                            styles.push(Jelo.CSS.getStyle(el[i], p, toInt));
                        }
                        return styles;
                    }
                    if (Jelo.Valid.isArray(p)) {
                        for (i = 0; i < p.length; i++) {
                            styles.push(Jelo.CSS.getStyle(el, p[i], toInt));
                        }
                        return styles;
                    }
                    if (p == "opacity") {
                        if (typeof el.style.filter == 'string') {
                            var m = el.style.filter.match(/alpha\(opacity=(.*)\)/i);
                            if (m) {
                                var fv = parseFloat(m[1]);
                                if (!isNaN(fv)) {
                                    return fv ? fv / 100 : 0;
                                }
                            }
                        }
                        return 1;
                    }
                    if (p == "float") {
                        p = "styleFloat";
                    }
                    p = toCamel(p);
                    if ((v = el.style[p])) {
                        ret = v.toString();
                    } else if (el.currentStyle && (v = el.currentStyle[p])) {
                        if (v == "auto") {
                            if ((v = el["offset" + p.replace(/^(.)/, function(m, l) {
                                return l.toUpperCase(); // initial cap
                            })])) {
                                ret = v + "px";
                            }
                        }
                        ret = v.toString();
                    }
                    return toInt ? parseInt(ret, 10) : ret;
                };
        }(),
        
        /**
         * Gets the current or computed value for an element's CSS property.
         * 
         * @function
         * @param {HTMLElement|HTMLElement[]|String} element One or more items
         *        to affect. If a string is supplied, it is considered a CSS
         *        selector and will match elements accordingly.
         * @param {String|String[]} property The CSS property to assign.
         * @param {String|String[]} value The value to assign.
         */
        setStyle     : function(el, p, v) {
            var /* counter */i, /* units */u;
            if (typeof el === "string") {
                el = Jelo.Dom.select(el);
            }
            if (Jelo.Valid.isArray(el)) {
                for (i = 0; i < el.length; i++) {
                    Jelo.CSS.setStyle(el[i], p, v);
                }
                return;
            }
            if (Jelo.Valid.isArray(p) || Jelo.Valid.isArray(v)) {
                if (Jelo.Valid.isArray(p) && Jelo.Valid.isArray(v) && (p.length == v.length)) {
                    for (i = 0; i < p.length; i++) {
                        Jelo.CSS.setStyle(el, p[i], v[i]);
                    }
                } else {
                    throw new Error('Jelo.CSS.setStyle: Properties and values must both be Arrays with the same length, or both be Strings.');
                }
                return;
            }
            p = toCamel(p);
            if ((/width|height|top|right|bottom|left|size/).test(p)) {
                u = v.replace(/[^(%|px|em)]/g, "");
                if (!u.length) {
                    u = "px";
                }
                v = parseInt(v, 10);
                if (isNaN(v)) {
                    v = 0;
                }
                v += u;
            }
            var s = el.style;
            if (p === "opacity") {
                if (Jelo.Env.isIE) {
                    s.zoom = 1;
                    s.filter = (s.filter || '').replace(/alpha\([^\)]*\)/gi, "") +
                        (v == 1 ? "" : " alpha(opacity=" + v * 100 + ")");
                } else {
                    s.opacity = parseFloat(v);
                }
            } else {
                s[p] = v;
            }
        },
        
        /**
         * Generates a random hexidecimal color, including the hash symbol (e.g.
         * #5181ff)
         * 
         * @returns {String} A "CSS-formatted" color.
         */
        randomColor  : function() {
            return '#' + (function(h) {
                return new Array(7 - h.length).join('0') + h;
            })((Math.random() * (0xFFFFFF + 1) << 0).toString(16));
        },
        
        /**
         * Gets a CSS stylesheet rule.
         * http://www.hunlock.com/blogs/Totally_Pwn_CSS_with_Javascript
         * 
         * @function
         * @param {String} selector CSS selector, exactly as entered in the stylesheet.
         * @param {Boolean=false} deleteFlag True to delete the matching rule.
         */
        getRule      : function(s, d) {
            s = s.toLowerCase && s.toLowerCase();
            if (document.styleSheets) {
                for (var i = 0; i < document.styleSheets.length; i++) {
                    var styleSheet = document.styleSheets[i];
                    var ii = 0; // TODO: convert to for loop
                    var cssRule = false;
                    do {
                        if (styleSheet.cssRules) {
                            cssRule = styleSheet.cssRules[ii];
                        } else {
                            cssRule = styleSheet.rules[ii];
                        }
                        if (cssRule) {
                            if (cssRule.selectorText.toLowerCase() == s) {
                                if (d) {
                                    if (styleSheet.cssRules) {
                                        styleSheet.deleteRule(ii);
                                    } else {
                                        styleSheet.removeRule(ii);
                                    }
                                    return true;
                                } else {
                                    return cssRule;
                                }
                            }
                        }
                        ii++;
                    } while (cssRule)
                }
            }
            return false;
        },
        /**
         * Deletes a CSS stylesheet rule.
         * http://www.hunlock.com/blogs/Totally_Pwn_CSS_with_Javascript
         * 
         * @function
         * @param {String} selector CSS selector, exactly as entered in the stylesheet.
         */
        deleteRule   : function(s) {
            return Jelo.CSS.getRule(s, true);
        },
        /**
         * Creates a new CSS stylesheet rule.
         * http://www.hunlock.com/blogs/Totally_Pwn_CSS_with_Javascript
         * 
         * @function
         * @param {String} selector CSS selector, exactly as entered in the stylesheet.
         * @returns {CSSRule} A new rule that can be modified as follows:
         * var r = Jelo.CSS.createRule('#test'); r.style.color = 'green';
         */
        createRule   : function(s) {
            if (document.styleSheets) {
                if (!getRule(s)) {
                    if (document.styleSheets[0].addRule) {
                        document.styleSheets[0].addRule(s, null, 0);
                    } else {
                        document.styleSheets[0].insertRule(s + ' { }', 0);
                    }
                }
            }
            return Jelo.CSS.getRule(s);
        }
    };
}();

/**
 * Convenience method. If two arguments are supplied, this is shorthand for
 * {@link Jelo.CSS.getStyle}. If three arguments are supplied, this is
 * shorthand for {@link Jelo.CSS.setStyle}.
 * 
 * @function
 * @name css
 * @memberOf Jelo
 */
Jelo.css = function() {
    if (arguments.length == 2) {
        return Jelo.CSS.getStyle(arguments[0], arguments[1]);
    }
    if (arguments.length == 3) {
        return Jelo.CSS.setStyle(arguments[0], arguments[1], arguments[2]);
    }
    throw new Error("Jelo.css(element, property) for getStyle, and Jelo.css(element, property, value) for setStyle.");
};
/*
 * ! Sizzle CSS Selector Engine - v1.0 Copyright 2009, The Dojo Foundation
 * Released under the MIT, BSD, and GPL Licenses. More information:
 * http://sizzlejs.com/
 */
(function() {
    
    var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?/g,
        done = 0,
        toString = Object.prototype.toString,
        hasDuplicate = false;
    
    var Sizzle = function(selector, context, results, seed) {
        results = results || [];
        var origContext = context = context || document;
        
        if (context.nodeType !== 1 && context.nodeType !== 9) {
            return [];
        }
        
        if (!selector || typeof selector !== "string") {
            return results;
        }
        
        var parts = [], sortOrder, m, set, checkSet, extra,
            prune = true,
            contextXML = isXML(context);
        
        // Reset the position of the chunker regexp (start from head)
        chunker.lastIndex = 0;
        
        while ((m = chunker.exec(selector)) !== null) {
            parts.push(m[1]);
            
            if (m[2]) {
                extra = RegExp.rightContext;
                break;
            }
        }
        
        if (parts.length > 1 && origPOS.exec(selector)) {
            if (parts.length === 2 && Expr.relative[parts[0]]) {
                set = posProcess(parts[0] + parts[1], context);
            } else {
                set = Expr.relative[parts[0]]
                    ? [context]
                    : Sizzle(parts.shift(), context);
                
                while (parts.length) {
                    selector = parts.shift();
                    
                    if (Expr.relative[selector]) selector += parts.shift();
                    
                    set = posProcess(selector, set);
                }
            }
        } else {
            // Take a shortcut and set the context if the root selector is an ID
            // (but not if it'll be faster if the inner selector is an ID)
            if (!seed && parts.length > 1 && context.nodeType === 9 &&
                !contextXML && Expr.match.ID.test(parts[0]) &&
                !Expr.match.ID.test(parts[parts.length - 1])) {
                var ret = Sizzle.find(parts.shift(), context, contextXML);
                context = ret.expr
                    ? Sizzle.filter(ret.expr, ret.set)[0]
                    : ret.set[0];
            }
            
            if (context) {
                var ret = seed
                    ? {
                        expr : parts.pop(),
                        set  : makeArray(seed)
                    }
                    : Sizzle.find(parts.pop(), parts.length === 1 &&
                        (parts[0] === "~" || parts[0] === "+") &&
                        context.parentNode
                        ? context.parentNode
                        : context, contextXML);
                set = ret.expr
                    ? Sizzle.filter(ret.expr, ret.set)
                    : ret.set;
                
                if (parts.length > 0) {
                    checkSet = makeArray(set);
                } else {
                    prune = false;
                }
                
                while (parts.length) {
                    var cur = parts.pop(),
                        pop = cur;
                    
                    if (!Expr.relative[cur]) {
                        cur = "";
                    } else {
                        pop = parts.pop();
                    }
                    
                    if (pop == null) {
                        pop = context;
                    }
                    
                    Expr.relative[cur](checkSet, pop, contextXML);
                }
            } else {
                checkSet = parts = [];
            }
        }
        
        if (!checkSet) {
            checkSet = set;
        }
        
        if (!checkSet) {
            throw "Syntax error, unrecognized expression: " + (cur || selector);
        }
        
        if (toString.call(checkSet) === "[object Array]") {
            if (!prune) {
                results.push.apply(results, checkSet);
            } else if (context && context.nodeType === 1) {
                for (var i = 0; checkSet[i] != null; i++) {
                    if (checkSet[i] &&
                        (checkSet[i] === true || checkSet[i].nodeType === 1 &&
                            contains(context, checkSet[i]))) {
                        results.push(set[i]);
                    }
                }
            } else {
                for (var i = 0; checkSet[i] != null; i++) {
                    if (checkSet[i] && checkSet[i].nodeType === 1) {
                        results.push(set[i]);
                    }
                }
            }
        } else {
            makeArray(checkSet, results);
        }
        
        if (extra) {
            Sizzle(extra, origContext, results, seed);
            Sizzle.uniqueSort(results);
        }
        
        return results;
    };
    
    Sizzle.uniqueSort = function(results) {
        if (sortOrder) {
            hasDuplicate = false;
            results.sort(sortOrder);
            
            if (hasDuplicate) {
                for (var i = 1; i < results.length; i++) {
                    if (results[i] === results[i - 1]) {
                        results.splice(i--, 1);
                    }
                }
            }
        }
    };
    
    Sizzle.matches = function(expr, set) {
        return Sizzle(expr, null, null, set);
    };
    
    Sizzle.find = function(expr, context, isXML) {
        var set, match;
        
        if (!expr) {
            return [];
        }
        
        for (var i = 0, l = Expr.order.length; i < l; i++) {
            var type = Expr.order[i], match;
            
            if ((match = Expr.match[type].exec(expr))) {
                var left = RegExp.leftContext;
                
                if (left.substr(left.length - 1) !== "\\") {
                    match[1] = (match[1] || "").replace(/\\/g, "");
                    set = Expr.find[type](match, context, isXML);
                    if (set != null) {
                        expr = expr.replace(Expr.match[type], "");
                        break;
                    }
                }
            }
        }
        
        if (!set) {
            set = context.getElementsByTagName("*");
        }
        
        return {
            set  : set,
            expr : expr
        };
    };
    
    Sizzle.filter = function(expr, set, inplace, not) {
        var old = expr,
            result = [],
            curLoop = set, match, anyFound,
            isXMLFilter = set && set[0] && isXML(set[0]);
        
        while (expr && set.length) {
            for (var type in Expr.filter) {
                if ((match = Expr.match[type].exec(expr)) != null) {
                    var filter = Expr.filter[type], found, item;
                    anyFound = false;
                    
                    if (curLoop == result) {
                        result = [];
                    }
                    
                    if (Expr.preFilter[type]) {
                        match = Expr.preFilter[type](match, curLoop, inplace, result, not, isXMLFilter);
                        
                        if (!match) {
                            anyFound = found = true;
                        } else if (match === true) {
                            continue;
                        }
                    }
                    
                    if (match) {
                        for (var i = 0; (item = curLoop[i]) != null; i++) {
                            if (item) {
                                found = filter(item, match, i, curLoop);
                                var pass = not ^ !!found;
                                
                                if (inplace && found != null) {
                                    if (pass) {
                                        anyFound = true;
                                    } else {
                                        curLoop[i] = false;
                                    }
                                } else if (pass) {
                                    result.push(item);
                                    anyFound = true;
                                }
                            }
                        }
                    }
                    
                    if (found !== undefined) {
                        if (!inplace) {
                            curLoop = result;
                        }
                        
                        expr = expr.replace(Expr.match[type], "");
                        
                        if (!anyFound) {
                            return [];
                        }
                        
                        break;
                    }
                }
            }
            
            // Improper expression
            if (expr == old) {
                if (anyFound == null) {
                    throw "Syntax error, unrecognized expression: " + expr;
                } else {
                    break;
                }
            }
            
            old = expr;
        }
        
        return curLoop;
    };
    
    var Expr = Sizzle.selectors = {
        order      : ["ID", "NAME", "TAG"],
        match      : {
            ID     : /#((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
            CLASS  : /\.((?:[\w\u00c0-\uFFFF_-]|\\.)+)/,
            NAME   : /\[name=['"]*((?:[\w\u00c0-\uFFFF_-]|\\.)+)['"]*\]/,
            ATTR   : /\[\s*((?:[\w\u00c0-\uFFFF_-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,
            TAG    : /^((?:[\w\u00c0-\uFFFF\*_-]|\\.)+)/,
            CHILD  : /:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,
            POS    : /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,
            PSEUDO : /:((?:[\w\u00c0-\uFFFF_-]|\\.)+)(?:\((['"]*)((?:\([^\)]+\)|[^\2\(\)]*)+)\2\))?/
        },
        attrMap    : {
            "class" : "className",
            "for"   : "htmlFor"
        },
        attrHandle : {
            href : function(elem) {
                return elem.getAttribute("href");
            }
        },
        relative   : {
            "+" : function(checkSet, part, isXML) {
                var isPartStr = typeof part === "string",
                    isTag = isPartStr && !/\W/.test(part),
                    isPartStrNotTag = isPartStr && !isTag;
                
                if (isTag && !isXML) {
                    part = part.toUpperCase();
                }
                
                for (var i = 0, l = checkSet.length, elem; i < l; i++) {
                    if ((elem = checkSet[i])) {
                        while ((elem = elem.previousSibling) &&
                            elem.nodeType !== 1) {}
                        
                        checkSet[i] = isPartStrNotTag || elem &&
                            elem.nodeName === part
                            ? elem || false
                            : elem === part;
                    }
                }
                
                if (isPartStrNotTag) {
                    Sizzle.filter(part, checkSet, true);
                }
            },
            ">" : function(checkSet, part, isXML) {
                var isPartStr = typeof part === "string";
                
                if (isPartStr && !/\W/.test(part)) {
                    part = isXML
                        ? part
                        : part.toUpperCase();
                    
                    for (var i = 0, l = checkSet.length; i < l; i++) {
                        var elem = checkSet[i];
                        if (elem) {
                            var parent = elem.parentNode;
                            checkSet[i] = parent.nodeName === part
                                ? parent
                                : false;
                        }
                    }
                } else {
                    for (var i = 0, l = checkSet.length; i < l; i++) {
                        var elem = checkSet[i];
                        if (elem) {
                            checkSet[i] = isPartStr
                                ? elem.parentNode
                                : elem.parentNode === part;
                        }
                    }
                    
                    if (isPartStr) {
                        Sizzle.filter(part, checkSet, true);
                    }
                }
            },
            ""  : function(checkSet, part, isXML) {
                var doneName = done++,
                    checkFn = dirCheck;
                
                if (!part.match(/\W/)) {
                    var nodeCheck = part = isXML
                        ? part
                        : part.toUpperCase();
                    checkFn = dirNodeCheck;
                }
                
                checkFn("parentNode", part, doneName, checkSet, nodeCheck, isXML);
            },
            "~" : function(checkSet, part, isXML) {
                var doneName = done++,
                    checkFn = dirCheck;
                
                if (typeof part === "string" && !part.match(/\W/)) {
                    var nodeCheck = part = isXML
                        ? part
                        : part.toUpperCase();
                    checkFn = dirNodeCheck;
                }
                
                checkFn("previousSibling", part, doneName, checkSet, nodeCheck, isXML);
            }
        },
        find       : {
            ID   : function(match, context, isXML) {
                if (typeof context.getElementById !== "undefined" && !isXML) {
                    var m = context.getElementById(match[1]);
                    return m
                        ? [m]
                        : [];
                }
            },
            NAME : function(match, context, isXML) {
                if (typeof context.getElementsByName !== "undefined") {
                    var ret = [],
                        results = context.getElementsByName(match[1]);
                    
                    for (var i = 0, l = results.length; i < l; i++) {
                        if (results[i].getAttribute("name") === match[1]) {
                            ret.push(results[i]);
                        }
                    }
                    
                    return ret.length === 0
                        ? null
                        : ret;
                }
            },
            TAG  : function(match, context) {
                return context.getElementsByTagName(match[1]);
            }
        },
        preFilter  : {
            CLASS  : function(match, curLoop, inplace, result, not, isXML) {
                match = " " + match[1].replace(/\\/g, "") + " ";
                
                if (isXML) {
                    return match;
                }
                
                for (var i = 0, elem; (elem = curLoop[i]) != null; i++) {
                    if (elem) {
                        if (not ^
                            (elem.className && (" " + elem.className + " ").indexOf(match) >= 0)) {
                            if (!inplace) result.push(elem);
                        } else if (inplace) {
                            curLoop[i] = false;
                        }
                    }
                }
                
                return false;
            },
            ID     : function(match) {
                return match[1].replace(/\\/g, "");
            },
            TAG    : function(match, curLoop) {
                for (var i = 0; curLoop[i] === false; i++) {}
                return curLoop[i] && isXML(curLoop[i])
                    ? match[1]
                    : match[1].toUpperCase();
            },
            CHILD  : function(match) {
                if (match[1] == "nth") {
                    // parse equations like 'even', 'odd', '5', '2n', '3n+2',
                    // '4n-1', '-n+6'
                    var test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(match[2] == "even" &&
                        "2n" ||
                        match[2] == "odd" &&
                        "2n+1" ||
                        !/\D/.test(match[2]) && "0n+" + match[2] || match[2]);
                    
                    // calculate the numbers (first)n+(last) including if they
                    // are negative
                    match[2] = (test[1] + (test[2] || 1)) - 0;
                    match[3] = test[3] - 0;
                }
                
                // TODO: Move to normal caching system
                match[0] = done++;
                
                return match;
            },
            ATTR   : function(match, curLoop, inplace, result, not, isXML) {
                var name = match[1].replace(/\\/g, "");
                
                if (!isXML && Expr.attrMap[name]) {
                    match[1] = Expr.attrMap[name];
                }
                
                if (match[2] === "~=") {
                    match[4] = " " + match[4] + " ";
                }
                
                return match;
            },
            PSEUDO : function(match, curLoop, inplace, result, not) {
                if (match[1] === "not") {
                    // If we're dealing with a complex expression, or a simple
                    // one
                    if (match[3].match(chunker).length > 1 ||
                        /^\w/.test(match[3])) {
                        match[3] = Sizzle(match[3], null, null, curLoop);
                    } else {
                        var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not);
                        if (!inplace) {
                            result.push.apply(result, ret);
                        }
                        return false;
                    }
                } else if (Expr.match.POS.test(match[0]) ||
                    Expr.match.CHILD.test(match[0])) {
                    return true;
                }
                
                return match;
            },
            POS    : function(match) {
                match.unshift(true);
                return match;
            }
        },
        filters    : {
            enabled  : function(elem) {
                return elem.disabled === false && elem.type !== "hidden";
            },
            disabled : function(elem) {
                return elem.disabled === true;
            },
            checked  : function(elem) {
                return elem.checked === true;
            },
            selected : function(elem) {
                // Accessing this property makes selected-by-default
                // options in Safari work properly
                elem.parentNode.selectedIndex;
                return elem.selected === true;
            },
            parent   : function(elem) {
                return !!elem.firstChild;
            },
            empty    : function(elem) {
                return !elem.firstChild;
            },
            has      : function(elem, i, match) {
                return !!Sizzle(match[3], elem).length;
            },
            header   : function(elem) {
                return /h\d/i.test(elem.nodeName);
            },
            text     : function(elem) {
                return "text" === elem.type;
            },
            radio    : function(elem) {
                return "radio" === elem.type;
            },
            checkbox : function(elem) {
                return "checkbox" === elem.type;
            },
            file     : function(elem) {
                return "file" === elem.type;
            },
            password : function(elem) {
                return "password" === elem.type;
            },
            submit   : function(elem) {
                return "submit" === elem.type;
            },
            image    : function(elem) {
                return "image" === elem.type;
            },
            reset    : function(elem) {
                return "reset" === elem.type;
            },
            button   : function(elem) {
                return "button" === elem.type ||
                    elem.nodeName.toUpperCase() === "BUTTON";
            },
            input    : function(elem) {
                return /input|select|textarea|button/i.test(elem.nodeName);
            }
        },
        setFilters : {
            first : function(elem, i) {
                return i === 0;
            },
            last  : function(elem, i, match, array) {
                return i === array.length - 1;
            },
            even  : function(elem, i) {
                return i % 2 === 0;
            },
            odd   : function(elem, i) {
                return i % 2 === 1;
            },
            lt    : function(elem, i, match) {
                return i < match[3] - 0;
            },
            gt    : function(elem, i, match) {
                return i > match[3] - 0;
            },
            nth   : function(elem, i, match) {
                return match[3] - 0 == i;
            },
            eq    : function(elem, i, match) {
                return match[3] - 0 == i;
            }
        },
        filter     : {
            PSEUDO : function(elem, match, i, array) {
                var name = match[1],
                    filter = Expr.filters[name];
                
                if (filter) {
                    return filter(elem, i, match, array);
                } else if (name === "contains") {
                    return (elem.textContent || elem.innerText || "").indexOf(match[3]) >= 0;
                } else if (name === "not") {
                    var not = match[3];
                    
                    for (i = 0, l = not.length; i < l; i++) {
                        if (not[i] === elem) {
                            return false;
                        }
                    }
                    
                    return true;
                }
            },
            CHILD  : function(elem, match) {
                var type = match[1],
                    node = elem;
                switch (type) {
                    case 'only' :
                    case 'first' :
                        while ((node = node.previousSibling)) {
                            if (node.nodeType === 1) return false;
                        }
                        if (type == 'first') return true;
                        node = elem;
                    case 'last' :
                        while ((node = node.nextSibling)) {
                            if (node.nodeType === 1) return false;
                        }
                        return true;
                    case 'nth' :
                        var first = match[2],
                            last = match[3];
                        
                        if (first == 1 && last == 0) {
                            return true;
                        }
                        
                        var doneName = match[0],
                            parent = elem.parentNode;
                        
                        if (parent &&
                            (parent.sizcache !== doneName || !elem.nodeIndex)) {
                            var count = 0;
                            for (node = parent.firstChild; node; node = node.nextSibling) {
                                if (node.nodeType === 1) {
                                    node.nodeIndex = ++count;
                                }
                            }
                            parent.sizcache = doneName;
                        }
                        
                        var diff = elem.nodeIndex - last;
                        if (first == 0) {
                            return diff == 0;
                        } else {
                            return (diff % first == 0 && diff / first >= 0);
                        }
                }
            },
            ID     : function(elem, match) {
                return elem.nodeType === 1 && elem.getAttribute("id") === match;
            },
            TAG    : function(elem, match) {
                return (match === "*" && elem.nodeType === 1) ||
                    elem.nodeName === match;
            },
            CLASS  : function(elem, match) {
                return (" " + (elem.className || elem.getAttribute("class")) + " ").indexOf(match) > -1;
            },
            ATTR   : function(elem, match) {
                var name = match[1],
                    result = Expr.attrHandle[name]
                        ? Expr.attrHandle[name](elem)
                        : elem[name] != null
                            ? elem[name]
                            : elem.getAttribute(name),
                    value = result + "",
                    type = match[2],
                    check = match[4];
                
                return result == null
                    ? type === "!="
                    : type === "="
                        ? value === check
                        : type === "*="
                            ? value.indexOf(check) >= 0
                            : type === "~="
                                ? (" " + value + " ").indexOf(check) >= 0
                                : !check
                                    ? value && result !== false
                                    : type === "!="
                                        ? value != check
                                        : type === "^="
                                            ? value.indexOf(check) === 0
                                            : type === "$="
                                                ? value.substr(value.length -
                                                    check.length) === check
                                                : type === "|="
                                                    ? value === check ||
                                                        value.substr(0, check.length +
                                                            1) === check + "-"
                                                    : false;
            },
            POS    : function(elem, match, i, array) {
                var name = match[2],
                    filter = Expr.setFilters[name];
                
                if (filter) {
                    return filter(elem, i, match, array);
                }
            }
        }
    };
    
    var origPOS = Expr.match.POS;
    
    for (var type in Expr.match) {
        Expr.match[type] = new RegExp(Expr.match[type].source +
            /(?![^\[]*\])(?![^\(]*\))/.source);
    }
    
    var makeArray = function(array, results) {
        array = Array.prototype.slice.call(array);
        
        if (results) {
            results.push.apply(results, array);
            return results;
        }
        
        return array;
    };
    
    // Perform a simple check to determine if the browser is capable of
    // converting a NodeList to an array using builtin methods.
    try {
        Array.prototype.slice.call(document.documentElement.childNodes);
        
        // Provide a fallback method if it does not work
    } catch (e) {
        makeArray = function(array, results) {
            var ret = results || [];
            
            if (toString.call(array) === "[object Array]") {
                Array.prototype.push.apply(ret, array);
            } else {
                if (typeof array.length === "number") {
                    for (var i = 0, l = array.length; i < l; i++) {
                        ret.push(array[i]);
                    }
                } else {
                    for (var i = 0; array[i]; i++) {
                        ret.push(array[i]);
                    }
                }
            }
            
            return ret;
        };
    }
    
    if (document.documentElement.compareDocumentPosition) {
        /** @ignore */
        sortOrder = function(a, b) {
            var ret = a.compareDocumentPosition(b) & 4
                ? -1
                : a === b
                    ? 0
                    : 1;
            if (ret === 0) {
                hasDuplicate = true;
            }
            return ret;
        };
    } else if ("sourceIndex" in document.documentElement) {
        /** @ignore */
        sortOrder = function(a, b) {
            var ret = a.sourceIndex - b.sourceIndex;
            if (ret === 0) {
                hasDuplicate = true;
            }
            return ret;
        };
    } else if (document.createRange) {
        /** @ignore */
        sortOrder = function(a, b) {
            var aRange = a.ownerDocument.createRange(),
                bRange = b.ownerDocument.createRange();
            aRange.selectNode(a);
            aRange.collapse(true);
            bRange.selectNode(b);
            bRange.collapse(true);
            var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
            if (ret === 0) {
                hasDuplicate = true;
            }
            return ret;
        };
    }
    
    // Check to see if the browser returns elements by name when
    // querying by getElementById (and provide a workaround)
    (function() {
        // We're going to inject a fake input element with a specified name
        var form = document.createElement("div"),
            id = "script" + (new Date).getTime();
        form.innerHTML = "<a name='" + id + "'/>";
        
        // Inject it into the root element, check its status, and remove it
        // quickly
        var root = document.documentElement;
        root.insertBefore(form, root.firstChild);
        
        // The workaround has to do additional checks after a getElementById
        // Which slows things down for other browsers (hence the branching)
        if (!!document.getElementById(id)) {
            Expr.find.ID = function(match, context, isXML) {
                if (typeof context.getElementById !== "undefined" && !isXML) {
                    var m = context.getElementById(match[1]);
                    return m
                        ? m.id === match[1] ||
                            typeof m.getAttributeNode !== "undefined" &&
                            m.getAttributeNode("id").nodeValue === match[1]
                            ? [m]
                            : undefined
                        : [];
                }
            };
            
            Expr.filter.ID = function(elem, match) {
                var node = typeof elem.getAttributeNode !== "undefined" &&
                    elem.getAttributeNode("id");
                return elem.nodeType === 1 && node && node.nodeValue === match;
            };
        }
        
        root.removeChild(form);
        root = form = null; // release memory in IE
    })();
    
    (function() {
        // Check to see if the browser returns only elements
        // when doing getElementsByTagName("*")
        
        // Create a fake element
        var div = document.createElement("div");
        div.appendChild(document.createComment(""));
        
        // Make sure no comments are found
        if (div.getElementsByTagName("*").length > 0) {
            Expr.find.TAG = function(match, context) {
                var results = context.getElementsByTagName(match[1]);
                
                // Filter out possible comments
                if (match[1] === "*") {
                    var tmp = [];
                    
                    for (var i = 0; results[i]; i++) {
                        if (results[i].nodeType === 1) {
                            tmp.push(results[i]);
                        }
                    }
                    
                    results = tmp;
                }
                
                return results;
            };
        }
        
        // Check to see if an attribute returns normalized href attributes
        div.innerHTML = "<a href='#'></a>";
        if (div.firstChild &&
            typeof div.firstChild.getAttribute !== "undefined" &&
            div.firstChild.getAttribute("href") !== "#") {
            Expr.attrHandle.href = function(elem) {
                return elem.getAttribute("href", 2);
            };
        }
        
        div = null; // release memory in IE
    })();
    
    if (document.querySelectorAll) (function() {
        var oldSizzle = Sizzle,
            div = document.createElement("div");
        div.innerHTML = "<p class='TEST'></p>";
        
        // Safari can't handle uppercase or unicode characters when
        // in quirks mode.
        if (div.querySelectorAll && div.querySelectorAll(".TEST").length === 0) {
            return;
        }
        
        Sizzle = function(query, context, extra, seed) {
            context = context || document;
            
            // Only use querySelectorAll on non-XML documents
            // (ID selectors don't work in non-HTML documents)
            if (!seed && context.nodeType === 9 && !isXML(context)) {
                try {
                    return makeArray(context.querySelectorAll(query), extra);
                } catch (e) {}
            }
            
            return oldSizzle(query, context, extra, seed);
        };
        
        for (var prop in oldSizzle) {
            Sizzle[prop] = oldSizzle[prop];
        }
        
        div = null; // release memory in IE
    })();
    
    if (document.getElementsByClassName &&
        document.documentElement.getElementsByClassName) (function() {
        var div = document.createElement("div");
        div.innerHTML = "<div class='test e'></div><div class='test'></div>";
        
        // Opera can't find a second classname (in 9.6)
        if (div.getElementsByClassName("e").length === 0) return;
        
        // Safari caches class attributes, doesn't catch changes (in 3.2)
        div.lastChild.className = "e";
        
        if (div.getElementsByClassName("e").length === 1) return;
        
        Expr.order.splice(1, 0, "CLASS");
        Expr.find.CLASS = function(match, context, isXML) {
            if (typeof context.getElementsByClassName !== "undefined" && !isXML) {
                return context.getElementsByClassName(match[1]);
            }
        };
        
        div = null; // release memory in IE
    })();
    
    function dirNodeCheck(dir, cur, doneName, checkSet, nodeCheck, isXML) {
        var sibDir = dir == "previousSibling" && !isXML;
        for (var i = 0, l = checkSet.length; i < l; i++) {
            var elem = checkSet[i];
            if (elem) {
                if (sibDir && elem.nodeType === 1) {
                    elem.sizcache = doneName;
                    elem.sizset = i;
                }
                elem = elem[dir];
                var match = false;
                
                while (elem) {
                    if (elem.sizcache === doneName) {
                        match = checkSet[elem.sizset];
                        break;
                    }
                    
                    if (elem.nodeType === 1 && !isXML) {
                        elem.sizcache = doneName;
                        elem.sizset = i;
                    }
                    
                    if (elem.nodeName === cur) {
                        match = elem;
                        break;
                    }
                    
                    elem = elem[dir];
                }
                
                checkSet[i] = match;
            }
        }
    }
    
    function dirCheck(dir, cur, doneName, checkSet, nodeCheck, isXML) {
        var sibDir = dir == "previousSibling" && !isXML;
        for (var i = 0, l = checkSet.length; i < l; i++) {
            var elem = checkSet[i];
            if (elem) {
                if (sibDir && elem.nodeType === 1) {
                    elem.sizcache = doneName;
                    elem.sizset = i;
                }
                elem = elem[dir];
                var match = false;
                
                while (elem) {
                    if (elem.sizcache === doneName) {
                        match = checkSet[elem.sizset];
                        break;
                    }
                    
                    if (elem.nodeType === 1) {
                        if (!isXML) {
                            elem.sizcache = doneName;
                            elem.sizset = i;
                        }
                        if (typeof cur !== "string") {
                            if (elem === cur) {
                                match = true;
                                break;
                            }
                            
                        } else if (Sizzle.filter(cur, [elem]).length > 0) {
                            match = elem;
                            break;
                        }
                    }
                    
                    elem = elem[dir];
                }
                
                checkSet[i] = match;
            }
        }
    }
    
    var contains = document.compareDocumentPosition
        ? function(a, b) {
            return a.compareDocumentPosition(b) & 16;
        }
        : function(a, b) {
            return a !== b && (a.contains
                ? a.contains(b)
                : true);
        };
    
    var isXML = function(elem) {
        return elem.nodeType === 9 &&
            elem.documentElement.nodeName !== "HTML" || !!elem.ownerDocument &&
            elem.ownerDocument.documentElement.nodeName !== "HTML";
    };
    
    var posProcess = function(selector, context) {
        var tmpSet = [],
            later = "", match,
            root = context.nodeType
                ? [context]
                : context;
        
        // Position selectors must be done after the filter
        // And so must :not(positional) so we move all PSEUDOs to the end
        while ((match = Expr.match.PSEUDO.exec(selector))) {
            later += match[0];
            selector = selector.replace(Expr.match.PSEUDO, "");
        }
        
        selector = Expr.relative[selector]
            ? selector + "*"
            : selector;
        
        for (var i = 0, l = root.length; i < l; i++) {
            Sizzle(selector, root[i], tmpSet);
        }
        
        return Sizzle.filter(later, tmpSet);
    };
    
    // EXPOSE
    window.Sizzle = Sizzle;
    
})();

/**
 * @namespace Provides methods to collect and filter DOM elements.
 */
Jelo.Dom = function() {
    
    /** @scope Jelo.Dom */
    return {
        /**
         * Converts a string of HTML to actual DOM elements.
         * 
         * @param {String} html The HTML to convert to DOM nodes.
         * @returns {Node} A DocumentFragment object containing the specified
         *          nodes.
         */
        fromString   : function(str) {
            var frag = document.createDocumentFragment();
            if (typeof str != 'string') {
                return frag;
            }
            var div = document.createElement('div');
            div.innerHTML = str;
            while (div.firstChild) {
                frag.appendChild(div.firstChild);
            }
            return frag;
        },
        /**
         * Reduces a set of DOM nodes to those that also match the given CSS
         * selector. The selector can be a full selector (for example, "div >
         * span.foo") and not just a fragment or simple selector.
         * 
         * @param {String} selector The CSS selector or xpath query.
         * @param {Array} [set] The set of elements to filter.
         */
        filter       : function(selector, set) {
            return Sizzle.matches(selector, set);
        },
        /**
         * Selects a group of elements that match a given CSS selector.
         * 
         * @param {String} selector The CSS selector or xpath query.
         * @param {Node} [context=document] The root node within which to
         *        conduct this search.
         * @param {Array} [results] The collection returned from this function.
         * @returns {Array}
         */
        select       : function(selector, context, results) {
            return (selector && selector.isArray)
                ? selector
                : Sizzle(selector, context, results);
        },
        /**
         * Selects the FIRST instance of a matching element.
         * 
         * @param {String} selector The CSS selector or xpath query.
         * @param {Node} [context=document] The root node within which to
         *        conduct this search.
         * @param {Array} [results] The collection returned from this function.
         * @returns {Node}
         */
        selectNode   : function(selector, context, results) {
            return (selector && selector.nodeType)
                ? selector
                : Sizzle(selector, context, results)[0];
        },
        /**
         * Finds the position of an element on the page.
         * 
         * @param {HTMLElement} The element to inspect
         * @returns {Array} [left, top] calculated in pixels, output as Numbers.
         */
        findPosition : function(el) {
            var l = 0;
            var t = 0;
            if (el.offsetParent) {
                do {
                    l += el.offsetLeft;
                    t += el.offsetTop;
                } while (el = el.offsetParent);
            }
            return [l, t];
        }
    };
}();

$ = window['$'] || Jelo.Dom.selectNode;
$$ = window['$$'] || Jelo.Dom.select;
/**
 * @namespace Cross-browser registration of event handlers, automatically normalizes the event object to provide web
 *            standard features such as preventDefault() and stopPropagation().
 */
Jelo.Event = function() {
    
    /** @private convenience */
    var D = document;
    
    /** @private */
    var isDomReady = false;
    
    /** @private */
    var domFunctions = [];
    
    /** @private */
    var handlers = [];
    
    /** @private */
    var fireDomReady = function() {
        isDomReady = true;
        for (var i = 0; i < domFunctions.length; i++) {
            var fn = domFunctions[i];
            try {
                fn();
            } catch (e) {
                // TODO: log these internally so they can be shown if the developer desires
            }
        }
        domFunctions = [];
    };
    
    /** @private */
    var init = function() {
        if (D.addEventListener) {
            D.addEventListener("DOMContentLoaded", fireDomReady, false);
        } else if (Jelo.Env.isIE) {
            D.write("<script id='ieDomReady' defer " + "src=//:><\/script>");
            var ieReady = D.getElementById("ieDomReady");
            ieReady.onreadystatechange = function() {
                if (ieReady.readyState === "complete") {
                    fireDomReady();
                }
            };
        } else {
            var oldOnload = (typeof window.onload == "function")
                ? window.onload
                : function() {};
            window.onload = function() {
                oldOnload();
                fireDomReady();
            };
        }
        if (Jelo.Env.isWebkit) {
            var timerDomReady = setInterval(function() {
                if (/loaded|complete/i.test(D.readyState)) {
                    fireDomReady.call(this);
                    clearInterval(timerDomReady);
                }
            }, 10);
        }
    }();
    
    function find(el, ev, fn) {
        var handlers = el._handlers;
        if (!handlers) {
            return -1;
        }
        var d = el.document || el;
        var w = d.parentWindow;
        for (var i = handlers.length - 1; i >= 0; i--) {
            var a = w._allHandlers[handlers[i]];
            if (a.eventType == ev && a.handler == fn) {
                return i;
            }
        }
        return -1;
    }
    
    function removeAllHandlers() {
        var w = this;
        var wa = w._allHandlers;
        for (var id in wa) {
            if (wa.hasOwnProperty(id)) {
                var h = wa[id];
                h.element.detachEvent('on' + h.eventType, h.wrappedHandler);
                delete wa[id];
            }
        }
    }
    
    /** @scope Jelo.Event */
    return {
        /**
         * Fires after all elements in the document is available. In most cases, this function may be executed before
         * window.onload and before images fully load, which can result in a faster page response time.
         * 
         * @see Jelo#onReady
         */
        onReady   : function(fn) {
            if (typeof fn === "function") {
                if (isDomReady) {
                    fn();
                } else {
                    domFunctions.push(fn);
                }
            }
        },
        /**
         * Start listening for an event. Multiple listeners can be registered to a single element. Can be accessed via
         * {@link Jelo.on}.
         * 
         * @param {HTMLElement} element HTML element to which Jelo should listen.
         * @param {String} eventName The type of event for which Jelo should listen, such as "click" or "mouseup".
         *        Should be all lowercase, and WITHOUT the IE prefix "on".
         * @param {Function} handler Method to be invoked when the event occurs. The execution scope ("this") will be
         *        the actual element that caught the event which, due to the DOM hierarchy, may be a child of the
         *        element registered via this function. The function is passed the following arguments:
         *        <ul>
         *        <li>target: The element that caught the event.</li>
         *        <li>event: The event object, normalized to conform to W3C standards.</li>
         *        </ul>
         */
        add       : function(el, ev, fn) {
            if (!el || !ev || !fn) {
                return;
            }
            if (Jelo.Valid.isArray(el)) {
                Jelo.each(el, function() {
                    Jelo.Event.add(this, ev, fn);
                });
                return;
            }
            var handler = function(e) {
                e = e || window.event;
                var event = {};
                var properties = ['type', 'shiftKey', 'ctrlKey', 'altKey', 'keyCode', 'charCode', 'button', 'which',
                    'clientX', 'clientY', 'mouseX', 'mouseY', 'metaKey', 'pageX', 'pageY', 'screenX', 'screenY',
                    'relatedTarget'];
                var len = properties.length;
                for (var i = 0; i < len; i++) {
                    event[properties[i]] = e[properties[i]];
                }
                target = e.target
                    ? e.target
                    : e.srcElement;
                if (target.nodeType === 3) {
                    target = target.parentNode;
                }
                event.preventDefault = function() {
                    if (e.preventDefault) {
                        e.preventDefault();
                    } else {
                        e.returnValue = false;
                    }
                };
                event.stopPropagation = function() {
                    if (e.stopPropagation) {
                        e.stopPropagation();
                    } else {
                        e.cancelBubble = true;
                    }
                };
                fn.call(target, target, event);
            };
            if (el.addEventListener) {
                el.addEventListener(ev, handler, false);
            } else if (el.attachEvent) {
                el.attachEvent("on" + ev, handler);
            } else {
                throw ("Jelo.Event.add: Could not observe " + ev + " on " + el);
            }
            var newEvent = {
                target  : el,
                event   : ev,
                handler : handler,
                fn      : fn
            };
            handlers.push(newEvent);
        },
        /**
         * Stop listening for an event. Safe to call even if no such listener has been registered. Can be accessed via
         * {@link Jelo.un}.
         * 
         * @param {HTMLElement} element HTML element to which Jelo should listen.
         * @param {String} eventName The type of event for which Jelo should listen, such as "click" or "mouseup".
         *        Should be all lowercase, and WITHOUT the IE prefix "on".
         * @param {Function} handler Method to be invoked when the event occurs. Anonymous handlers cannot be
         *        unregistered at this time.
         */
        remove    : function(el, ev, fn) {
            if (Jelo.Valid.isArray(el)) {
                Jelo.each(el, function() {
                    Jelo.Event.remove(this, ev, fn);
                });
                return;
            }
            if (!!el && (typeof ev == 'string') && (typeof fn == 'function')) {
                for (var i = 0; i < handlers.length; i++) {
                    var jeh = handlers[i];
                    var ml = el === jeh.target;
                    var mv = ev === jeh.event;
                    var mf = fn === jeh.fn;
                    if (ml && mv && mf) {
                        if (el.removeEventListener) {
                            el.removeEventListener(ev, jeh.handler, false);
                        } else if (el.detachEvent) {
                            el.detachEvent("on" + ev, jeh.handler);
                        } else {
                            throw ("Jelo.Event.remove: Could not remove " + ev + " from " + el);
                        }
                        handlers.splice(i, 1);
                        break;
                    }
                }
            } else {
                throw ("Syntax Error. Jelo.Event.remove(DOMElement, Event:String, Function");
            }
        },
        
        /**
         * @deprecated Bootstrap the next version of Jelo.Event.add and Jelo.Event.remove. The only difference you
         *             should notice as a developer is that the arguments passed to the OLD event handler were (target,
         *             event). The NEW arguments are (event) to fit in line with most implementations out there. See
         *             also: {@link Jelo.Event.isFixed}.
         */
        normalize : function() {
            Jelo.Event.add = (D.addEventListener)
                ? function(el, ev, fn) {
                    if (Jelo.Valid.isArray(el)) {
                        Jelo.each(el, function() {
                            Jelo.Event.add(this, ev, fn);
                        });
                        return;
                    }
                    el.addEventListener(ev, fn, false);
                }
                : function(el, ev, fn) {
                    if (Jelo.Valid.isArray(el)) {
                        Jelo.each(el, function() {
                            Jelo.Event.add(this, ev, fn);
                        });
                        return;
                    }
                    if (find(el, ev, fn) != -1) {
                        return;
                    }
                    var wh = function(e) {
                        e = e || window.event;
                        var event = {
                            _event          : e,
                            type            : e.type,
                            target          : e.srcElement,
                            currentTarget   : el,
                            relatedTarget   : e.fromElement
                                ? e.fromElement
                                : e.toElement,
                            eventPhase      : (e.srcElement == el)
                                ? 2
                                : 3,
                            clientX         : e.clientX,
                            clientY         : e.clientY,
                            screenX         : e.screenX,
                            screenY         : e.screenY,
                            altKey          : e.altKey,
                            ctrlKey         : e.ctrlKey,
                            shiftKey        : e.shiftKey,
                            charCode        : e.charCode || e.keyCode,
                            keyCode         : e.keyCode || e.charCode,
                            button          : e.button
                                ? {
                                    1 : 0,
                                    4 : 1,
                                    2 : 2
                                }[e.button]
                                : (e.which
                                    ? e.which - 1
                                    : -1),
                            which           : e.which || e.button,
                            stopPropagation : function() {
                                this._event.cancelBubble = true;
                            },
                            preventDefault  : function() {
                                this._event.returnValue = false;
                            }
                        };
                        fn.call(el, event);
                    };
                    el.attachEvent('on' + ev, wh);
                    var h = {
                        element        : el,
                        eventType      : ev,
                        handler        : fn,
                        wrappedHandler : wh
                    };
                    var d = el.document || el;
                    var w = d.parentWindow;
                    var id = 'h' + Jelo.uID();
                    if (!w._allHandlers) {
                        w._allHandlers = {};
                    }
                    w._allHandlers[id] = h;
                    if (!el._handlers) {
                        el._handlers = [];
                    }
                    el._handlers.push(id);
                    if (!w._onunloadRegistered) {
                        w.attachEvent('onunload', removeAllHandlers);
                        w._onunloadRegistered = true;
                    }
                };
            Jelo.Event.remove = (D.removeEventListener)
                ? function(el, ev, fn) {
                    if (Jelo.Valid.isArray(el)) {
                        Jelo.each(el, function() {
                            Jelo.Event.remove(this, ev, fn);
                        });
                        return;
                    }
                    el.removeEventListener(ev, fn, false);
                }
                : function(el, ev, fn) {
                    if (Jelo.Valid.isArray(el)) {
                        Jelo.each(el, function() {
                            Jelo.Event.remove(this, ev, fn);
                        });
                        return;
                    }
                    var i = find(el, ev, fn);
                    if (i == -1) {
                        return;
                    }
                    var d = el.document || el;
                    var w = d.parentWindow;
                    var hid = el._handlers[i];
                    var h = w._allHandlers[hid];
                    el.detachEvent('on' + ev, h.wrappedHandler);
                    el._handlers.splice(i, 1);
                    delete w._allHandlers[hid];
                };
            Jelo.on = Jelo.Event.add;
            Jelo.un = Jelo.Event.remove;
            Jelo.Event.isFixed = function() {
                return true;
            }
        },
        /**
         * @deprecated True when {@link Jelo.Event.normalize} has been called.
         */
        isFixed   : function() {
            return false;
        }
        
    };
}();

Jelo.Event.fix = Jelo.Event.normalize;

/**
 * Alias for {@link Jelo.Event.add}.
 * 
 * @function
 * @memberOf Jelo
 */
Jelo.on = Jelo.Event.add;

/**
 * Alias for {@link Jelo.Event.remove}.
 * 
 * @function
 * @memberOf Jelo
 */
Jelo.un = Jelo.Event.remove;

/**
 * Alias for {@link Jelo.Event.remove}.
 * 
 * @function
 * @memberOf Jelo.Event
 */
Jelo.Event.rem = Jelo.Event.remove;

/**
 * Alias for {@link Jelo.Event.onReady}.
 * 
 * @function
 * @memberOf Jelo
 */
Jelo.onReady = Jelo.Event.onReady;
/**
 * @namespace Robust AJAX object. Includes concepts from http://adamv.com/dev/
 * and http://extjs.com/ as well as JDOMPer.
 */
Jelo.Ajax = function() {
    
    var d = window.document;
    
    var Status = {
        OK: 200,
        Created: 201,
        Accepted: 202,
        NoContent: 204,
        BadRequest: 400,
        Forbidden: 403,
        NotFound: 404,
        Gone: 410,
        ServerError: 500
    };
    
    var cache = {};
    
    var getFromCache = function(/* url */u, /* success */ s, /* failure */ f, /* callback */ fn, /* config */ c) {
        var /* responseText */ r = cache[u] || false, /* http */ h;
        if (r) {
            h = {
                status: Status.OK,
                readyState: 4,
                responseText: r
            };
            if (s.isFunction) {
                s.apply(h, [h, c]);
            }
            if (fn.isFunction) {
                fn.apply(h, [h, c]);
            }
            return true;
        }
        if (f.isFunction) {
            f.apply(h, [h, c]);
        }
        return false;
    };
    
    /** @private http://www.ajaxpatterns.org/ */
    var xhr = function() { // don't call this just "x", Bad Stuff happens
        try {
            return new XMLHttpRequest();
        } catch (e) {
        }
        try {
            return new ActiveXObject("Msxml2.XMLHTTP");
        } catch (f) {
        }
        try {
            return new ActiveXObject("Microsoft.XMLHTTP");
        } catch (g) {
        }
        throw new Error("Jelo.Ajax.request: XMLHttpRequest not supported.");
    }();
    
    function loadScript(/* url */u, /* callback */ fn) {
        var s = d.createElement("script"), e = d.documentElement;
        s.type = "text/javascript";
        if (s.readyState) {
            s.onreadystatechange = function() {
                if (s.readyState == "loaded" || s.readyState == "complete") {
                    s.onreadystatechange = null;
                    if (fn.isFunction) {
                        fn();
                    }
                }
            };
        } else {
            s.onload = function() {
                if (fn.isFunction) {
                    fn();
                }
            };
        }
        s.src = u;
        e.insertBefore(s, e.firstChild);
    }
    
        /** @scope Jelo.Ajax */
        return {
            
            /**
             * @returns True if an abortable {@link Jelo.Ajax.request} call is pending.
             * Pending requests made using {abortable: false} are not counted here.
             */
            isBusy     : function() {
                return ((xhr.readyState !== 0) && (xhr.readyState !== 4));
            },
            
            /**
             * Performs an AJAX call without XMLHttpRequest objects. Spiffy. Google
             * "jdomp", the guy has posted the idea on a bunch of web dev forums,
             * but I'm not sure if he has an official website. <br>
             * Note: parameters should be passed as a configuration object.
             *
             * @param {Object} config A configuration object.
             * @param {String} config.url Target URL to load (the script can be
             * dynamically generated by a server-side language, but the output
             * Content-Type must be text/javascript)
             * @param {Boolean} [config.cache=false] If false, add a timestamp to
             * the call to avoid caching the response. If true, it also assigns the
             * script a unique ID that can be referred to later.
             * @param {Object} [config.params] Additional parameters to pass to the
             * script via its query string. Not particularly useful for .js files,
             * but potentially useful if the script is served via PHP or another
             * server-side language.
             * @param {Function} [config.callback] Method to invoke after the
             * JDOMPed script executes. NOTE: Do NOT use nested callbacks to JDOMP
             * scripts with additional callbacks! If you do, callbacks beyond the
             * first will be executed multiple times!
             */
            jdomp      : function() {
                var config = arguments[0] || false;
                if (!config || !config.url) {
                    return;
                }
                var cache = config.cache || false;
                var now = new Date().getTime();
                var params = config.params || null;
                var url = config.url;
                url += (cache) ? "?jdompCache=true" : "?jdompCache=" + now;
                for (var p in params) {
                    if (params.hasOwnProperty(p)) {
                        url += "&" + escape(p) + "=" + escape(params[p]);
                    }
                }
                var h = d.getElementsByTagName("head")[0] || false;
                var e = d.getElementById("script-jdomp") || false;
                if (e) {
                    h.removeChild(e);
                }
                var s = d.createElement("script");
                s.id = "script-jdomp";
                s.type = "text/javascript";
                s.src = url;
                var alreadyExists = false;
                if (cache) {
                    s.id += "-" + config.url.replace(/[^a-z]/gi, "");
                    alreadyExists = d.getElementById(s.id) || false;
                    if (alreadyExists) {
                        h.removeChild(alreadyExists);
                    }
                }
                h.appendChild(s);
                if (typeof config.callback === "function") {
                    var c = d.createElement("script");
                    c.type = "text/javascript";
                    c.id = s.id + "-callback";
                    c.text = "new " + config.callback; // try changing to ['(', ')();'].join(config.callback)
                    alreadyExists = d.getElementById(c.id) || false;
                    if (alreadyExists) {
                        h.removeChild(alreadyExists);
                    }
                    h.appendChild(c);
                }
            },
            
            /**
             * Traditional AJAX request, supports (optional) caching. Parameters
             * should be passed in a configuration object.
             *
             * @param {Object} config A configuration object
             * @param {String} config.url The URL to send a request to.
             * @param {String} [config.method="GET"] GET, POST, PUT or DELETE.
             * @param {Object} [config.data] Parameters to pass to the URL.
             * @param {Object} [config.params] Alias for config.data
             * @param {Boolean} [config.cache=false] True to save the response in the
             * local cache, or retrieve the stored response if available. False to always
             * make a new request and return fresh results.
             * @param {Boolean} [abortable=false] If true, this call will interrupt any
             * pending AJAX requests which are also abortable. Each request that is NOT
             * abortable can execute simultaneously with other requests.
             * @param {Function} [config.success] Method to invoke when the request
             * successfully completed (200 or 304 HTTP status code). The function
             * gets passed the XMLHttpRequest object and the original config object.
             * In the callback function, "this" refers to the XMLHttpRequest object.
             * If the response Content-Type is text/xml, <strong>this.responseXML</strong>
             * should be available. Otherwise, get the response using
             * <strong>this.responseText</strong>.
             * @param {Function} [config.failure] Method to invoke when the request
             * was NOT successfully completed. The function gets passed the
             * XMLHttpRequest object and the original config object. In the callback
             * function, "this" refers to the XMLHttpRequest object. The status code
             * is available as <strong>this.status</strong>.
             * @param {Function} [config.callback]Method to invoke when the request
             * returns, <em>whether or not the call was successful</em>. Can be
             * useful for cleanup or notification purposes. The function gets passed
             * the XMLHttpRequest object and the original config object. In the
             * callback function, "this" refers to the XMLHttpRequest object. If
             * included, this method will be invoked AFTER both the success and
             * failure functions (if applicable).
             */
            request    : function() {
                var config = arguments[0] || {};
                if (!config || !config.url) {
                    throw new Error("Jelo.Ajax.request: Required configuration option missing: url");
                }
                var x = config.abortable ? xhr : function() {
                    try {
                        return new XMLHttpRequest();
                    } catch (e) {
                    }
                    try {
                        return new ActiveXObject("Msxml2.XMLHTTP");
                    } catch (f) {
                    }
                    try {
                        return new ActiveXObject("Microsoft.XMLHTTP");
                    } catch (g) {
                    }
                    throw new Error("Jelo.Ajax.request: XMLHttpRequest not supported.");
                }();
                if ((x.readyState !== 0) && (x.readyState !== 4)) {
                    // only one request at a time, new ones zap old ones
                    x.abort();
                }
                var u = config.url;
                var p = config.params || config.data || {};
                var q = '';
                var m = (config.method || "GET").toUpperCase();
                var c = config.cache || false;
                var cs = config.success || false;
                var cf = config.failure || false;
                var cc = config.callback || false;
                var a = (config.args && config.args.isArray) ? config.args : [];
                var e = (/\?/).test(u); // existing url params
                var px = config.proxy;
                if (px) {
                    u = px + "?url=" + u;
                }
                if ((m !== "GET") && (m !== "POST") && (m !== "PUT") && (m !== "DELETE")) {
                    throw new Error("Jelo.Ajax.request: Method must be one of GET, POST, PUT, DELETE");
                }
                if (typeof p == "object") {
                    for (var param in p) {
                        if (p.hasOwnProperty(param)) {
                            q += (q.indexOf('?') !== -1 ? '&' : '?') + Jelo.Format.urlencode(param) + "=" +
                            Jelo.Format.urlencode(p[param]);
                        }
                    }
                } else 
                    if (typeof p == "string") {
                        q += p; // raw post fields
                    }
                var uCache = u + q; // don't include timestamp
                if (c) {
                    var inCache = getFromCache(uCache, cs, cf, cc, config);
                    if (!!inCache) {
                        return;
                    }
                }
                
                // onreadystatechange
                var orsc = function() {
                    if (x.readyState == 4) {
                        if ((/^(0|2)/).test(x.status)) {
                            if (c) {
                                cache[uCache] = x.responseText;
                            }
                            if (cs.isFunction) {
                                cs.apply(x, [x, config]);
                            }
                        } else {
                            if (cf.isFunction) {
                                cf.apply(x, [x, config]);
                            }
                        }
                        if (cc.isFunction) {
                            cc.apply(x, [x, config]);
                        }
                    }
                };
                
                switch (m) {
                    case "GET":
                        u += q;
                        u += (u.indexOf('?') !== -1 ? '&' : '?') + '_nocache=' + new Date().getTime();
                        x.open(m, u, true);
                        x.onreadystatechange = orsc;
                        x.setRequestHeader("X-Requested-With", 'XMLHttpRequest');
                        x.send(null);
                        break;
                    case "POST":
                        q = q.split("?", 2)[1];
                        x.open(m, u, true);
                        x.onreadystatechange = orsc;
                        x.setRequestHeader("X-Requested-With", 'XMLHttpRequest');
                        x.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                        x.setRequestHeader("Content-length", q.length);
                        x.setRequestHeader("Connection", "close");
                        x.send(q);
                        break;
                    default:
                        throw new Error("Jelo.Ajax.request: Method " + m + " not yet implemented.");
                }
            },
            /**
             * http://www.nczonline.net/blog/2009/06/23/loading-javascript-without-blocking/
             * 
             * @param {String} Address of the remote script. Should be of type text/javascript.
             * @param {Function} Callback function, to be executed after the script is done loading.
             */
            loadScript : loadScript
        };
}();
/** @namespace Animation support. */
Jelo.Anim = function() {
    /** @private convenience */
    var JF = Jelo.Format,
        jcg = Jelo.CSS.getStyle,
        jcs = Jelo.CSS.setStyle,
        pi = function(n) {
            return parseInt(n, 10);
        };
    
    /** @private constants */
    var _ = {
        d   : 0.5, // default duration
        a   : [], // holds animation objects
        r   : [], // contains tasks that need to be removed
        t   : null, // single timer to animate everything
        f   : 60, // desired frames per second
        now : function() {
            return new Date().getTime();
        }
    };
    _.i = Math.round(1000 / _.f); // interval
    
    /** @private Strategy pattern to determine animation behavior. */
    var S = function() {
        /** @private percentComplete */
        var pc = function() {
            var d = this.endTime - this.startTime;
            var r = 1 - ((this.endTime - _.now()) / d);
            var p = this.easing(r);
            return Math.round(p * 1000) / 1000;
        };
        return {
            Border      : function() {/* TODO */},
            Color       : function() {
                var p = pc.call(this);
                var f = this.startVal;
                var t = this.endVal;
                f = (f.substring(0, 3) === "rgb")
                    ? JF.rgbStringToArray(f)
                    : JF.hexToRGB(f);
                t = (t.substring(0, 3) === "rgb")
                    ? JF.rgbStringToArray(t)
                    : JF.hexToRGB(t);
                var v = [];
                for (var i = 0; i < 3; i++) {
                    var delta = Math.floor(t[i] - f[i]);
                    var current = Math.floor(p * delta);
                    v[i] = current + f[i];
                }
                v = "rgb(" + v[0] + "," + v[1] + "," + v[2] + ")";
                jcs(this.element, this.property, v);
            },
            ComboPx     : function() {
                var rx, getVal, x, y, v, x0, y0, p, deltaValue, currDelta, top, right, bottom, left, i;
                switch (this.property) {
                    case "backgroundPositionX" :
                        rx = /\-?[0-9]+px/;
                        if (rx.test(this.startVal) && rx.test(this.endVal)) {
                            /** @private */
                            getVal = function(val) {
                                return val.replace(/[^0-9\-]/g, "");
                            };
                            p = pc.call(this);
                            y0 = Jelo.css(this.element, "background-position-y");
                            x = [getVal(this.startVal), getVal(this.endVal)];
                            y = [getVal(y0), getVal(y0)];
                            deltaValue = [pi(x[1]) - pi(x[0]), pi(y[1]) - pi(y[0])];
                            currDelta = [Math.floor(p * deltaValue[0]), Math.floor(p * deltaValue[1])];
                            v = [currDelta[0] + pi(x[0]), currDelta[1] + pi(y[0])];
                            jcs(this.element, "background-position", v[0] + "px " + v[1] + "px");
                        }
                        break;
                    case "backgroundPositionY" :
                        rx = /\-?[0-9]+px/;
                        if (rx.test(this.startVal) && rx.test(this.endVal)) {
                            /** @private */
                            getVal = function(val) {
                                return val.replace(/[^0-9\-]/g, "");
                            };
                            p = pc.call(this);
                            x0 = Jelo.css(this.element, "background-position-x");
                            x = [getVal(x0), getVal(x0)];
                            y = [getVal(this.startVal), getVal(this.endVal)];
                            deltaValue = [pi(x[1]) - pi(x[0]), pi(y[1]) - pi(y[0])];
                            currDelta = [Math.floor(p * deltaValue[0]), Math.floor(p * deltaValue[1])];
                            v = [currDelta[0] + pi(x[0]), currDelta[1] + pi(y[0])];
                            jcs(this.element, "background-position", v[0] + "px " + v[1] + "px");
                        }
                        break;
                    case "backgroundPosition" :
                        rx = /\-?[0-9]+px \-?[0-9]+px/;
                        if (rx.test(this.startVal) && rx.test(this.endVal)) {
                            /** @private */
                            getVal = function(val, index) {
                                return val.split(" ")[index].replace(/[^0-9\-]/g, "");
                            };
                            p = pc.call(this);
                            x = [getVal(this.startVal, 0), getVal(this.endVal, 0)];
                            y = [getVal(this.startVal, 1), getVal(this.endVal, 1)];
                            deltaValue = [pi(x[1]) - pi(x[0]), pi(y[1]) - pi(y[0])];
                            currDelta = [Math.floor(p * deltaValue[0]), Math.floor(p * deltaValue[1])];
                            v = [currDelta[0] + pi(x[0]), currDelta[1] + pi(y[0])];
                            jcs(this.element, this.property, v[0] + "px " + v[1] + "px");
                        }
                        break;
                    default :
                        rx = /\-?[0-9]+px \-?[0-9]+px \-?[0-9]+px \-?[0-9]+px/;
                        if (rx.test(this.startVal) && rx.test(this.endVal)) {
                            /** @private */
                            getVal = function(val, index) {
                                return val.split(" ")[index].replace(/[^0-9\-]/g, "");
                            };
                            p = pc.call(this);
                            top = [getVal(this.startVal, 0), getVal(this.endVal, 0)];
                            right = [getVal(this.startVal, 1), getVal(this.endVal, 1)];
                            bottom = [getVal(this.startVal, 2), getVal(this.endVal, 2)];
                            left = [getVal(this.startVal, 3), getVal(this.endVal, 3)];
                            deltaValue = [pi(top[1]) - pi(top[0]), pi(right[1]) - pi(right[0]),
                                pi(bottom[1]) - pi(bottom[0]), pi(left[1]) - pi(left[0])];
                            currDelta = [];
                            for (i = 0; i < 4; i++) {
                                currDelta[i] = Math.floor(p * deltaValue[i]);
                            }
                            v = [currDelta[0] + pi(top[0]), currDelta[1] + pi(right[0]), currDelta[2] + pi(bottom[0]),
                                currDelta[3] + pi(left[0])];
                            jcs(this.element, this.property, v[0] + "px " + v[1] + "px " + v[2] + "px " + v[3] + "px");
                        }
                }
            },
            Numerical   : function() {
                var p = pc.call(this);
                var deltaValue = parseFloat(this.endVal, 10) - parseFloat(this.startVal, 10);
                var currDelta = p * deltaValue;
                var v = currDelta + parseFloat(this.startVal, 10);
                switch (this.property) {
                    case "opacity" :
                        v = parseFloat(v);
                        if (v < 0) {
                            v = 0;
                        }
                        if (v > 1) {
                            v = 1;
                        }
                        break;
                    case "zIndex" :
                        v = pi(v);
                        break;
                }
                jcs(this.element, this.property, v);
            },
            NumericalPx : function() {
                var p = pc.call(this);
                var deltaValue = pi(this.endVal) - pi(this.startVal);
                var currDelta = Math.floor(p * deltaValue);
                var v = currDelta + pi(this.startVal);
                jcs(this.element, this.property, v + this.unit);
            }
        };
    }();
    
    /** @private Strategy pattern to determine easing behavior */
    var Easing = {
        LINEAR    : function(x) { // no easing
            return x;
        },
        IN        : function(x) { // accelerate
            return Math.pow(x, 3);
        },
        OUT       : function(x) { // decelerate
            return 1 - Math.pow(1 - x, 3);
        },
        SMOOTH    : function(x) { // accelerate then decelerate
            return x < 0.5
                ? 2 * x * x
                : -2 * Math.pow(x - 1, 2) + 1;
        },
        OVERSHOOT : function(x) { // go past, then come back
            var s = 1.70158;
            return (x -= 1) * x * ((s + 1) * x + s) + 1;
        },
        SPRING    : function(x) { // repeated overshoot
            return 1 - (Math.cos(x * 4.5 * Math.PI) * Math.exp(-x * 6));
        },
        WOBBLE    : function(x) { // forward, then back, then forward
            return (-Math.cos(3 * x * Math.PI) / 2) + 0.5;
        }
    };
    
    /** @private accessible as Jelo.Anim.ate */
    var animate = function(config) {
        if (!config) {
            return;
        }
        
        if (Jelo.Valid.isArray(config.me)) {
            var cfg = config;
            Jelo.each(config.me, function() {
                cfg.me = this;
                animate(cfg);
            });
            return;
        }
        
        // validate the animation target element
        var m = config.me || false;
        if (!m) {
            throw new Error('Jelo.Anim.ate: Missing required configuration option me:HTMLElement|String');
        } else if (m.isArray) {
            m = m[0];
        } else if (typeof m === "string") {
            m = Jelo.Dom.selectNode(m);
        }
        if (!m) {
            // need to reverify after the "else" clauses
            throw new Error('Jelo.Anim.ate: Missing required configuration option me:HTMLElement|String');
        }
        
        // validate the css property to animate
        var css = config.css || false;
        if (!css || (typeof css !== "string")) {
            throw new Error('Jelo.Anim.ate: Missing required configuration option css:String');
        }
        var c = JF.hyphenatedToCamelCase(css);
        
        // validate the starting value
        var f = config.from;
        if (!f && typeof f !== "number") {
            f = jcg(m, c);
            if (!f && typeof f !== "number") {
                f = jcg(m, css);
            }
            if (!f && typeof f !== "number") {
                f = 0; // specify config.from to override this behavior
            }
        }
        if (f === "auto") {
            f = 0; // specify config.from to override this behavior
        }
        
        // validate the ending value
        var t = '' + config.to;
        if (!t && typeof t !== "number") {
            throw new Error('Jelo.Anim.ate() Missing required configuration option to:String|Number');
        }
        
        // prepare the pre- and post-animation callback functions
        var b = (typeof config.before === "function")
            ? config.before
            : Jelo.emptyFn;
        var a = (typeof config.after === "function")
            ? config.after
            : Jelo.emptyFn;
        
        // validate the animation duration
        var d = parseFloat(config.duration);
        if (isNaN(d)) {
            d = _.d;
        }
        d = Math.floor(Math.abs(d * 1000)); // convert s to ms
        
        // validate the animation easing method
        var e = config.easing || Jelo.Anim.Easing.LINEAR;
        if ((typeof e === "string") && e.length) {
            e = Jelo.Anim.Easing[e.toUpperCase()] || Jelo.Anim.Easing.LINEAR;
        } else if (typeof e != "function") {
            throw new Error("Jelo.Anim.ate: Easing must be a string such as 'linear', 'out', etc. or a function.");
        }
        
        // clean up animation conflicts, also improves performance
        var temp = [];
        for (var i = 0; i < _.a.length; i++) {
            var ti = _.a[i];
            if ((ti.element != m) || (JF.hyphenatedToCamelCase(ti.property) != c)) {
                temp.push(ti);
            }
        }
        _.a = temp;
        
        // strategy pattern to get an animation function
        var animFn = function(c) {
            switch (c) {
                case "border" :
                case "borderBottom" :
                case "borderLeft" :
                case "borderRight" :
                case "borderTop" :
                    return S.Border;
                case "backgroundColor" :
                case "color" :
                    return S.Color;
                case "backgroundPositionX" :
                case "backgroundPositionY" :
                case "backgroundPosition" :
                case "margin" :
                case "padding" :
                    return S.ComboPx;
                case "lineHeight" :
                case "opacity" :
                case "zIndex" :
                    return S.Numerical;
                case "bottom" :
                case "height" :
                case "left" :
                case "marginBottom" :
                case "marginLeft" :
                case "marginRight" :
                case "marginTop" :
                case "paddingBottom" :
                case "paddingLeft" :
                case "paddingRight" :
                case "paddingTop" :
                case "right" :
                case "top" :
                case "width" :
                    return S.NumericalPx;
                default :
                    return Jelo.emptyFn;
            }
        }(c);
        
        // build the actual animation object
        var animObj = {
            // for convenience in before and after functions
            me        : config.me,
            css       : config.css,
            duration  : config.duration,
            before    : config.before,
            after     : config.after,
            from      : config.from,
            to        : config.to,
            
            // used internally
            element   : m,
            property  : c,
            startVal  : f,
            endVal    : t,
            callback  : a,
            startTime : _.now(),
            endTime   : _.now() + d,
            fn        : animFn,
            easing    : e,
            unit      : function() {
                var unit = t.replace(/(\-?[0-9]+)(.*)?/, '$2');
                return unit.length
                    ? unit
                    : 'px';
            }()
        };
        _.a.push(animObj);
        
        b.call(animObj);
        run();
    };
    
    /** @private Handles each frame of animation. */
    var run = function() {
        var /* counter */i, /* callbacks */c, /* r[i] */ri, /* t[i] */ti, /* value */v, /* timer */t;
        if (!_.t) {
            _.t = setInterval(function() {
                if (_.r.length) {
                    c = [];
                    for (i = _.r.length - 1; i >= 0; i--) {
                        ri = _.r[i];
                        ti = _.a[ri];
                        v = null;
                        switch (ti.fn) {
                            case S.Border :
                                v = ti.endVal; // TODO: doublecheck this
                                // assignment
                                break;
                            case S.ComboPx :
                                v = ti.endVal; // TODO: doublecheck this
                                // assignment
                                break;
                            case S.Color :
                                v = "#" + ti.endVal.replace(/#/, "");
                                break;
                            case S.NumericalPx :
                                v = pi(ti.endVal) + ti.unit;
                                break;
                            case S.Numerical :
                                v = parseFloat(ti.endVal);
                                break;
                            default :
                                v = ti.endVal;
                        }
                        jcs(ti.element, ti.property, v);
                        c.push(ti);
                        _.a.splice(ri, 1);
                    }
                    _.r = [];
                    for (i = 0; i < c.length; i++) {
                        ti = c[i];
                        ti.callback.call(ti);
                    }
                }
                if (_.a.length) {
                    for (i = 0; i < _.a.length; i++) {
                        t = _.a[i];
                        if (_.now() < t.endTime) {
                            t.fn.call(t);
                        } else {
                            _.r.push(i);
                        }
                    }
                }
            }, _.i);
        }
    };
    
    /** @scope Jelo.Anim */
    return {
        /**
         * Easing functions.
         * <ul>
         * <li>LINEAR: no easing, default</li>
         * <li>IN: accelerate (speed up)</li>
         * <li>OUT: decelerate (slow down)</li>
         * <li>SMOOTH: accelerate, then decelerate</li>
         * <li>OVERSHOOT: go past the end value, then come back. occasionally causes errors in IE depending on the
         * animated property.</li>
         * <li>SPRING: repeated overshoot around the end value</li>
         * <li>WOBBLE: forward, back, then forward again in a single duration</li>
         * </ul>
         * 
         * @type Object
         */
        Easing             : Easing,
        /**
         * Animate a CSS property of a given element.
         * 
         * @function
         * @param {Object} config A configuration object.
         * @param {HTMLElement|String} config.me Object to animate, or a CSS selector for which the first matching
         *        element will be used.
         * @param {String} config.css Property to animate.
         * @param {String|Number} [config.from] Starting value for the animation.
         * @param config.to {String|Number} Ending value for the animation
         * @param {Function} [config.before] Method to invoke immediately before the animation starts.
         * @param {Function} [config.after] Method to invoke immediately after the animation finishes.
         * @param {Number} [config.duration=0.5] How many seconds the animation should last.
         * @param {Function|String} [config.easing="linear"] How to calculate property values. If a string is supplied,
         *        it must match a Jelo.Anim.Easing property name (case insensitive).
         */
        ate                : animate,
        /**
         * @function
         * @return {Boolean} True if there are currently animations in the queue.
         */
        ating              : function() {
            return !!_.a.length;
        },
        /**
         * Immediately halts and cancels all pending animations. References are not stored, the animations are gone.
         */
        stopAll            : function() {
            _.r = [];
            _.a = [];
            if (_.t) {
                clearTimeout(_.t);
                _.t = null;
            }
        },
        /**
         * Changes the default animation duration, used whenever the duration property is not explicitly set.
         * 
         * @param {Number} Seconds, as a whole or decimal number.
         */
        setDefaultDuration : function(d) {
            if (Jelo.Valid.isNumber(d)) {
                _.d = d;
            }
        }
    };
}();
/**
 * @namespace Provides drag and drop functionality. Not 100% complete.
 */
Jelo.DD = function() {
    /** @private convenience */
    function pi(n) {
        return parseInt(n, 10);
    }
    
    /** @private constants */
    var _ = {
        minOffset   : 5, // px until drag
        zHigh       : 20000,
        zHigher     : 20001,
        isDragging  : false,
        item        : null,
        mouseX      : null,
        mouseY      : null,
        lastDragged : null,
        dropTargets : []
    };
    
    /** @private whether drag-drop is on or off */
    var active = false;
    
    /** @private */
    var zeroNaN = function(n) {
        return isNaN(n) ? 0 : n;
    };
    
    /** @private event handler */
    var mouseDown = function(t, e) {
        if (!active) {
            return true;
        }
        if (Jelo.Event.isFixed()) {
            e = t;
        }
        t = this;
        while (!t.jeloDragTarget && t !== null) {
            t = t.parentNode || t.parentElement;
        }
        if (t === null) {
            return false;
        }
        
        var d = t.jeloDragTarget;
        
        // store orig state
        var iPos = findPosition(d);
        console.log(iPos);
        d.jeloDragT = Jelo.css(d, "top");
        d.jeloDragL = Jelo.css(d, "left");
        d.jeloDragP = Jelo.css(d, "position");
        d.jeloDragO = Jelo.css(d, "opacity");
        d.jeloDragZ = Jelo.css(d, "z-index");
        
        // get data for new state
        Jelo.css(d, "position", "absolute");
        Jelo.css(d, "top", iPos.y + "px");
        Jelo.css(d, "left", iPos.x + "px");
        d.jeloDragTop = pi(Jelo.css(d, "top"));
        d.jeloDragLeft = pi(Jelo.css(d, "left"));
        
        // return to orig state
        Jelo.css(d, "position", d.jeloDragP);
        Jelo.css(d, "left", d.jeloDragL);
        Jelo.css(d, "top", d.jeloDragT);
        
        Jelo.on(document, "mousemove", mouseMove);
        Jelo.on(document, "mouseup", mouseUp);
        _.item = d;
        _.isDragging = true;
        _.mouseX = e.clientX;
        _.mouseY = e.clientY;
        
        return false;
    };
    
    /** @private event handler */
    var mouseMove = function(t, e) {
        if (!active || !_.isDragging || !_.item) {
            return true;
        }
        if (Jelo.Event.isFixed()) {
            e = t;
        }
        var x = e.clientX - _.mouseX;
        var y = e.clientY - _.mouseY;
        var ax = Math.abs(e.clientX - _.mouseX);
        var ay = Math.abs(e.clientY - _.mouseY);
        if (Math.round((ax + ay) / 2) > _.minOffset) {
            var d = _.item;
            Jelo.css(d, "position", "absolute");
            Jelo.css(d, "opacity", 0.7);
            Jelo.css(d, "z-index", _.zHigher);
            Jelo.css(d, "top", pi(d.jeloDragTop + y) + "px");
            Jelo.css(d, "left", pi(d.jeloDragLeft + x) + "px");
            document.body.appendChild(d);
        }
        return false;
    };
    
    /** @private event handler */
    var mouseUp = function(t, e) {
        if (!active) {
            return true;
        }
        if (Jelo.Event.isFixed()) {
            e = t;
        }
        if (_.item) {
            if (_.lastDragged != _.item) {
                _.zHigh++;
                _.zHigher++;
            }
            _.lastDragged = _.item;
            Jelo.css(_.item, "opacity", _.item.jeloDragO);
            Jelo.css(_.item, "z-index", _.zHigh);
            if (typeof _.item.jeloDragOnMouseUp == "function") {
                _.item.jeloDragOnMouseUp.call(_.item, _.item.jeloDragHandle);
            }
            
            // handle drops
            Jelo.each(_.dropTargets, function() {
                if (typeof this.jeloOnDrop == "function") {
                    var t = findPosition(_.item);
                    var p = findPosition(this);
                    var r = p.x + zeroNaN(pi(Jelo.css(this, "width"))); // right edge
                    var b = p.y + zeroNaN(pi(Jelo.css(this, "height"))); // bottom edge
                    var mx = e.clientX;
                    var my = e.clientY;
                    if (mx > p.x && mx < r && my > p.y && my < b) {
                        var d = _.item;
                        Jelo.css(d, "position", "static");
                        Jelo.css(d, "left", "auto");
                        Jelo.css(d, "top", "auto");
                        this.appendChild(_.item);
                        this.jeloOnDrop.call(this, _.item,
                            _.item.jeloDragHandle);
                    }
                }
            });
            
        }
        Jelo.un(document, "mousemove", mouseMove);
        Jelo.un(document, "mouseup", mouseUp);
        _.mouseX = null;
        _.mouseY = null;
        _.item = null;
        _.isDragging = false;
    };
    
    var detectDrop = function(t, e) {
        if (Jelo.Event.isFixed()) {
            e = t;
        }
        console.log('test');
    };
    
    /** @private utility */
    var findPosition = function(o) {
        var offsetX = zeroNaN(pi(Jelo.css(o, "margin-left")));
        var offsetY = zeroNaN(pi(Jelo.css(o, "margin-top")));
        var curleft = 0, curtop = 0;
        if (o.offsetParent) {
            curleft = o.offsetLeft;
            curtop = o.offsetTop;
            while ((o = o.offsetParent)) {
                curleft += o.offsetLeft;
                curtop += o.offsetTop;
            }
        }
        return {
            "x" : curleft - offsetX,
            "y" : curtop - offsetY
        };
    };
    
    /** @scope Jelo.DD */
    return {
        /**
         * @param {HTMLElement} element The item to investigate.
         * @returns {Boolean} True if element will respond to drag events.
         */
        isDraggy       : function(el) {
            return (el && !!el.jeloDraggy);
        },
        /**
         * @param {HTMLElement} element The item to investigate.
         * @returns {Boolean} True if element will respond to drop events.
         */
        isDroppy       : function(el) {
            return (el && !!el.jeloDroppy);
        },
        /**
         * Starts or stops listening for drag events on a given element.
         *
         * @param {HTMLElement} element The item to affect.
         * @param {Boolean} [bool=true] True to start listening for drag events, false
         * to stop.
         * @param {HTMLElement} [handle=element] The area that, when dragged,
         * will move the element. Defaults to the whole element itself.
         * @param {Function} [fn] Method to invoke when the element is dropped.
         * The execution context ("this") will be the element itself, and the
         * argument passed will be the handle object.
         */
        setDraggy      : function(el, bool, handle, fn) {
            bool = (typeof bool == "boolean") ? bool : true;
            handle = handle || el;
            if (Jelo.Valid.isElement(el) && Jelo.Valid.isElement(handle)) {
                handle.jeloDragTarget = el;
                Jelo.on(handle, "mousedown", mouseDown);
                Jelo.css(handle, "cursor", "move");
                el.jeloDraggy = bool;
                el.jeloDragHandle = handle;
                if (typeof fn == "function") {
                    el.jeloDragOnMouseUp = fn;
                }
            }
        },
        /**
         * Starts or stops listening for drop events on a given element.
         *
         * @param {HTMLElement} element The item to affect.
         * @param {Boolean} [bool=true] True to start listening for drop events, false
         * to stop.
         * @param {HTMLElement} [handle=element] The area that, when dropped
         * upon, will register the event. Defaults to the whole element itself.
         * @param {Function} [fn] Method to invoke when something is dropped on
         * this element. The execution context ("this") will be the drop zone
         * element itself, and the arguments passed will be the dropped element
         * and the drop zone's handle, respectively.
         */
        setDroppy      : function(el, bool, handle, fn) {
            bool = (typeof bool == "boolean") ? bool : true;
            handle = handle || el;
            if (Jelo.Valid.isElement(el) && Jelo.Valid.isElement(handle)) {
                handle.jeloDropTarget = el;
                el.jeloDroppy = bool;
                el.jeloDropHandle = handle;
                if (typeof fn == "function") {
                    el.jeloOnDrop = fn;
                }
                _.dropTargets.push(el);
            }
        },
        /**
         * Turns on drag-drop support.
         */
        on             : function() {
            active = true;
        },
        /**
         * Turns on drag-drop support. Elements retain any registered drag
         * handlers while drag-drop support is off, but no such events are
         * triggered.
         */
        off            : function() {
            active = false;
        },
        /**
         * Sets how far an element must be dragged before it "counts" as a drag.
         *
         * @param {Number} n How many pixels an element must be dragged before
         * it "counts" as a drag.
         */
        setMinimumDrag : function(n) {
            n = pi(n);
            if (!isNaN(n)) {
                _.minOffset = n;
            }
        }
    };
}();
