//     Underscore.js 1.1.4
//     (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
//     Underscore is freely distributable under the MIT license.
//     Portions of Underscore are inspired or borrowed from Prototype,
//     Oliver Steele's Functional, and John Resig's Micro-Templating.
//     For all details and documentation:
//     http://documentcloud.github.com/underscore

(function() {

  // Baseline setup
  // --------------

  // Establish the root object, `window` in the browser, or `global` on the server.
  var root = this;

  // Save the previous value of the `_` variable.
  var previousUnderscore = root._;

  // Establish the object that gets returned to break out of a loop iteration.
  var breaker = {};

  // Save bytes in the minified (but not gzipped) version:
  var ArrayProto = Array.prototype, ObjProto = Object.prototype;

  // Create quick reference variables for speed access to core prototypes.
  var slice            = ArrayProto.slice,
      unshift          = ArrayProto.unshift,
      toString         = ObjProto.toString,
      hasOwnProperty   = ObjProto.hasOwnProperty;

  // All **ECMAScript 5** native function implementations that we hope to use
  // are declared here.
  var
    nativeForEach      = ArrayProto.forEach,
    nativeMap          = ArrayProto.map,
    nativeReduce       = ArrayProto.reduce,
    nativeReduceRight  = ArrayProto.reduceRight,
    nativeFilter       = ArrayProto.filter,
    nativeEvery        = ArrayProto.every,
    nativeSome         = ArrayProto.some,
    nativeIndexOf      = ArrayProto.indexOf,
    nativeLastIndexOf  = ArrayProto.lastIndexOf,
    nativeIsArray      = Array.isArray,
    nativeKeys         = Object.keys;

  // Create a safe reference to the Underscore object for use below.
  var _ = function(obj) { return new wrapper(obj); };

  // Export the Underscore object for **CommonJS**, with backwards-compatibility
  // for the old `require()` API. If we're not in CommonJS, add `_` to the
  // global object.
  if (typeof module !== 'undefined' && module.exports) {
    module.exports = _;
    _._ = _;
  } else {
    root._ = _;
  }

  // Current version.
  _.VERSION = '1.1.4';

  // Collection Functions
  // --------------------

  // The cornerstone, an `each` implementation, aka `forEach`.
  // Handles objects implementing `forEach`, arrays, and raw objects.
  // Delegates to **ECMAScript 5**'s native `forEach` if available.
  var each = _.each = _.forEach = function(obj, iterator, context) {
    var value;
    if (obj == null) return;
    if (nativeForEach && obj.forEach === nativeForEach) {
      obj.forEach(iterator, context);
    } else if (_.isNumber(obj.length)) {
      for (var i = 0, l = obj.length; i < l; i++) {
        if (iterator.call(context, obj[i], i, obj) === breaker) return;
      }
    } else {
      for (var key in obj) {
        if (hasOwnProperty.call(obj, key)) {
          if (iterator.call(context, obj[key], key, obj) === breaker) return;
        }
      }
    }
  };

  // Return the results of applying the iterator to each element.
  // Delegates to **ECMAScript 5**'s native `map` if available.
  _.map = function(obj, iterator, context) {
    var results = [];
    if (obj == null) return results;
    if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
    each(obj, function(value, index, list) {
      results[results.length] = iterator.call(context, value, index, list);
    });
    return results;
  };

  // **Reduce** builds up a single result from a list of values, aka `inject`,
  // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
  _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
    var initial = memo !== void 0;
    if (obj == null) obj = [];
    if (nativeReduce && obj.reduce === nativeReduce) {
      if (context) iterator = _.bind(iterator, context);
      return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
    }
    each(obj, function(value, index, list) {
      if (!initial && index === 0) {
        memo = value;
        initial = true;
      } else {
        memo = iterator.call(context, memo, value, index, list);
      }
    });
    if (!initial) throw new TypeError("Reduce of empty array with no initial value");
    return memo;
  };

  // The right-associative version of reduce, also known as `foldr`.
  // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
  _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
    if (obj == null) obj = [];
    if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
      if (context) iterator = _.bind(iterator, context);
      return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
    }
    var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
    return _.reduce(reversed, iterator, memo, context);
  };

  // Return the first value which passes a truth test. Aliased as `detect`.
  _.find = _.detect = function(obj, iterator, context) {
    var result;
    any(obj, function(value, index, list) {
      if (iterator.call(context, value, index, list)) {
        result = value;
        return true;
      }
    });
    return result;
  };

  // Return all the elements that pass a truth test.
  // Delegates to **ECMAScript 5**'s native `filter` if available.
  // Aliased as `select`.
  _.filter = _.select = function(obj, iterator, context) {
    var results = [];
    if (obj == null) return results;
    if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
    each(obj, function(value, index, list) {
      if (iterator.call(context, value, index, list)) results[results.length] = value;
    });
    return results;
  };

  // Return all the elements for which a truth test fails.
  _.reject = function(obj, iterator, context) {
    var results = [];
    if (obj == null) return results;
    each(obj, function(value, index, list) {
      if (!iterator.call(context, value, index, list)) results[results.length] = value;
    });
    return results;
  };

  // Determine whether all of the elements match a truth test.
  // Delegates to **ECMAScript 5**'s native `every` if available.
  // Aliased as `all`.
  _.every = _.all = function(obj, iterator, context) {
    iterator = iterator || _.identity;
    var result = true;
    if (obj == null) return result;
    if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
    each(obj, function(value, index, list) {
      if (!(result = result && iterator.call(context, value, index, list))) return breaker;
    });
    return result;
  };

  // Determine if at least one element in the object matches a truth test.
  // Delegates to **ECMAScript 5**'s native `some` if available.
  // Aliased as `any`.
  var any = _.some = _.any = function(obj, iterator, context) {
    iterator = iterator || _.identity;
    var result = false;
    if (obj == null) return result;
    if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
    each(obj, function(value, index, list) {
      if (result = iterator.call(context, value, index, list)) return breaker;
    });
    return result;
  };

  // Determine if a given value is included in the array or object using `===`.
  // Aliased as `contains`.
  _.include = _.contains = function(obj, target) {
    var found = false;
    if (obj == null) return found;
    if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
    any(obj, function(value) {
      if (found = value === target) return true;
    });
    return found;
  };

  // Invoke a method (with arguments) on every item in a collection.
  _.invoke = function(obj, method) {
    var args = slice.call(arguments, 2);
    return _.map(obj, function(value) {
      return (method ? value[method] : value).apply(value, args);
    });
  };

  // Convenience version of a common use case of `map`: fetching a property.
  _.pluck = function(obj, key) {
    return _.map(obj, function(value){ return value[key]; });
  };

  // Return the maximum element or (element-based computation).
  _.max = function(obj, iterator, context) {
    if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj);
    var result = {computed : -Infinity};
    each(obj, function(value, index, list) {
      var computed = iterator ? iterator.call(context, value, index, list) : value;
      computed >= result.computed && (result = {value : value, computed : computed});
    });
    return result.value;
  };

  // Return the minimum element (or element-based computation).
  _.min = function(obj, iterator, context) {
    if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj);
    var result = {computed : Infinity};
    each(obj, function(value, index, list) {
      var computed = iterator ? iterator.call(context, value, index, list) : value;
      computed < result.computed && (result = {value : value, computed : computed});
    });
    return result.value;
  };

  // Sort the object's values by a criterion produced by an iterator.
  _.sortBy = function(obj, iterator, context) {
    return _.pluck(_.map(obj, function(value, index, list) {
      return {
        value : value,
        criteria : iterator.call(context, value, index, list)
      };
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }), 'value');
  };

  // Use a comparator function to figure out at what index an object should
  // be inserted so as to maintain order. Uses binary search.
  _.sortedIndex = function(array, obj, iterator) {
    iterator = iterator || _.identity;
    var low = 0, high = array.length;
    while (low < high) {
      var mid = (low + high) >> 1;
      iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid;
    }
    return low;
  };

  // Safely convert anything iterable into a real, live array.
  _.toArray = function(iterable) {
    if (!iterable)                return [];
    if (iterable.toArray)         return iterable.toArray();
    if (_.isArray(iterable))      return iterable;
    if (_.isArguments(iterable))  return slice.call(iterable);
    return _.values(iterable);
  };

  // Return the number of elements in an object.
  _.size = function(obj) {
    return _.toArray(obj).length;
  };

  // Array Functions
  // ---------------

  // Get the first element of an array. Passing **n** will return the first N
  // values in the array. Aliased as `head`. The **guard** check allows it to work
  // with `_.map`.
  _.first = _.head = function(array, n, guard) {
    return n && !guard ? slice.call(array, 0, n) : array[0];
  };

  // Returns everything but the first entry of the array. Aliased as `tail`.
  // Especially useful on the arguments object. Passing an **index** will return
  // the rest of the values in the array from that index onward. The **guard**
  // check allows it to work with `_.map`.
  _.rest = _.tail = function(array, index, guard) {
    return slice.call(array, _.isUndefined(index) || guard ? 1 : index);
  };

  // Get the last element of an array.
  _.last = function(array) {
    return array[array.length - 1];
  };

  // Trim out all falsy values from an array.
  _.compact = function(array) {
    return _.filter(array, function(value){ return !!value; });
  };

  // Return a completely flattened version of an array.
  _.flatten = function(array) {
    return _.reduce(array, function(memo, value) {
      if (_.isArray(value)) return memo.concat(_.flatten(value));
      memo[memo.length] = value;
      return memo;
    }, []);
  };

  // Return a version of the array that does not contain the specified value(s).
  _.without = function(array) {
    var values = slice.call(arguments, 1);
    return _.filter(array, function(value){ return !_.include(values, value); });
  };

  // Produce a duplicate-free version of the array. If the array has already
  // been sorted, you have the option of using a faster algorithm.
  // Aliased as `unique`.
  _.uniq = _.unique = function(array, isSorted) {
    return _.reduce(array, function(memo, el, i) {
      if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el;
      return memo;
    }, []);
  };

  // Produce an array that contains every item shared between all the
  // passed-in arrays.
  _.intersect = function(array) {
    var rest = slice.call(arguments, 1);
    return _.filter(_.uniq(array), function(item) {
      return _.every(rest, function(other) {
        return _.indexOf(other, item) >= 0;
      });
    });
  };

  // Zip together multiple lists into a single array -- elements that share
  // an index go together.
  _.zip = function() {
    var args = slice.call(arguments);
    var length = _.max(_.pluck(args, 'length'));
    var results = new Array(length);
    for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i);
    return results;
  };

  // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
  // we need this function. Return the position of the first occurrence of an
  // item in an array, or -1 if the item is not included in the array.
  // Delegates to **ECMAScript 5**'s native `indexOf` if available.
  // If the array is large and already in sort order, pass `true`
  // for **isSorted** to use binary search.
  _.indexOf = function(array, item, isSorted) {
    if (array == null) return -1;
    if (isSorted) {
      var i = _.sortedIndex(array, item);
      return array[i] === item ? i : -1;
    }
    if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
    for (var i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
    return -1;
  };


  // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
  _.lastIndexOf = function(array, item) {
    if (array == null) return -1;
    if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
    var i = array.length;
    while (i--) if (array[i] === item) return i;
    return -1;
  };

  // Generate an integer Array containing an arithmetic progression. A port of
  // the native Python `range()` function. See
  // [the Python documentation](http://docs.python.org/library/functions.html#range).
  _.range = function(start, stop, step) {
    var args  = slice.call(arguments),
        solo  = args.length <= 1,
        start = solo ? 0 : args[0],
        stop  = solo ? args[0] : args[1],
        step  = args[2] || 1,
        len   = Math.max(Math.ceil((stop - start) / step), 0),
        idx   = 0,
        range = new Array(len);
    while (idx < len) {
      range[idx++] = start;
      start += step;
    }
    return range;
  };

  // Function (ahem) Functions
  // ------------------

  // Create a function bound to a given object (assigning `this`, and arguments,
  // optionally). Binding with arguments is also known as `curry`.
  _.bind = function(func, obj) {
    var args = slice.call(arguments, 2);
    return function() {
      return func.apply(obj || {}, args.concat(slice.call(arguments)));
    };
  };

  // Bind all of an object's methods to that object. Useful for ensuring that
  // all callbacks defined on an object belong to it.
  _.bindAll = function(obj) {
    var funcs = slice.call(arguments, 1);
    if (funcs.length == 0) funcs = _.functions(obj);
    each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
    return obj;
  };

  // Memoize an expensive function by storing its results.
  _.memoize = function(func, hasher) {
    var memo = {};
    hasher = hasher || _.identity;
    return function() {
      var key = hasher.apply(this, arguments);
      return key in memo ? memo[key] : (memo[key] = func.apply(this, arguments));
    };
  };

  // Delays a function for the given number of milliseconds, and then calls
  // it with the arguments supplied.
  _.delay = function(func, wait) {
    var args = slice.call(arguments, 2);
    return setTimeout(function(){ return func.apply(func, args); }, wait);
  };

  // Defers a function, scheduling it to run after the current call stack has
  // cleared.
  _.defer = function(func) {
    return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
  };

  // Internal function used to implement `_.throttle` and `_.debounce`.
  var limit = function(func, wait, debounce) {
    var timeout;
    return function() {
      var context = this, args = arguments;
      var throttler = function() {
        timeout = null;
        func.apply(context, args);
      };
      if (debounce) clearTimeout(timeout);
      if (debounce || !timeout) timeout = setTimeout(throttler, wait);
    };
  };

  // Returns a function, that, when invoked, will only be triggered at most once
  // during a given window of time.
  _.throttle = function(func, wait) {
    return limit(func, wait, false);
  };

  // Returns a function, that, as long as it continues to be invoked, will not
  // be triggered. The function will be called after it stops being called for
  // N milliseconds.
  _.debounce = function(func, wait) {
    return limit(func, wait, true);
  };

  // Returns the first function passed as an argument to the second,
  // allowing you to adjust arguments, run code before and after, and
  // conditionally execute the original function.
  _.wrap = function(func, wrapper) {
    return function() {
      var args = [func].concat(slice.call(arguments));
      return wrapper.apply(this, args);
    };
  };

  // Returns a function that is the composition of a list of functions, each
  // consuming the return value of the function that follows.
  _.compose = function() {
    var funcs = slice.call(arguments);
    return function() {
      var args = slice.call(arguments);
      for (var i=funcs.length-1; i >= 0; i--) {
        args = [funcs[i].apply(this, args)];
      }
      return args[0];
    };
  };

  // Object Functions
  // ----------------

  // Retrieve the names of an object's properties.
  // Delegates to **ECMAScript 5**'s native `Object.keys`
  _.keys = nativeKeys || function(obj) {
    if (_.isArray(obj)) return _.range(0, obj.length);
    var keys = [];
    for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key;
    return keys;
  };

  // Retrieve the values of an object's properties.
  _.values = function(obj) {
    return _.map(obj, _.identity);
  };

  // Return a sorted list of the function names available on the object.
  // Aliased as `methods`
  _.functions = _.methods = function(obj) {
    return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort();
  };

  // Extend a given object with all the properties in passed-in object(s).
  _.extend = function(obj) {
    each(slice.call(arguments, 1), function(source) {
      for (var prop in source) obj[prop] = source[prop];
    });
    return obj;
  };

  // Create a (shallow-cloned) duplicate of an object.
  _.clone = function(obj) {
    return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
  };

  // Invokes interceptor with the obj, and then returns obj.
  // The primary purpose of this method is to "tap into" a method chain, in
  // order to perform operations on intermediate results within the chain.
  _.tap = function(obj, interceptor) {
    interceptor(obj);
    return obj;
  };

  // Perform a deep comparison to check if two objects are equal.
  _.isEqual = function(a, b) {
    // Check object identity.
    if (a === b) return true;
    // Different types?
    var atype = typeof(a), btype = typeof(b);
    if (atype != btype) return false;
    // Basic equality test (watch out for coercions).
    if (a == b) return true;
    // One is falsy and the other truthy.
    if ((!a && b) || (a && !b)) return false;
    // Unwrap any wrapped objects.
    if (a._chain) a = a._wrapped;
    if (b._chain) b = b._wrapped;
    // One of them implements an isEqual()?
    if (a.isEqual) return a.isEqual(b);
    // Check dates' integer values.
    if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime();
    // Both are NaN?
    if (_.isNaN(a) && _.isNaN(b)) return false;
    // Compare regular expressions.
    if (_.isRegExp(a) && _.isRegExp(b))
      return a.source     === b.source &&
             a.global     === b.global &&
             a.ignoreCase === b.ignoreCase &&
             a.multiline  === b.multiline;
    // If a is not an object by this point, we can't handle it.
    if (atype !== 'object') return false;
    // Check for different array lengths before comparing contents.
    if (a.length && (a.length !== b.length)) return false;
    // Nothing else worked, deep compare the contents.
    var aKeys = _.keys(a), bKeys = _.keys(b);
    // Different object sizes?
    if (aKeys.length != bKeys.length) return false;
    // Recursive comparison of contents.
    for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false;
    return true;
  };

  // Is a given array or object empty?
  _.isEmpty = function(obj) {
    if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
    for (var key in obj) if (hasOwnProperty.call(obj, key)) return false;
    return true;
  };

  // Is a given value a DOM element?
  _.isElement = function(obj) {
    return !!(obj && obj.nodeType == 1);
  };

  // Is a given value an array?
  // Delegates to ECMA5's native Array.isArray
  _.isArray = nativeIsArray || function(obj) {
    return toString.call(obj) === '[object Array]';
  };

  // Is a given variable an arguments object?
  _.isArguments = function(obj) {
    return !!(obj && hasOwnProperty.call(obj, 'callee'));
  };

  // Is a given value a function?
  _.isFunction = function(obj) {
    return !!(obj && obj.constructor && obj.call && obj.apply);
  };

  // Is a given value a string?
  _.isString = function(obj) {
    return !!(obj === '' || (obj && obj.charCodeAt && obj.substr));
  };

  // Is a given value a number?
  _.isNumber = function(obj) {
    return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed));
  };

  // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript
  // that does not equal itself.
  _.isNaN = function(obj) {
    return obj !== obj;
  };

  // Is a given value a boolean?
  _.isBoolean = function(obj) {
    return obj === true || obj === false;
  };

  // Is a given value a date?
  _.isDate = function(obj) {
    return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear);
  };

  // Is the given value a regular expression?
  _.isRegExp = function(obj) {
    return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false));
  };

  // Is a given value equal to null?
  _.isNull = function(obj) {
    return obj === null;
  };

  // Is a given variable undefined?
  _.isUndefined = function(obj) {
    return obj === void 0;
  };

  // Utility Functions
  // -----------------

  // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
  // previous owner. Returns a reference to the Underscore object.
  _.noConflict = function() {
    root._ = previousUnderscore;
    return this;
  };

  // Keep the identity function around for default iterators.
  _.identity = function(value) {
    return value;
  };

  // Run a function **n** times.
  _.times = function (n, iterator, context) {
    for (var i = 0; i < n; i++) iterator.call(context, i);
  };

  // Add your own custom functions to the Underscore object, ensuring that
  // they're correctly added to the OOP wrapper as well.
  _.mixin = function(obj) {
    each(_.functions(obj), function(name){
      addToWrapper(name, _[name] = obj[name]);
    });
  };

  // Generate a unique integer id (unique within the entire client session).
  // Useful for temporary DOM ids.
  var idCounter = 0;
  _.uniqueId = function(prefix) {
    var id = idCounter++;
    return prefix ? prefix + id : id;
  };

  // By default, Underscore uses ERB-style template delimiters, change the
  // following template settings to use alternative delimiters.
  _.templateSettings = {
    evaluate    : /<%([\s\S]+?)%>/g,
    interpolate : /<%=([\s\S]+?)%>/g
  };

  // JavaScript micro-templating, similar to John Resig's implementation.
  // Underscore templating handles arbitrary delimiters, preserves whitespace,
  // and correctly escapes quotes within interpolated code.
  _.template = function(str, data) {
    var c  = _.templateSettings;
    var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' +
      'with(obj||{}){__p.push(\'' +
      str.replace(/\\/g, '\\\\')
         .replace(/'/g, "\\'")
         .replace(c.interpolate, function(match, code) {
           return "'," + code.replace(/\\'/g, "'") + ",'";
         })
         .replace(c.evaluate || null, function(match, code) {
           return "');" + code.replace(/\\'/g, "'")
                              .replace(/[\r\n\t]/g, ' ') + "__p.push('";
         })
         .replace(/\r/g, '\\r')
         .replace(/\n/g, '\\n')
         .replace(/\t/g, '\\t')
         + "');}return __p.join('');";
    var func = new Function('obj', tmpl);
    return data ? func(data) : func;
  };

  // The OOP Wrapper
  // ---------------

  // If Underscore is called as a function, it returns a wrapped object that
  // can be used OO-style. This wrapper holds altered versions of all the
  // underscore functions. Wrapped objects may be chained.
  var wrapper = function(obj) { this._wrapped = obj; };

  // Expose `wrapper.prototype` as `_.prototype`
  _.prototype = wrapper.prototype;

  // Helper function to continue chaining intermediate results.
  var result = function(obj, chain) {
    return chain ? _(obj).chain() : obj;
  };

  // A method to easily add functions to the OOP wrapper.
  var addToWrapper = function(name, func) {
    wrapper.prototype[name] = function() {
      var args = slice.call(arguments);
      unshift.call(args, this._wrapped);
      return result(func.apply(_, args), this._chain);
    };
  };

  // Add all of the Underscore functions to the wrapper object.
  _.mixin(_);

  // Add all mutator Array functions to the wrapper.
  each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
    var method = ArrayProto[name];
    wrapper.prototype[name] = function() {
      method.apply(this._wrapped, arguments);
      return result(this._wrapped, this._chain);
    };
  });

  // Add all accessor Array functions to the wrapper.
  each(['concat', 'join', 'slice'], function(name) {
    var method = ArrayProto[name];
    wrapper.prototype[name] = function() {
      return result(method.apply(this._wrapped, arguments), this._chain);
    };
  });

  // Start chaining a wrapped Underscore object.
  wrapper.prototype.chain = function() {
    this._chain = true;
    return this;
  };

  // Extracts the result from a wrapped and chained object.
  wrapper.prototype.value = function() {
    return this._wrapped;
  };

})();


$$ = jQuery;
Object.extend = jQuery.extend;

document.viewport = {
  getDimensions: function() {
    return { width: $(window).width(), height: $(window).height() };
  },

  getScrollOffsets: function() {
    return [
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop  || document.body.scrollTop
    ];
  }
};

Effect = {
  toggle: function(id, type, options) {
    $('#' + id).animate({height: 'toggle'}, options || {});
  },
	Highlight: function(element, options, speed, callback) {
		$(element).effect("highlight", options || {});
	}
};

Control = {
	Modal: function(selector, options) {
    options = options || {};
    var optionsMap = {
      'jQPos': 'position'
    };

    var eventsMap = {
      'afterOpen': 'open'
    };

    for(var i in options) {
      if(optionsMap[i]) {
        options[optionsMap[i]] = options[i];
        delete options[i];
      }
    }

    for(var event in eventsMap) {
      if(options[event]) {
        $(selector).bind(eventsMap[event], options[event]);
      }
    }

    $.extend(options, {
      autoOpen: false
    });

    if($.type(selector) == 'string' && (selector.indexOf('#') !== 0))
      selector = '#' + selector;

    if($(selector).data('modal'))
      return $(selector).data('modal');

    var modal = $(selector).modal(options);
    return modal ? modal.data('modal') : $(selector);
  }
};

//Prototype's Element contstructor :-)
function Element(tag, options) {
  options = options || {};
  return $(document.createElement(tag)).attr(options);
}

jQuery.extend(Control.Modal, {
  open: function(el, opts) {
    var modal = new this(el, opts);
    if(modal.open)
      modal.open();
  },
  close: function(){
    $.ui.modal.instances.each(function(modal){ modal.close(); });
  }
});

(function($) {

  var prototypeMethods = {
    Array: ["each", "map", "reduce", "inject", "reduceRight", "detect", "select", "reject", "all", "any", "include", "invoke", "pluck", "max", "min", "sortBy", "sortedIndex", "size", "first", "rest", "last", "compact", "flatten", "without", "uniq", "intersect", "zip", "indexOf", "lastIndexOf", "range"],
    Function: ["bind", "bindAll", "memoize", "delay", "defer", "throttle", "debounce", "wrap", "compose"]
  };

  for(var obj in prototypeMethods) {
    _.each(prototypeMethods[obj], function(method, i) {
      window[obj].prototype[method] = function() {
        var args = Array.prototype.slice.apply(arguments);
        args.unshift(this);
        return _[method].apply(window, args);
      };
    });
  }

  Array.prototype.uniq = (function(original) {
    return function() {
      if($.type(this[0]) == 'object') {
        var jsonArray = [];
        var newArray = [];
        this.each(function(value, i, orig){
          if(jQuery.inArray(jQuery.toJSON(value), jsonArray) == -1) {
            jsonArray.push(jQuery.toJSON(value));
            newArray.push(value);
          }
        });
        return newArray;
      }
      else
        return original.apply(this);
    };
  })(Array.prototype.uniq);

  $.extend(Function.prototype, {
    argumentNames: function() {
      var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1]
        .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '')
        .replace(/\s+/g, '').split(',');
      return names.length == 1 && !names[0] ? [] : names;
    }
  });

  $.extend(String.prototype, (function() {
    function blank(){
      return (/^\s*$/).test(this);
    }

    function capitalize() {
      return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();
    }

    function stripTags() {
      return this.replace(/<\w+(\s+("[^"]*"|'[^']*'|[^>])+)?>|<\/\w+>/gi, '');
    }

    function escapeHTML() {
      return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
    }

    function evalJSON() {
      return $.parseJSON(this + '');
    }

    function unescapeHTML() {
      return this.stripTags().replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&');
    }

    function toQueryParams(separator) {
      var match = this.match(/([^?#]*)(#.*)?$/);
      if (!match) return { };

      return match[1].split(separator || '&').inject({ }, function(hash, pair) {
        if ((pair = pair.split('='))[0]) {
          var key = decodeURIComponent(pair.shift()),
              value = pair.length > 1 ? pair.join('=') : pair[0];

          if (value != undefined) value = decodeURIComponent(value);

          if (key in hash) {
            if (!$.type(hash[key]) == 'array') hash[key] = [hash[key]];
            hash[key].push(value);
          }
          else hash[key] = value;
        }
        return hash;
      });
    }

    return {
      blank        : blank,
      capitalize   : capitalize,
      empty        : blank,
      escapeHTML   : escapeHTML,
      evalJSON     : evalJSON,
      stripHTML    : stripTags,
      stripTags    : stripTags,
      toQueryParams: toQueryParams,
      unescapeHTML : unescapeHTML
    };
  })());

  RegExp.escape = function(str) {
    return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
  };

})(jQuery);

(function($) {
  var $break = { };

  $.Utility = {
    Array: function(array) {
      this.initialize(array);
    },

    Hash: function(object) {
      this.initialize(object);
    }
  };

  $.Utility.Hash.prototype = {

    initialize: function(object) {
      this._object = object;
      return this;
    },

    each: function(iterator, context) {
      var index = 0;
      try {
        for (var key in this._object) {
          var value = this._object[key], pair = [key, value];
          pair.key = key;
          pair.value = value;
          iterator(pair);
        }
      } catch (e) {
      if (e != $break) throw e;
      }
      return this;
    },

    keys: function() {
      var keys = [];
      for (var key in this._object) keys.push(key);
      return keys;
    },

    merge: function(object) {
      for (var prop in object) {
        this._object[prop] = object[prop];
      }
      return this;
    },

    get: function(key) {
      for(var prop in this._object) {
        if(prop == key)
          return this._object[prop];
      }
      return undefined;
    },

    toQueryString: function() {
      return $.param(this._object);
    }

  };

  $.Utility.Array.prototype = {
    initialize: function(array) {
			if (!array) return;
      this._array = array;
      this.length = this._array.length;
      for(var i = 0; i < this.length; i++) {
        this[i] = this._array[i];
      }
    },

    each: function(iterator, context) {
      for (var i = 0, length = this.length; i < length; i++)
        iterator.call(context, this[i], i);
    },

    map: function(callback) {
      var ret = [], value;
      for (var i = 0, length = this.length; i < length; i++) {
        value = callback(this[ i ], i);

        if (value !== null) {
          ret[ret.length] = value;
        }
      }
      return ret.concat.apply([], ret);
    },

    compact: function() {
      var ret = [];
      this.each(function(el, i) {
        if(el !== null)
          ret.push(el);
      });
      return ret;
    },

    push: [].push,
    sort: [].sort,
    splice: [].splice
  };

})(jQuery);

(function() {
  prototypeAliases = {
    'addClassName'   : 'addClass',
    'cumulativeOffset': 'offset',
    'down'					 : 'find',
    'fire'           : 'trigger',
    'getHeight'      : 'height',
    'getStyle'       : 'css',
    'getWidth'       : 'width',
    'getValue'       : 'val',
    'hasClassName'   : 'hasClass',
    'match'          : 'is',
    'observe'        : 'bind',
    'readAttribute'  : 'attr',
    'removeClassName': 'removeClass',
    'replace'        : 'replaceWith',
    'select'         : 'find',
    'setStyle'       : 'css',
    'setValue'       : 'val',
    'stopObserving'  : 'unbind',
    'toggleClassName': 'toggleClass',
    'up'             : 'closest',
    'update'         : 'html',
		'writeAttribute' : 'attr'
  };

  for(var i in prototypeAliases){
    $.fn[i] = $.fn[prototypeAliases[i]];
  }

})();

function $F(element_or_id) {
  if(element_or_id.nodeType || (element_or_id instanceof jQuery)) {
    return $(element_or_id).val();
  }
  else
    return $('#' + element_or_id).val();
}

if(!$H)
	var $H = function(object) {
		return new jQuery.Utility.Hash(object);
	};

if(!$A)
  var $A = function(array) {
    return new jQuery.Utility.Array(array);
  };

(function($){
  $.fn.classNames = function() {
    return this.attr('class').split(/\s/);
  };

  $.fn.disable = function() {
    return this.attr('disabled', true);
  };

  $.fn.enable = function(){
    return this.attr('disabled', false);
  };

  $.fn.getDimensions = function() {
    var height = this.getHeight();
    var width = this.getWidth();
    return {'height': height, 'width': width};
  };

  $.fn.identify = function() {
    if(typeof $.idCounter == 'undefined')
      $.idCounter = 0;

    var el = this.first();
    var id = el.attr('id');
    if (id) return id;
    do { $.idCounter++; id = 'anonymous_element_' + $.idCounter; } while ($('#' + id).length > 0);
    el.attr('id', id);
    return id;
  };

  //$(this.element).insert({top: selected});
  $.fn.insert = function(elementOrHash) {
    if((elementOrHash instanceof jQuery) || ($.type(elementOrHash) == 'string')){
      var element = elementOrHash;
      this.append(element);
    }

    else {
      var insertionHash = elementOrHash;
      for(var position in insertionHash){
        switch(position) {
        case 'top':
          this.prepend(insertionHash[position]);
          break;
        case 'bottom':
          this.append(insertionHash[position]);
          break;
        case 'before':
          this.before(insertionHash[position]);
          break;
        case 'after':
          this.after(insertionHash[position]);
          break;
        default:
          this.append(insertionHash[position]);
          break;
        }
      }
    }
    return this;
  };

  $.fn.invoke = function() {
    var args = Array.prototype.slice.call(arguments);
    var method = args.shift();
    this[method].apply(this, args);
    return this;
  };

	$.fn.positionedOffset = function() {
		var _offset = this.position();
		return [_offset.left, _offset.top];
	};

  $.fn.request = function(opts) {
		var jqopts = {
			url: this.attr('action'),
			type: this.attr('method') || 'GET',
			data: this.serialize()
		};
		new Ajax.Request(jqopts.url, $.extend(opts, jqopts));
		return this;
	};

  $.fn.serialize = (function(oldSerialize) {
    return function(toHash) {
      var params = {};
      if(toHash)
        return $(this).serializeArray().inject(function(acc, obj) {
          acc[obj.name] = obj.value;
          return acc;
        }, {});
      else
        return oldSerialize.apply(this, arguments);
    };
  })($.fn.serialize);

  $.fn.setCaretPosition = function(position) {
    return this.each(function(i, el) {
      if (position == 'end')
        position = $(el).val().length;

      if (el.createTextRange) {
        var range = element.createTextRange();
        range.move('character', position);
        range.select();
      } else {
        el.focus();
        if (el.setSelectionRange)
          el.setSelectionRange(position, position);
      }
    });
  };

  $.toJSON = function(o) {
    if (typeof(JSON) == 'object' && JSON.stringify)
      return JSON.stringify(o);

    var type = typeof(o);

    if (o === null)
        return "null";

    if (type == "undefined")
        return undefined;

    if (type == "number" || type == "boolean")
        return o + "";

    if (type == "string")
        return o;

    if (type == 'object') {
      if (typeof o.toJSON == "function")
        return $.toJSON( o.toJSON() );

      if (o.constructor === Date) {
        var month = o.getUTCMonth() + 1;
        if (month < 10) month = '0' + month;

        var day = o.getUTCDate();
        if (day < 10) day = '0' + day;

        var year = o.getUTCFullYear();

        var hours = o.getUTCHours();
        if (hours < 10) hours = '0' + hours;

        var minutes = o.getUTCMinutes();
        if (minutes < 10) minutes = '0' + minutes;

        var seconds = o.getUTCSeconds();
        if (seconds < 10) seconds = '0' + seconds;

        var milli = o.getUTCMilliseconds();
        if (milli < 100) milli = '0' + milli;
        if (milli < 10) milli = '0' + milli;

        return '"' + year + '-' + month + '-' + day + 'T' +
                     hours + ':' + minutes + ':' + seconds +
                     '.' + milli + 'Z"';
      }

      if (o.constructor === Array) {
        var ret = [];
        for (var i = 0; i < o.length; i++)
            ret.push( $.toJSON(o[i]) || "null" );

        return "[" + ret.join(",") + "]";
      }

      var pairs = [];
      for (var k in o) {
        var name;
        type = typeof k;

        if (type == "number")
          name = '"' + k + '"';
        else if (type == "string")
          name = k;
        else
          continue;  //skip non-string or number keys

        if (typeof o[k] == "function")
          continue;  //skip pairs where the value is a function.

        var val = $.toJSON(o[k]);

        pairs.push(name + ":" + val);
      }

      return "{" + pairs.join(", ") + "}";
    }
  };

  $.fn.viewportOffset =  function() {
    var element = this;
    return [element.cumulativeOffset().left - $(window).scrollLeft(), element.cumulativeOffset().top - $(window).scrollTop()];
  };

  $.fn.visible = function() {
    return this.is(':visible');
  };

  $.event.special.mouseleaveintent = {
    setup: function(data, namespaces) {
      var elem = this, $elem = $(elem), timer;
      var options = {
        timeout: 500
      };

      $elem.bind('mouseleave.intent', function(e) {
        var event = e;
        timer = setTimeout(function() {
          $.event.special.mouseleaveintent._handler.call(this, event);
        }.bind(this), options.timeout);
      });

      $elem.bind('mouseenter', function() {
        clearTimeout(timer);
      });
    },

    teardown: function(namespaces) {
      var elem = this, $elem = $(elem);
      $elem.unbind('mouseleave.intent');
    },

    _handler: function(event) {
      var elem = this, $elem = $(elem);
      event.type = 'mouseleaveintent';
      $.event.handle.apply(this, arguments);
    }
  };


  (function($,doc,outside){
    $.map(
      'click dblclick mousemove mousedown mouseup mouseover mouseout change select submit keydown keypress keyup'.split(' '),
      function( event_name ) { jq_addOutsideEvent( event_name ); }
    );

    jq_addOutsideEvent( 'focusin',  'focus' + outside );
    jq_addOutsideEvent( 'focusout', 'blur' + outside );
    $.addOutsideEvent = jq_addOutsideEvent;
    function jq_addOutsideEvent( event_name, outside_event_name ) {

      outside_event_name = outside_event_name || event_name + outside;
      var elems = $(),
      event_namespaced = event_name + '.' + outside_event_name + '-special-event';
      $.event.special[ outside_event_name ] = {

        setup: function(){
         elems = elems.add( this );
          if ( elems.length === 1 ) {
            $(doc).bind( event_namespaced, handle_event );
          }
        },
        teardown: function(){
          elems = elems.not( this );
          if ( elems.length === 0 ) {
            $(doc).unbind( event_namespaced );
          }
        },

        add: function( handleObj ) {
          var old_handler = handleObj.handler;
          handleObj.handler = function( event, elem ) {
            event.target = elem;
            old_handler.apply( this, arguments );
          };
        }
      };

      function handle_event( event ) {
        $(elems).each(function(){
          var elem = $(this);

          if ( this !== event.target && !elem.has(event.target).length ) {
            elem.triggerHandler( outside_event_name, [ event.target ] );
          }
        });
      }
    }

  })($, document, 'outside');


  $.extend($.Event.prototype, {
    stop: $.Event.prototype.preventDefault,
    element: function(){
      return $(this.target);
    }
  });

})(jQuery);

var Ajax = {
  Request: function(url, options) {
    var optionMap = {
      'asynchronous': 'async',
      'method'      : 'type',
      'onComplete'  : 'success',
      'onFailure'   : 'error',
      'onSuccess'   : 'success',
      'postBody'    : 'data',
      'parameters'  : 'data'
    };

    for(var prop in options) {
      if(optionMap[prop]) {
        options[optionMap[prop]] = options[prop];
        delete options[prop];
      }
    }

    options = $.extend(options, {url: url});
    if(options.requestHeaders)
      options.beforeSend = function (request){
        for(var header in options.requestHeaders){
          request.setRequestHeader(header, options.requestHeaders[header]);
        }
      };

    $.ajax(options);
  }
};

if($.widget) {
  $.widget('ui.modal', $.ui.dialog, {
    _init: function(options) {
      var self = this;
      this.options.modal = (this.element.data('overlay') !== undefined) ? this.element.data('overlay') : true;
      $('.ui-widget-overlay').die('click.ui-modal');
      $('.ui-widget-overlay').live('click.ui-modal', function() {
        self.close();
      });
      this.uiDialog = this.element;
      this.container = this.element.parent();
      $.ui.modal.instances.push(this);
      if ( this.options.autoOpen ) {
        this.open();
      }
    },

    destroy: function() {
      if (this.overlay) {
        this.overlay.destroy();
      }
      this.element.unbind('.dialog').
        removeData('dialog').
        removeClass('ui-dialog-content ui-widget-content').
        hide().appendTo('body');

      return this;
    },

    open: function() {
      if (this._isOpen) { return this; }
      var data = this.element.data();
      this.uiDialog = this.element = this.element.detach().data(data).appendTo('body');
      return $.ui.dialog.prototype.open.apply(this);
    },

    close: function() {
      var data = this.element.data();
      this.uiDialog = this.element = this.element.detach().data(data).appendTo(this.container);
      return $.ui.dialog.prototype.close.apply(this);
    },

    _size: function() {},

    _create: function() {
      if(this.options.draggable)
        this._makeDraggable();
    },

    _makeDraggable: function() {
      this.element.draggable({
        cancel: '.modal_window .close',
        handle: '.modal_header',
        containment: 'document'
      });

    }
  });

  $.extend($.ui.modal, {
    instances: []
  });
}



(function($) {

  $.extend($.Widget.prototype, {
    _bindings: function() {
      var bindings = [], prop;
      for(prop in this) {
        if(prop.match($.Widget.revent) && $.type(this[prop]) == 'function'){
          var target = RegExp.$1, event = RegExp.$2;
          bindings.push({
            method: this[prop],
            target: target,
            event: event
          });
        }
      }
      return bindings;
    }
  });

  $.Widget.revent = /([\w\s\.#\[\]="']+)?(?:\s|^)(change|click|contextmenu|dblclick|keydown|keyup|keypress|mousedown|mousemove|mouseout|mouseover|mouseup|reset|windowresize|resize|windowscroll|scroll|select|submit|dblclick|focusin|focusout|load|unload|ready|hashchange|mouseenter|mouseleave)/; 

  $.behavior = function(name, base, prototype) {
    $.widget(name, base, prototype);

    var namespace = name.split( "." )[ 0 ];
    name = name.split( "." )[ 1 ];
    
    var object = $[ namespace ][ name ];
    var behavior = $[ namespace ][ name ] = function(options, element) {
      if ( arguments.length ) {
        object.call(this, options, element);
        $.each(this._bindings(), $.proxy(function(i, binding){
          var handler = $.proxy(binding.method, this);
          if(binding.target)
            this.element.delegate(binding.target, binding.event, handler);
          else
            this.element.bind(binding.event, handler);
        }, this));
      }
    };

    var proto = $[ namespace ][ name ].prototype = object.prototype;

    // add _super method if we're overriding any methods on base class
    if(base.prototype){
      for(var method in proto){
        proto[method] = ($.isFunction(proto[method]) && $.isFunction(base.prototype[method])) ? (function(name, fn){
          return function() {
            var tmp = this._super;
            
            // Add a new ._super() method that is the same method
            // but on the super-class
            this._super = base.prototype[name];
            
            // The method only need to be bound temporarily, so we
            // remove it when we're done executing
            var ret = fn.apply(this, arguments);        
            this._super = tmp;
            
            return ret;
          };
        })(method, proto[ method ]) : proto[ method ];
      }
    }

    $.widget.bridge(name, $[ namespace ][ name ]);

    var plugin = $.fn[ name ];

    //extend plugin method with .live delegation
    $.fn[ name ] = function( options ) {
      $.each(this.selector.split(/,\s?/), function(i, selector) {
        var behavior = $[ namespace ][ name ].prototype,
            parts = selector.split(' '),
            context = parts.length > 1 ? parts[0] : document;
        selector = (parts.length > 1 ? parts.slice(1) : parts).join(' ');
        $.each(behavior._bindings(), $.proxy(function(i, binding) {
          var handler = function(event) {
            if(!$(this).data(name)) {
              var instance = $(this)[name].call($(this)).data(name);
              instance[event.type].call(instance, event, this);
            }
          };
          if(!binding.target){
            $(context).delegate(selector, binding.event, handler);
          }
        }, this));
      });
      var returnVal = plugin.call(this, options);
      return returnVal;
    };
    
    return behavior;
  };

})(jQuery);


var Class = {
  create: function() {
    var parent = null, properties = $.makeArray(arguments);
    if ($.isFunction(properties[0])) parent = properties.shift();

    var klass = function() {
      this.initialize.apply(this, arguments);
    };

    klass.superclass = parent;
    klass.subclasses = [];
    klass.addMethods = Class.addMethods;

    if (parent) {
      var subclass = function() { };
      subclass.prototype = parent.prototype;
      klass.prototype = new subclass;
      parent.subclasses.push(klass);
    }

    for (var i = 0; i < properties.length; i++)
      klass.addMethods(properties[i]);

    if (!klass.prototype.initialize){
      klass.prototype.initialize = function() {};
    }

    klass.prototype.constructor = klass;

    return klass;
  },

  addMethods: function(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = [];
    for(var prop in source){
      properties.push(prop);
    }

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && $.isFunction(value) && $.argumentNames(value)[0] == "$super") {

        var method = value, value = $.extend($.wrap((function(m) {
          return function() { return ancestor[m].apply(this, arguments); };
        })(property), method), {
          valueOf:  function() { return method; },
          toString: function() { return method.toString(); }
        });
      }
      this.prototype[property] = value;
    }

    return this;
  }
};

$.counter = 0;

var Behavior = {

  create: function() {
    var params = $.makeArray(arguments),
        name = $.type(params[0]) == 'string' ? params.shift() : 'anonymous-behavior' + $.counter++,
        className = 'Groupon.' + name,
        parent = $.isFunction(params[0]) ? params.shift() : null,
        methods = params[0];
        args = [className, methods];

    this.name = name;

    if(parent)
      args.splice(1, 0, parent);

    for(var method in methods) {
      if(method.match(/^on(.+)/)){
        var event = RegExp.$1;
        methods[event] = methods[method];
      }
    }
    methods._init = methods.initialize;
    var behavior = $.behavior.apply(window, args);
    window[name] = behavior;
    $.extend(behavior, Behavior.ClassMethods);
    return behavior;
  },

  ClassMethods : {
    attach : function(element) {
      var name = this.prototype.widgetName;
      return $(element)[name].call($(element)).data(name);
    }
  }

};

(function($) {

  $.extend({
    keys: function(obj) {
      var keys = [];
      for (var key in obj) keys.push(key);
      return keys;
    },

    argumentNames: function(func) {
      var names = func.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(/, ?/);
      return names.length == 1 && !names[0] ? [] : names;
    },

    bind: function(func, scope) {
      return function() {
        return func.apply(scope, $.makeArray(arguments));
      };
    },

    wrap: function(func, wrapper) {
      var __method = func;
      return function() {
        return wrapper.apply(this, [$.bind(__method, this)].concat($.makeArray(arguments)));
      };
    },

    delegate: function(rules) {
      return function(e) {
        var target = $(e.target), parent = null;
        for (var selector in rules) {
          if (target.is(selector) || ((parent = target.parents(selector)) && parent.length > 0)) {
            return rules[selector].apply(this, $.makeArray(arguments));
          }
          parent = null;
        }
        return this;
      };
    }
  });

  $.fn.extend({
    attach: function() {
      var that = this;
      var args = $.makeArray(arguments), behavior = args.shift();
      if(behavior.attach){
        return $(this)[behavior.prototype.widgetName].call($(this));
      }
      return this.each(function() {
        behavior.call(this);
      });
    },

    attachAndReturn: function() {
      var that = this;
      var args = $.makeArray(arguments), behavior = args[0], name = behavior.prototype.widgetName;
      return $.map(this[name].apply(this, arguments), function(el) {
        return $(el).data(name);
      });
    }

  });

  Remote = Behavior.create({
    initialize: function(options) {
      if (this.element.attr('nodeName') == 'FORM') this.element.attach(Remote.Form, options);
      else{
        this.element.attach(Remote.Link, options);
      }
    }
  });

  Remote.Base = Behavior.create({
    initialize : function(options) {
      var self = this;
      this.options = $.extend({
        beforeSend: function(xhr) {
          if(self.element.trigger('beforeSend', [xhr]) === false) {
            return false;
          }
        },
        success: function(e, response, status) {
          self.element.trigger('success', [e, response, status]);
        },
        complete: function(xhr) {
          self.element.trigger('complete', xhr);
        },
        error: function(xhr, status, error) {
          self.element.trigger('error', [xhr, status, error]);
        }
      }, options);
    },

    _makeRequest : function(options) {
      this.element.trigger('beforeAjax', [options]);
      $.ajax(options);
      return false;
    }
  });

  Remote.Link = Behavior.create(Remote.Base, {
    onclick: function(e) {
      e.preventDefault();
      var options = $.extend({ 
        url: this.element.attr('href'), 
        type: 'GET'
      }, this.options);
      return this._makeRequest(options);
    }
  });

  Remote.Form = Behavior.create(Remote.Base, {
    onclick: function(e) {
      var target = e.target;

      if ($.inArray(target.nodeName.toLowerCase(), ['input', 'button']) >= 0 && target.type.match(/submit|image/))
        this._submitButton = target;
    },
    onsubmit: function() {
      var data = this.element.find('input:not(".prompting"), select, textarea').serializeArray();

      if (this._submitButton) data.push({ name: this._submitButton.name, value: this._submitButton.value });

      var options = $.extend({
        url : this.element.attr('action'),
        type : this.element.attr('method') || 'GET',
        data : data
      }, this.options);

      this._makeRequest(options);

      return false;
    }
  });

  $.ajaxSetup({
    beforeSend: function(xhr) {
      if (!this.dataType)
        xhr.setRequestHeader("Accept", "text/javascript, text/html, application/xml, text/xml, */*");
    }
  });
  

})(jQuery);


var Event = {
  KEY_BACKSPACE: 8,
  KEY_TAB:       9,
  KEY_RETURN:   13,
  KEY_ESC:      27,
  KEY_LEFT:     37,
  KEY_UP:       38,
  KEY_RIGHT:    39,
  KEY_DOWN:     40,
  KEY_DELETE:   46,
  KEY_HOME:     36,
  KEY_END:      35,
  KEY_PAGEUP:   33,
  KEY_PAGEDOWN: 34,
  KEY_INSERT:   45
};

Event.addBehavior = function(rules) {
  $(function(){
    for(var rule in rules){
      var selectors = rule;
      var behavior = rules[rule];
      $.each(selectors.split(/,\s+/), function(index, selector){
        var parts = selector.split(/:(?=[a-z]+$)/i), css = parts[0], event = parts[1];
        if(event){
          $(css).live(event, behavior);
        }
        else {
          $(css).attach(behavior);
        }
      });
    }
  });
};

Event.onReady = function(callback) {
  $(callback);
};

Event.delegate = $.delegate;


$.behavior('ujs.remote', {
  _init: function() {
    if(this.element.attr('nodeName').toLowerCase() === 'form'){
      this.element.remoteForm(this.options);
    }
    else
      this.element.remoteLink(this.options);
  }
});

$.behavior('ujs.remoteBase', {
  _init: function() {
    var self = this;
    this.ajaxOptions = {
      beforeSend: function(xhr) {
        if (!this.dataType)
          xhr.setRequestHeader("Accept", "text/javascript, text/html, application/xml, text/xml, */*");

        $(self.element).find("#spinner").show();
        if(self._trigger('beforeSend', null, xhr) === false) {
          return false;
        }
      },
      success: function(response) {
        self._trigger('success', null, response);
      },
      complete: function(xhr) {
        self._trigger('complete', null, xhr);
      },
      error: function(xhr, status, error) {
        self._trigger('error', null, [xhr, status, error]);
      }
    };
  },

  _makeRequest : function(options) {
    this._trigger('beforeAjax', null, options);
    $.ajax(options);
    return false;
  }
});

$.behavior('ujs.remoteLink', $.ujs.remoteBase, {
  click: function() {
    var ajaxOptions = $.extend({
      url: this.element.attr('href'),
      type: 'GET'
    }, this.ajaxOptions);
    return this._makeRequest(ajaxOptions);
  }
});

$.behavior('ujs.remoteForm', $.ujs.remoteBase, {
  click: function(e) {
    this._submitButton = e.target;
  },

  submit: function(e) {
    var data = this.element.serializeArray();
    if(this._submitButton && $(this._submitButton).attr('value'))
      data.push({
        name: this._submitButton.name,
        value: this._submitButton.value
      });

    var ajaxOptions = $.extend({
      url: this.element.attr('action'),
      type: this.element.attr('method') || 'GET',
      data: data
    }, this.ajaxOptions);

    this._makeRequest(ajaxOptions);
    return false;
  }

});

$.behavior('ujs.railsBase', {
  options: {
    csrfToken: $('meta[name=csrf-token]').attr('content'),
    csrfParam: $('meta[name=csrf-param]').attr('content')
  }
});

$.behavior('ujs.formLink', $.ujs.railsBase, {
  _init: function() {
    this.options= $.extend(this.options, {
      form: {
        method: 'POST',
        action: this.element.attr('href')
      },
      meta: {
        name: '_method',
        value: this.element.data('method'),
        type: 'hidden'
      }
    });
  },

  click: function(e) {
    e.preventDefault();
    var form = $('<form/>').attr(this.options.form).append($('<input/>').attr(this.options.meta));

    if (this.options.csrfParam && this.options.csrfToken) {
      form.append($('<input/>').attr({
        name: 'csrf_param',
        value: this.options.csrfToken,
        type: 'hidden'
      }));
    }
    form.hide().appendTo('body').submit();
  }
});

$.behavior('ujs.confirm', {
  click: function(e) {
    if(!confirm(this.element.data('confirm')))
      return false;
  }
});

$(function() {
  $('[data-confirm]').confirm();
  $('[data-remote]').remote();
  $('a[data-method]:not([data-remote])').formLink();
});



Groupon.Attributes = {};

var AttributeReader = function(element) {
	this.initialize(element);
}

AttributeReader.prototype = {
	BOOLEAN: /^false|true$/,
  DATE:    /^\d{1,2}\/\d\d?\/\d{4}$/,
  JSON:    /^\{.*\}$/,

	initialize: function(element) {
		this.element = element;
		Groupon.Attributes[ $(this.element).readAttribute("id") ] = this.typeCoerce( $(this.element).readAttribute("data-value") );
	},
	
	typeCoerce: function(val) {
    if ( this.BOOLEAN.test(val) ) return val == "false" ? false : true;
    if ( this.JSON.test(val) )    return (typeof Prototype != 'undefined') ? val.evalJSON() : jQuery.parseJSON( val );
    if ( this.DATE.test(val) )    return Date.parse(val);
    if ( !isNaN(val) )            return parseFloat(val);
    
    return val;
  }
}

Event.addBehavior({
  '.js_attribute': function() {
		new AttributeReader(this);
	}
});

$.behavior('Groupon.PromptingField', {
  _init: function() {
    this.changed = false;
    this.focusout();
    this.element.up('form').observe('click', this.handleFormSubmission.bind(this));
  },

  setPrompt: function() {
    this.element.setValue('');
    this.element.addClassName('prompting');
    this.element.setValue(this.element.readAttribute('title'));
    this.changed = false;
  },

  clearPrompt: function() {
    this.element.setValue('');
    this.element.removeClassName('prompting');
  },

  defaultPromptEnabled: function() {
    var whitespace_regex = /(#[^;]*;|\s)/,
        value = this.element.getValue() || '',
        title = this.element.readAttribute('title') || '';
    return value.replace(whitespace_regex, '') == title.replace(whitespace_regex, '');
  },

  focusout: function(event) {
    if (this.element.getValue().blank()) {
      this.setPrompt();
    }
  },

  focusin: function(event) {
    if(this.defaultPromptEnabled()) {
        this.clearPrompt();
    }
  },

  handleFormSubmission: function(event) {
    var element = event.element();
    if (element.match('input[type=submit]') || element.match('button[type=submit]') || element.match('input[type=image]') || element.match('button[type=submit] span') ) {
      if(this.defaultPromptEnabled()) {
        this.clearPrompt();
      }
    }
  },

  keyup: function(event) { this.changed = true; },
  change: function(event) { this.element.removeClassName('prompting'); this.changed = true; }

});


$(function() {
  $('input.prompting_field, textarea.prompting_field').PromptingField();
});

/*
  mustache.js — Logic-less templates in JavaScript

  See http://mustache.github.com/ for more info.
*/

var Mustache = function() {
  var Renderer = function() {};

  Renderer.prototype = {
    otag: "{{",
    ctag: "}}",
    pragmas: {},
    buffer: [],
    pragmas_implemented: {
      "IMPLICIT-ITERATOR": true
    },
    context: {},

    render: function(template, context, partials, in_recursion) {
      // reset buffer & set context
      if(!in_recursion) {
        this.context = context;
        this.buffer = []; // TODO: make this non-lazy
      }

      // fail fast
      if(!this.includes("", template)) {
        if(in_recursion) {
          return template;
        } else {
          this.send(template);
          return;
        }
      }

      template = this.render_pragmas(template);
      var html = this.render_section(template, context, partials);
      if(in_recursion) {
        return this.render_tags(html, context, partials, in_recursion);
      }

      this.render_tags(html, context, partials, in_recursion);
    },

    /*
      Sends parsed lines
    */
    send: function(line) {
      if(line != "") {
        this.buffer.push(line);
      }
    },

    /*
      Looks for %PRAGMAS
    */
    render_pragmas: function(template) {
      // no pragmas
      if(!this.includes("%", template)) {
        return template;
      }

      var that = this;
      var regex = new RegExp(this.otag + "%([\\w_-]+) ?([\\w]+=[\\w]+)?"
        + this.ctag);
      return template.replace(regex, function(match, pragma, options) {
        if(!that.pragmas_implemented[pragma]) {
          throw({message: "This implementation of mustache doesn't understand the '"
            + pragma + "' pragma"});
        }
        that.pragmas[pragma] = {};
        if(options) {
          var opts = options.split("=");
          that.pragmas[pragma][opts[0]] = opts[1];
        }
        return "";
        // ignore unknown pragmas silently
      });
    },

    /*
      Tries to find a partial in the cuurent scope and render it
    */
    render_partial: function(name, context, partials) {
      name = this.trim(name);
      if(!partials || !partials[name]) {
        throw({message: "unknown_partial '" + name + "'"});
      }
      if(typeof(context[name]) != "object") {
        return this.render(partials[name], context, partials, true);
      }
      return this.render(partials[name], context[name], partials, true);
    },

    /*
      Renders inverted (^) and normal (#) sections
    */
    render_section: function(template, context, partials) {
      if(!this.includes("#", template) && !this.includes("^", template)) {
        return template;
      }

      var that = this;
      // CSW - Added "+?" so it finds the tighest bound, not the widest
      var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
              "\\s*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
              "\\s*", "mg");

      // for each {{#foo}}{{/foo}} section do...
      return template.replace(regex, function(match, type, name, content) {
        var value = that.find(name, context);
        if(type == "^") { // inverted section
          if(!value || that.is_array(value) && value.length == 0) {
            // false or empty list, render it
            return that.render(content, context, partials, true);
          } else {
            return "";
          }
        } else if(type == "#") { // normal section
          if(that.is_array(value)) { // Enumerable, Let's loop!
            return that.map(value, function(row) {
              return that.render(content, that.create_context(row),
                partials, true);
            }).join("");
          } else if(that.is_object(value)) { // Object, Use it as subcontext!
            return that.render(content, that.create_context(value),
              partials, true);
          } else if(typeof value === "function") {
            // higher order section
            return value.call(context, content, function(text) {
              return that.render(text, context, partials, true);
            });
          } else if(value) { // boolean section
            return that.render(content, context, partials, true);
          } else {
            return "";
          }
        }
      });
    },

    /*
      Replace {{foo}} and friends with values from our view
    */
    render_tags: function(template, context, partials, in_recursion) {
      // tit for tat
      var that = this;

      var new_regex = function() {
        return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\/#\^]+?)\\1?" +
          that.ctag + "+", "g");
      };

      var regex = new_regex();
      var lines = template.split("\n");
       for (var i=0; i < lines.length; i++) {
         lines[i] = lines[i].replace(regex, function(match, operator, name) {
           switch(operator) {
             case "!": // ignore comments
               return "";
             case "=": // set new delimiters, rebuild the replace regexp
               that.set_delimiters(name);
               regex = new_regex();
               return "";
             case ">": // render partial
               return that.render_partial(name, context, partials);
             case "{": // the triple mustache is unescaped
               return that.find(name, context);
             default: // escape the value
               return that.escape(that.find(name, context));
           }
         }, this);
         if(!in_recursion) {
           this.send(lines[i]);
         }
       }

       if(in_recursion) {
         return lines.join("\n");
       }
    },

    set_delimiters: function(delimiters) {
      var dels = delimiters.split(" ");
      this.otag = this.escape_regex(dels[0]);
      this.ctag = this.escape_regex(dels[1]);
    },

    escape_regex: function(text) {
      // thank you Simon Willison
      if(!arguments.callee.sRE) {
        var specials = [
          '/', '.', '*', '+', '?', '|',
          '(', ')', '[', ']', '{', '}', '\\'
        ];
        arguments.callee.sRE = new RegExp(
          '(\\' + specials.join('|\\') + ')', 'g'
        );
      }
    return text.replace(arguments.callee.sRE, '\\$1');
    },

    /*
      find `name` in current `context`. That is find me a value
      from the view object
    */
    find: function(name, context) {
      name = this.trim(name);

      // Checks whether a value is thruthy or false or 0
      function is_kinda_truthy(bool) {
        return bool === false || bool === 0 || bool;
      }

      if(is_kinda_truthy(context[name])) {
        var value = context[name];
      } else if(is_kinda_truthy(this.context[name])) {
        var value = this.context[name];
      }

      if(typeof value === "function") {
        return value.apply(context);
      }
      if(value !== undefined) {
        return value;
      }
      // silently ignore unkown variables
      return "";
    },

    // Utility methods

    /* includes tag */
    includes: function(needle, haystack) {
      return haystack.indexOf(this.otag + needle) != -1;
    },

    /*
      Does away with nasty characters
    */
    escape: function(s) {
      return ((s == null) ? "" : s).toString().replace(/&(?!\w+;)|["<>\\]/g, function(s) {
        switch(s) {
          case "&": return "&amp;";
          case "\\": return "\\\\";;
          case '"': return '\"';;
          case "<": return "&lt;";
          case ">": return "&gt;";
          default: return s;
        }
      });
    },

    // by @langalex, support for arrays of strings
    create_context: function(_context) {
      if(this.is_object(_context)) {
        return _context;
      } else if(this.pragmas["IMPLICIT-ITERATOR"]) {
        var iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator || ".";
        var ctx = {};
        ctx[iterator] = _context;
        return ctx;
      }
    },

    is_object: function(a) {
      return a && typeof a == "object";
    },

    is_array: function(a) {
      return Object.prototype.toString.call(a) === '[object Array]';
    },

    /*
      Gets rid of leading and trailing whitespace
    */
    trim: function(s) {
      return s.replace(/^\s*|\s*$/g, "");
    },

    /*
      Why, why, why? Because IE. Cry, cry cry.
    */
    map: function(array, fn) {
      if (typeof array.map == "function") {
        return array.map(fn);
      } else {
        var r = [];
        var l = array.length;
        for(var i=0;i<l;i++) {
          r.push(fn(array[i]));
        }
        return r;
      }
    }
  };

  return({
    name: "mustache.js",
    version: "0.3.0-dev",

    /*
      Turns a template and view into HTML
    */
    to_html: function(template, view, partials, send_fun) {
      var renderer = new Renderer();
      if(send_fun) {
        renderer.send = send_fun;
      }
      renderer.render(template, view, partials);
      if(!send_fun) {
        return renderer.buffer.join("\n");
      }
    }
  });
}();

var StickyElement = Class.create({
  
  initialize: function(element, options) {
    this.element = $(element);
    if(this.element.get)
      this.element = this.element.get(0);
    if (!this.element)
      throw('ArgumentError: you must initialize StickyElement with an element');

    this.options = Object.extend({
      offset: 0,
      containment: false,
      position: this.isIE() ? 'absolute' : 'fixed'
    }, options || {});
    
    this.isStuck  = false;
    this.isFrozen = false;
    this.top = $(this.element).viewportOffset()[1];
    
    this.setupElement();
    this.setupContainer();
    if(this.options.containment)
      this.setupContainment();
    this.setupShim();
    this.setupEvents();
  },
  
  freeze: function() {
    if (!this.isStuck) return;
    
    this.preFreezeElementStyles = {
      'left': this.element.style.left,
      'top':  this.element.style.top
    };
    if(this.options.position == 'fixed') {
      if (this.preFreezeElementStyles.left === '') this.preFreezeElementStyles.left = null;
      if (this.preFreezeElementStyles.top  === '') this.preFreezeElementStyles.top  = null;
      this.container.addClassName('frozen');//.setStyle({width: this.element.getWidth() + 'px'});
      var topValue = document.viewport.getScrollOffsets()[1] + this.options.offset - $(this.container).cumulativeOffset().top;

      if(topValue < 0){
        topValue = 0;
      }
      $(this.element).setStyle({
        'top':  topValue + 'px'
      });

      if(this.containment && $(this.element).cumulativeOffset().top + $(this.element).getHeight() > this.containment[3])
        $(this.element).setStyle({
          top: parseInt($(this.element).getStyle('top'), 10) - (($(this.element).cumulativeOffset().top + $(this.element).getHeight()) - this.containment[3]) + 'px'
        });
    }
    
    this.isFrozen = true;
    this.isStuck = false;
  },
  
  getPosition: function() {
    var scroll = document.viewport.getScrollOffsets()[1];
    var top = 0;
    if(this.containment || (this.group && this.group.containment)) {
      var containment = this.containment || this.group.containment;
      if(scroll < containment[1]){
        top = 0;
      }
      
      else if(scroll > containment[3])
        top = 1000;
      else 
        top = scroll + this.options.offset - $(this.container).cumulativeOffset().top;
    }
    
    else {
      top = scroll + this.options.offset - $(this.container).cumulativeOffset().top;
    }
    return {
      top: top,
      left: 0
    };
  },

  isIE: function() {
    try{
      return $('html').hasClass('ie6');
    }
    catch(error){
      return false;
    }
    return false;
  },
  
  setupContainer: function() {
    if(typeof Prototype != 'undefined')
      this.container = new Element('div', { 'class': 'sticky_element_container sticky_element_container_' + $(this.element).readAttribute('id') });
    else
      this.container = jQuery('<div class="sticky_element_container sticky_element_container_' + $(this.element).readAttribute('id') + '"></div>');
    $(this.element).wrap(this.container);

    if($(this.element).parent)
      this.container = $(this.element).parent('.sticky_element_container');
  },
  
  setupContainment: function() {
    var o = this.options;
    if(o.containment) {
      if(o.containment == 'parent') o.containment = this.element.parentNode;
  		if(o.containment == 'document' || o.containment == 'window') this.containment = [
  			0 - this.element.offset.relative.left - this.element.offset.parent.left,
  			0 - this.element.offset.relative.top - this.element.offset.parent.top,
  			$(o.containment == 'document' ? document : window).getDimensions().width,
  			($(o.containment == 'document' ? document : window).getDimensions().height || document.body.parentNode.scrollHeight)
  		];

  		if(!(/^(document|window|parent)$/).test(o.containment) && o.containment.constructor != Array) {
  			var ce = $$(o.containment)[0]; if(!ce) return;
  			var co = $(ce).cumulativeOffset();
  			var over = ($(ce).getStyle('overflow') != 'hidden');
  			this.containment = [
  				co.left + (parseInt($(ce).getStyle("borderLeftWidth"),10) || 0) + (parseInt($(ce).getStyle("paddingLeft"),10) || 0),
  				co.top + (parseInt($(ce).getStyle("borderTopWidth"),10) || 0) + (parseInt($(ce).getStyle("paddingTop"),10) || 0),
  				co.left + (over ? Math.max(ce.scrollWidth,ce.offsetWidth) : ce.offsetWidth) - (parseInt($(ce).getStyle("borderLeftWidth"),10) || 0) - (parseInt($(ce).getStyle("paddingRight"),10) || 0),
  				co.top + ce.offsetHeight
  			];
  		} else if(o.containment.constructor == Array) {
  			this.containment = o.containment;
  		}
    }
  },
  
  setupElement: function() {
    $(this.element).addClassName('sticky_element');
    $(this.element).setStyle({ 'width': $(this.element).getStyle('width') });
  },
  
  setupEvents: function() {
    var self = this;
    if(typeof Prototype != 'undefined')
      Event.observe(window, 'scroll', function(event) {
        this._handleScroll();
      }.bind(this));
    else {
      $(window).bind('scroll', function(event) {
        this._handleScroll();
      }.bind(this));
      $(window).bind('load', function() {
        self.setupContainment();
      });
    }
  },

  _handleScroll: function() {
    var yOffset = this.container.viewportOffset()[1] + 50;
      var withinYBounds = true;
      if(this.containment){
        withinYBounds = (document.viewport.getScrollOffsets()[1] + this.element.offsetHeight) < this.containment[3];
      }

      if(!this.isFrozen && !withinYBounds)
        this.freeze();

      if(this.options.position == 'fixed') {
        if ( withinYBounds && !this.isStuck && (yOffset - this.options.offset <= 0) ){
          if(this.containment && this.isFrozen){
            this.unfreeze();
          }

          else if(!this.containment && !this.isFrozen){
            this.stick();
          }
          else if(this.containment){
            this.stick();
          }
        }
        else if ( this.containment && ($(this.element).offset().top <= (this.containment[1] + 50)) ) {
          this.unstick();
        }

        if(!this.isFrozen && !withinYBounds){
          this.freeze();
        }
      }
      
      if(this.options.position == 'absolute') {
        if ( withinYBounds ){
          if(this.containment && this.isFrozen){
            this.unfreeze();
          }

          else if(!this.containment && !this.isFrozen){
            //this.stick();
            this.isStuck = true;
            $(this.element).setStyle({top: this.getPosition().top + 'px'});
          }
          else if(this.containment){
            //this.stick();
            $(this.element).setStyle({top: this.getPosition().top + 'px'});
          }
        }

      else if(!this.isFrozen && !withinYBounds)
        this.freeze();
      }

  },
  
  setupShim: function() {
    if(typeof Prototype != 'undefined')
      this.shim = new Element('div', { 'class': 'sticky_element_shim' });
    else
      this.shim = jQuery('<div class="sticky_element_shim"></div>');
    this.shim.setStyle({
      'height': this._sumElementStyles('marginTop',  'borderTopWidth',  'paddingTop',  'height', 'paddingBottom', 'borderBottomWidth', 'marginBottom'),
      'width':  this._sumElementStyles('marginLeft', 'borderLeftWidth', 'paddingLeft', 'width',  'paddingRight',  'borderRightWidth',  'marginRight' )
    });
    $(this.container).insert(this.shim);
  },
  
  stick: function() {
    this.previousElementStyles = {
      'left': this.element.style.left,
      'top':  this.element.style.top
    };
    if (this.previousElementStyles.left === '') this.previousElementStyles.left = null;
    if (this.previousElementStyles.top  === '') this.previousElementStyles.top  = null;
    
    this.container.addClassName('stuck');
    var left;
    try{
      left = $(this.container).viewportOffset()[0];
    }
    catch(err) {
      left = 0;
    }
    $(this.element).setStyle({ 
      'top':  this.options.offset + 'px' 
    });
    this.isStuck = true;
  },
  
  _sumElementStyles: function() {
    var sum = 0;
    $A(arguments).each( function(style) {
      sum += (parseInt($(this.element).getStyle(style), 10) || 0);
    }.bind(this));
    return sum + 'px';
  },
  
  unfreeze: function() {
    if (!this.isFrozen) return; 
    
    this.isFrozen = false;
    this.container.removeClassName('frozen');
    if(this.preFreezeElementStyles)
      $(this.element).setStyle(this.preFreezeElementStyles);
  },
  
  unstick: function() {
    this.unfreeze();
    this.isStuck = false;
    this.container.removeClassName('stuck');
    if(this.previousElementStyles && this.previousElementStyles.top && this.previousElementStyles.left)
      $(this.element).setStyle(this.previousElementStyles);
  }
  
});

var StickyElementGroup = Class.create({
  
  initialize: function(options) {
    this.options = Object.extend({
      containment: null
    }, options || {});
    
    this.isFrozen = false;
    this.stickyElements = [];
    this.setupContainment();
    this.setupEvents();
  },
  
  freeze: function() {
    this.isFrozen = true;
    this.stickyElements.each(function(sticky){
      sticky.freeze();
    });
  },
  
  getBottom: function() {
    var bottom = 0;
    this.stickyElements.each( function(stickyElement) {
      var newBottom = stickyElement.element.positionedOffset()[1] + stickyElement.element.getHeight();
      if (newBottom > bottom)
        bottom = newBottom;
    });
    return document.viewport.getScrollOffsets()[1] + bottom;
  },
  
  groupHeight: function() {
    var height = 0;
    this.stickyElements.each( function(stickyElement) {
      var newHeight = stickyElement.element.getHeight() + stickyElement.options.offset + 20;
      if (newHeight > height)
        height = newHeight;
    });
    return height;
  },
  
  getBottomOfFreezeElement: function() {
    var el = $(this.options.freezeAtBottomOf);
    if (!el) return null;
    return el.cumulativeOffset()[1] + el.getHeight();
  },
  
  setupContainment: function() {
    var o = this.options;
    if(o.containment) {
      if(o.containment == 'parent') o.containment = this.element.parentNode;
  		if(o.containment == 'document' || o.containment == 'window') this.containment = [
  			0 - this.element.offset.relative.left - this.element.offset.parent.left,
  			0 - this.element.offset.relative.top - this.element.offset.parent.top,
  			$(o.containment == 'document' ? document : window).getDimensions().width,
  			($(o.containment == 'document' ? document : window).getDimensions().height || document.body.parentNode.scrollHeight)
  		];

  		if(!(/^(document|window|parent)$/).test(o.containment) && o.containment.constructor != Array) {
  			var ce = $$(o.containment)[0]; if(!ce) return;
  			var co = ce.cumulativeOffset();
  			var over = (ce.getStyle('overflow') != 'hidden');
  			this.containment = [
  				co.left + (parseInt(ce.getStyle("borderLeftWidth"),10) || 0) + (parseInt(ce.getStyle("paddingLeft"),10) || 0),
  				co.top + (parseInt(ce.getStyle("borderTopWidth"),10) || 0) + (parseInt(ce.getStyle("paddingTop"),10) || 0),
  				co.left + ce.offsetWidth - (parseInt(ce.getStyle("borderLeftWidth"),10) || 0) - (parseInt(ce.getStyle("paddingRight"),10) || 0),
  				co.top + ce.offsetHeight - (parseInt(ce.getStyle("borderTopWidth"),10) || 0) - (parseInt(ce.getStyle("paddingBottom"),10) || 0)
  			];
  		} else if(o.containment.constructor == Array) {
  			this.containment = o.containment;
  		}
    }
  },
  
  getTop: function() {
    var top = 100000;
    this.stickyElements.each( function(stickyElement) {
      var newTop = stickyElement.element.cumulativeOffset()[1];
      if (newTop < top)
        top = newTop;
    });
    return top;
  },
  
  push: function(stickyElement) {
    this.stickyElements.push(stickyElement);
    stickyElement.group = this;
    stickyElement.offset = this.calculateCumulativeOffsets();
  },
  
  calculateCumulativeOffsets: function() {
    var offset = 0;
    this.stickyElements.each(function(sticky, i){
      offset += $(sticky.element).height() + sticky.options.offset;
    });
    return offset;
  },
  
  setupEvents: function() {
    
    $(window).bind('scroll', function(event) {
      var withinYBounds = true;
      if(this.containment)
        withinYBounds = ( document.viewport.getScrollOffsets()[1] > this.containment[1] ) && 
                        ( (document.viewport.getScrollOffsets()[1] + this.groupHeight()) < this.containment[3] );
      
      if ( !withinYBounds && !this.isFrozen )
        this.freeze();
      else if ( this.isFrozen && withinYBounds){
        this.unfreeze();
      }
    }.bind(this));
  },
  
  unfreeze: function() {
    this.isFrozen = false;
    this.stickyElements.each(function(sticky) {
      sticky.unfreeze();
    });
  }
  
});

var Countdown = Class.create({
  initialize: function(opts) {
    var defaults = {
      periods : ["days", "hours", "minutes", "seconds"],
      lengths : ["86400","3600","60","1"],
      domContainer : '#countdown_container',
      beforeWidget: "",
      afterWidget: "",
      pad: false,
      numOfIntervalsToShow: 3,
      numOfIntervalsToPad: 2,
      expiringSoonThreshold: null,
      showIfZero: {
        days: false,
        hours: false,
        minutes: false,
        seconds: false
      },
      outputFormat: {
        days: "{days} day(s) ",
        hours: "{hours} hour(s) ",
        minutes: "{minutes} minute(s) ",
        seconds: "{seconds} second(s)"
      }
    };
    this.opts = Object.extend(defaults, opts);
    this.targetTime = Date.parse(this.opts.targetTime);
    this.timer = {};
    this._buildInitialDom();

    return this;
  },

  tick: function(){
    var remainingTimeForInterval,
        now = new Date().getTime(),
        difference = (this.targetTime - now)/1000,
        timeIntervalsArray = [],
        storeIfZero = false,
        lengths = this.opts.lengths,
        periods = this.opts.periods,
        periodLength = periods.length,
        pad = this.opts.pad,
        numOfIntervalsToPad = this.opts.numOfIntervalsToPad,
        expiringSoonThreshold = this.opts.expiringSoonThreshold,
        showIfZero = this.opts.showIfZero,
        timeHash = {};

    if (expiringSoonThreshold && (difference < expiringSoonThreshold)) {
      $$(this.opts.domContainer).first().addClassName("expiring_soon");
    }

    for (var j = 0; j < periodLength; j++) {
      timeHash = {};

      if (Math.floor(difference) >= lengths[j]){
        storeIfZero = true;
        remainingTimeForInterval = Math.floor(difference / lengths[j]).toString();
        if (j > (3 - numOfIntervalsToPad) && pad && remainingTimeForInterval < 10) {
          remainingTimeForInterval = "0" + remainingTimeForInterval;
        }
        timeHash[periods[j]] =  remainingTimeForInterval;
        timeIntervalsArray.push(timeHash);  
        difference = (difference % lengths[j]);
      } else if (storeIfZero || showIfZero[periods[j]]) {
        timeHash[periods[j]] =  ((j > (3- numOfIntervalsToPad)) && pad) ? '00' : '0';
        timeIntervalsArray.push(timeHash);
      }
    }
    if (timeIntervalsArray.length > this.opts.numOfIntervalsToShow) {
      timeIntervalsArray = timeIntervalsArray.splice(0, this.opts.numOfIntervalsToShow);
    }
    var timeHash = timeIntervalsArray.inject(function(acc, obj) { return Object.extend(acc, obj); }, {});
    this.update(timeHash);
    return timeHash;
  },

  start: function() {
    var self = this;
    this.tick();
    this.timer = setInterval(function(){ self.tick(); }, 1000);
    return self;
  },

  stop: function() {
    clearInterval(this.timer);
    return self;
  },

  update: function(timeHash) {
    var self = this;
    // if inserting into dom by dom parts
    if(typeof this.opts.domContainer == "object") {
      this.opts.periods.each(function(interval){
        if (timeHash[interval] !== undefined){
          var tmpHash = {};
          tmpHash[interval] = timeHash[interval];
          $$(self.opts.domContainer.container +  " " + self.opts.domContainer[interval]).first().innerHTML = self.template(tmpHash);
        }
      });
    } else {
      var container = $$(this.opts.domContainer);
      //prototype compatibility
      if (container.first) {
        container = container.first();
      }
      container.update(this.opts.beforeWidget + this.template(timeHash) + this.opts.afterWidget);
    }
    return this;
  },
  
  template: function(obj) {
    var output = "";
    for (var interval in obj) {
      if (obj.hasOwnProperty(interval)) {
        var intervalTemplate = this.opts.outputFormat[interval].replace(/\((\w+)\)/g, function(a, b){
          return obj[interval] > 1 || obj[interval] === 0 ? b : "";
        });
        if (this.opts.outputFormat[interval]) {
          output += this.interpolate(intervalTemplate, obj);
        }
      }
    }
    return output;
  },

  // Simple templating. This also exists as a prototype method we add to String, but putting
  // this here keeps Countdown without external dependencies.
  interpolate: function(string, o) {
    return string.replace( /\{([^\{\}]*)\}/g,
      function (a, b) {
        var r = o[b];
        return typeof r === 'string' || typeof r === 'number' ? r : a;
      }
    );
  },

  destroy: function() {
    clearInterval(this.timer);
    this.targetTime = null;
  },
  
  _buildInitialDom: function() {
    if(typeof this.opts.domContainer == "object") {
      $$(this.opts.domContainer.container).first().insert({top: this.opts.beforeWidget});
      $$(this.opts.domContainer.container).first().insert({bottom: this.opts.afterWidget});
    }
  }
});

if (typeof Groupon.ui === 'undefined') Groupon.ui = {};

Groupon.ui.onModalClose = function(func) {
  if(Control.Modal.current){
    $(document).observe("modal:closed", func);
  }
  else{
    func.call(this);
  }
};

Groupon.ui.SelectBoxBehavior = Behavior.create({
  initialize: function() {
    var self = this;
    this.open = false;
    if(!this.element.readAttribute('id'))
      this.element.writeAttribute('id', new Date().getTime());
    this.element.select("li:first-child").invoke("observe", "click", self.doToggle.bind(self));
    this.element.select("li:not(li:first-child) a").invoke("observe", "click", self.onSelect.bind(self));
    this.element.select('li:not(li:first-child)').invoke('hide');
    $$("body").first().observe("click", self.onWindowClick.bind(self));
  },
  
  doToggle: function(e) {
    e.stop();
    this.open ? this.doClose(e) : this.doOpen(e);
  },
  
  onSelect: function(e) {
    var clickedOn = $(e.target).up("li");
    var firstLi = $(this.element).select("li:first-child");

    if(clickedOn instanceof jQuery) {
      clickedOn = clickedOn.get(0);
      firstLi = firstLi.get(0);
    }

    if(clickedOn == firstLi) {
      e.stop();
      this.doToggle(e);
    }
    else {
      e.stop();
      var self = this;
      var selected = $(e.element()).up("li").remove().addClassName("with_bg selected").observe("click", self.doToggle.bind(self));
      $(this.element).select("li:first-child").invoke("removeClassName", "with_bg selected");
      $(this.element).insert({top: selected});
      this.doClose(e);
      window.location = e.target.href;
    }
  },
  
  onWindowClick: function(e) {
    if(($(e.target).up('#' + $(this.element).readAttribute('id'))).length === 0){
      this.doClose();
    }
  },
  
  doOpen: function(e) {
    $('#header').setStyle({ 'zIndex': 3 });
    $(this.element).select("li:not(li:first-child)").invoke("show");
    this.open = true;
  },
  
  doClose: function(e) {
    this.element.select("li:not(li:first-child)").invoke("hide");
    $('header').setStyle({ 'zIndex': null });
    this.open = false;
  }
});

var numericalTextFieldBehavior = Behavior.create({
  //add class of .numeric to any text field and this will restrict input to only numbers
  initialize: function(decimal) {
    this.decimal = decimal || ".";
  },
  onkeypress: function(e) {
    var key = e.charCode ? e.charCode : e.keyCode ? e.keyCode : 0;
    // allow enter/return key (only when in an input box)
    if(key == 13 && this.element.nodeName.toLowerCase() == "input")
    {
      return true;
    }
    else if(key == 13)
    {
      return false;
    }
    var allow = false;
    // allow Ctrl+A
    if((e.ctrlKey && key == 97 /* firefox */) || (e.ctrlKey && key == 65) /* opera */) return true;
    // allow Ctrl+X (cut)
    if((e.ctrlKey && key == 120 /* firefox */) || (e.ctrlKey && key == 88) /* opera */) return true;
    // allow Ctrl+C (copy)
    if((e.ctrlKey && key == 99 /* firefox */) || (e.ctrlKey && key == 67) /* opera */) return true;
    // allow Ctrl+Z (undo)
    if((e.ctrlKey && key == 122 /* firefox */) || (e.ctrlKey && key == 90) /* opera */) return true;
    // allow or deny Ctrl+V (paste), Shift+Ins
    if((e.ctrlKey && key == 118 /* firefox */) || (e.ctrlKey && key == 86) || /* opera */
    (e.shiftKey && key == 45)) return true;
    // if a number was not pressed
    if(key < 48 || key > 57)
    {
      /* '-' only allowed at start */
      if(key == 45 && this.element.value.length === 0) return true;
      /* only one decimal separator allowed */
      if(key == this.decimal.charCodeAt(0) && this.element.value.indexOf(this.decimal) != -1)
      {
        allow = false;
      }
      // check for other keys that have special purposes
      if(key != 8 /* backspace */ &&
        key != 9 /* tab */ &&
        key != 13 /* enter */ &&
        key != 35 /* end */ &&
        key != 36 /* home */ &&
        key != 37 /* left */ &&
        key != 39 /* right */ &&
        key != 46 /* del */)
      {
        allow = false;
      }
      else
      {
        // for detecting special keys (listed above)
        // IE does not support 'charCode' and ignores them in keypress anyway
        if(typeof e.charCode != "undefined")
        {
          // special keys have 'keyCode' and 'which' the same (e.g. backspace)
          if(e.keyCode == e.which && e.which !== 0)
          {
            allow = true;
          }
          // or keyCode != 0 and 'charCode'/'which' = 0
          else if(e.keyCode !== 0 && e.charCode === 0 && e.which === 0)
          {
            allow = true;
          }
        }
      }
      // if key pressed is the this.decimal and it is not already in the field
      if(key == this.decimal.charCodeAt(0) && this.element.value.indexOf(this.decimal) == -1)
      {
        allow = true;
      }
    }
    else
    {
      allow = true;
    }
    return allow;
  },
  onkeydown: function(e) {
    if (e.keyCode == Event.KEY_UP || e.keyCode == Event.KEY_DOWN) {
      e.stop();
      var newValue = this.element.getValue();
      if (newValue === '') newValue = 0;
      newValue = parseInt(newValue, 10);
      (e.keyCode === Event.KEY_UP) ? newValue++ : newValue--;
      if (newValue < 0) newValue = 0;
      this.element.setValue(newValue);
    }
  }
});

Groupon.TwitterWidget = function(opts){
  if (Groupon.Attributes.env == 'production')
    LazyLoader.load('http://widgets.twimg.com/j/2/widget.js', function(){
      return new TWTR.Widget({
        version: 2,
    		id: 'twitter-list-widget',
        type: 'list',
        rpp: 5,
        interval: 6000,
        title: 'Groupon Presents',
        subject: opts.title,
        width: 216,
        height: 300,
        theme: {
          shell: {
            background: '#fff',
            color: '#000'
          },
          tweets: {
            background: '#ffffff',
            color: '#444444',
            links: '#0980ca'
          }
        },
        features: {
          scrollbar: true,
          loop: false,
          live: true,
          hashtags: true,
          timestamp: true,
          avatars: true,
          behavior: 'all'
        }
      }).render().setList(opts.account, 'tweets-around-town').start();
    });
};

Groupon.ui.ClassEvents = {
  fire: function(key) {
      if (!this._events || !this._events[key]) return false;
      this._events[key].each( function(func) { func.apply(); });
      return true;
    },

  observe: function(key, func) {
    this._events = this._events || {};
    this._events[key] = this._events[key] || [];
    this._events[key].push( func.bind(this) );
    return this;
  }
};

Groupon.Analytics = {
  debug: false,

  experimentLayerSlotMap: {
    'deal_page_030311'      : 2,
    'subscribe_030311'      : 4,
    'post_subscribe_030311' : 5,
    'all_deals_032511'      : 1
  },

  trackLoad: function(category, action, label) {
    this.trackEvent('trackLoad', [category, action, label]);
  },

  trackClick: function(category, action, label) {
    this.trackEvent('trackClick', [category, action, label]);
  },

  trackEvent: function(eventType, params, optional) {
    // track new_membership_foo_bar as category: new_membership, action: foo_bar
    if (typeof(params) == 'string'){
      if (params.search(/new_membership/) > -1) {
        params = ['new_membership', params.replace('new_membership',''), 'undefined'];
      } else {
        params = [params, 'undefined', 'undefined'];
      }
    }

    optional = Object.extend({ 'click': params[2], 'alt': params[1] }, optional);

    this.log("trackEvent", eventType, params, optional);

    this.Backend.Google.trackEvent(params[0], params[1], params[2]);
    _groanalytics.push(['trackEvent', params[0], params[1], params[2]]);
  },

  setCustomVar: function(label, value, slot, scope) {
    pageTracker._setCustomVar(slot, label, value, scope);
  },

  setDivisionCustomVar: function(newDivision) {
    this.setCustomVar("Division", newDivision, 3, this.Backend.Google.sessionScope);
  },

  setExperimentCustomVar: function(layerId, experimentId, variantId) {
    slotId       = this.experimentLayerSlotMap[layerId] || 5;

    layerId      = layerId.replace(/[\s]/g, '').replace(/[^\w]/g, '-');
    experimentId = experimentId.replace(/[\s]/g, '').replace(/[^\w]/g, '-');
    variantId    = variantId.replace(/[\s]/g, '').replace(/[^\w]/g, '-');

    name = "Exp-" + layerId;
    safeExperimentId = experimentId + "/" + variantId;
    this.setCustomVar(name, safeExperimentId, slotId, this.Backend.Google.visitorScope);
  },

  enablePDFTracking: function() {
    $$('a[href$=pdf]').each(function(a) {
      $(a).observe('click', function(event) {
        Groupon.Analytics.trackEvent('trackClick', ['DownloadPrepFlyer', a.pathname, location.pathname]);
      });
    });
  },

  log: function() {
    if (typeof console != 'undefined' && this.debug) {
      console['log'].apply(console, arguments);
    }
  }
};

Groupon.Analytics.Backend = {};

Groupon.Analytics.Backend.Local = {
  initialize: function() {
    if (typeof _groanalytics != 'undefined') {
      for (var i = 0; i < _groanalytics.length; i++) {
        this.push(_groanalytics[i]);
      }
    }

    window._groanalytics = Groupon.Analytics.Backend.Local;
  },

  push: function(methodAndArgs) {
    var method = methodAndArgs[0];
    this[method].apply(Groupon.Analytics.Backend.Local, methodAndArgs.slice(1));
  },

  trackEvent: function(category, action, label) {
    var paramString = "cat=" + category + "&act=" + action + "&lab=" + label;
    this.fireTracking("events", paramString);
  },

  fireTracking: function(type, paramString) {
    //this.sendAsyncRequest("/analytic/" + type + "?" + paramString);
    return true;
  }
};

Groupon.Analytics.Backend.Google = {
  visitorScope: 1,
  sessionScope: 2,
  pageScope: 3,
  maxCustomVarLength: 64,

  trackEvent: function(category, action, label) {
    (function triggerPageTracker() {
      if (typeof pageTracker == 'undefined') {
        setTimeout(triggerPageTracker, 1500);
      } else {
        pageTracker._trackEvent(category, action, label);
      }
    }());
  },

  trimStringLength: function(string, maxLength) {
    if (string.length > maxLength) {
      string = string.slice(string.length - maxLength);
    }
    return string;
  },

  getVariantId: function() {
    if(typeof utmx != 'undefined'){
      var variantId = utmx('combination');
      if (typeof variantId == 'undefined') {
        return '';
      } else {
        return variantId;
      }
    }
  }
};

Groupon.Analytics.EventTracking = Behavior.create({
  initialize: function() {
    this.trackOnClick = !$(this.element).hasClassName('trackNow');

    if (!this.trackOnClick) {
      this.track('trackLoad');
    }
  },

  onclick: function() {
    if (!this.trackOnClick) { return; }
    this.track('trackClick');
  },

  track: function(eventType) {
    if (this.element.readAttribute("data-google-analytics-tags") != undefined) {
      Groupon.Analytics.trackEvent(eventType, this.constructParams(this.element.readAttribute("data-google-analytics-tags").split(" ")));
    }

    var thisBehavior = this;
    $(this.element).classNames().each(function(classname) {
      if (classname.indexOf("E-") != -1) {
        Groupon.Analytics.trackEvent(eventType, thisBehavior.constructParams(classname.replace("E-","").split("_")));
      }
    });
  },
  
  constructParams: function(params) {
    for (i = 0; i < 3; i++) {
      params[i] = params[i] || " ";
    }
    return params;
  }
});

Event.addBehavior({
  '.G_event': Groupon.Analytics.EventTracking
});

Groupon.Analytics.Backend.Local.initialize();

/**
 * @author Ryan Johnson <http://syntacticx.com/>
 * @copyright 2008 PersonalGrid Corporation <http://personalgrid.com/>
 * @package LivePipe UI
 * @license MIT
 * @url http://livepipe.net/controls/hotkey/
 * @attribution http://www.quirksmode.org/js/cookies.html
 */

/*global document, Prototype, $A */


var Cookie = {
  build: function() {
    return $A(arguments).compact().join("; ");
  },
  secondsFromNow: function(seconds) {
    var d = new Date();
    d.setTime(d.getTime() + (seconds * 1000));
    return d.toGMTString();
  },
  set: function(name,value,seconds, domain){
    var expiry = seconds ? 'expires=' + Cookie.secondsFromNow(seconds) : null;
    var cookieDomain = domain ? 'domain=' + domain : null;
    document.cookie = Cookie.build(name + "=" + value, expiry, "path=/", cookieDomain);
  },
  get: function(name){
    var valueMatch = new RegExp(name + "=([^;]+)").exec(document.cookie);
    return valueMatch ? valueMatch[1] : null;
  },
  unset: function(name, domain){
    Cookie.set(name,'',-1, domain);
  }
};


/* Establish top-level behavior namespace */
if (typeof Groupon.ui == 'undefined') {
  Groupon.ui = {};
}

if (typeof Groupon.ui.Modals == 'undefined') {
  Groupon.ui.Modals = {};
}

Groupon.Application = function () {
  var division = '';
  var loggedIn = false;
  var connectedWithFacebook = false;
  var facebookTemplateBundleId = null;
  var _needsAttributesOnFacebookDisconnect = false;

  return {
    // State Helpers
    setDivision: function(div) {
      division = div;
    },
    getDivision: function() {
      return division;
    },
    setLoggedIn: function() {
      loggedIn = true;
    },
    isLoggedIn: function() {
      return loggedIn;
    },
    setConnectedWithFacebook: function() {
      connectedWithFacebook = true;
    },
    isConnectedWithFacebook: function() {
      return connectedWithFacebook;
    },
    setNeedsAttributesOnFacebookDisconnect: function() {
      _needsAttributesOnFacebookDisconnect = true;
    },
    needsAttributesOnFacebookDisconnect: function() {
      return _needsAttributesOnFacebookDisconnect;
    },
    logOut: function() {
      window.location.href = "/logout";
    },
    reloadPage: function() {
      window.location.reload(true);
    },

    // Ajax helpers
    get: function(url, parameters) {
      parameters.method = 'get';
      return new Ajax.Request(url, parameters);
    },
    post: function(url, parameters) {
      parameters.method = 'post';
      return new Ajax.Request(url, parameters);
    },
    put: function(url, parameters) {
      parameters.method = 'put';
      return new Ajax.Request(url, parameters);
    },
    del: function(url, parameters) {
      parameters.method = 'delete';
      return new Ajax.Request(url, parameters);
    },
    // General helpers
    popup: function(name, url) {
      return window.open(url, name);
    }
  };
}();

// behavior class applied to anything intended to close a modal window
var modalCloseBehavior = Behavior.create({
  initialize: function() {},

  onclick: function(e) {
    e.stop();
    Control.Modal.close();
  }
});

//fire global close event whenever modal is closed
if(typeof(Control) != 'undefined')
  Object.extend(Control.Modal.Observers,{
      afterClose: function(){
        Control.Overlay.hide(this.options.fade ? this.options.fadeDuration : false);
        Control.Modal.current = false;
        this.overlayFinishedOpening = false;
        $(document).fire("modal:closed");
      }
  });

//completely disable modals for mobile devices
Event.onReady(function(){
  if(Groupon.Attributes.mobile_device)
    Object.extend(Control.Modal.Observers,{
      afterOpen: function(){
        if(!$(this.container).hasClassName('multi_option_modal')){
          this.close();
          return false;
        }
      }
    });
});

Behavior.create('Linkify', {
  initialize: function() {
    this.genericURL = /(^|[\n ])([\w]+?:\/\/[\w]+[^ \\"\n\r\t<]*)/gim;
    this.startsWithWWW = /(^|[\n ])((www)\.[^ \\"\t\n\r<]*)/gim;
    this.email = /(^|[\n ])([a-z0-9&\-_\.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)/gim;
    this._linkify();
  },

  _linkify: function() {
    var html = this.element.html();
    html = html.replace(this.genericURL, '$1<a href="$2">$2</a>');
    html = html.replace(this.startsWithWWW, '$1<a href="http://$2">$2</a>');
    html = html.replace(this.email, '$1<a href="mailto:$2@$3">$2@$3</a>');
    this.element.html(html);
  }
});

Groupon.ui.showHideBehavior = Behavior.create({
  initialize: function(){
    if(this.element.next('.toggle_content'))
      this.element.next('.toggle_content').hide();
  },

  onclick: function(e){
    e.stop();
    var target = $(e.target).hasClassName('toggle_handle') ? $(e.target) : $(e.target).up('.toggle_handle');
    var contentToToggle = target.next('.toggle_content');
    contentToToggle.toggle(100);
    target.toggleClassName('open');
  }
});

Groupon.DoubleClickPreventer = Behavior.create({
  disable_form_on_submit: function(event) {
    if(this.element.disabled) { event.stop(); }
    this.element.disable();
    this.element.up('form').submit();
    return this;
  },

  disable_field_on_submit: function(event) {
    if(this.element.hasClass('prevent_double-clicking')) {return event.stop();}
    this.element.addClass('prevent_double-clicking');
    return this;
  },

  onclick: function(event) {
    if(this.element.tagName == 'FORM') {
      this.disable_form_on_submit(event);
    }
    else {
      this.disable_field_on_submit(event);
    }
  }
});

Groupon.ExternalLink = Behavior.create({
  initialize: function(force){
    this.force = force || false;
  },

  onclick: function(e) {
    var domain = new RegExp("^https?://" + document.domain);
    var redirect = new RegExp("redirect/");
    var el = e.target.nodeName != "A" ? $(e.target).up('a') : e.target;
    if((!domain.test(el.href) && $(el).readAttribute("rel") != "skip_external") || this.force || redirect.test(el.href)){
      e.stop();
      window.open($(el).readAttribute('href'));
    }
  }
});

Groupon.Subscription = {
  autoSubscribeEmail: false,

  setAutoSubscribe: function(emailAddress) {
    this.autoSubscribeEmail = emailAddress;
  },

  prepareForm: function() {
    var form = new Element('form', {method: 'POST', action: '/subscriptions'}).hide();
    form.insert(new Element('input', {type: 'text', name: 'subscription[division_id]', value: Groupon.Application.getDivision()}));
    form.insert(new Element('input', {type: 'text', name: 'subscription[email_address]', value: this.autoSubscribeEmail}));
    $(document.body).insert(form);
    return form;
  },

  doAutoSubscribe: function() {
    if (this.autoSubscribeEmail) {
      this.prepareForm().submit();
      return true;
    }
    return false;
  }
};

Event.addBehavior({
  'ul.alerts li.error': function() { new Effect.Highlight( this, { startcolor: '#ff6699' }); },
  'ul.alerts li.info': function() { new Effect.Highlight( this, { startcolor: '#ffcc66' }); },
  'ul.alerts li.success': function() { new Effect.Highlight( this, { startcolor: '#ccff66' }); },
  'ul.form_errors': function() { new Effect.Highlight( this, { startcolor: '#ff6699' }); },
  'a.disabled:click': function(e) {
    e.stop();
  }
});

/*
 * Progress Indicators for asynchronous activity
 */
ProgressIndicator = {};
ProgressIndicator.Spinner = Class.create({
  spinnerElementClass : 'submitRemoteSpinner',
  initialize : function(element, insertion, spinnerUrl) {
    this.sourceElement = element;
    this.insertion = insertion || 'after';
    this.spinnerUrl = spinnerUrl || '/images/icons/spinner.gif';
  },
  start : function() {
    var progress_markup = $span({ 'class' : this.spinnerElementClass }, $img({ src : this.spinnerUrl, border : 0}));
    var position = {};
    position[this.insertion] = progress_markup;
    this.sourceElement.insert(position);
    switch (this.insertion) {
      case 'after':
        this.spinnerElement = this.sourceElement.next();
        break;
      case 'before':
        this.spinnerElement = this.sourceElement.next();
        break;
      case 'top':
        this.spinnerElement = this.sourceElement.childElements().first();
        break;
      default:
        break;
    }
  },
  stop : function() {
    if (this.spinnerElement.up()) this.spinnerElement.remove();
  }
});
ProgressIndicator.Spinner.start = function(sourceElement, insertion) {
  var indicator = new this(sourceElement, insertion);
  indicator.start();
  return indicator;
};

var EventDelegationBehavior = Behavior.create({

  onclick: Event.delegate({

    '.bitlify.tw': function(e) {
      e.stop();
      Groupon.BitlifiedLink.twitter(e);
    },

    // Facebook links with class of 'bitlify' and 'fb'
    '.bitlify.fb.share_url_with_facebook': function(e) {
      e.stop();
      Groupon.BitlifiedLink.facebook(e);
    }

  })

});

if(typeof numericalTextFieldBehavior != 'undefined')
  Event.addBehavior({
    'input.numerical': numericalTextFieldBehavior
  });

Groupon.ui.Plaxo = {
  'openPlaxoWindow' : function(e) {
    if (e.target.className == "gmail") {
      window.open('https://www.plaxo.com/ab_chooser?t=import&cb=http://groupon.thepoint.local:3000/plaxo.html&noactivex=&actionType=&ab=gmail&signin.email=&signin.password=&x=53&y=23',"plaxo");
    } else {
      showPlaxoABChooser('user_referral_plaxo_recipient_list', '/plaxo.html');
    }
    //https://www.plaxo.com/ab_chooser?t=import&cb=http://groupon.thepoint.local:3000/plaxo.html&noactivex=&actionType=&ab=gmail&signin.email=&signin.password=&x=53&y=23
  },

  'openPlaxoModal' : function(selector) {
    Control.Modal.close();
    Control.Modal.open($(selector), {
      beforeOpen: function() {
        $('control_overlay').setStyle({
          width: '100%'
        });
      },
      fade: false,
      overlayOpacity: 0.3
    });
  },

  'onABCommComplete' : function(data) {
    // We just want the email
    var emailString = data.map(function(name_email_pair) {
      return name_email_pair[1];
    }).join(", ");

    $('#refer_modal #user_referral_plaxo_recipient_list, #plaxo_modal_window #user_referral_plaxo_recipient_list').each(function() {
      $(this).val('');
      $(this).removeClass('prompting');
      $(this).val(emailString);
    });

    $('.step1').hide();
    $('.step2').show();
    $('.manual').hide();
  },

  'fireGoalAndSubmit' : function(ev) {
    ev.stop();

    try {
      var gwoTracker=_gat._getTracker("UA-1250257-14");
      gwoTracker._setDomainName(".groupon.com");
      gwoTracker._trackPageview("/0453891241/goal");
      $(this).parents('form').submit();
    } catch (err) {
      // Swallow the error.
    }
  }
};

Event.addBehavior({
  'body': EventDelegationBehavior,

  '.disabled_on_submit': Groupon.DoubleClickPreventer,

  '.lightbox': function(){
    $(this).find('.modal_window, .plaxo_modal_window').eq(0).modal();
  },

  '.announcement_dismissal:click' : function(e) {
    var announcement_id = $(this).attr('id').match(/_(\d+)/)[1];
    $('#' + this.id).up("#announcement").toggle("fast");
    var announcementCookies = (Cookie.get("dismiss_announcements") || " ").split("&");
    Cookie.set('dismiss_announcements', e.target.href.match(/announcements=((?:\d,?)*)/)[1].split(/,/).concat(announcementCookies).join("&"), (365*24*60*60));
    e.stop();
  },

  '.social ul li a:click' : function(e) {
     Groupon.Analytics.trackClick('Follow', this.classNames()+"_follow", '');
  },

  '.link_out:click' : function(){
    $(this).classNames().each(function(classname){
      if(classname.indexOf("E-")>-1){
        var params = classname.replace("E-","").split("_");
        for(i=0;i<3;i++){
          params[i] = params[i] || " ";
        }
        Groupon.Analytics.trackClick(params[0],params[1],params[2]);
      }
    });
  },

  '.linkify': Linkify,

  'ul.mail_services li:click' : Groupon.ui.Plaxo.openPlaxoWindow,

  '#refer_modal a.import_contacts:click' : Groupon.ui.Plaxo.openPlaxoWindow,

  '.manual:click' : function(ev) {
    ev.stop();
    $('#user_referral_manual_recipient_list').toggle();
    ev.target.toggleClass('off');
  },

  '#new_drawer_anchor a:click' : function(e) {
    var el = e.element();
    if(el.hasClassName('animating')){return false;}

    el.addClassName('animating');

    Effect.toggle('division_new','slide',{duration: 0.25, queue: 'end'});

    if($('#division_new').is(':visible')){
      Groupon.Analytics.trackClick('CitySelector','close','cities');
    }
    else {
      Groupon.Analytics.trackClick('CitySelector','open','cities');
    }
    setTimeout(function(){el.removeClassName('animating');},300);
    e.stop();
    return this;
  },

  '#hide_division_new:click' : function(e){
    Effect.toggle('division_new','slide',{duration:0.25});
    Groupon.Analytics.trackClick('CitySelector','close','cities');
  },

  '#state_chooser li:click' : function(e){
    $$('#state_chooser li.selected').invoke('removeClassName', 'selected');
    var el = e.target;
    $(el).addClassName('selected');
    if($(el).readAttribute('id') == "state_Country" || $(el).readAttribute('id') == "state_Canada"){
      var theList = $(el).readAttribute('id').substring($(el).readAttribute('id').indexOf("_")+1).toLowerCase();
      $$('#division_list_new .current').invoke('removeClassName', 'current');
      $$('#division_list_new .' + theList + '').invoke('addClassName', 'current');

    } else {
      $$('#division_list_new .current').invoke('removeClassName', 'current');
      $$('.' + el.id).invoke('addClassName', 'current');
    }
  },

  '.article p a:click' : function(){
    Groupon.Analytics.trackClick('Editorial', 'click_link', this.innerHTML);
  },

  '#follow_drawer_anchor': function() {
    $.ajax({
      cache: false,
      url: "/subscription/user-subscribed",
      success: function(response, status, xhr){
        if(response === "true"){
          $("#follow_drawer_anchor").remove();
        }
      }
    });
  }, 

  '#follow_drawer_anchor a:click' : function(e) {
    if (Groupon.Subscription.doAutoSubscribe()) {
      e.stop();
      return;
    }

    var el = e.element();

    Effect.toggle('follow_drawer', 'slide', { duration: 0.25, queue: 'drawer' });

    if($('#follow_drawer').visible()){
      Groupon.Analytics.trackClick('Drawer', 'close', 'alerts');
    }else{
      Groupon.Analytics.trackClick('Drawer', 'open', 'alerts');
    }

    e.stop();
    setActiveDrawerAnchor('follow_drawer_anchor');
  },

  '.fb_button:click' : function(){
    Groupon.Analytics.trackClick('FBConnect', 'login_attempt', '');
  },
  '#hide_follow:click' : function(e) {
    Effect.toggle('follow_drawer', 'slide', { duration: 0.25 });
    setActiveDrawerAnchor();
  },
  '#hide_follow.division:click' : function(e) {
    var close_subscribe_drawer = Cookie.get("close_subscribe_drawer");
    if(close_subscribe_drawer === null){
      close_subscribe_drawer = Groupon.Application.getDivision();
    } else {
      close_subscribe_drawer += '%2C'+Groupon.Application.getDivision();
    }
    Cookie.set("close_subscribe_drawer", close_subscribe_drawer, Math.pow(10, 8));
  },
  '#hide_follow.channel:click' : function(e) {
    var close_subscribe_drawer = Cookie.get("close_channel_subscribe_drawer");
    if(close_subscribe_drawer === null){
      close_subscribe_drawer = Groupon.Attributes.channel_permalink;
    } else {
      close_subscribe_drawer += '%2C'+Groupon.Attributes.channel_permalink;
    }
    Cookie.set("close_channel_subscribe_drawer", close_subscribe_drawer, Math.pow(10, 8));
  },
  '#admin_link:click' : function(e) {
    Effect.toggle('admin_nav', 'slide', { duration: 0.25 });
  },
  'ul.alerts span#close:click' : function(e) {
    e.stop();
    $("#alerts").toggle("fast");
  },
	'#closeAjaxAlert:click' : function() {
    $("#alerts").toggle("fast");
		e.stop();
		removeAlert();
	},
  '.modal_close, #post_purchase #close:click' : modalCloseBehavior,
  '.collapsible_panel .toggle_handle': Groupon.ui.showHideBehavior,

  '.auto_select:click' : function(e){
    e.target.select();
  },

  '.choose_your_area': function() {
    $(this).modal({
      close: function() {
        var timesClosed = parseInt((Cookie.get("closed_area_modal") || 0), 10) + 1;
        Cookie.set("closed_area_modal", timesClosed, 10*365*24*60*60);
        if(timesClosed >= 2){
          Cookie.set("home_area", Groupon.Attributes.primary_area, 10*365*24*60*60);
        }
      }
    });
  },

  '.collapsible_faq .toggle_handle': Groupon.ui.showHideBehavior,

  'a.put:click, a[data-method="put"]:click': function(e) {
    e.stop();
    var form = new Element('form', { action: $(this).readAttribute('href'), method: "post" });
    form.insert( new Element('input', { type: 'hidden', name: '_method', value: 'put'}) );
    $(document.body).insert(form);
    form.submit();
  },

  '.zebra tbody > tr:not(.header_row):nth-child(even), .zebra > li:nth-child(even)' : function() {
    $(this).addClassName("odd");
  },

  'a.opens_modal:click': function() {
    var modal = new Control.Modal($(this).attr('href'), { overlayOpacity: 0.3, className: 'modal', fade: false });
    modal.open();
  },

  '#touch_footer a': function() {
    this.writeAttribute('rel', 'skip_external');
  },

  'a[href^="http"]': Groupon.ExternalLink,

  '.clickable:click': function(e) {
    var a = this.down('a');
    if (!a) return;

    e.stop();

    if (e.shiftKey || e.metaKey)
      window.open( a.getAttribute('href'), a.readAttribute('href').replace(/[^\w]+/g, '_') );
    else
      window.location = a.getAttribute('href');
  },

  '#invites_modal form.reward_invites': Remote,

  '.user_rewards_status .user_level.linked_level': function(e) {
    this.setAttribute('title', 'Learn more about ' + this.innerHTML);
  },

  '.user_rewards_status .user_level.linked_level:click': function(e) {
    Control.Modal.open('level_unlocked_modal', {
      afterOpen: function() {
        if ($('local_rewards_link'))
          $('local_rewards_link').remove();
      }
    });
  },

  '.jpeg-or-png-only:change' : function(e) {
    var elem = e.element();
    var file = $F(elem).split('.').last();
    if (/jpg|png|jpeg/i.test(file)) {
      return;
    }

    alert('You can only upload a JPEG or PNG file.');
    elem.value = '';
  },

  '#faqs': function() {
    var newFaqs = this.clone(true);

    newFaqs.select('li').each( function(li, i) {
      li.select(':not(h4)').invoke('remove');
      li.select('h4').each( function(h4) {
        var link = new Element('a', { 'href': '#faq_' + i });
        link.update(h4.innerHTML);
        h4.update(link);
      });
    });

    this.insert({ before: newFaqs });

    this.select('li').each( function(li, i) {
      li.writeAttribute('id', 'faq_' + i);
    });
  },

  '#touch_footer a:click': function() {
    Cookie.set('vmobile', 'true');
  },

  'form.unfollow_merchant:submit, form.follow_merchant:submit': function(e) {
    e.stop();
    var form = $(this);
    form.select("button").addClassName("updating_follow_state");
    form.select("button span").text("Updating...");
    form.request({
      onSuccess: function(response) {
        form.trigger('follow');
        form.replaceWith(response.template);
        $$(".follow_merchant_form").select("button").removeClassName("updating_follow_state");
      },
      requestHeaders: {
        'Accept': 'application/json'
      }
    });
  },

  'form.unfollow_merchant button, form.follow_merchant button': function() {
    var currentState,
        button = $(this),
        span = $(button).down('span'),
        currentText = span.innerHTML || span.html(),
        followText = $F('follow_button_text'),
        unfollowText = $F('unfollow_button_text'),
        unfollowHoverText = $F('unfollow_button_hover_text');

    $(button).observe('mouseover', function(e) {
      if (currentStateIs('unfollow')) {
        button.writeAttribute("value", unfollowHoverText);
        span.update(unfollowHoverText);
        button.addClassName("unfollow_hover");
      }
    });

    $(button).observe('mouseout', function(e) {
      if (currentStateIs('unfollow')) {
        button.writeAttribute("value", currentText);
        span.update(currentText);
        button.removeClassName("unfollow_hover");
      }
    });

    function currentStateIs (state) {
      return button.hasClassName(state);
    }
  },

  '#new_subscription': function() {
    if(typeof jQuery != 'undefined') {
      $(this).validate();
    } else if(typeof Validation != 'undefined') {
      new Validation(this, {
        onFormValidate: function(result, form) {
          // re-enable after DoubleClickPreventer kicks in
          $(form).down('input[type=submit]').removeClassName('disabled');
        }
      });
    }
  }
});

// pass in activeAnchorId as a string, or leave blank to clear active states
function setActiveDrawerAnchor(activeAnchorId) {
  if (typeof activeAnchorId == "undefined") activeAnchorId = '';
  anchors = ['follow_drawer_anchor'];
  anchors.each(function(e){
    if (e == activeAnchorId) {
      $(e).toggleClassName('active');
    } else {
      if($(e)){
      $(e).removeClassName('active');
    }
    }
  });
}

Event.onReady(function() {
  $$('a.bitlify.now').each(function(e) {
    BitlyClient.shorten(e.href, 'BitlyCB.updateLinkWithShortUrl');
  });

  $('#header_nav li, #header .links li').bind('mouseenter', function() {
    $(this).addClass('hover');
  });

  $('#header_nav li, #header .links li').bind('mouseleaveintent', function() {
    $(this).removeClass('hover');
  });
});

// Simplifies converting standard link to a shortened URL using http://bit.ly
// on an event so URLs are converted on demand vs. on every page load.
//
// Implemented under the assumption that the request would be made on (and
// passed in) a "click" event.  Other situations should be tested.
Groupon.BitlifiedLink = {
  originalUrl: null,
  pattern: null,

  // Bitlifies the standard-format Twitter message, which ends with a link
  // back to Groupon.
  twitter: function(e){
    pattern = new RegExp("http[s]?://(?:www\.)?twitter.com/.*?(http://.*$)");
    this.pattern = pattern;
    this.create(pattern, e);
  },

  mail: function(e){
    this.pattern = /body=(?:.+)(http.+)$/;
    this.create(this.pattern, e);
  },

  // Bitlifies the standard-format Facebook message, which ends with a link
  // back to Groupon. Pass in the event
  facebook: function(e){
    e.stop();
    doBitlify = function(){
      var longUrl = e.element().readAttribute('href');
      // If this is a Facebook share link, we want to scrub the url.
      var fb_share_base = "facebook.com/share.php?u=";
      var index = longUrl.indexOf(fb_share_base);
      if (index > 0) {
        longUrl = longUrl.slice(index + fb_share_base.length);
      }

      BitlyClient.shorten(longUrl, 'Groupon.FacebookConnect.shareBitlyCBURL');
    };
    FB.getLoginStatus(function(response) {
      if (response.session) {
        // user successfully logged in
        doBitlify();
      } else {
        FB.login(function(r){
          doBitlify();
        });
      }
    },true);

  },

  // Issues a #shorten request to Bitly with the section of the
  // request's URL which matches the supplied pattern and delegates the
  // response to BiltlyCB#popupWindowWithShortUrl.
  create: function(pattern, e){
    e.stop(); // stop the original click event so we don't go anywhere
    // Store the current URL so the callback can gain access to it
    // & replace the appropriate content with the new bit.ly URL.
    this.originalUrl = e.element().readAttribute('href');

    // Search for the supplied pattern in the request's URL. If a match
    // is found, send that URL to bit.ly to be shortened...and call
    // BiltlyCB.popupWindowWithShortUrl when the result is returned (the BitlyCB
    // namespace is required by the bit.ly JS library).
    var extracted = this.originalUrl.match(pattern);

    if(extracted && extracted[1]){
      Groupon.popup = window.open();
      BitlyClient.shorten(unescape(extracted[1]), 'BitlyCB.popupWindowWithShortUrl');
    }
  }
};



// Bitly requires its callbacks to be namespaced underBitlyCB,
// if not, they won't work.
var BitlyCB = {
  popupWindowWithShortUrl: function(data) {
    var first_result;
    var s = '';

    // The response is keyed by the URL, so we need to grab the first one
    // (it's possible to send up multiple URLs at once...but we don't in
    // this case).
    for( var r in data.results ) {
      first_result = data.results[r]; break;
    }

    // Replace the old long URL w/ the bit.ly URL
    var extracted = Groupon.BitlifiedLink.originalUrl.match(Groupon.BitlifiedLink.pattern);
    new_url = Groupon.BitlifiedLink.originalUrl.replace(extracted[1], first_result["shortUrl"]);

    // Open the new, nice URL in a new window ;-)
    Groupon.popup.location = new_url;
  },

  // Finds an <a> that matches the long url and updates its href and text
  updateLinkWithShortUrl: function(data) {
    for( var r in data.results ) {
      var shortUrl = data.results[r]["shortUrl"];
      if (shortUrl) {
        $$('a.bitlify.now[href=' + r + ']').each(function(link) {
          link.setAttribute('href', shortUrl);
          link.update(shortUrl);
        });
      }
    }
  }
};

/* Easy way to leverage our alert messaging via a JS call in RJS templates */
var addAlertViaRJS = function(className, message){
  if($$("#alerts").length === 0)
    $("#alerts_content").insert("<div id='alerts'></div>");
  var alertsList = document.createElement('ul');
  $(alertsList).addClassName('alerts');

  var alertItem = document.createElement("li");
  $(alertItem).addClassName(className);
  $(alertItem).update("<div>" + message + "</div>");

  $(alertsList).insert($(alertItem));
  $("#alerts").update(alertsList);
};

var addAlert = addAlertViaRJS;

var removeAlert = function(){
  $("#alerts").update('');
};

/**
  Trigger a modal if the document.location.hash corresponds with a modal on the page
*/
if(document.location.hash) {
  var elem = $(document.location.hash.replace('#', ''));
  if (elem && elem.hasClassName('modal_window')) {
    new Control.Modal(elem).open();
  }
}

Groupon.Now = Groupon.Now || {};

Groupon.Now.Analytics = {

  CATEGORY_NOW_SLIDER: "NowSlider",
  CATEGORY_NOW_SEARCH: "NowSearch",
  CATEGORY_NOW_CATEGORY: "NowCategory",

  LABEL_NEXT: "NowForward",
  LABEL_PREVIOUS: "NowBack",
  LABEL_CHANGE_SEARCH: "NowChangeSearch",
  LABEL_CHANGE_SEARCH_CANCEL: "NowChangeSearchCancel",
  LABEL_CHANGE_CATEGORY: "NowChangeCategory",
  
  ACTION_NOW_PREFIX: "Now",

  nextClickCount: 0,
  previousClickCount: 0,

  trackDealView: function(permalink) {
    this._insertTrackingPixel(permalink);
  },

  trackNextClick: function() {
    this._trackClick(this.CATEGORY_NOW_SLIDER, this.LABEL_NEXT, this.ACTION_NOW_PREFIX + ++this.nextClickCount);
  },

  trackPreviousClick: function() {
    this._trackClick(this.CATEGORY_NOW_SLIDER, this.LABEL_PREVIOUS, this.ACTION_NOW_PREFIX + ++this.previousClickCount);
  },

  trackChangeSearch: function() {
    this._trackClick(this.CATEGORY_NOW_SEARCH, this.LABEL_CHANGE_SEARCH);
  },

  trackChangeSearchCancel: function() {
    this._trackClick(this.CATEGORY_NOW_SEARCH, this.LABEL_CHANGE_SEARCH_CANCEL);
  },

  trackChangeCategory: function() {
    this._trackClick(this.CATEGORY_NOW_CATEGORY, this.LABEL_CHANGE_CATEGORY);
  },

  _trackClick: function(category, action, label) {
    Groupon.Analytics.trackClick(category || "", action || "", label || "");
  },

  _insertTrackingPixel: function(permalink) {
    $('body').append($("<img />", {
      'src': this._trackingPixelSrc(permalink),
      'class': 'tracking-pixel ' + permalink,
      'height': 1,
      'width': 1
    }));
  },

  _trackingPixelSrc: function(permalink) {
    var src = "/analytic/view.gif?";

    src += '&ref=' + encodeURIComponent(document.referrer);
    src += '&d=' + encodeURIComponent(permalink);

    return src;
  },

  _resetClickCounters: function() {
    this.nextClickCount = 0;
    this.previousClickCount = 0;
  }

};

