var jsTemplates = {
    globalnav : {
           Accordion: '<li id="#{id}"><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:onClick" class="accordion_hd clickable"><br /><ul id="#{pnavsetChild}" class="accordion_content" style="display:none;" attachPoint="containerNode"></ul></li>          ',
           ArtistryInActionSubNav: '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="containerNode" class="invisible">        <div class="psubnav_artistry_in_action_heading panelnav_content"><img alt="ARTISTS UP CLOSE" src="/images/pnav/headers/pnav_artistryinaction_videos.gif" /></div>        <div class="psubnav_artistry_in_action_content panelnav_content">            <p>Nos Maquilleurs professionnels vous invitent en backstage et vous livrent tous leurs secrets et leurs techniques pour un maquillage de pro !<br /> D&eacute;couvrez en vid&eacute;os leurs conseils d\'application et leurs astuces.</p>        </div>        <div attachPoint="featuredContainerNode">               </div>        <div class="psubnav_artistry_in_action_btn_container"><a href="#" id="psubnav_artistry_in_action_btnprevious"><img alt="PREVIOUS" src="/images/pnav/headers/pnav_artistryinaction_previous_off.gif" /></a></div>        <div attachPoint="previousContainerNode" class="hidden">               </div>    </div></div>',
           Panel: '<div class="panel" id="#{id}">    <div class="panelnav_container clearfix">        <div id="#{id}_close" attachEvent="click:_onClickClose" class="closelight">x</div>        <div attachPoint="containerNode"></div>    </div></div> ',
           PanelNav: '<li id="#{id}" class="globalnav_hd clickable" attachPoint="containerNode"><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick"></li>',
           PanelSubNav: '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="containerNode" class="invisible">           </div></div>',
           ProductSubNav : '<div id="#{id}" class="panelnav_subnav">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="containerNode" class="invisible">        <div id="#{id}_links" class="panelnav_detaillink_container" attachPoint="detailLinksContainerNode">            </div>        <div id="#{id}_cat" class="panelnav_detail_container" attachPoint="detailContainerNode">            </div>        <ul id="#{id}_catlist" class="panelnav_accordion_container hidden" attachPoint="accordionContainerNode">        </ul>    </div></div>',
           SearchSubNav : '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="resultsMessageNode" class="search_results_message"></div>    <div class="search_results_hd"><img id="#{id}_hd" src="/images/search/h_top_searches.gif" alt="" /></div>    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="contentResultsContainer" class="hidden search_content_results"><div attachPoint="contentResultsNode"></div>    </div>    <div attachPoint="resultsNode">    </div>    <!--<div id="search_result_pages"></div> note: was result_pgs--></div>',
           DiscontinuedSubNav : '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>    <div attachPoint="containerNode" class="invisible">        <img id="#{id}_hd" class="panelnav_disc_hd" src="/images/goodbyes/headers/h_discontinued_prods.gif" width="250" alt="Goodbyes" />        <p attachPoint="panelDescriptionNode" class="panelnav_disc_descr"></p>        <div attachPoint="featuredNode">        </div>    </div></div>',
           SectionDescSubNav : '<div id="#{id}" class="panelnav_subnav panelnav_detail_container">    <div attachPoint="progressNode" class="progress"><br></div>        <div attachPoint="contentNode" class="invisible">        <img attachPoint="hdNode" class="panelnav_section_hd" width="250" />        <p attachPoint="panelDescriptionNode" class="panelnav_section_descr"></p>        <div class="panelnav_detail_container" attachPoint="containerNode">            </div>        </div></div>',
           Detail: '<li id="#{id}" class="panelnav_link panelnav_detailItem #{baseClass}" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}"><div class="panelnav_detail">        <div class="panelnav_detail_text">            <h3>            <img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" class="panelnav_detail_hd" /></h3>            <p>#{description}</p>        </div>        <div class="panelnav_thumb"><img id="#{id}_thumb" src="#{thumbPath}" width="56" height="56" alt="" /></div>    </div></a></li>',
           SimpleDetail : '<li id="#{id}" class="panelnav_link panelnav_detailItem #{baseClass}" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}"><div class="panelnav_detail nothumb">        <div class="panelnav_detail_text">            <h3><img id="#{id}_hd" src="#{hdPath}" class="panelnav_detail_hd" alt="#{displayName}" /></h3>            <p>#{description}</p>        </div>        <div class="panelnav_hspacer"><br /></div>    </div></a></li>',
           CollectionCategoryDetail : '<div id="#{id}"><div id="#{id}_cat" attachPoint="categoryDetailNode" class="panelnav_link" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick"><div class="panelnav_detail"><div class="panelnav_detail_text"><h3><img id="#{id}_hd" src="#{hdPath}" style="height: 18px;" alt="#{displayName}" /></h3><p>#{description}</p></div><div class="panelnav_thumb"><img src="#{thumbPath}" width="56" height="56" alt="" /></div></div></div><div class="clear"><br /></div><ul id="#{id}_catlist" class="hidden" attachPoint="accordionContainerNode"></ul></div>',
           ProductCategoryDetail : '<div id="#{id}" class="panelnav_link" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <div class="panelnav_detail">        <div class="panelnav_detail_text">            <h3><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" class="panelnav_catdetail_hd" /></h3>            <p>#{description}</p>        </div>        <div class="panelnav_thumb"><img src="#{thumbPath}" width="56" height="56" alt="" /></div>    </div></div>',
           SearchQuickBuyDetail : '<div id="#{id}" class="panelnav_link panelnav_link_quickbuy panelnav_detailItem" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut">    <div class="panelnav_detail">        <div class="panelnav_detail_text">            <h3><a href="#{url}"><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" class="panelnav_detail_hd" /></a></h3>            <a href="#{url}">            <p attachPoint="shadenameNode" class="panelnav_shadename hidden">#{shadename}</p>            <p attachPoint="descriptionNode" class="hidden">#{description}</p>            </a>            <input type="image" src="/images/products/btn/btn_add_to_bag_93.gif" id="#{id}_btn_add" value="" class="panelnav_btn_add" />            <span attachPoint="inventoryStatusNode" class="inventory_status"></span>        </div>        <div class="smoosh_small" style="background-color: #{hex};"><a href="#{url}"><img class="thumb" src="#{thumbPath}" alt="#{displayName}" /></a></div>        <div attachPoint="cartConfirmNode"></div>    </div></div>',
           SearchProductDetail : '<div id="#{id}" class="panelnav_link panelnav_link_search panelnav_detailItem" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}">        <div class="panelnav_detail" attachPoint="panelDetailNode">            <div class="panelnav_detail_text">                <h3><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" /></h3>                <p>#{description}</p>                <img id="#{id}_actionimg" src="/images/search/btn_view_shades_off.gif" class="panelnav_btn_view_shades">            </div>            <div class="smoosh_small" style="background-color: #{hex};"><img src="#{thumbPath}" width="56" height="56" alt="#{displayName}" /></div>        </div>    </a></div>',
           SearchGiftcard : '<div class="panelnav_link panelnav_link_search" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}">        <div class="panelnav_detail" attachPoint="panelDetailNode">            <div class="panelnav_detail_text">                <h3><img id="#{id}_hd" src="#{hdPath}" alt="#{displayName}" /></h3>                <p>#{description}</p>                <img src="/search/images/btn_view_select_value_off.gif" class="panelnav_btn_view_shades" attachPoint="actionImgNode">            </div>            <div class="smoosh_small" style="background-color: #{hex};"><img src="#{thumbPath}" alt="#{displayName}" /></div>        </div>    </a></div>',
           ContentSearchDetail : '<div id="#{id}" class="panelnav_link panelnav_link_search_content" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut,click:_onClick">    <a href="#{url}"><div class="panelnav_detail nothumb">        <div class="panelnav_detail_text">            <p>#{description}</p>        </div>        <div class="panelnav_hspacer"><br /></div>    </div></a></div> \n',
           headerLi : '<li id="#{id}" class="link_hd clickable" attachpoint="containerNode">    <a href="#{url}"><img src="#{hdPath}" alt="#{displayName}" attachPoint="hdNode"></a></li> \n',
           headerDiv : '<div id="#{id}" class="link_hd clickable" attachpoint="containerNode">    <a href="#{url}"><img src="#{hdPath}" alt="#{displayName}" attachPoint="hdNode"></a></div> \n'
    },
    product : {
           cartAdd : '<div id="#{id}" class="overlay-container cart-add-overlay-container">    <div class="overlay-content-popover popover-prod">        <div class="close-container"><a class="close-link"></a></div>        <img src="#{smooshPath}" width="56" height="56" alt="#{prodName}" class="thumb" id="smoosh_img_#{id}"  style="background-color: #{hex};" />        <div class="popover-desc">            <span class="popover-title">#{prodName}</span>            <span attachpoint="inventoryStatusNode" class="inventory-status"></span>            <p>                <span attachpoint="swatchTitleNode"> </span>                 <span attachpoint="finishNameNode"> </span>            </p>                    #{price}        </div>        <div class="btn-container">            <input class="btn-remove hidden" type="image" src="/images/account/btn/btn_pop_remove_white_off.gif" alt="Remove" name="btn_favorites_remove_#{id}" id="btn_favorites_remove_#{id}" value="" attachpoint="removeNode" />            <input class="btn-add" type="image" src="/images/popup/btn_add_to_bag.gif" alt="#{addToBag}" name="prod_sku_#{id}" id="prod_sku_#{id}" value="" attachpoint="addToBagNode" />        </div>        <div class="popover-btm"></div>    </div></div>',
           cartConfirm: '<div id="#{id}" class="overlay-container cart-confirm-overlay-container">    <div class="overlay-content-popover popover-message">        <div class="close-container"><a class="close-link"></a></div>        <div attachPoint="cartConfirmDisplayNode">            <div class="popover-desc">                <span class="popover-title thank_you">#{text_thank_you}</span>                <p><span attachPoint="prodNameNode"></span><span attachPoint="shadeNameNode">&nbsp;- </span>&nbsp;<span attachPoint="addedMessageNode"></span></p>                <p class="cart-confirm-fss-message"></p>                  </div>            <span class="popover-btn-checkout"><a href="/checkout/viewcart.tmpl"><img src="/images/popup/btn_checkout.gif" alt="#{text_checkout}" class="btn-checkout"></a></span>            <span class="popover-btn-favorites"><a href="/account/favorites.tmpl"><img src="/images/popup/btn_favourites.gif" alt="#{text_favorites}" class="btn-favorites" /></a></span>            <div class="close-link continue-link"><img src="/images/popup/btn_continue_shopping.gif" alt="#{text_continue_shopping}" class="btn-continue"></div>        </div>        <div class="popover-desc hidden" attachPoint="cartConfirmErrorNode">             <span class="popover-title sorry">#{text_sorry}</span>            <p><span attachPoint="errorMessageNode"></span></p>              </div>        <div class="popover-btm"></div>    </div></div>',
           hexSwatch : '<div id="#{id}" class="swatch_hex_container" attachEvent="click:_onClick">    <div class="swatch_hex" attachPoint="hexNode" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut" style="background-color: #{hex};"><br /></div>    <div class="tooltip" attachPoint="tooltipNode" style="background-color: #{hex};">#{name} #{inventory_status}</div></div>',
           hexSwatchImage : '<div id="#{id}" class="swatch_hex_container" attachPoint="shadeContainerNode" attachEvent="click:_onClick">    <div class="swatch_hex swatch_hex_smoosh" attachPoint="hexNode" attachEvent="mouseover:_onMouseOver,mouseout:_onMouseOut" style="background-color: #{hex};"><img src="#{smooshThumb}" width="12" height="12" alt=""></div>    <div class="tooltip" attachPoint="tooltipNode" style="background-color: #{hex};">#{name} #{inventory_status}</div></div>',
           thumbSwatch : '<div id="#{id}" class="swatch-thumb-container">    <a href="#" class="swatch-thumb"><img src="#{smooshThumb}" /><span class="tooltip" attachPoint="tooltipNode" style="background-color: #{hex};">#{text_select} #{name} #{text_toshop}</span></a></div>',
           swatchCard : '<div id="#{id}" class="overlay-container swatchcard-container">    <div class="swatchcard">        <img id="smoosh-img-#{id}" class="swatch-lg" src="#{smooshPath}" alt="">        <div class="close-container"><a class="close-link"></a></div>        <div class="card-desc">            <p id="shade-name-#{id}" class="shade-name"> </p>            <p>                <span id="shade-description-#{id}"> </span>&nbsp;                <a href="javascript:void();" class="card-finish"><span id="shade-finish-swatchcards"> </span><span id="shade-finish-description-swatchcards" class="tooltip"> </span></a>            </p>            <div class="card-icons">                <a href="javascript:void(0);" id="limited-flag"><img src="/images/products/common/icon_limitedlife.gif" /><span class="tooltip">#{text_limited}</span></a>                <a href="javascript:void(0);" id="pro-flag"><img src="/images/products/common/icon_pro.gif"/><span class="tooltip">#{text_macpro}</span></a>            </div>            <p class="card-price">#{price}</p>        </div>        <div id="inventory-status-swatchcards" class="inventory-status"></div>        <input class="btn-add inventory-status-conditional" type="image" src="/images/products/btn/btn_add_to_bag_168.gif" alt="" id="prod-sku-#{id}" value="#{skuPath}" />    </div></div>'
    }

}
/*  Prototype JavaScript framework, version 1.6.0.3
 *  (c) 2005-2008 Sam Stephenson
 *
 *  Prototype is freely distributable under the terms of an MIT-style license.
 *  For details, see the Prototype web site: http://www.prototypejs.org/
 *
 *--------------------------------------------------------------------------*/

var Prototype = {
  Version: '1.6.0.3',

  Browser: {
    IE:     !!(window.attachEvent &&
      navigator.userAgent.indexOf('Opera') === -1),
    Opera:  navigator.userAgent.indexOf('Opera') > -1,
    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 &&
      navigator.userAgent.indexOf('KHTML') === -1,
    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)
  },

  BrowserFeatures: {
    XPath: !!document.evaluate,
    SelectorsAPI: !!document.querySelector,
    ElementExtensions: !!window.HTMLElement,
    SpecificElementExtensions:
      document.createElement('div')['__proto__'] &&
      document.createElement('div')['__proto__'] !==
        document.createElement('form')['__proto__']
  },

  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',
  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

  emptyFunction: function() { },
  K: function(x) { return x }
};

if (Prototype.Browser.MobileSafari)
  Prototype.BrowserFeatures.SpecificElementExtensions = false;


/* Based on Alex Arnell's inheritance implementation. */
var Class = {
  create: function() {
    var parent = null, properties = $A(arguments);
    if (Object.isFunction(properties[0]))
      parent = properties.shift();

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

    Object.extend(klass, Class.Methods);
    klass.superclass = parent;
    klass.subclasses = [];

    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 = Prototype.emptyFunction;

    klass.prototype.constructor = klass;

    return klass;
  }
};

Class.Methods = {
  addMethods: function(source) {
    var ancestor   = this.superclass && this.superclass.prototype;
    var properties = Object.keys(source);

    if (!Object.keys({ toString: true }).length)
      properties.push("toString", "valueOf");

    for (var i = 0, length = properties.length; i < length; i++) {
      var property = properties[i], value = source[property];
      if (ancestor && Object.isFunction(value) &&
          value.argumentNames().first() == "$super") {
        var method = value;
        value = (function(m) {
          return function() { return ancestor[m].apply(this, arguments) };
        })(property).wrap(method);

        value.valueOf = method.valueOf.bind(method);
        value.toString = method.toString.bind(method);
      }
      this.prototype[property] = value;
    }

    return this;
  }
};

var Abstract = { };

Object.extend = function(destination, source) {
  for (var property in source)
    destination[property] = source[property];
  return destination;
};

Object.extend(Object, {
  inspect: function(object) {
    try {
      if (Object.isUndefined(object)) return 'undefined';
      if (object === null) return 'null';
      return object.inspect ? object.inspect() : String(object);
    } catch (e) {
      if (e instanceof RangeError) return '...';
      throw e;
    }
  },

  toJSON: function(object) {
    var type = typeof object;
    switch (type) {
      case 'undefined':
      case 'function':
      case 'unknown': return;
      case 'boolean': return object.toString();
    }

    if (object === null) return 'null';
    if (object.toJSON) return object.toJSON();
    if (Object.isElement(object)) return;

    var results = [];
    for (var property in object) {
      var value = Object.toJSON(object[property]);
      if (!Object.isUndefined(value))
        results.push(property.toJSON() + ': ' + value);
    }

    return '{' + results.join(', ') + '}';
  },

  toQueryString: function(object) {
    return $H(object).toQueryString();
  },

  toHTML: function(object) {
    return object && object.toHTML ? object.toHTML() : String.interpret(object);
  },

  keys: function(object) {
    var keys = [];
    for (var property in object)
      keys.push(property);
    return keys;
  },

  values: function(object) {
    var values = [];
    for (var property in object)
      values.push(object[property]);
    return values;
  },

  clone: function(object) {
    return Object.extend({ }, object);
  },

  isElement: function(object) {
    return !!(object && object.nodeType == 1);
  },

  isArray: function(object) {
    return object != null && typeof object == "object" &&
      'splice' in object && 'join' in object;
  },

  isHash: function(object) {
    return object instanceof Hash;
  },

  isFunction: function(object) {
    return typeof object == "function";
  },

  isString: function(object) {
    return typeof object == "string";
  },

  isNumber: function(object) {
    return typeof object == "number";
  },

  isUndefined: function(object) {
    return typeof object == "undefined";
  }
});

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

  bind: function() {
    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;
    var __method = this, args = $A(arguments), object = args.shift();
    return function() {
      return __method.apply(object, args.concat($A(arguments)));
    }
  },

  bindAsEventListener: function() {
    var __method = this, args = $A(arguments), object = args.shift();
    return function(event) {
      return __method.apply(object, [event || window.event].concat(args));
    }
  },

  curry: function() {
    if (!arguments.length) return this;
    var __method = this, args = $A(arguments);
    return function() {
      return __method.apply(this, args.concat($A(arguments)));
    }
  },

  delay: function() {
    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;
    return window.setTimeout(function() {
      return __method.apply(__method, args);
    }, timeout);
  },

  defer: function() {
    var args = [0.01].concat($A(arguments));
    return this.delay.apply(this, args);
  },

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

  methodize: function() {
    if (this._methodized) return this._methodized;
    var __method = this;
    return this._methodized = function() {
      return __method.apply(null, [this].concat($A(arguments)));
    };
  }
});

Date.prototype.toJSON = function() {
  return '"' + this.getUTCFullYear() + '-' +
    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +
    this.getUTCDate().toPaddedString(2) + 'T' +
    this.getUTCHours().toPaddedString(2) + ':' +
    this.getUTCMinutes().toPaddedString(2) + ':' +
    this.getUTCSeconds().toPaddedString(2) + 'Z"';
};

var Try = {
  these: function() {
    var returnValue;

    for (var i = 0, length = arguments.length; i < length; i++) {
      var lambda = arguments[i];
      try {
        returnValue = lambda();
        break;
      } catch (e) { }
    }

    return returnValue;
  }
};

RegExp.prototype.match = RegExp.prototype.test;

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

/*--------------------------------------------------------------------------*/

var PeriodicalExecuter = Class.create({
  initialize: function(callback, frequency) {
    this.callback = callback;
    this.frequency = frequency;
    this.currentlyExecuting = false;

    this.registerCallback();
  },

  registerCallback: function() {
    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
  },

  execute: function() {
    this.callback(this);
  },

  stop: function() {
    if (!this.timer) return;
    clearInterval(this.timer);
    this.timer = null;
  },

  onTimerEvent: function() {
    if (!this.currentlyExecuting) {
      try {
        this.currentlyExecuting = true;
        this.execute();
      } finally {
        this.currentlyExecuting = false;
      }
    }
  }
});
Object.extend(String, {
  interpret: function(value) {
    return value == null ? '' : String(value);
  },
  specialChar: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '\\': '\\\\'
  }
});

Object.extend(String.prototype, {
  gsub: function(pattern, replacement) {
    var result = '', source = this, match;
    replacement = arguments.callee.prepareReplacement(replacement);

    while (source.length > 0) {
      if (match = source.match(pattern)) {
        result += source.slice(0, match.index);
        result += String.interpret(replacement(match));
        source  = source.slice(match.index + match[0].length);
      } else {
        result += source, source = '';
      }
    }
    return result;
  },

  sub: function(pattern, replacement, count) {
    replacement = this.gsub.prepareReplacement(replacement);
    count = Object.isUndefined(count) ? 1 : count;

    return this.gsub(pattern, function(match) {
      if (--count < 0) return match[0];
      return replacement(match);
    });
  },

  scan: function(pattern, iterator) {
    this.gsub(pattern, iterator);
    return String(this);
  },

  truncate: function(length, truncation) {
    length = length || 30;
    truncation = Object.isUndefined(truncation) ? '...' : truncation;
    return this.length > length ?
      this.slice(0, length - truncation.length) + truncation : String(this);
  },

  strip: function() {
    return this.replace(/^\s+/, '').replace(/\s+$/, '');
  },

  stripTags: function() {
    return this.replace(/<\/?[^>]+>/gi, '');
  },

  stripScripts: function() {
    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');
  },

  extractScripts: function() {
    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');
    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');
    return (this.match(matchAll) || []).map(function(scriptTag) {
      return (scriptTag.match(matchOne) || ['', ''])[1];
    });
  },

  evalScripts: function() {
    return this.extractScripts().map(function(script) { return eval(script) });
  },

  escapeHTML: function() {
    var self = arguments.callee;
    self.text.data = this;
    return self.div.innerHTML;
  },

  unescapeHTML: function() {
    var div = new Element('div');
    div.innerHTML = this.stripTags();
    return div.childNodes[0] ? (div.childNodes.length > 1 ?
      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :
      div.childNodes[0].nodeValue) : '';
  },

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

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

        if (key in hash) {
          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];
          hash[key].push(value);
        }
        else hash[key] = value;
      }
      return hash;
    });
  },

  toArray: function() {
    return this.split('');
  },

  succ: function() {
    return this.slice(0, this.length - 1) +
      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);
  },

  times: function(count) {
    return count < 1 ? '' : new Array(count + 1).join(this);
  },

  camelize: function() {
    var parts = this.split('-'), len = parts.length;
    if (len == 1) return parts[0];

    var camelized = this.charAt(0) == '-'
      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)
      : parts[0];

    for (var i = 1; i < len; i++)
      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

    return camelized;
  },

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

  underscore: function() {
    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();
  },

  dasherize: function() {
    return this.gsub(/_/,'-');
  },

  inspect: function(useDoubleQuotes) {
    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {
      var character = String.specialChar[match[0]];
      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);
    });
    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';
    return "'" + escapedString.replace(/'/g, '\\\'') + "'";
  },

  toJSON: function() {
    return this.inspect(true);
  },

  unfilterJSON: function(filter) {
    return this.sub(filter || Prototype.JSONFilter, '#{1}');
  },

  isJSON: function() {
    var str = this;
    if (str.blank()) return false;
    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');
    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);
  },

  evalJSON: function(sanitize) {
    var json = this.unfilterJSON();
    try {
      if (!sanitize || json.isJSON()) return eval('(' + json + ')');
    } catch (e) { }
    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());
  },

  include: function(pattern) {
    return this.indexOf(pattern) > -1;
  },

  startsWith: function(pattern) {
    return this.indexOf(pattern) === 0;
  },

  endsWith: function(pattern) {
    var d = this.length - pattern.length;
    return d >= 0 && this.lastIndexOf(pattern) === d;
  },

  empty: function() {
    return this == '';
  },

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

  interpolate: function(object, pattern) {
    return new Template(this, pattern).evaluate(object);
  }
});

if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {
  escapeHTML: function() {
    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  },
  unescapeHTML: function() {
    return this.stripTags().replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');
  }
});

String.prototype.gsub.prepareReplacement = function(replacement) {
  if (Object.isFunction(replacement)) return replacement;
  var template = new Template(replacement);
  return function(match) { return template.evaluate(match) };
};

String.prototype.parseQuery = String.prototype.toQueryParams;

Object.extend(String.prototype.escapeHTML, {
  div:  document.createElement('div'),
  text: document.createTextNode('')
});

String.prototype.escapeHTML.div.appendChild(String.prototype.escapeHTML.text);

var Template = Class.create({
  initialize: function(template, pattern) {
    this.template = template.toString();
    this.pattern = pattern || Template.Pattern;
  },

  evaluate: function(object) {
    if (Object.isFunction(object.toTemplateReplacements))
      object = object.toTemplateReplacements();

    return this.template.gsub(this.pattern, function(match) {
      if (object == null) return '';

      var before = match[1] || '';
      if (before == '\\') return match[2];

      var ctx = object, expr = match[3];
      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;
      match = pattern.exec(expr);
      if (match == null) return before;

      while (match != null) {
        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];
        ctx = ctx[comp];
        if (null == ctx || '' == match[3]) break;
        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);
        match = pattern.exec(expr);
      }

      return before + String.interpret(ctx);
    });
  }
});
Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

var $break = { };

var Enumerable = {
  each: function(iterator, context) {
    var index = 0;
    try {
      this._each(function(value) {
        iterator.call(context, value, index++);
      });
    } catch (e) {
      if (e != $break) throw e;
    }
    return this;
  },

  eachSlice: function(number, iterator, context) {
    var index = -number, slices = [], array = this.toArray();
    if (number < 1) return array;
    while ((index += number) < array.length)
      slices.push(array.slice(index, index+number));
    return slices.collect(iterator, context);
  },

  all: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = true;
    this.each(function(value, index) {
      result = result && !!iterator.call(context, value, index);
      if (!result) throw $break;
    });
    return result;
  },

  any: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result = false;
    this.each(function(value, index) {
      if (result = !!iterator.call(context, value, index))
        throw $break;
    });
    return result;
  },

  collect: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];
    this.each(function(value, index) {
      results.push(iterator.call(context, value, index));
    });
    return results;
  },

  detect: function(iterator, context) {
    var result;
    this.each(function(value, index) {
      if (iterator.call(context, value, index)) {
        result = value;
        throw $break;
      }
    });
    return result;
  },

  findAll: function(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  },

  grep: function(filter, iterator, context) {
    iterator = iterator || Prototype.K;
    var results = [];

    if (Object.isString(filter))
      filter = new RegExp(filter);

    this.each(function(value, index) {
      if (filter.match(value))
        results.push(iterator.call(context, value, index));
    });
    return results;
  },

  include: function(object) {
    if (Object.isFunction(this.indexOf))
      if (this.indexOf(object) != -1) return true;

    var found = false;
    this.each(function(value) {
      if (value == object) {
        found = true;
        throw $break;
      }
    });
    return found;
  },

  inGroupsOf: function(number, fillWith) {
    fillWith = Object.isUndefined(fillWith) ? null : fillWith;
    return this.eachSlice(number, function(slice) {
      while(slice.length < number) slice.push(fillWith);
      return slice;
    });
  },

  inject: function(memo, iterator, context) {
    this.each(function(value, index) {
      memo = iterator.call(context, memo, value, index);
    });
    return memo;
  },

  invoke: function(method) {
    var args = $A(arguments).slice(1);
    return this.map(function(value) {
      return value[method].apply(value, args);
    });
  },

  max: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value >= result)
        result = value;
    });
    return result;
  },

  min: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var result;
    this.each(function(value, index) {
      value = iterator.call(context, value, index);
      if (result == null || value < result)
        result = value;
    });
    return result;
  },

  partition: function(iterator, context) {
    iterator = iterator || Prototype.K;
    var trues = [], falses = [];
    this.each(function(value, index) {
      (iterator.call(context, value, index) ?
        trues : falses).push(value);
    });
    return [trues, falses];
  },

  pluck: function(property) {
    var results = [];
    this.each(function(value) {
      results.push(value[property]);
    });
    return results;
  },

  reject: function(iterator, context) {
    var results = [];
    this.each(function(value, index) {
      if (!iterator.call(context, value, index))
        results.push(value);
    });
    return results;
  },

  sortBy: function(iterator, context) {
    return this.map(function(value, index) {
      return {
        value: value,
        criteria: iterator.call(context, value, index)
      };
    }).sort(function(left, right) {
      var a = left.criteria, b = right.criteria;
      return a < b ? -1 : a > b ? 1 : 0;
    }).pluck('value');
  },

  toArray: function() {
    return this.map();
  },

  zip: function() {
    var iterator = Prototype.K, args = $A(arguments);
    if (Object.isFunction(args.last()))
      iterator = args.pop();

    var collections = [this].concat(args).map($A);
    return this.map(function(value, index) {
      return iterator(collections.pluck(index));
    });
  },

  size: function() {
    return this.toArray().length;
  },

  inspect: function() {
    return '#<Enumerable:' + this.toArray().inspect() + '>';
  }
};

Object.extend(Enumerable, {
  map:     Enumerable.collect,
  find:    Enumerable.detect,
  select:  Enumerable.findAll,
  filter:  Enumerable.findAll,
  member:  Enumerable.include,
  entries: Enumerable.toArray,
  every:   Enumerable.all,
  some:    Enumerable.any
});
function $A(iterable) {
  if (!iterable) return [];
  if (iterable.toArray) return iterable.toArray();
  var length = iterable.length || 0, results = new Array(length);
  while (length--) results[length] = iterable[length];
  return results;
}

if (Prototype.Browser.WebKit) {
  $A = function(iterable) {
    if (!iterable) return [];
    // In Safari, only use the `toArray` method if it's not a NodeList.
    // A NodeList is a function, has an function `item` property, and a numeric
    // `length` property. Adapted from Google Doctype.
    if (!(typeof iterable === 'function' && typeof iterable.length ===
        'number' && typeof iterable.item === 'function') && iterable.toArray)
      return iterable.toArray();
    var length = iterable.length || 0, results = new Array(length);
    while (length--) results[length] = iterable[length];
    return results;
  };
}

Array.from = $A;

Object.extend(Array.prototype, Enumerable);

if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;

Object.extend(Array.prototype, {
  _each: function(iterator) {
    for (var i = 0, length = this.length; i < length; i++)
      iterator(this[i]);
  },

  clear: function() {
    this.length = 0;
    return this;
  },

  first: function() {
    return this[0];
  },

  last: function() {
    return this[this.length - 1];
  },

  compact: function() {
    return this.select(function(value) {
      return value != null;
    });
  },

  flatten: function() {
    return this.inject([], function(array, value) {
      return array.concat(Object.isArray(value) ?
        value.flatten() : [value]);
    });
  },

  without: function() {
    var values = $A(arguments);
    return this.select(function(value) {
      return !values.include(value);
    });
  },

  reverse: function(inline) {
    return (inline !== false ? this : this.toArray())._reverse();
  },

  reduce: function() {
    return this.length > 1 ? this : this[0];
  },

  uniq: function(sorted) {
    return this.inject([], function(array, value, index) {
      if (0 == index || (sorted ? array.last() != value : !array.include(value)))
        array.push(value);
      return array;
    });
  },

  intersect: function(array) {
    return this.uniq().findAll(function(item) {
      return array.detect(function(value) { return item === value });
    });
  },

  clone: function() {
    return [].concat(this);
  },

  size: function() {
    return this.length;
  },

  inspect: function() {
    return '[' + this.map(Object.inspect).join(', ') + ']';
  },

  toJSON: function() {
    var results = [];
    this.each(function(object) {
      var value = Object.toJSON(object);
      if (!Object.isUndefined(value)) results.push(value);
    });
    return '[' + results.join(', ') + ']';
  }
});

// use native browser JS 1.6 implementation if available
if (Object.isFunction(Array.prototype.forEach))
  Array.prototype._each = Array.prototype.forEach;

if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {
  i || (i = 0);
  var length = this.length;
  if (i < 0) i = length + i;
  for (; i < length; i++)
    if (this[i] === item) return i;
  return -1;
};

if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {
  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;
  var n = this.slice(0, i).reverse().indexOf(item);
  return (n < 0) ? n : i - n - 1;
};

Array.prototype.toArray = Array.prototype.clone;

function $w(string) {
  if (!Object.isString(string)) return [];
  string = string.strip();
  return string ? string.split(/\s+/) : [];
}

if (Prototype.Browser.Opera){
  Array.prototype.concat = function() {
    var array = [];
    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);
    for (var i = 0, length = arguments.length; i < length; i++) {
      if (Object.isArray(arguments[i])) {
        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)
          array.push(arguments[i][j]);
      } else {
        array.push(arguments[i]);
      }
    }
    return array;
  };
}
Object.extend(Number.prototype, {
  toColorPart: function() {
    return this.toPaddedString(2, 16);
  },

  succ: function() {
    return this + 1;
  },

  times: function(iterator, context) {
    $R(0, this, true).each(iterator, context);
    return this;
  },

  toPaddedString: function(length, radix) {
    var string = this.toString(radix || 10);
    return '0'.times(length - string.length) + string;
  },

  toJSON: function() {
    return isFinite(this) ? this.toString() : 'null';
  }
});

$w('abs round ceil floor').each(function(method){
  Number.prototype[method] = Math[method].methodize();
});
function $H(object) {
  return new Hash(object);
};

var Hash = Class.create(Enumerable, (function() {

  function toQueryPair(key, value) {
    if (Object.isUndefined(value)) return key;
    return key + '=' + encodeURIComponent(String.interpret(value));
  }

  return {
    initialize: function(object) {
      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);
    },

    _each: function(iterator) {
      for (var key in this._object) {
        var value = this._object[key], pair = [key, value];
        pair.key = key;
        pair.value = value;
        iterator(pair);
      }
    },

    set: function(key, value) {
      return this._object[key] = value;
    },

    get: function(key) {
      // simulating poorly supported hasOwnProperty
      if (this._object[key] !== Object.prototype[key])
        return this._object[key];
    },

    unset: function(key) {
      var value = this._object[key];
      delete this._object[key];
      return value;
    },

    toObject: function() {
      return Object.clone(this._object);
    },

    keys: function() {
      return this.pluck('key');
    },

    values: function() {
      return this.pluck('value');
    },

    index: function(value) {
      var match = this.detect(function(pair) {
        return pair.value === value;
      });
      return match && match.key;
    },

    merge: function(object) {
      return this.clone().update(object);
    },

    update: function(object) {
      return new Hash(object).inject(this, function(result, pair) {
        result.set(pair.key, pair.value);
        return result;
      });
    },

    toQueryString: function() {
      return this.inject([], function(results, pair) {
        var key = encodeURIComponent(pair.key), values = pair.value;

        if (values && typeof values == 'object') {
          if (Object.isArray(values))
            return results.concat(values.map(toQueryPair.curry(key)));
        } else results.push(toQueryPair(key, values));
        return results;
      }).join('&');
    },

    inspect: function() {
      return '#<Hash:{' + this.map(function(pair) {
        return pair.map(Object.inspect).join(': ');
      }).join(', ') + '}>';
    },

    toJSON: function() {
      return Object.toJSON(this.toObject());
    },

    clone: function() {
      return new Hash(this);
    }
  }
})());

Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;
Hash.from = $H;
var ObjectRange = Class.create(Enumerable, {
  initialize: function(start, end, exclusive) {
    this.start = start;
    this.end = end;
    this.exclusive = exclusive;
  },

  _each: function(iterator) {
    var value = this.start;
    while (this.include(value)) {
      iterator(value);
      value = value.succ();
    }
  },

  include: function(value) {
    if (value < this.start)
      return false;
    if (this.exclusive)
      return value < this.end;
    return value <= this.end;
  }
});

var $R = function(start, end, exclusive) {
  return new ObjectRange(start, end, exclusive);
};

var Ajax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};

Ajax.Responders = {
  responders: [],

  _each: function(iterator) {
    this.responders._each(iterator);
  },

  register: function(responder) {
    if (!this.include(responder))
      this.responders.push(responder);
  },

  unregister: function(responder) {
    this.responders = this.responders.without(responder);
  },

  dispatch: function(callback, request, transport, json) {
    this.each(function(responder) {
      if (Object.isFunction(responder[callback])) {
        try {
          responder[callback].apply(responder, [request, transport, json]);
        } catch (e) { }
      }
    });
  }
};

Object.extend(Ajax.Responders, Enumerable);

Ajax.Responders.register({
  onCreate:   function() { Ajax.activeRequestCount++ },
  onComplete: function() { Ajax.activeRequestCount-- }
});

Ajax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'post',
      asynchronous: true,
      contentType:  'application/x-www-form-urlencoded',
      encoding:     'UTF-8',
      parameters:   '',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});

Ajax.Request = Class.create(Ajax.Base, {
  _complete: false,

  initialize: function($super, url, options) {
    $super(options);
    this.transport = Ajax.getTransport();
    this.request(url);
  },

  request: function(url) {
    this.url = url;
    this.method = this.options.method;
    var params = Object.clone(this.options.parameters);

    if (!['get', 'post'].include(this.method)) {
      // simulate other verbs over post
      params['_method'] = this.method;
      this.method = 'post';
    }

    this.parameters = params;

    if (params = Object.toQueryString(params)) {
      // when GET, append parameters to URL
      if (this.method == 'get')
        this.url += (this.url.include('?') ? '&' : '?') + params;
      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))
        params += '&_=';
    }

    try {
      var response = new Ajax.Response(this);
      if (this.options.onCreate) this.options.onCreate(response);
      Ajax.Responders.dispatch('onCreate', this, response);

      this.transport.open(this.method.toUpperCase(), this.url,
        this.options.asynchronous);

      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

      this.transport.onreadystatechange = this.onStateChange.bind(this);
      this.setRequestHeaders();

      this.body = this.method == 'post' ? (this.options.postBody || params) : null;
      this.transport.send(this.body);

      /* Force Firefox to handle ready state 4 for synchronous requests */
      if (!this.options.asynchronous && this.transport.overrideMimeType)
        this.onStateChange();

    }
    catch (e) {
      this.dispatchException(e);
    }
  },

  onStateChange: function() {
    var readyState = this.transport.readyState;
    if (readyState > 1 && !((readyState == 4) && this._complete))
      this.respondToReadyState(this.transport.readyState);
  },

  setRequestHeaders: function() {
    var headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'X-Prototype-Version': Prototype.Version,
      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'
    };

    if (this.method == 'post') {
      headers['Content-type'] = this.options.contentType +
        (this.options.encoding ? '; charset=' + this.options.encoding : '');

      /* Force "Connection: close" for older Mozilla browsers to work
       * around a bug where XMLHttpRequest sends an incorrect
       * Content-length header. See Mozilla Bugzilla #246651.
       */
      if (this.transport.overrideMimeType &&
          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)
            headers['Connection'] = 'close';
    }

    // user-defined headers
    if (typeof this.options.requestHeaders == 'object') {
      var extras = this.options.requestHeaders;

      if (Object.isFunction(extras.push))
        for (var i = 0, length = extras.length; i < length; i += 2)
          headers[extras[i]] = extras[i+1];
      else
        $H(extras).each(function(pair) { headers[pair.key] = pair.value });
    }

    for (var name in headers)
      this.transport.setRequestHeader(name, headers[name]);
  },

  success: function() {
    var status = this.getStatus();
    return !status || (status >= 200 && status < 300);
  },

  getStatus: function() {
    try {
      return this.transport.status || 0;
    } catch (e) { return 0 }
  },

  respondToReadyState: function(readyState) {
    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

    if (state == 'Complete') {
      try {
        this._complete = true;
        (this.options['on' + response.status]
         || this.options['on' + (this.success() ? 'Success' : 'Failure')]
         || Prototype.emptyFunction)(response, response.headerJSON);
      } catch (e) {
        this.dispatchException(e);
      }

      var contentType = response.getHeader('Content-type');
      if (this.options.evalJS == 'force'
          || (this.options.evalJS && this.isSameOrigin() && contentType
          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))
        this.evalResponse();
    }

    try {
      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);
      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);
    } catch (e) {
      this.dispatchException(e);
    }

    if (state == 'Complete') {
      // avoid memory leak in MSIE: clean up
      this.transport.onreadystatechange = Prototype.emptyFunction;
    }
  },

  isSameOrigin: function() {
    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);
    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({
      protocol: location.protocol,
      domain: document.domain,
      port: location.port ? ':' + location.port : ''
    }));
  },

  getHeader: function(name) {
    try {
      return this.transport.getResponseHeader(name) || null;
    } catch (e) { return null }
  },

  evalResponse: function() {
    try {
      return eval((this.transport.responseText || '').unfilterJSON());
    } catch (e) {
      this.dispatchException(e);
    }
  },

  dispatchException: function(exception) {
    (this.options.onException || Prototype.emptyFunction)(this, exception);
    Ajax.Responders.dispatch('onException', this, exception);
  }
});

Ajax.Request.Events =
  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

Ajax.Response = Class.create({
  initialize: function(request){
    this.request = request;
    var transport  = this.transport  = request.transport,
        readyState = this.readyState = transport.readyState;

    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {
      this.status       = this.getStatus();
      this.statusText   = this.getStatusText();
      this.responseText = String.interpret(transport.responseText);
      this.headerJSON   = this._getHeaderJSON();
    }

    if(readyState == 4) {
      var xml = transport.responseXML;
      this.responseXML  = Object.isUndefined(xml) ? null : xml;
      this.responseJSON = this._getResponseJSON();
    }
  },

  status:      0,
  statusText: '',

  getStatus: Ajax.Request.prototype.getStatus,

  getStatusText: function() {
    try {
      return this.transport.statusText || '';
    } catch (e) { return '' }
  },

  getHeader: Ajax.Request.prototype.getHeader,

  getAllHeaders: function() {
    try {
      return this.getAllResponseHeaders();
    } catch (e) { return null }
  },

  getResponseHeader: function(name) {
    return this.transport.getResponseHeader(name);
  },

  getAllResponseHeaders: function() {
    return this.transport.getAllResponseHeaders();
  },

  _getHeaderJSON: function() {
    var json = this.getHeader('X-JSON');
    if (!json) return null;
    json = decodeURIComponent(escape(json));
    try {
      return json.evalJSON(this.request.options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  },

  _getResponseJSON: function() {
    var options = this.request.options;
    if (!options.evalJSON || (options.evalJSON != 'force' &&
      !(this.getHeader('Content-type') || '').include('application/json')) ||
        this.responseText.blank())
          return null;
    try {
      return this.responseText.evalJSON(options.sanitizeJSON ||
        !this.request.isSameOrigin());
    } catch (e) {
      this.request.dispatchException(e);
    }
  }
});

Ajax.Updater = Class.create(Ajax.Request, {
  initialize: function($super, container, url, options) {
    this.container = {
      success: (container.success || container),
      failure: (container.failure || (container.success ? null : container))
    };

    options = Object.clone(options);
    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.updateContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    $super(url, options);
  },

  updateContent: function(responseText) {
    var receiver = this.container[this.success() ? 'success' : 'failure'],
        options = this.options;

    if (!options.evalScripts) responseText = responseText.stripScripts();

    if (receiver = $(receiver)) {
      if (options.insertion) {
        if (Object.isString(options.insertion)) {
          var insertion = { }; insertion[options.insertion] = responseText;
          receiver.insert(insertion);
        }
        else options.insertion(receiver, responseText);
      }
      else receiver.update(responseText);
    }
  }
});

Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {
  initialize: function($super, container, url, options) {
    $super(options);
    this.onComplete = this.options.onComplete;

    this.frequency = (this.options.frequency || 2);
    this.decay = (this.options.decay || 1);

    this.updater = { };
    this.container = container;
    this.url = url;

    this.start();
  },

  start: function() {
    this.options.onComplete = this.updateComplete.bind(this);
    this.onTimerEvent();
  },

  stop: function() {
    this.updater.options.onComplete = undefined;
    clearTimeout(this.timer);
    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);
  },

  updateComplete: function(response) {
    if (this.options.decay) {
      this.decay = (response.responseText == this.lastText ?
        this.decay * this.options.decay : 1);

      this.lastText = response.responseText;
    }
    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);
  },

  onTimerEvent: function() {
    this.updater = new Ajax.Updater(this.container, this.url, this.options);
  }
});
function $(element) {
  if (arguments.length > 1) {
    for (var i = 0, elements = [], length = arguments.length; i < length; i++)
      elements.push($(arguments[i]));
    return elements;
  }
  if (Object.isString(element))
    element = document.getElementById(element);
  return Element.extend(element);
}

if (Prototype.BrowserFeatures.XPath) {
  document._getElementsByXPath = function(expression, parentElement) {
    var results = [];
    var query = document.evaluate(expression, $(parentElement) || document,
      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    for (var i = 0, length = query.snapshotLength; i < length; i++)
      results.push(Element.extend(query.snapshotItem(i)));
    return results;
  };
}

/*--------------------------------------------------------------------------*/

if (!window.Node) var Node = { };

if (!Node.ELEMENT_NODE) {
  // DOM level 2 ECMAScript Language Binding
  Object.extend(Node, {
    ELEMENT_NODE: 1,
    ATTRIBUTE_NODE: 2,
    TEXT_NODE: 3,
    CDATA_SECTION_NODE: 4,
    ENTITY_REFERENCE_NODE: 5,
    ENTITY_NODE: 6,
    PROCESSING_INSTRUCTION_NODE: 7,
    COMMENT_NODE: 8,
    DOCUMENT_NODE: 9,
    DOCUMENT_TYPE_NODE: 10,
    DOCUMENT_FRAGMENT_NODE: 11,
    NOTATION_NODE: 12
  });
}

(function() {
  var element = this.Element;
  this.Element = function(tagName, attributes) {
    attributes = attributes || { };
    tagName = tagName.toLowerCase();
    var cache = Element.cache;
    if (Prototype.Browser.IE && attributes.name) {
      tagName = '<' + tagName + ' name="' + attributes.name + '">';
      delete attributes.name;
      return Element.writeAttribute(document.createElement(tagName), attributes);
    }
    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));
    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);
  };
  Object.extend(this.Element, element || { });
  if (element) this.Element.prototype = element.prototype;
}).call(window);

Element.cache = { };

Element.Methods = {
  visible: function(element) {
    return $(element).style.display != 'none';
  },

  toggle: function(element) {
    element = $(element);
    Element[Element.visible(element) ? 'hide' : 'show'](element);
    return element;
  },

  hide: function(element) {
    element = $(element);
    element.style.display = 'none';
    return element;
  },

  show: function(element) {
    element = $(element);
    element.style.display = '';
    return element;
  },

  remove: function(element) {
    element = $(element);
    element.parentNode.removeChild(element);
    return element;
  },

  update: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);
    content = Object.toHTML(content);
    element.innerHTML = content.stripScripts();
    content.evalScripts.bind(content).defer();
    return element;
  },

  replace: function(element, content) {
    element = $(element);
    if (content && content.toElement) content = content.toElement();
    else if (!Object.isElement(content)) {
      content = Object.toHTML(content);
      var range = element.ownerDocument.createRange();
      range.selectNode(element);
      content.evalScripts.bind(content).defer();
      content = range.createContextualFragment(content.stripScripts());
    }
    element.parentNode.replaceChild(content, element);
    return element;
  },

  insert: function(element, insertions) {
    element = $(element);

    if (Object.isString(insertions) || Object.isNumber(insertions) ||
        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))
          insertions = {bottom:insertions};

    var content, insert, tagName, childNodes;

    for (var position in insertions) {
      content  = insertions[position];
      position = position.toLowerCase();
      insert = Element._insertionTranslations[position];

      if (content && content.toElement) content = content.toElement();
      if (Object.isElement(content)) {
        insert(element, content);
        continue;
      }

      content = Object.toHTML(content);

      tagName = ((position == 'before' || position == 'after')
        ? element.parentNode : element).tagName.toUpperCase();

      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

      if (position == 'top' || position == 'after') childNodes.reverse();
      childNodes.each(insert.curry(element));

      content.evalScripts.bind(content).defer();
    }

    return element;
  },

  wrap: function(element, wrapper, attributes) {
    element = $(element);
    if (Object.isElement(wrapper))
      $(wrapper).writeAttribute(attributes || { });
    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);
    else wrapper = new Element('div', wrapper);
    if (element.parentNode)
      element.parentNode.replaceChild(wrapper, element);
    wrapper.appendChild(element);
    return wrapper;
  },

  inspect: function(element) {
    element = $(element);
    var result = '<' + element.tagName.toLowerCase();
    $H({'id': 'id', 'className': 'class'}).each(function(pair) {
      var property = pair.first(), attribute = pair.last();
      var value = (element[property] || '').toString();
      if (value) result += ' ' + attribute + '=' + value.inspect(true);
    });
    return result + '>';
  },

  recursivelyCollect: function(element, property) {
    element = $(element);
    var elements = [];
    while (element = element[property])
      if (element.nodeType == 1)
        elements.push(Element.extend(element));
    return elements;
  },

  ancestors: function(element) {
    return $(element).recursivelyCollect('parentNode');
  },

  descendants: function(element) {
    return $(element).select("*");
  },

  firstDescendant: function(element) {
    element = $(element).firstChild;
    while (element && element.nodeType != 1) element = element.nextSibling;
    return $(element);
  },

  immediateDescendants: function(element) {
    if (!(element = $(element).firstChild)) return [];
    while (element && element.nodeType != 1) element = element.nextSibling;
    if (element) return [element].concat($(element).nextSiblings());
    return [];
  },

  previousSiblings: function(element) {
    return $(element).recursivelyCollect('previousSibling');
  },

  nextSiblings: function(element) {
    return $(element).recursivelyCollect('nextSibling');
  },

  siblings: function(element) {
    element = $(element);
    return element.previousSiblings().reverse().concat(element.nextSiblings());
  },

  match: function(element, selector) {
    if (Object.isString(selector))
      selector = new Selector(selector);
    return selector.match($(element));
  },

  up: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(element.parentNode);
    var ancestors = element.ancestors();
    return Object.isNumber(expression) ? ancestors[expression] :
      Selector.findElement(ancestors, expression, index);
  },

  down: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return element.firstDescendant();
    return Object.isNumber(expression) ? element.descendants()[expression] :
      Element.select(element, expression)[index || 0];
  },

  previous: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));
    var previousSiblings = element.previousSiblings();
    return Object.isNumber(expression) ? previousSiblings[expression] :
      Selector.findElement(previousSiblings, expression, index);
  },

  next: function(element, expression, index) {
    element = $(element);
    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));
    var nextSiblings = element.nextSiblings();
    return Object.isNumber(expression) ? nextSiblings[expression] :
      Selector.findElement(nextSiblings, expression, index);
  },

  select: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element, args);
  },

  adjacent: function() {
    var args = $A(arguments), element = $(args.shift());
    return Selector.findChildElements(element.parentNode, args).without(element);
  },

  identify: function(element) {
    element = $(element);
    var id = element.readAttribute('id'), self = arguments.callee;
    if (id) return id;
    do { id = 'anonymous_element_' + self.counter++ } while ($(id));
    element.writeAttribute('id', id);
    return id;
  },

  readAttribute: function(element, name) {
    element = $(element);
    if (Prototype.Browser.IE) {
      var t = Element._attributeTranslations.read;
      if (t.values[name]) return t.values[name](element, name);
      if (t.names[name]) name = t.names[name];
      if (name.include(':')) {
        return (!element.attributes || !element.attributes[name]) ? null :
         element.attributes[name].value;
      }
    }
    return element.getAttribute(name);
  },

  writeAttribute: function(element, name, value) {
    element = $(element);
    var attributes = { }, t = Element._attributeTranslations.write;

    if (typeof name == 'object') attributes = name;
    else attributes[name] = Object.isUndefined(value) ? true : value;

    for (var attr in attributes) {
      name = t.names[attr] || attr;
      value = attributes[attr];
      if (t.values[attr]) name = t.values[attr](element, value);
      if (value === false || value === null)
        element.removeAttribute(name);
      else if (value === true)
        element.setAttribute(name, name);
      else element.setAttribute(name, value);
    }
    return element;
  },

  getHeight: function(element) {
    return $(element).getDimensions().height;
  },

  getWidth: function(element) {
    return $(element).getDimensions().width;
  },

  classNames: function(element) {
    return new Element.ClassNames(element);
  },

  hasClassName: function(element, className) {
    if (!(element = $(element))) return;
    var elementClassName = element.className;
    return (elementClassName.length > 0 && (elementClassName == className ||
      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));
  },

  addClassName: function(element, className) {
    if (!(element = $(element))) return;
    if (!element.hasClassName(className))
      element.className += (element.className ? ' ' : '') + className;
    return element;
  },

  removeClassName: function(element, className) {
    if (!(element = $(element))) return;
    element.className = element.className.replace(
      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();
    return element;
  },

  toggleClassName: function(element, className) {
    if (!(element = $(element))) return;
    return element[element.hasClassName(className) ?
      'removeClassName' : 'addClassName'](className);
  },

  // removes whitespace-only text node children
  cleanWhitespace: function(element) {
    element = $(element);
    var node = element.firstChild;
    while (node) {
      var nextNode = node.nextSibling;
      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))
        element.removeChild(node);
      node = nextNode;
    }
    return element;
  },

  empty: function(element) {
    return $(element).innerHTML.blank();
  },

  descendantOf: function(element, ancestor) {
    element = $(element), ancestor = $(ancestor);

    if (element.compareDocumentPosition)
      return (element.compareDocumentPosition(ancestor) & 8) === 8;

    if (ancestor.contains)
      return ancestor.contains(element) && ancestor !== element;

    while (element = element.parentNode)
      if (element == ancestor) return true;

    return false;
  },

  scrollTo: function(element) {
    element = $(element);
    var pos = element.cumulativeOffset();
    window.scrollTo(pos[0], pos[1]);
    return element;
  },

  getStyle: function(element, style) {
    element = $(element);
    style = style == 'float' ? 'cssFloat' : style.camelize();
    var value = element.style[style];
    if (!value || value == 'auto') {
      var css = document.defaultView.getComputedStyle(element, null);
      value = css ? css[style] : null;
    }
    if (style == 'opacity') return value ? parseFloat(value) : 1.0;
    return value == 'auto' ? null : value;
  },

  getOpacity: function(element) {
    return $(element).getStyle('opacity');
  },

  setStyle: function(element, styles) {
    element = $(element);
    var elementStyle = element.style, match;
    if (Object.isString(styles)) {
      element.style.cssText += ';' + styles;
      return styles.include('opacity') ?
        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;
    }
    for (var property in styles)
      if (property == 'opacity') element.setOpacity(styles[property]);
      else
        elementStyle[(property == 'float' || property == 'cssFloat') ?
          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :
            property] = styles[property];

    return element;
  },

  setOpacity: function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;
    return element;
  },

  getDimensions: function(element) {
    element = $(element);
    var display = element.getStyle('display');
    if (display != 'none' && display != null) // Safari bug
      return {width: element.offsetWidth, height: element.offsetHeight};

    // All *Width and *Height properties give 0 on elements with display none,
    // so enable the element temporarily
    var els = element.style;
    var originalVisibility = els.visibility;
    var originalPosition = els.position;
    var originalDisplay = els.display;
    els.visibility = 'hidden';
    els.position = 'absolute';
    els.display = 'block';
    var originalWidth = element.clientWidth;
    var originalHeight = element.clientHeight;
    els.display = originalDisplay;
    els.position = originalPosition;
    els.visibility = originalVisibility;
    return {width: originalWidth, height: originalHeight};
  },

  makePositioned: function(element) {
    element = $(element);
    var pos = Element.getStyle(element, 'position');
    if (pos == 'static' || !pos) {
      element._madePositioned = true;
      element.style.position = 'relative';
      // Opera returns the offset relative to the positioning context, when an
      // element is position relative but top and left have not been defined
      if (Prototype.Browser.Opera) {
        element.style.top = 0;
        element.style.left = 0;
      }
    }
    return element;
  },

  undoPositioned: function(element) {
    element = $(element);
    if (element._madePositioned) {
      element._madePositioned = undefined;
      element.style.position =
        element.style.top =
        element.style.left =
        element.style.bottom =
        element.style.right = '';
    }
    return element;
  },

  makeClipping: function(element) {
    element = $(element);
    if (element._overflow) return element;
    element._overflow = Element.getStyle(element, 'overflow') || 'auto';
    if (element._overflow !== 'hidden')
      element.style.overflow = 'hidden';
    return element;
  },

  undoClipping: function(element) {
    element = $(element);
    if (!element._overflow) return element;
    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;
    element._overflow = null;
    return element;
  },

  cumulativeOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  positionedOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      element = element.offsetParent;
      if (element) {
        if (element.tagName.toUpperCase() == 'BODY') break;
        var p = Element.getStyle(element, 'position');
        if (p !== 'static') break;
      }
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  absolutize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'absolute') return element;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    var offsets = element.positionedOffset();
    var top     = offsets[1];
    var left    = offsets[0];
    var width   = element.clientWidth;
    var height  = element.clientHeight;

    element._originalLeft   = left - parseFloat(element.style.left  || 0);
    element._originalTop    = top  - parseFloat(element.style.top || 0);
    element._originalWidth  = element.style.width;
    element._originalHeight = element.style.height;

    element.style.position = 'absolute';
    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.width  = width + 'px';
    element.style.height = height + 'px';
    return element;
  },

  relativize: function(element) {
    element = $(element);
    if (element.getStyle('position') == 'relative') return element;
    // Position.prepare(); // To be done manually by Scripty when it needs it.

    element.style.position = 'relative';
    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);
    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

    element.style.top    = top + 'px';
    element.style.left   = left + 'px';
    element.style.height = element._originalHeight;
    element.style.width  = element._originalWidth;
    return element;
  },

  cumulativeScrollOffset: function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.scrollTop  || 0;
      valueL += element.scrollLeft || 0;
      element = element.parentNode;
    } while (element);
    return Element._returnOffset(valueL, valueT);
  },

  getOffsetParent: function(element) {
    if (element.offsetParent) return $(element.offsetParent);
    if (element == document.body) return $(element);

    while ((element = element.parentNode) && element != document.body)
      if (Element.getStyle(element, 'position') != 'static')
        return $(element);

    return $(document.body);
  },

  viewportOffset: function(forElement) {
    var valueT = 0, valueL = 0;

    var element = forElement;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;

      // Safari fix
      if (element.offsetParent == document.body &&
        Element.getStyle(element, 'position') == 'absolute') break;

    } while (element = element.offsetParent);

    element = forElement;
    do {
      if (!Prototype.Browser.Opera || (element.tagName && (element.tagName.toUpperCase() == 'BODY'))) {
        valueT -= element.scrollTop  || 0;
        valueL -= element.scrollLeft || 0;
      }
    } while (element = element.parentNode);

    return Element._returnOffset(valueL, valueT);
  },

  clonePosition: function(element, source) {
    var options = Object.extend({
      setLeft:    true,
      setTop:     true,
      setWidth:   true,
      setHeight:  true,
      offsetTop:  0,
      offsetLeft: 0
    }, arguments[2] || { });

    // find page position of source
    source = $(source);
    var p = source.viewportOffset();

    // find coordinate system to use
    element = $(element);
    var delta = [0, 0];
    var parent = null;
    // delta [0,0] will do fine with position: fixed elements,
    // position:absolute needs offsetParent deltas
    if (Element.getStyle(element, 'position') == 'absolute') {
      parent = element.getOffsetParent();
      delta = parent.viewportOffset();
    }

    // correct by body offsets (fixes Safari)
    if (parent == document.body) {
      delta[0] -= document.body.offsetLeft;
      delta[1] -= document.body.offsetTop;
    }

    // set position
    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';
    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';
    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';
    if (options.setHeight) element.style.height = source.offsetHeight + 'px';
    return element;
  }
};

Element.Methods.identify.counter = 1;

Object.extend(Element.Methods, {
  getElementsBySelector: Element.Methods.select,
  childElements: Element.Methods.immediateDescendants
});

Element._attributeTranslations = {
  write: {
    names: {
      className: 'class',
      htmlFor:   'for'
    },
    values: { }
  }
};

if (Prototype.Browser.Opera) {
  Element.Methods.getStyle = Element.Methods.getStyle.wrap(
    function(proceed, element, style) {
      switch (style) {
        case 'left': case 'top': case 'right': case 'bottom':
          if (proceed(element, 'position') === 'static') return null;
        case 'height': case 'width':
          // returns '0px' for hidden elements; we want it to return null
          if (!Element.visible(element)) return null;

          // returns the border-box dimensions rather than the content-box
          // dimensions, so we subtract padding and borders from the value
          var dim = parseInt(proceed(element, style), 10);

          if (dim !== element['offset' + style.capitalize()])
            return dim + 'px';

          var properties;
          if (style === 'height') {
            properties = ['border-top-width', 'padding-top',
             'padding-bottom', 'border-bottom-width'];
          }
          else {
            properties = ['border-left-width', 'padding-left',
             'padding-right', 'border-right-width'];
          }
          return properties.inject(dim, function(memo, property) {
            var val = proceed(element, property);
            return val === null ? memo : memo - parseInt(val, 10);
          }) + 'px';
        default: return proceed(element, style);
      }
    }
  );

  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(
    function(proceed, element, attribute) {
      if (attribute === 'title') return element.title;
      return proceed(element, attribute);
    }
  );
}

else if (Prototype.Browser.IE) {
  // IE doesn't report offsets correctly for static elements, so we change them
  // to "relative" to get the values, then change them back.
  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(
    function(proceed, element) {
      element = $(element);
      // IE throws an error if element is not in document
      try { element.offsetParent }
      catch(e) { return $(document.body) }
      var position = element.getStyle('position');
      if (position !== 'static') return proceed(element);
      element.setStyle({ position: 'relative' });
      var value = proceed(element);
      element.setStyle({ position: position });
      return value;
    }
  );

  $w('positionedOffset viewportOffset').each(function(method) {
    Element.Methods[method] = Element.Methods[method].wrap(
      function(proceed, element) {
        element = $(element);
        try { element.offsetParent }
        catch(e) { return Element._returnOffset(0,0) }
        var position = element.getStyle('position');
        if (position !== 'static') return proceed(element);
        // Trigger hasLayout on the offset parent so that IE6 reports
        // accurate offsetTop and offsetLeft values for position: fixed.
        var offsetParent = element.getOffsetParent();
        if (offsetParent && offsetParent.getStyle('position') === 'fixed')
          offsetParent.setStyle({ zoom: 1 });
        element.setStyle({ position: 'relative' });
        var value = proceed(element);
        element.setStyle({ position: position });
        return value;
      }
    );
  });

  Element.Methods.cumulativeOffset = Element.Methods.cumulativeOffset.wrap(
    function(proceed, element) {
      try { element.offsetParent }
      catch(e) { return Element._returnOffset(0,0) }
      return proceed(element);
    }
  );

  Element.Methods.getStyle = function(element, style) {
    element = $(element);
    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();
    var value = element.style[style];
    if (!value && element.currentStyle) value = element.currentStyle[style];

    if (style == 'opacity') {
      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))
        if (value[1]) return parseFloat(value[1]) / 100;
      return 1.0;
    }

    if (value == 'auto') {
      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))
        return element['offset' + style.capitalize()] + 'px';
      return null;
    }
    return value;
  };

  Element.Methods.setOpacity = function(element, value) {
    function stripAlpha(filter){
      return filter.replace(/alpha\([^\)]*\)/gi,'');
    }
    element = $(element);
    var currentStyle = element.currentStyle;
    if ((currentStyle && !currentStyle.hasLayout) ||
      (!currentStyle && element.style.zoom == 'normal'))
        element.style.zoom = 1;

    var filter = element.getStyle('filter'), style = element.style;
    if (value == 1 || value === '') {
      (filter = stripAlpha(filter)) ?
        style.filter = filter : style.removeAttribute('filter');
      return element;
    } else if (value < 0.00001) value = 0;
    style.filter = stripAlpha(filter) +
      'alpha(opacity=' + (value * 100) + ')';
    return element;
  };

  Element._attributeTranslations = {
    read: {
      names: {
        'class': 'className',
        'for':   'htmlFor'
      },
      values: {
        _getAttr: function(element, attribute) {
          return element.getAttribute(attribute, 2);
        },
        _getAttrNode: function(element, attribute) {
          var node = element.getAttributeNode(attribute);
          return node ? node.value : "";
        },
        _getEv: function(element, attribute) {
          attribute = element.getAttribute(attribute);
          return attribute ? attribute.toString().slice(23, -2) : null;
        },
        _flag: function(element, attribute) {
          return $(element).hasAttribute(attribute) ? attribute : null;
        },
        style: function(element) {
          return element.style.cssText.toLowerCase();
        },
        title: function(element) {
          return element.title;
        }
      }
    }
  };

  Element._attributeTranslations.write = {
    names: Object.extend({
      cellpadding: 'cellPadding',
      cellspacing: 'cellSpacing'
    }, Element._attributeTranslations.read.names),
    values: {
      checked: function(element, value) {
        element.checked = !!value;
      },

      style: function(element, value) {
        element.style.cssText = value ? value : '';
      }
    }
  };

  Element._attributeTranslations.has = {};

  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +
      'encType maxLength readOnly longDesc frameBorder').each(function(attr) {
    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;
    Element._attributeTranslations.has[attr.toLowerCase()] = attr;
  });

  (function(v) {
    Object.extend(v, {
      href:        v._getAttr,
      src:         v._getAttr,
      type:        v._getAttr,
      action:      v._getAttrNode,
      disabled:    v._flag,
      checked:     v._flag,
      readonly:    v._flag,
      multiple:    v._flag,
      onload:      v._getEv,
      onunload:    v._getEv,
      onclick:     v._getEv,
      ondblclick:  v._getEv,
      onmousedown: v._getEv,
      onmouseup:   v._getEv,
      onmouseover: v._getEv,
      onmousemove: v._getEv,
      onmouseout:  v._getEv,
      onfocus:     v._getEv,
      onblur:      v._getEv,
      onkeypress:  v._getEv,
      onkeydown:   v._getEv,
      onkeyup:     v._getEv,
      onsubmit:    v._getEv,
      onreset:     v._getEv,
      onselect:    v._getEv,
      onchange:    v._getEv
    });
  })(Element._attributeTranslations.read.values);
}

else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1) ? 0.999999 :
      (value === '') ? '' : (value < 0.00001) ? 0 : value;
    return element;
  };
}

else if (Prototype.Browser.WebKit) {
  Element.Methods.setOpacity = function(element, value) {
    element = $(element);
    element.style.opacity = (value == 1 || value === '') ? '' :
      (value < 0.00001) ? 0 : value;

    if (value == 1)
      if(element.tagName.toUpperCase() == 'IMG' && element.width) {
        element.width++; element.width--;
      } else try {
        var n = document.createTextNode(' ');
        element.appendChild(n);
        element.removeChild(n);
      } catch (e) { }

    return element;
  };

  // Safari returns margins on body which is incorrect if the child is absolutely
  // positioned.  For performance reasons, redefine Element#cumulativeOffset for
  // KHTML/WebKit only.
  Element.Methods.cumulativeOffset = function(element) {
    var valueT = 0, valueL = 0;
    do {
      valueT += element.offsetTop  || 0;
      valueL += element.offsetLeft || 0;
      if (element.offsetParent == document.body)
        if (Element.getStyle(element, 'position') == 'absolute') break;

      element = element.offsetParent;
    } while (element);

    return Element._returnOffset(valueL, valueT);
  };
}

if (Prototype.Browser.IE || Prototype.Browser.Opera) {
  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements
  Element.Methods.update = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) return element.update().insert(content);

    content = Object.toHTML(content);
    var tagName = element.tagName.toUpperCase();

    if (tagName in Element._insertionTranslations.tags) {
      $A(element.childNodes).each(function(node) { element.removeChild(node) });
      Element._getContentFromAnonymousElement(tagName, content.stripScripts())
        .each(function(node) { element.appendChild(node) });
    }
    else element.innerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

if ('outerHTML' in document.createElement('div')) {
  Element.Methods.replace = function(element, content) {
    element = $(element);

    if (content && content.toElement) content = content.toElement();
    if (Object.isElement(content)) {
      element.parentNode.replaceChild(content, element);
      return element;
    }

    content = Object.toHTML(content);
    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

    if (Element._insertionTranslations.tags[tagName]) {
      var nextSibling = element.next();
      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());
      parent.removeChild(element);
      if (nextSibling)
        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });
      else
        fragments.each(function(node) { parent.appendChild(node) });
    }
    else element.outerHTML = content.stripScripts();

    content.evalScripts.bind(content).defer();
    return element;
  };
}

Element._returnOffset = function(l, t) {
  var result = [l, t];
  result.left = l;
  result.top = t;
  return result;
};

Element._getContentFromAnonymousElement = function(tagName, html) {
  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];
  if (t) {
    div.innerHTML = t[0] + html + t[1];
    t[2].times(function() { div = div.firstChild });
  } else div.innerHTML = html;
  return $A(div.childNodes);
};

Element._insertionTranslations = {
  before: function(element, node) {
    element.parentNode.insertBefore(node, element);
  },
  top: function(element, node) {
    element.insertBefore(node, element.firstChild);
  },
  bottom: function(element, node) {
    element.appendChild(node);
  },
  after: function(element, node) {
    element.parentNode.insertBefore(node, element.nextSibling);
  },
  tags: {
    TABLE:  ['<table>',                '</table>',                   1],
    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],
    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],
    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],
    SELECT: ['<select>',               '</select>',                  1]
  }
};

(function() {
  Object.extend(this.tags, {
    THEAD: this.tags.TBODY,
    TFOOT: this.tags.TBODY,
    TH:    this.tags.TD
  });
}).call(Element._insertionTranslations);

Element.Methods.Simulated = {
  hasAttribute: function(element, attribute) {
    attribute = Element._attributeTranslations.has[attribute] || attribute;
    var node = $(element).getAttributeNode(attribute);
    return !!(node && node.specified);
  }
};

Element.Methods.ByTag = { };

Object.extend(Element, Element.Methods);

if (!Prototype.BrowserFeatures.ElementExtensions &&
    document.createElement('div')['__proto__']) {
  window.HTMLElement = { };
  window.HTMLElement.prototype = document.createElement('div')['__proto__'];
  Prototype.BrowserFeatures.ElementExtensions = true;
}

Element.extend = (function() {
  if (Prototype.BrowserFeatures.SpecificElementExtensions)
    return Prototype.K;

  var Methods = { }, ByTag = Element.Methods.ByTag;

  var extend = Object.extend(function(element) {
    if (!element || element._extendedByPrototype ||
        element.nodeType != 1 || element == window) return element;

    var methods = Object.clone(Methods),
      tagName = element.tagName.toUpperCase(), property, value;

    // extend methods for specific tags
    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

    for (property in methods) {
      value = methods[property];
      if (Object.isFunction(value) && !(property in element))
        element[property] = value.methodize();
    }

    element._extendedByPrototype = Prototype.emptyFunction;
    return element;

  }, {
    refresh: function() {
      // extend methods for all tags (Safari doesn't need this)
      if (!Prototype.BrowserFeatures.ElementExtensions) {
        Object.extend(Methods, Element.Methods);
        Object.extend(Methods, Element.Methods.Simulated);
      }
    }
  });

  extend.refresh();
  return extend;
})();

Element.hasAttribute = function(element, attribute) {
  if (element.hasAttribute) return element.hasAttribute(attribute);
  return Element.Methods.Simulated.hasAttribute(element, attribute);
};

Element.addMethods = function(methods) {
  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

  if (!methods) {
    Object.extend(Form, Form.Methods);
    Object.extend(Form.Element, Form.Element.Methods);
    Object.extend(Element.Methods.ByTag, {
      "FORM":     Object.clone(Form.Methods),
      "INPUT":    Object.clone(Form.Element.Methods),
      "SELECT":   Object.clone(Form.Element.Methods),
      "TEXTAREA": Object.clone(Form.Element.Methods)
    });
  }

  if (arguments.length == 2) {
    var tagName = methods;
    methods = arguments[1];
  }

  if (!tagName) Object.extend(Element.Methods, methods || { });
  else {
    if (Object.isArray(tagName)) tagName.each(extend);
    else extend(tagName);
  }

  function extend(tagName) {
    tagName = tagName.toUpperCase();
    if (!Element.Methods.ByTag[tagName])
      Element.Methods.ByTag[tagName] = { };
    Object.extend(Element.Methods.ByTag[tagName], methods);
  }

  function copy(methods, destination, onlyIfAbsent) {
    onlyIfAbsent = onlyIfAbsent || false;
    for (var property in methods) {
      var value = methods[property];
      if (!Object.isFunction(value)) continue;
      if (!onlyIfAbsent || !(property in destination))
        destination[property] = value.methodize();
    }
  }

  function findDOMClass(tagName) {
    var klass;
    var trans = {
      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",
      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",
      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",
      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",
      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":
      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":
      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":
      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":
      "FrameSet", "IFRAME": "IFrame"
    };
    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName + 'Element';
    if (window[klass]) return window[klass];
    klass = 'HTML' + tagName.capitalize() + 'Element';
    if (window[klass]) return window[klass];

    window[klass] = { };
    window[klass].prototype = document.createElement(tagName)['__proto__'];
    return window[klass];
  }

  if (F.ElementExtensions) {
    copy(Element.Methods, HTMLElement.prototype);
    copy(Element.Methods.Simulated, HTMLElement.prototype, true);
  }

  if (F.SpecificElementExtensions) {
    for (var tag in Element.Methods.ByTag) {
      var klass = findDOMClass(tag);
      if (Object.isUndefined(klass)) continue;
      copy(T[tag], klass.prototype);
    }
  }

  Object.extend(Element, Element.Methods);
  delete Element.ByTag;

  if (Element.extend.refresh) Element.extend.refresh();
  Element.cache = { };
};

document.viewport = {
  getDimensions: function() {
    var dimensions = { }, B = Prototype.Browser;
    $w('width height').each(function(d) {
      var D = d.capitalize();
      if (B.WebKit && !document.evaluate) {
        // Safari <3.0 needs self.innerWidth/Height
        dimensions[d] = self['inner' + D];
      } else if (B.Opera && parseFloat(window.opera.version()) < 9.5) {
        // Opera <9.5 needs document.body.clientWidth/Height
        dimensions[d] = document.body['client' + D]
      } else {
        dimensions[d] = document.documentElement['client' + D];
      }
    });
    return dimensions;
  },

  getWidth: function() {
    return this.getDimensions().width;
  },

  getHeight: function() {
    return this.getDimensions().height;
  },

  getScrollOffsets: function() {
    return Element._returnOffset(
      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,
      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);
  }
};
/* Portions of the Selector class are derived from Jack Slocum's DomQuery,
 * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style
 * license.  Please see http://www.yui-ext.com/ for more information. */

var Selector = Class.create({
  initialize: function(expression) {
    this.expression = expression.strip();

    if (this.shouldUseSelectorsAPI()) {
      this.mode = 'selectorsAPI';
    } else if (this.shouldUseXPath()) {
      this.mode = 'xpath';
      this.compileXPathMatcher();
    } else {
      this.mode = "normal";
      this.compileMatcher();
    }

  },

  shouldUseXPath: function() {
    if (!Prototype.BrowserFeatures.XPath) return false;

    var e = this.expression;

    // Safari 3 chokes on :*-of-type and :empty
    if (Prototype.Browser.WebKit &&
     (e.include("-of-type") || e.include(":empty")))
      return false;

    // XPath can't do namespaced attributes, nor can it read
    // the "checked" property from DOM nodes
    if ((/(\[[\w-]*?:|:checked)/).test(e))
      return false;

    return true;
  },

  shouldUseSelectorsAPI: function() {
    if (!Prototype.BrowserFeatures.SelectorsAPI) return false;

    if (!Selector._div) Selector._div = new Element('div');

    // Make sure the browser treats the selector as valid. Test on an
    // isolated element to minimize cost of this check.
    try {
      Selector._div.querySelector(this.expression);
    } catch(e) {
      return false;
    }

    return true;
  },

  compileMatcher: function() {
    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,
        c = Selector.criteria, le, p, m;

    if (Selector._cache[e]) {
      this.matcher = Selector._cache[e];
      return;
    }

    this.matcher = ["this.matcher = function(root) {",
                    "var r = root, h = Selector.handlers, c = false, n;"];

    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :
            new Template(c[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.matcher.push("return h.unique(n);\n}");
    eval(this.matcher.join('\n'));
    Selector._cache[this.expression] = this.matcher;
  },

  compileXPathMatcher: function() {
    var e = this.expression, ps = Selector.patterns,
        x = Selector.xpath, le, m;

    if (Selector._cache[e]) {
      this.xpath = Selector._cache[e]; return;
    }

    this.matcher = ['.//*'];
    while (e && le != e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        if (m = e.match(ps[i])) {
          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :
            new Template(x[i]).evaluate(m));
          e = e.replace(m[0], '');
          break;
        }
      }
    }

    this.xpath = this.matcher.join('');
    Selector._cache[this.expression] = this.xpath;
  },

  findElements: function(root) {
    root = root || document;
    var e = this.expression, results;

    switch (this.mode) {
      case 'selectorsAPI':
        // querySelectorAll queries document-wide, then filters to descendants
        // of the context element. That's not what we want.
        // Add an explicit context to the selector if necessary.
        if (root !== document) {
          var oldId = root.id, id = $(root).identify();
          e = "#" + id + " " + e;
        }

        results = $A(root.querySelectorAll(e)).map(Element.extend);
        root.id = oldId;

        return results;
      case 'xpath':
        return document._getElementsByXPath(this.xpath, root);
      default:
       return this.matcher(root);
    }
  },

  match: function(element) {
    this.tokens = [];

    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;
    var le, p, m;

    while (e && le !== e && (/\S/).test(e)) {
      le = e;
      for (var i in ps) {
        p = ps[i];
        if (m = e.match(p)) {
          // use the Selector.assertions methods unless the selector
          // is too complex.
          if (as[i]) {
            this.tokens.push([i, Object.clone(m)]);
            e = e.replace(m[0], '');
          } else {
            // reluctantly do a document-wide search
            // and look for a match in the array
            return this.findElements(document).include(element);
          }
        }
      }
    }

    var match = true, name, matches;
    for (var i = 0, token; token = this.tokens[i]; i++) {
      name = token[0], matches = token[1];
      if (!Selector.assertions[name](element, matches)) {
        match = false; break;
      }
    }

    return match;
  },

  toString: function() {
    return this.expression;
  },

  inspect: function() {
    return "#<Selector:" + this.expression.inspect() + ">";
  }
});

Object.extend(Selector, {
  _cache: { },

  xpath: {
    descendant:   "//*",
    child:        "/*",
    adjacent:     "/following-sibling::*[1]",
    laterSibling: '/following-sibling::*',
    tagName:      function(m) {
      if (m[1] == '*') return '';
      return "[local-name()='" + m[1].toLowerCase() +
             "' or local-name()='" + m[1].toUpperCase() + "']";
    },
    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",
    id:           "[@id='#{1}']",
    attrPresence: function(m) {
      m[1] = m[1].toLowerCase();
      return new Template("[@#{1}]").evaluate(m);
    },
    attr: function(m) {
      m[1] = m[1].toLowerCase();
      m[3] = m[5] || m[6];
      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);
    },
    pseudo: function(m) {
      var h = Selector.xpath.pseudos[m[1]];
      if (!h) return '';
      if (Object.isFunction(h)) return h(m);
      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);
    },
    operators: {
      '=':  "[@#{1}='#{3}']",
      '!=': "[@#{1}!='#{3}']",
      '^=': "[starts-with(@#{1}, '#{3}')]",
      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",
      '*=': "[contains(@#{1}, '#{3}')]",
      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",
      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"
    },
    pseudos: {
      'first-child': '[not(preceding-sibling::*)]',
      'last-child':  '[not(following-sibling::*)]',
      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',
      'empty':       "[count(*) = 0 and (count(text()) = 0)]",
      'checked':     "[@checked]",
      'disabled':    "[(@disabled) and (@type!='hidden')]",
      'enabled':     "[not(@disabled) and (@type!='hidden')]",
      'not': function(m) {
        var e = m[6], p = Selector.patterns,
            x = Selector.xpath, le, v;

        var exclusion = [];
        while (e && le != e && (/\S/).test(e)) {
          le = e;
          for (var i in p) {
            if (m = e.match(p[i])) {
              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);
              exclusion.push("(" + v.substring(1, v.length - 1) + ")");
              e = e.replace(m[0], '');
              break;
            }
          }
        }
        return "[not(" + exclusion.join(" and ") + ")]";
      },
      'nth-child':      function(m) {
        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);
      },
      'nth-last-child': function(m) {
        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);
      },
      'nth-of-type':    function(m) {
        return Selector.xpath.pseudos.nth("position() ", m);
      },
      'nth-last-of-type': function(m) {
        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);
      },
      'first-of-type':  function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);
      },
      'last-of-type':   function(m) {
        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);
      },
      'only-of-type':   function(m) {
        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);
      },
      nth: function(fragment, m) {
        var mm, formula = m[6], predicate;
        if (formula == 'even') formula = '2n+0';
        if (formula == 'odd')  formula = '2n+1';
        if (mm = formula.match(/^(\d+)$/)) // digit only
          return '[' + fragment + "= " + mm[1] + ']';
        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
          if (mm[1] == "-") mm[1] = -1;
          var a = mm[1] ? Number(mm[1]) : 1;
          var b = mm[2] ? Number(mm[2]) : 0;
          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +
          "((#{fragment} - #{b}) div #{a} >= 0)]";
          return new Template(predicate).evaluate({
            fragment: fragment, a: a, b: b });
        }
      }
    }
  },

  criteria: {
    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',
    className:    'n = h.className(n, r, "#{1}", c);    c = false;',
    id:           'n = h.id(n, r, "#{1}", c);           c = false;',
    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',
    attr: function(m) {
      m[3] = (m[5] || m[6]);
      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);
    },
    pseudo: function(m) {
      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');
      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);
    },
    descendant:   'c = "descendant";',
    child:        'c = "child";',
    adjacent:     'c = "adjacent";',
    laterSibling: 'c = "laterSibling";'
  },

  patterns: {
    // combinators must be listed first
    // (and descendant needs to be last combinator)
    laterSibling: /^\s*~\s*/,
    child:        /^\s*>\s*/,
    adjacent:     /^\s*\+\s*/,
    descendant:   /^\s/,

    // selectors follow
    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,
    id:           /^#([\w\-\*]+)(\b|$)/,
    className:    /^\.([\w\-\*]+)(\b|$)/,
    pseudo:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,
    attrPresence: /^\[((?:[\w]+:)?[\w]+)\]/,
    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/
  },

  // for Selector.match and Element#match
  assertions: {
    tagName: function(element, matches) {
      return matches[1].toUpperCase() == element.tagName.toUpperCase();
    },

    className: function(element, matches) {
      return Element.hasClassName(element, matches[1]);
    },

    id: function(element, matches) {
      return element.id === matches[1];
    },

    attrPresence: function(element, matches) {
      return Element.hasAttribute(element, matches[1]);
    },

    attr: function(element, matches) {
      var nodeValue = Element.readAttribute(element, matches[1]);
      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);
    }
  },

  handlers: {
    // UTILITY FUNCTIONS
    // joins two collections
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        a.push(node);
      return a;
    },

    // marks an array of nodes for counting
    mark: function(nodes) {
      var _true = Prototype.emptyFunction;
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = _true;
      return nodes;
    },

    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node._countedByPrototype = undefined;
      return nodes;
    },

    // mark each child node with its position (for nth calls)
    // "ofType" flag indicates whether we're indexing for nth-of-type
    // rather than nth-child
    index: function(parentNode, reverse, ofType) {
      parentNode._countedByPrototype = Prototype.emptyFunction;
      if (reverse) {
        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {
          var node = nodes[i];
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
        }
      } else {
        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)
          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;
      }
    },

    // filters out duplicates and extends all nodes
    unique: function(nodes) {
      if (nodes.length == 0) return nodes;
      var results = [], n;
      for (var i = 0, l = nodes.length; i < l; i++)
        if (!(n = nodes[i])._countedByPrototype) {
          n._countedByPrototype = Prototype.emptyFunction;
          results.push(Element.extend(n));
        }
      return Selector.handlers.unmark(results);
    },

    // COMBINATOR FUNCTIONS
    descendant: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, node.getElementsByTagName('*'));
      return results;
    },

    child: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        for (var j = 0, child; child = node.childNodes[j]; j++)
          if (child.nodeType == 1 && child.tagName != '!') results.push(child);
      }
      return results;
    },

    adjacent: function(nodes) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        var next = this.nextElementSibling(node);
        if (next) results.push(next);
      }
      return results;
    },

    laterSibling: function(nodes) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        h.concat(results, Element.nextSiblings(node));
      return results;
    },

    nextElementSibling: function(node) {
      while (node = node.nextSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    previousElementSibling: function(node) {
      while (node = node.previousSibling)
        if (node.nodeType == 1) return node;
      return null;
    },

    // TOKEN FUNCTIONS
    tagName: function(nodes, root, tagName, combinator) {
      var uTagName = tagName.toUpperCase();
      var results = [], h = Selector.handlers;
      if (nodes) {
        if (combinator) {
          // fastlane for ordinary descendant combinators
          if (combinator == "descendant") {
            for (var i = 0, node; node = nodes[i]; i++)
              h.concat(results, node.getElementsByTagName(tagName));
            return results;
          } else nodes = this[combinator](nodes);
          if (tagName == "*") return nodes;
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.tagName.toUpperCase() === uTagName) results.push(node);
        return results;
      } else return root.getElementsByTagName(tagName);
    },

    id: function(nodes, root, id, combinator) {
      var targetNode = $(id), h = Selector.handlers;
      if (!targetNode) return [];
      if (!nodes && root == document) return [targetNode];
      if (nodes) {
        if (combinator) {
          if (combinator == 'child') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (targetNode.parentNode == node) return [targetNode];
          } else if (combinator == 'descendant') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Element.descendantOf(targetNode, node)) return [targetNode];
          } else if (combinator == 'adjacent') {
            for (var i = 0, node; node = nodes[i]; i++)
              if (Selector.handlers.previousElementSibling(targetNode) == node)
                return [targetNode];
          } else nodes = h[combinator](nodes);
        }
        for (var i = 0, node; node = nodes[i]; i++)
          if (node == targetNode) return [targetNode];
        return [];
      }
      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];
    },

    className: function(nodes, root, className, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      return Selector.handlers.byClassName(nodes, root, className);
    },

    byClassName: function(nodes, root, className) {
      if (!nodes) nodes = Selector.handlers.descendant([root]);
      var needle = ' ' + className + ' ';
      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {
        nodeClassName = node.className;
        if (nodeClassName.length == 0) continue;
        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))
          results.push(node);
      }
      return results;
    },

    attrPresence: function(nodes, root, attr, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var results = [];
      for (var i = 0, node; node = nodes[i]; i++)
        if (Element.hasAttribute(node, attr)) results.push(node);
      return results;
    },

    attr: function(nodes, root, attr, value, operator, combinator) {
      if (!nodes) nodes = root.getElementsByTagName("*");
      if (nodes && combinator) nodes = this[combinator](nodes);
      var handler = Selector.operators[operator], results = [];
      for (var i = 0, node; node = nodes[i]; i++) {
        var nodeValue = Element.readAttribute(node, attr);
        if (nodeValue === null) continue;
        if (handler(nodeValue, value)) results.push(node);
      }
      return results;
    },

    pseudo: function(nodes, name, value, root, combinator) {
      if (nodes && combinator) nodes = this[combinator](nodes);
      if (!nodes) nodes = root.getElementsByTagName("*");
      return Selector.pseudos[name](nodes, value, root);
    }
  },

  pseudos: {
    'first-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.previousElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'last-child': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        if (Selector.handlers.nextElementSibling(node)) continue;
          results.push(node);
      }
      return results;
    },
    'only-child': function(nodes, value, root) {
      var h = Selector.handlers;
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))
          results.push(node);
      return results;
    },
    'nth-child':        function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root);
    },
    'nth-last-child':   function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true);
    },
    'nth-of-type':      function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, false, true);
    },
    'nth-last-of-type': function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, formula, root, true, true);
    },
    'first-of-type':    function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, false, true);
    },
    'last-of-type':     function(nodes, formula, root) {
      return Selector.pseudos.nth(nodes, "1", root, true, true);
    },
    'only-of-type':     function(nodes, formula, root) {
      var p = Selector.pseudos;
      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);
    },

    // handles the an+b logic
    getIndices: function(a, b, total) {
      if (a == 0) return b > 0 ? [b] : [];
      return $R(1, total).inject([], function(memo, i) {
        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);
        return memo;
      });
    },

    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type
    nth: function(nodes, formula, root, reverse, ofType) {
      if (nodes.length == 0) return [];
      if (formula == 'even') formula = '2n+0';
      if (formula == 'odd')  formula = '2n+1';
      var h = Selector.handlers, results = [], indexed = [], m;
      h.mark(nodes);
      for (var i = 0, node; node = nodes[i]; i++) {
        if (!node.parentNode._countedByPrototype) {
          h.index(node.parentNode, reverse, ofType);
          indexed.push(node.parentNode);
        }
      }
      if (formula.match(/^\d+$/)) { // just a number
        formula = Number(formula);
        for (var i = 0, node; node = nodes[i]; i++)
          if (node.nodeIndex == formula) results.push(node);
      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b
        if (m[1] == "-") m[1] = -1;
        var a = m[1] ? Number(m[1]) : 1;
        var b = m[2] ? Number(m[2]) : 0;
        var indices = Selector.pseudos.getIndices(a, b, nodes.length);
        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {
          for (var j = 0; j < l; j++)
            if (node.nodeIndex == indices[j]) results.push(node);
        }
      }
      h.unmark(nodes);
      h.unmark(indexed);
      return results;
    },

    'empty': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++) {
        // IE treats comments as element nodes
        if (node.tagName == '!' || node.firstChild) continue;
        results.push(node);
      }
      return results;
    },

    'not': function(nodes, selector, root) {
      var h = Selector.handlers, selectorType, m;
      var exclusions = new Selector(selector).findElements(root);
      h.mark(exclusions);
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node._countedByPrototype) results.push(node);
      h.unmark(exclusions);
      return results;
    },

    'enabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (!node.disabled && (!node.type || node.type !== 'hidden'))
          results.push(node);
      return results;
    },

    'disabled': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.disabled) results.push(node);
      return results;
    },

    'checked': function(nodes, value, root) {
      for (var i = 0, results = [], node; node = nodes[i]; i++)
        if (node.checked) results.push(node);
      return results;
    }
  },

  operators: {
    '=':  function(nv, v) { return nv == v; },
    '!=': function(nv, v) { return nv != v; },
    '^=': function(nv, v) { return nv == v || nv && nv.startsWith(v); },
    '$=': function(nv, v) { return nv == v || nv && nv.endsWith(v); },
    '*=': function(nv, v) { return nv == v || nv && nv.include(v); },
    '$=': function(nv, v) { return nv.endsWith(v); },
    '*=': function(nv, v) { return nv.include(v); },
    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },
    '|=': function(nv, v) { return ('-' + (nv || "").toUpperCase() +
     '-').include('-' + (v || "").toUpperCase() + '-'); }
  },

  split: function(expression) {
    var expressions = [];
    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
      expressions.push(m[1].strip());
    });
    return expressions;
  },

  matchElements: function(elements, expression) {
    var matches = $$(expression), h = Selector.handlers;
    h.mark(matches);
    for (var i = 0, results = [], element; element = elements[i]; i++)
      if (element._countedByPrototype) results.push(element);
    h.unmark(matches);
    return results;
  },

  findElement: function(elements, expression, index) {
    if (Object.isNumber(expression)) {
      index = expression; expression = false;
    }
    return Selector.matchElements(elements, expression || '*')[index || 0];
  },

  findChildElements: function(element, expressions) {
    expressions = Selector.split(expressions.join(','));
    var results = [], h = Selector.handlers;
    for (var i = 0, l = expressions.length, selector; i < l; i++) {
      selector = new Selector(expressions[i].strip());
      h.concat(results, selector.findElements(element));
    }
    return (l > 1) ? h.unique(results) : results;
  }
});

if (Prototype.Browser.IE) {
  Object.extend(Selector.handlers, {
    // IE returns comment nodes on getElementsByTagName("*").
    // Filter them out.
    concat: function(a, b) {
      for (var i = 0, node; node = b[i]; i++)
        if (node.tagName !== "!") a.push(node);
      return a;
    },

    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.
    unmark: function(nodes) {
      for (var i = 0, node; node = nodes[i]; i++)
        node.removeAttribute('_countedByPrototype');
      return nodes;
    }
  });
}

function $$() {
  return Selector.findChildElements(document, $A(arguments));
}
var Form = {
  reset: function(form) {
    $(form).reset();
    return form;
  },

  serializeElements: function(elements, options) {
    if (typeof options != 'object') options = { hash: !!options };
    else if (Object.isUndefined(options.hash)) options.hash = true;
    var key, value, submitted = false, submit = options.submit;

    var data = elements.inject({ }, function(result, element) {
      if (!element.disabled && element.name) {
        key = element.name; value = $(element).getValue();
        if (value != null && element.type != 'file' && (element.type != 'submit' || (!submitted &&
            submit !== false && (!submit || key == submit) && (submitted = true)))) {
          if (key in result) {
            // a key is already present; construct an array of values
            if (!Object.isArray(result[key])) result[key] = [result[key]];
            result[key].push(value);
          }
          else result[key] = value;
        }
      }
      return result;
    });

    return options.hash ? data : Object.toQueryString(data);
  }
};

Form.Methods = {
  serialize: function(form, options) {
    return Form.serializeElements(Form.getElements(form), options);
  },

  getElements: function(form) {
    return $A($(form).getElementsByTagName('*')).inject([],
      function(elements, child) {
        if (Form.Element.Serializers[child.tagName.toLowerCase()])
          elements.push(Element.extend(child));
        return elements;
      }
    );
  },

  getInputs: function(form, typeName, name) {
    form = $(form);
    var inputs = form.getElementsByTagName('input');

    if (!typeName && !name) return $A(inputs).map(Element.extend);

    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {
      var input = inputs[i];
      if ((typeName && input.type != typeName) || (name && input.name != name))
        continue;
      matchingInputs.push(Element.extend(input));
    }

    return matchingInputs;
  },

  disable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('disable');
    return form;
  },

  enable: function(form) {
    form = $(form);
    Form.getElements(form).invoke('enable');
    return form;
  },

  findFirstElement: function(form) {
    var elements = $(form).getElements().findAll(function(element) {
      return 'hidden' != element.type && !element.disabled;
    });
    var firstByIndex = elements.findAll(function(element) {
      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;
    }).sortBy(function(element) { return element.tabIndex }).first();

    return firstByIndex ? firstByIndex : elements.find(function(element) {
      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());
    });
  },

  focusFirstElement: function(form) {
    form = $(form);
    form.findFirstElement().activate();
    return form;
  },

  request: function(form, options) {
    form = $(form), options = Object.clone(options || { });

    var params = options.parameters, action = form.readAttribute('action') || '';
    if (action.blank()) action = window.location.href;
    options.parameters = form.serialize(true);

    if (params) {
      if (Object.isString(params)) params = params.toQueryParams();
      Object.extend(options.parameters, params);
    }

    if (form.hasAttribute('method') && !options.method)
      options.method = form.method;

    return new Ajax.Request(action, options);
  }
};

/*--------------------------------------------------------------------------*/

Form.Element = {
  focus: function(element) {
    $(element).focus();
    return element;
  },

  select: function(element) {
    $(element).select();
    return element;
  }
};

Form.Element.Methods = {
  serialize: function(element) {
    element = $(element);
    if (!element.disabled && element.name) {
      var value = element.getValue();
      if (value != undefined) {
        var pair = { };
        pair[element.name] = value;
        return Object.toQueryString(pair);
      }
    }
    return '';
  },

  getValue: function(element) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    return Form.Element.Serializers[method](element);
  },

  setValue: function(element, value) {
    element = $(element);
    var method = element.tagName.toLowerCase();
    Form.Element.Serializers[method](element, value);
    return element;
  },

  clear: function(element) {
    $(element).value = '';
    return element;
  },

  present: function(element) {
    return $(element).value != '';
  },

  activate: function(element) {
    element = $(element);
    try {
      element.focus();
      if (element.select && (element.tagName.toLowerCase() != 'input' ||
          !['button', 'reset', 'submit'].include(element.type)))
        element.select();
    } catch (e) { }
    return element;
  },

  disable: function(element) {
    element = $(element);
    element.disabled = true;
    return element;
  },

  enable: function(element) {
    element = $(element);
    element.disabled = false;
    return element;
  }
};

/*--------------------------------------------------------------------------*/

var Field = Form.Element;
var $F = Form.Element.Methods.getValue;

/*--------------------------------------------------------------------------*/

Form.Element.Serializers = {
  input: function(element, value) {
    switch (element.type.toLowerCase()) {
      case 'checkbox':
      case 'radio':
        return Form.Element.Serializers.inputSelector(element, value);
      default:
        return Form.Element.Serializers.textarea(element, value);
    }
  },

  inputSelector: function(element, value) {
    if (Object.isUndefined(value)) return element.checked ? element.value : null;
    else element.checked = !!value;
  },

  textarea: function(element, value) {
    if (Object.isUndefined(value)) return element.value;
    else element.value = value;
  },

  select: function(element, value) {
    if (Object.isUndefined(value))
      return this[element.type == 'select-one' ?
        'selectOne' : 'selectMany'](element);
    else {
      var opt, currentValue, single = !Object.isArray(value);
      for (var i = 0, length = element.length; i < length; i++) {
        opt = element.options[i];
        currentValue = this.optionValue(opt);
        if (single) {
          if (currentValue == value) {
            opt.selected = true;
            return;
          }
        }
        else opt.selected = value.include(currentValue);
      }
    }
  },

  selectOne: function(element) {
    var index = element.selectedIndex;
    return index >= 0 ? this.optionValue(element.options[index]) : null;
  },

  selectMany: function(element) {
    var values, length = element.length;
    if (!length) return null;

    for (var i = 0, values = []; i < length; i++) {
      var opt = element.options[i];
      if (opt.selected) values.push(this.optionValue(opt));
    }
    return values;
  },

  optionValue: function(opt) {
    // extend element because hasAttribute may not be native
    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;
  }
};

/*--------------------------------------------------------------------------*/

Abstract.TimedObserver = Class.create(PeriodicalExecuter, {
  initialize: function($super, element, frequency, callback) {
    $super(callback, frequency);
    this.element   = $(element);
    this.lastValue = this.getValue();
  },

  execute: function() {
    var value = this.getValue();
    if (Object.isString(this.lastValue) && Object.isString(value) ?
        this.lastValue != value : String(this.lastValue) != String(value)) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  }
});

Form.Element.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.Observer = Class.create(Abstract.TimedObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});

/*--------------------------------------------------------------------------*/

Abstract.EventObserver = Class.create({
  initialize: function(element, callback) {
    this.element  = $(element);
    this.callback = callback;

    this.lastValue = this.getValue();
    if (this.element.tagName.toLowerCase() == 'form')
      this.registerFormCallbacks();
    else
      this.registerCallback(this.element);
  },

  onElementEvent: function() {
    var value = this.getValue();
    if (this.lastValue != value) {
      this.callback(this.element, value);
      this.lastValue = value;
    }
  },

  registerFormCallbacks: function() {
    Form.getElements(this.element).each(this.registerCallback, this);
  },

  registerCallback: function(element) {
    if (element.type) {
      switch (element.type.toLowerCase()) {
        case 'checkbox':
        case 'radio':
          Event.observe(element, 'click', this.onElementEvent.bind(this));
          break;
        default:
          Event.observe(element, 'change', this.onElementEvent.bind(this));
          break;
      }
    }
  }
});

Form.Element.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.Element.getValue(this.element);
  }
});

Form.EventObserver = Class.create(Abstract.EventObserver, {
  getValue: function() {
    return Form.serialize(this.element);
  }
});
if (!window.Event) var Event = { };

Object.extend(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,

  cache: { },

  relatedTarget: function(event) {
    var element;
    switch(event.type) {
      case 'mouseover': element = event.fromElement; break;
      case 'mouseout':  element = event.toElement;   break;
      default: return null;
    }
    return Element.extend(element);
  }
});

Event.Methods = (function() {
  var isButton;

  if (Prototype.Browser.IE) {
    var buttonMap = { 0: 1, 1: 4, 2: 2 };
    isButton = function(event, code) {
      return event.button == buttonMap[code];
    };

  } else if (Prototype.Browser.WebKit) {
    isButton = function(event, code) {
      switch (code) {
        case 0: return event.which == 1 && !event.metaKey;
        case 1: return event.which == 1 && event.metaKey;
        default: return false;
      }
    };

  } else {
    isButton = function(event, code) {
      return event.which ? (event.which === code + 1) : (event.button === code);
    };
  }

  return {
    isLeftClick:   function(event) { return isButton(event, 0) },
    isMiddleClick: function(event) { return isButton(event, 1) },
    isRightClick:  function(event) { return isButton(event, 2) },

    element: function(event) {
      event = Event.extend(event);

      var node          = event.target,
          type          = event.type,
          currentTarget = event.currentTarget;

      if (currentTarget && currentTarget.tagName) {
        // Firefox screws up the "click" event when moving between radio buttons
        // via arrow keys. It also screws up the "load" and "error" events on images,
        // reporting the document as the target instead of the original image.
        if (type === 'load' || type === 'error' ||
          (type === 'click' && currentTarget.tagName.toLowerCase() === 'input'
            && currentTarget.type === 'radio'))
              node = currentTarget;
      }
      if (node.nodeType == Node.TEXT_NODE) node = node.parentNode;
      return Element.extend(node);
    },

    findElement: function(event, expression) {
      var element = Event.element(event);
      if (!expression) return element;
      var elements = [element].concat(element.ancestors());
      return Selector.findElement(elements, expression, 0);
    },

    pointer: function(event) {
      var docElement = document.documentElement,
      body = document.body || { scrollLeft: 0, scrollTop: 0 };
      return {
        x: event.pageX || (event.clientX +
          (docElement.scrollLeft || body.scrollLeft) -
          (docElement.clientLeft || 0)),
        y: event.pageY || (event.clientY +
          (docElement.scrollTop || body.scrollTop) -
          (docElement.clientTop || 0))
      };
    },

    pointerX: function(event) { return Event.pointer(event).x },
    pointerY: function(event) { return Event.pointer(event).y },

    stop: function(event) {
      Event.extend(event);
      event.preventDefault();
      event.stopPropagation();
      event.stopped = true;
    }
  };
})();

Event.extend = (function() {
  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {
    m[name] = Event.Methods[name].methodize();
    return m;
  });

  if (Prototype.Browser.IE) {
    Object.extend(methods, {
      stopPropagation: function() { this.cancelBubble = true },
      preventDefault:  function() { this.returnValue = false },
      inspect: function() { return "[object Event]" }
    });

    return function(event) {
      if (!event) return false;
      if (event._extendedByPrototype) return event;

      event._extendedByPrototype = Prototype.emptyFunction;
      var pointer = Event.pointer(event);
      Object.extend(event, {
        target: event.srcElement,
        relatedTarget: Event.relatedTarget(event),
        pageX:  pointer.x,
        pageY:  pointer.y
      });
      return Object.extend(event, methods);
    };

  } else {
    Event.prototype = Event.prototype || document.createEvent("HTMLEvents")['__proto__'];
    Object.extend(Event.prototype, methods);
    return Prototype.K;
  }
})();

Object.extend(Event, (function() {
  var cache = Event.cache;

  function getEventID(element) {
    if (element._prototypeEventID) return element._prototypeEventID[0];
    arguments.callee.id = arguments.callee.id || 1;
    return element._prototypeEventID = [++arguments.callee.id];
  }

  function getDOMEventName(eventName) {
    if (eventName && eventName.include(':')) return "dataavailable";
    return eventName;
  }

  function getCacheForID(id) {
    return cache[id] = cache[id] || { };
  }

  function getWrappersForEventName(id, eventName) {
    var c = getCacheForID(id);
    return c[eventName] = c[eventName] || [];
  }

  function createWrapper(element, eventName, handler) {
    var id = getEventID(element);
    var c = getWrappersForEventName(id, eventName);
    if (c.pluck("handler").include(handler)) return false;

    var wrapper = function(event) {
      if (!Event || !Event.extend ||
        (event.eventName && event.eventName != eventName))
          return false;

      Event.extend(event);
      handler.call(element, event);
    };

    wrapper.handler = handler;
    c.push(wrapper);
    return wrapper;
  }

  function findWrapper(id, eventName, handler) {
    var c = getWrappersForEventName(id, eventName);
    return c.find(function(wrapper) { return wrapper.handler == handler });
  }

  function destroyWrapper(id, eventName, handler) {
    var c = getCacheForID(id);
    if (!c[eventName]) return false;
    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));
  }

  function destroyCache() {
    for (var id in cache)
      for (var eventName in cache[id])
        cache[id][eventName] = null;
  }


  // Internet Explorer needs to remove event handlers on page unload
  // in order to avoid memory leaks.
  if (window.attachEvent) {
    window.attachEvent("onunload", destroyCache);
  }

  // Safari has a dummy event handler on page unload so that it won't
  // use its bfcache. Safari <= 3.1 has an issue with restoring the "document"
  // object when page is returned to via the back button using its bfcache.
  if (Prototype.Browser.WebKit) {
    window.addEventListener('unload', Prototype.emptyFunction, false);
  }

  return {
    observe: function(element, eventName, handler) {
      element = $(element);
      var name = getDOMEventName(eventName);

      var wrapper = createWrapper(element, eventName, handler);
      if (!wrapper) return element;

      if (element.addEventListener) {
        element.addEventListener(name, wrapper, false);
      } else {
        element.attachEvent("on" + name, wrapper);
      }

      return element;
    },

    stopObserving: function(element, eventName, handler) {
      element = $(element);
      var id = getEventID(element), name = getDOMEventName(eventName);

      if (!handler && eventName) {
        getWrappersForEventName(id, eventName).each(function(wrapper) {
          element.stopObserving(eventName, wrapper.handler);
        });
        return element;

      } else if (!eventName) {
        Object.keys(getCacheForID(id)).each(function(eventName) {
          element.stopObserving(eventName);
        });
        return element;
      }

      var wrapper = findWrapper(id, eventName, handler);
      if (!wrapper) return element;

      if (element.removeEventListener) {
        element.removeEventListener(name, wrapper, false);
      } else {
        element.detachEvent("on" + name, wrapper);
      }

      destroyWrapper(id, eventName, handler);

      return element;
    },

    fire: function(element, eventName, memo) {
      element = $(element);
      if (element == document && document.createEvent && !element.dispatchEvent)
        element = document.documentElement;

      var event;
      if (document.createEvent) {
        event = document.createEvent("HTMLEvents");
        event.initEvent("dataavailable", true, true);
      } else {
        event = document.createEventObject();
        event.eventType = "ondataavailable";
      }

      event.eventName = eventName;
      event.memo = memo || { };

      if (document.createEvent) {
        element.dispatchEvent(event);
      } else {
        element.fireEvent(event.eventType, event);
      }

      return Event.extend(event);
    }
  };
})());

Object.extend(Event, Event.Methods);

Element.addMethods({
  fire:          Event.fire,
  observe:       Event.observe,
  stopObserving: Event.stopObserving
});

Object.extend(document, {
  fire:          Element.Methods.fire.methodize(),
  observe:       Element.Methods.observe.methodize(),
  stopObserving: Element.Methods.stopObserving.methodize(),
  loaded:        false
});

(function() {
  /* Support for the DOMContentLoaded event is based on work by Dan Webb,
     Matthias Miller, Dean Edwards and John Resig. */

  var timer;

  function fireContentLoadedEvent() {
    if (document.loaded) return;
    if (timer) window.clearInterval(timer);
    document.fire("dom:loaded");
    document.loaded = true;
  }

  if (document.addEventListener) {
    if (Prototype.Browser.WebKit) {
      timer = window.setInterval(function() {
        if (/loaded|complete/.test(document.readyState))
          fireContentLoadedEvent();
      }, 0);

      Event.observe(window, "load", fireContentLoadedEvent);

    } else {
      document.addEventListener("DOMContentLoaded",
        fireContentLoadedEvent, false);
    }

  } else {
    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");
    $("__onDOMContentLoaded").onreadystatechange = function() {
      if (this.readyState == "complete") {
        this.onreadystatechange = null;
        fireContentLoadedEvent();
      }
    };
  }
})();
/*------------------------------- DEPRECATED -------------------------------*/

Hash.toQueryString = Object.toQueryString;

var Toggle = { display: Element.toggle };

Element.Methods.childOf = Element.Methods.descendantOf;

var Insertion = {
  Before: function(element, content) {
    return Element.insert(element, {before:content});
  },

  Top: function(element, content) {
    return Element.insert(element, {top:content});
  },

  Bottom: function(element, content) {
    return Element.insert(element, {bottom:content});
  },

  After: function(element, content) {
    return Element.insert(element, {after:content});
  }
};

var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

// This should be moved to script.aculo.us; notice the deprecated methods
// further below, that map to the newer Element methods.
var Position = {
  // set to true if needed, warning: firefox performance problems
  // NOT neeeded for page scrolling, only if draggable contained in
  // scrollable elements
  includeScrollOffsets: false,

  // must be called before calling withinIncludingScrolloffset, every time the
  // page is scrolled
  prepare: function() {
    this.deltaX =  window.pageXOffset
                || document.documentElement.scrollLeft
                || document.body.scrollLeft
                || 0;
    this.deltaY =  window.pageYOffset
                || document.documentElement.scrollTop
                || document.body.scrollTop
                || 0;
  },

  // caches x/y coordinate pair to use with overlap
  within: function(element, x, y) {
    if (this.includeScrollOffsets)
      return this.withinIncludingScrolloffsets(element, x, y);
    this.xcomp = x;
    this.ycomp = y;
    this.offset = Element.cumulativeOffset(element);

    return (y >= this.offset[1] &&
            y <  this.offset[1] + element.offsetHeight &&
            x >= this.offset[0] &&
            x <  this.offset[0] + element.offsetWidth);
  },

  withinIncludingScrolloffsets: function(element, x, y) {
    var offsetcache = Element.cumulativeScrollOffset(element);

    this.xcomp = x + offsetcache[0] - this.deltaX;
    this.ycomp = y + offsetcache[1] - this.deltaY;
    this.offset = Element.cumulativeOffset(element);

    return (this.ycomp >= this.offset[1] &&
            this.ycomp <  this.offset[1] + element.offsetHeight &&
            this.xcomp >= this.offset[0] &&
            this.xcomp <  this.offset[0] + element.offsetWidth);
  },

  // within must be called directly before
  overlap: function(mode, element) {
    if (!mode) return 0;
    if (mode == 'vertical')
      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /
        element.offsetHeight;
    if (mode == 'horizontal')
      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /
        element.offsetWidth;
  },

  // Deprecation layer -- use newer Element methods now (1.5.2).

  cumulativeOffset: Element.Methods.cumulativeOffset,

  positionedOffset: Element.Methods.positionedOffset,

  absolutize: function(element) {
    Position.prepare();
    return Element.absolutize(element);
  },

  relativize: function(element) {
    Position.prepare();
    return Element.relativize(element);
  },

  realOffset: Element.Methods.cumulativeScrollOffset,

  offsetParent: Element.Methods.getOffsetParent,

  page: Element.Methods.viewportOffset,

  clone: function(source, target, options) {
    options = options || { };
    return Element.clonePosition(target, source, options);
  }
};

/*--------------------------------------------------------------------------*/

if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){
  function iter(name) {
    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";
  }

  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?
  function(element, className) {
    className = className.toString().strip();
    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);
    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];
  } : function(element, className) {
    className = className.toString().strip();
    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);
    if (!classNames && !className) return elements;

    var nodes = $(element).getElementsByTagName('*');
    className = ' ' + className + ' ';

    for (var i = 0, child, cn; child = nodes[i]; i++) {
      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||
          (classNames && classNames.all(function(name) {
            return !name.toString().blank() && cn.include(' ' + name + ' ');
          }))))
        elements.push(Element.extend(child));
    }
    return elements;
  };

  return function(className, parentElement) {
    return $(parentElement || document.body).getElementsByClassName(className);
  };
}(Element.Methods);

/*--------------------------------------------------------------------------*/

Element.ClassNames = Class.create();
Element.ClassNames.prototype = {
  initialize: function(element) {
    this.element = $(element);
  },

  _each: function(iterator) {
    this.element.className.split(/\s+/).select(function(name) {
      return name.length > 0;
    })._each(iterator);
  },

  set: function(className) {
    this.element.className = className;
  },

  add: function(classNameToAdd) {
    if (this.include(classNameToAdd)) return;
    this.set($A(this).concat(classNameToAdd).join(' '));
  },

  remove: function(classNameToRemove) {
    if (!this.include(classNameToRemove)) return;
    this.set($A(this).without(classNameToRemove).join(' '));
  },

  toString: function() {
    return $A(this).join(' ');
  }
};

Object.extend(Element.ClassNames.prototype, Enumerable);

/*--------------------------------------------------------------------------*/

Element.addMethods();// script.aculo.us scriptaculous.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Scriptaculous = {
  Version: '1.8.2',
  require: function(libraryName) {
    // inserting via DOM fails in Safari 2.0, so brute force approach
    document.write('<script type="text/javascript" src="'+libraryName+'"><\/script>');
  },
  REQUIRED_PROTOTYPE: '1.6.0.3',
  load: function() {
    function convertVersionString(versionString) {
      var v = versionString.replace(/_.*|\./g, '');
      v = parseInt(v + '0'.times(4-v.length));
      return versionString.indexOf('_') > -1 ? v-1 : v;
    }

    if((typeof Prototype=='undefined') ||
       (typeof Element == 'undefined') ||
       (typeof Element.Methods=='undefined') ||
       (convertVersionString(Prototype.Version) <
        convertVersionString(Scriptaculous.REQUIRED_PROTOTYPE)))
       throw("script.aculo.us requires the Prototype JavaScript framework >= " +
        Scriptaculous.REQUIRED_PROTOTYPE);

    var js = /scriptaculous\.js(\?.*)?$/;
    $$('head script[src]').findAll(function(s) {
      return s.src.match(js);
    }).each(function(s) {
      var path = s.src.replace(js, ''),
      includes = s.src.match(/\?.*load=([a-z,]*)/);
      (includes ? includes[1] : 'builder,effects,dragdrop,controls,slider,sound').split(',').each(
       function(include) { Scriptaculous.require(path+include+'.js') });
    });
  }
};

Scriptaculous.load();// script.aculo.us builder.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

var Builder = {
  NODEMAP: {
    AREA: 'map',
    CAPTION: 'table',
    COL: 'table',
    COLGROUP: 'table',
    LEGEND: 'fieldset',
    OPTGROUP: 'select',
    OPTION: 'select',
    PARAM: 'object',
    TBODY: 'table',
    TD: 'table',
    TFOOT: 'table',
    TH: 'table',
    THEAD: 'table',
    TR: 'table'
  },
  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
  //       due to a Firefox bug
  node: function(elementName) {
    elementName = elementName.toUpperCase();

    // try innerHTML approach
    var parentTag = this.NODEMAP[elementName] || 'div';
    var parentElement = document.createElement(parentTag);
    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
    } catch(e) {}
    var element = parentElement.firstChild || null;

    // see if browser added wrapping tags
    if(element && (element.tagName.toUpperCase() != elementName))
      element = element.getElementsByTagName(elementName)[0];

    // fallback to createElement approach
    if(!element) element = document.createElement(elementName);

    // abort if nothing could be created
    if(!element) return;

    // attributes (or text)
    if(arguments[1])
      if(this._isStringOrNumber(arguments[1]) ||
        (arguments[1] instanceof Array) ||
        arguments[1].tagName) {
          this._children(element, arguments[1]);
        } else {
          var attrs = this._attributes(arguments[1]);
          if(attrs.length) {
            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
              parentElement.innerHTML = "<" +elementName + " " +
                attrs + "></" + elementName + ">";
            } catch(e) {}
            element = parentElement.firstChild || null;
            // workaround firefox 1.0.X bug
            if(!element) {
              element = document.createElement(elementName);
              for(attr in arguments[1])
                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
            }
            if(element.tagName.toUpperCase() != elementName)
              element = parentElement.getElementsByTagName(elementName)[0];
          }
        }

    // text, or array of children
    if(arguments[2])
      this._children(element, arguments[2]);

     return $(element);
  },
  _text: function(text) {
     return document.createTextNode(text);
  },

  ATTR_MAP: {
    'className': 'class',
    'htmlFor': 'for'
  },

  _attributes: function(attributes) {
    var attrs = [];
    for(attribute in attributes)
      attrs.push((attribute in this.ATTR_MAP ? this.ATTR_MAP[attribute] : attribute) +
          '="' + attributes[attribute].toString().escapeHTML().gsub(/"/,'&quot;') + '"');
    return attrs.join(" ");
  },
  _children: function(element, children) {
    if(children.tagName) {
      element.appendChild(children);
      return;
    }
    if(typeof children=='object') { // array can hold nodes and text
      children.flatten().each( function(e) {
        if(typeof e=='object')
          element.appendChild(e);
        else
          if(Builder._isStringOrNumber(e))
            element.appendChild(Builder._text(e));
      });
    } else
      if(Builder._isStringOrNumber(children))
        element.appendChild(Builder._text(children));
  },
  _isStringOrNumber: function(param) {
    return(typeof param=='string' || typeof param=='number');
  },
  build: function(html) {
    var element = this.node('div');
    $(element).update(html.strip());
    return element.down();
  },
  dump: function(scope) {
    if(typeof scope != 'object' && typeof scope != 'function') scope = window; //global scope

    var tags = ("A ABBR ACRONYM ADDRESS APPLET AREA B BASE BASEFONT BDO BIG BLOCKQUOTE BODY " +
      "BR BUTTON CAPTION CENTER CITE CODE COL COLGROUP DD DEL DFN DIR DIV DL DT EM FIELDSET " +
      "FONT FORM FRAME FRAMESET H1 H2 H3 H4 H5 H6 HEAD HR HTML I IFRAME IMG INPUT INS ISINDEX "+
      "KBD LABEL LEGEND LI LINK MAP MENU META NOFRAMES NOSCRIPT OBJECT OL OPTGROUP OPTION P "+
      "PARAM PRE Q S SAMP SCRIPT SELECT SMALL SPAN STRIKE STRONG STYLE SUB SUP TABLE TBODY TD "+
      "TEXTAREA TFOOT TH THEAD TITLE TR TT U UL VAR").split(/\s+/);

    tags.each( function(tag){
      scope[tag] = function() {
        return Builder.node.apply(Builder, [tag].concat($A(arguments)));
      };
    });
  }
};// script.aculo.us effects.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
//  Justin Palmer (http://encytemedia.com/)
//  Mark Pilgrim (http://diveintomark.org/)
//  Martin Bialasinki
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
  var color = '#';
  if (this.slice(0,4) == 'rgb(') {
    var cols = this.slice(4,this.length-1).split(',');
    var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
  } else {
    if (this.slice(0,1) == '#') {
      if (this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
      if (this.length==7) color = this.toLowerCase();
    }
  }
  return (color.length==7 ? color : (arguments[0] || this));
};

/*--------------------------------------------------------------------------*/

Element.collectTextNodes = function(element) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      (node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
  }).flatten().join('');
};

Element.collectTextNodesIgnoreClass = function(element, className) {
  return $A($(element).childNodes).collect( function(node) {
    return (node.nodeType==3 ? node.nodeValue :
      ((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
        Element.collectTextNodesIgnoreClass(node, className) : ''));
  }).flatten().join('');
};

Element.setContentZoom = function(element, percent) {
  element = $(element);
  element.setStyle({fontSize: (percent/100) + 'em'});
  if (Prototype.Browser.WebKit) window.scrollBy(0,0);
  return element;
};

Element.getInlineOpacity = function(element){
  return $(element).style.opacity || '';
};

Element.forceRerendering = function(element) {
  try {
    element = $(element);
    var n = document.createTextNode(' ');
    element.appendChild(n);
    element.removeChild(n);
  } catch(e) { }
};

/*--------------------------------------------------------------------------*/

var Effect = {
  _elementDoesNotExistError: {
    name: 'ElementDoesNotExistError',
    message: 'The specified DOM element does not exist, but is required for this effect to operate'
  },
  Transitions: {
    linear: Prototype.K,
    sinoidal: function(pos) {
      return (-Math.cos(pos*Math.PI)/2) + .5;
    },
    reverse: function(pos) {
      return 1-pos;
    },
    flicker: function(pos) {
      var pos = ((-Math.cos(pos*Math.PI)/4) + .75) + Math.random()/4;
      return pos > 1 ? 1 : pos;
    },
    wobble: function(pos) {
      return (-Math.cos(pos*Math.PI*(9*pos))/2) + .5;
    },
    pulse: function(pos, pulses) {
      return (-Math.cos((pos*((pulses||5)-.5)*2)*Math.PI)/2) + .5;
    },
    spring: function(pos) {
      return 1 - (Math.cos(pos * 4.5 * Math.PI) * Math.exp(-pos * 6));
    },
    none: function(pos) {
      return 0;
    },
    full: function(pos) {
      return 1;
    }
  },
  DefaultOptions: {
    duration:   1.0,   // seconds
    fps:        100,   // 100= assume 66fps max.
    sync:       false, // true for combining
    from:       0.0,
    to:         1.0,
    delay:      0.0,
    queue:      'parallel'
  },
  tagifyText: function(element) {
    var tagifyStyle = 'position:relative';
    if (Prototype.Browser.IE) tagifyStyle += ';zoom:1';

    element = $(element);
    $A(element.childNodes).each( function(child) {
      if (child.nodeType==3) {
        child.nodeValue.toArray().each( function(character) {
          element.insertBefore(
            new Element('span', {style: tagifyStyle}).update(
              character == ' ' ? String.fromCharCode(160) : character),
              child);
        });
        Element.remove(child);
      }
    });
  },
  multiple: function(element, effect) {
    var elements;
    if (((typeof element == 'object') ||
        Object.isFunction(element)) &&
       (element.length))
      elements = element;
    else
      elements = $(element).childNodes;

    var options = Object.extend({
      speed: 0.1,
      delay: 0.0
    }, arguments[2] || { });
    var masterDelay = options.delay;

    $A(elements).each( function(element, index) {
      new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
    });
  },
  PAIRS: {
    'slide':  ['SlideDown','SlideUp'],
    'blind':  ['BlindDown','BlindUp'],
    'appear': ['Appear','Fade']
  },
  toggle: function(element, effect) {
    element = $(element);
    effect = (effect || 'appear').toLowerCase();
    var options = Object.extend({
      queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
    }, arguments[2] || { });
    Effect[element.visible() ?
      Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
  }
};

Effect.DefaultOptions.transition = Effect.Transitions.sinoidal;

/* ------------- core effects ------------- */

Effect.ScopedQueue = Class.create(Enumerable, {
  initialize: function() {
    this.effects  = [];
    this.interval = null;
  },
  _each: function(iterator) {
    this.effects._each(iterator);
  },
  add: function(effect) {
    var timestamp = new Date().getTime();

    var position = Object.isString(effect.options.queue) ?
      effect.options.queue : effect.options.queue.position;

    switch(position) {
      case 'front':
        // move unstarted effects after this effect
        this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
            e.startOn  += effect.finishOn;
            e.finishOn += effect.finishOn;
          });
        break;
      case 'with-last':
        timestamp = this.effects.pluck('startOn').max() || timestamp;
        break;
      case 'end':
        // start effect after last queued effect has finished
        timestamp = this.effects.pluck('finishOn').max() || timestamp;
        break;
    }

    effect.startOn  += timestamp;
    effect.finishOn += timestamp;

    if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
      this.effects.push(effect);

    if (!this.interval)
      this.interval = setInterval(this.loop.bind(this), 15);
  },
  remove: function(effect) {
    this.effects = this.effects.reject(function(e) { return e==effect });
    if (this.effects.length == 0) {
      clearInterval(this.interval);
      this.interval = null;
    }
  },
  loop: function() {
    var timePos = new Date().getTime();
    for(var i=0, len=this.effects.length;i<len;i++)
      this.effects[i] && this.effects[i].loop(timePos);
  }
});

Effect.Queues = {
  instances: $H(),
  get: function(queueName) {
    if (!Object.isString(queueName)) return queueName;

    return this.instances.get(queueName) ||
      this.instances.set(queueName, new Effect.ScopedQueue());
  }
};
Effect.Queue = Effect.Queues.get('global');

Effect.Base = Class.create({
  position: null,
  start: function(options) {
    function codeForEvent(options,eventName){
      return (
        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
      );
    }
    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
    this.currentFrame = 0;
    this.state        = 'idle';
    this.startOn      = this.options.delay*1000;
    this.finishOn     = this.startOn+(this.options.duration*1000);
    this.fromToDelta  = this.options.to-this.options.from;
    this.totalTime    = this.finishOn-this.startOn;
    this.totalFrames  = this.options.fps*this.options.duration;

    this.render = (function() {
      function dispatch(effect, eventName) {
        if (effect.options[eventName + 'Internal'])
          effect.options[eventName + 'Internal'](effect);
        if (effect.options[eventName])
          effect.options[eventName](effect);
      }

      return function(pos) {
        if (this.state === "idle") {
          this.state = "running";
          dispatch(this, 'beforeSetup');
          if (this.setup) this.setup();
          dispatch(this, 'afterSetup');
        }
        if (this.state === "running") {
          pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
          this.position = pos;
          dispatch(this, 'beforeUpdate');
          if (this.update) this.update(pos);
          dispatch(this, 'afterUpdate');
        }
      };
    })();

    this.event('beforeStart');
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).add(this);
  },
  loop: function(timePos) {
    if (timePos >= this.startOn) {
      if (timePos >= this.finishOn) {
        this.render(1.0);
        this.cancel();
        this.event('beforeFinish');
        if (this.finish) this.finish();
        this.event('afterFinish');
        return;
      }
      var pos   = (timePos - this.startOn) / this.totalTime,
          frame = (pos * this.totalFrames).round();
      if (frame > this.currentFrame) {
        this.render(pos);
        this.currentFrame = frame;
      }
    }
  },
  cancel: function() {
    if (!this.options.sync)
      Effect.Queues.get(Object.isString(this.options.queue) ?
        'global' : this.options.queue.scope).remove(this);
    this.state = 'finished';
  },
  event: function(eventName) {
    if (this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
    if (this.options[eventName]) this.options[eventName](this);
  },
  inspect: function() {
    var data = $H();
    for(property in this)
      if (!Object.isFunction(this[property])) data.set(property, this[property]);
    return '#<Effect:' + data.inspect() + ',options:' + $H(this.options).inspect() + '>';
  }
});

Effect.Parallel = Class.create(Effect.Base, {
  initialize: function(effects) {
    this.effects = effects || [];
    this.start(arguments[1]);
  },
  update: function(position) {
    this.effects.invoke('render', position);
  },
  finish: function(position) {
    this.effects.each( function(effect) {
      effect.render(1.0);
      effect.cancel();
      effect.event('beforeFinish');
      if (effect.finish) effect.finish(position);
      effect.event('afterFinish');
    });
  }
});

Effect.Tween = Class.create(Effect.Base, {
  initialize: function(object, from, to) {
    object = Object.isString(object) ? $(object) : object;
    var args = $A(arguments), method = args.last(),
      options = args.length == 5 ? args[3] : null;
    this.method = Object.isFunction(method) ? method.bind(object) :
      Object.isFunction(object[method]) ? object[method].bind(object) :
      function(value) { object[method] = value };
    this.start(Object.extend({ from: from, to: to }, options || { }));
  },
  update: function(position) {
    this.method(position);
  }
});

Effect.Event = Class.create(Effect.Base, {
  initialize: function() {
    this.start(Object.extend({ duration: 0 }, arguments[0] || { }));
  },
  update: Prototype.emptyFunction
});

Effect.Opacity = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    // make this work on IE on elements without 'layout'
    if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
      this.element.setStyle({zoom: 1});
    var options = Object.extend({
      from: this.element.getOpacity() || 0.0,
      to:   1.0
    }, arguments[1] || { });
    this.start(options);
  },
  update: function(position) {
    this.element.setOpacity(position);
  }
});

Effect.Move = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      x:    0,
      y:    0,
      mode: 'relative'
    }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    this.element.makePositioned();
    this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
    this.originalTop  = parseFloat(this.element.getStyle('top')  || '0');
    if (this.options.mode == 'absolute') {
      this.options.x = this.options.x - this.originalLeft;
      this.options.y = this.options.y - this.originalTop;
    }
  },
  update: function(position) {
    this.element.setStyle({
      left: (this.options.x  * position + this.originalLeft).round() + 'px',
      top:  (this.options.y  * position + this.originalTop).round()  + 'px'
    });
  }
});

// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
  return new Effect.Move(element,
    Object.extend({ x: toLeft, y: toTop }, arguments[3] || { }));
};

Effect.Scale = Class.create(Effect.Base, {
  initialize: function(element, percent) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      scaleX: true,
      scaleY: true,
      scaleContent: true,
      scaleFromCenter: false,
      scaleMode: 'box',        // 'box' or 'contents' or { } with provided values
      scaleFrom: 100.0,
      scaleTo:   percent
    }, arguments[2] || { });
    this.start(options);
  },
  setup: function() {
    this.restoreAfterFinish = this.options.restoreAfterFinish || false;
    this.elementPositioning = this.element.getStyle('position');

    this.originalStyle = { };
    ['top','left','width','height','fontSize'].each( function(k) {
      this.originalStyle[k] = this.element.style[k];
    }.bind(this));

    this.originalTop  = this.element.offsetTop;
    this.originalLeft = this.element.offsetLeft;

    var fontSize = this.element.getStyle('font-size') || '100%';
    ['em','px','%','pt'].each( function(fontSizeType) {
      if (fontSize.indexOf(fontSizeType)>0) {
        this.fontSize     = parseFloat(fontSize);
        this.fontSizeType = fontSizeType;
      }
    }.bind(this));

    this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;

    this.dims = null;
    if (this.options.scaleMode=='box')
      this.dims = [this.element.offsetHeight, this.element.offsetWidth];
    if (/^content/.test(this.options.scaleMode))
      this.dims = [this.element.scrollHeight, this.element.scrollWidth];
    if (!this.dims)
      this.dims = [this.options.scaleMode.originalHeight,
                   this.options.scaleMode.originalWidth];
  },
  update: function(position) {
    var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
    if (this.options.scaleContent && this.fontSize)
      this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
    this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
  },
  finish: function(position) {
    if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
  },
  setDimensions: function(height, width) {
    var d = { };
    if (this.options.scaleX) d.width = width.round() + 'px';
    if (this.options.scaleY) d.height = height.round() + 'px';
    if (this.options.scaleFromCenter) {
      var topd  = (height - this.dims[0])/2;
      var leftd = (width  - this.dims[1])/2;
      if (this.elementPositioning == 'absolute') {
        if (this.options.scaleY) d.top = this.originalTop-topd + 'px';
        if (this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
      } else {
        if (this.options.scaleY) d.top = -topd + 'px';
        if (this.options.scaleX) d.left = -leftd + 'px';
      }
    }
    this.element.setStyle(d);
  }
});

Effect.Highlight = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || { });
    this.start(options);
  },
  setup: function() {
    // Prevent executing on elements not in the layout flow
    if (this.element.getStyle('display')=='none') { this.cancel(); return; }
    // Disable background image during the effect
    this.oldStyle = { };
    if (!this.options.keepBackgroundImage) {
      this.oldStyle.backgroundImage = this.element.getStyle('background-image');
      this.element.setStyle({backgroundImage: 'none'});
    }
    if (!this.options.endcolor)
      this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
    if (!this.options.restorecolor)
      this.options.restorecolor = this.element.getStyle('background-color');
    // init color calculations
    this._base  = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
    this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
  },
  update: function(position) {
    this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
      return m+((this._base[i]+(this._delta[i]*position)).round().toColorPart()); }.bind(this)) });
  },
  finish: function() {
    this.element.setStyle(Object.extend(this.oldStyle, {
      backgroundColor: this.options.restorecolor
    }));
  }
});

Effect.ScrollTo = function(element) {
  var options = arguments[1] || { },
  scrollOffsets = document.viewport.getScrollOffsets(),
  elementOffsets = $(element).cumulativeOffset();

  if (options.offset) elementOffsets[1] += options.offset;

  return new Effect.Tween(null,
    scrollOffsets.top,
    elementOffsets[1],
    options,
    function(p){ scrollTo(scrollOffsets.left, p.round()); }
  );
};

/* ------------- combination effects ------------- */

Effect.Fade = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  var options = Object.extend({
    from: element.getOpacity() || 1.0,
    to:   0.0,
    afterFinishInternal: function(effect) {
      if (effect.options.to!=0) return;
      effect.element.hide().setStyle({opacity: oldOpacity});
    }
  }, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Appear = function(element) {
  element = $(element);
  var options = Object.extend({
  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
  to:   1.0,
  // force Safari to render floated elements properly
  afterFinishInternal: function(effect) {
    effect.element.forceRerendering();
  },
  beforeSetup: function(effect) {
    effect.element.setOpacity(effect.options.from).show();
  }}, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Puff = function(element) {
  element = $(element);
  var oldStyle = {
    opacity: element.getInlineOpacity(),
    position: element.getStyle('position'),
    top:  element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height
  };
  return new Effect.Parallel(
   [ new Effect.Scale(element, 200,
      { sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
     new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
     Object.extend({ duration: 1.0,
      beforeSetupInternal: function(effect) {
        Position.absolutize(effect.effects[0].element);
      },
      afterFinishInternal: function(effect) {
         effect.effects[0].element.hide().setStyle(oldStyle); }
     }, arguments[1] || { })
   );
};

Effect.BlindUp = function(element) {
  element = $(element);
  element.makeClipping();
  return new Effect.Scale(element, 0,
    Object.extend({ scaleContent: false,
      scaleX: false,
      restoreAfterFinish: true,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping();
      }
    }, arguments[1] || { })
  );
};

Effect.BlindDown = function(element) {
  element = $(element);
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: 0,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping();
    }
  }, arguments[1] || { }));
};

Effect.SwitchOff = function(element) {
  element = $(element);
  var oldOpacity = element.getInlineOpacity();
  return new Effect.Appear(element, Object.extend({
    duration: 0.4,
    from: 0,
    transition: Effect.Transitions.flicker,
    afterFinishInternal: function(effect) {
      new Effect.Scale(effect.element, 1, {
        duration: 0.3, scaleFromCenter: true,
        scaleX: false, scaleContent: false, restoreAfterFinish: true,
        beforeSetup: function(effect) {
          effect.element.makePositioned().makeClipping();
        },
        afterFinishInternal: function(effect) {
          effect.element.hide().undoClipping().undoPositioned().setStyle({opacity: oldOpacity});
        }
      });
    }
  }, arguments[1] || { }));
};

Effect.DropOut = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left'),
    opacity: element.getInlineOpacity() };
  return new Effect.Parallel(
    [ new Effect.Move(element, {x: 0, y: 100, sync: true }),
      new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
    Object.extend(
      { duration: 0.5,
        beforeSetup: function(effect) {
          effect.effects[0].element.makePositioned();
        },
        afterFinishInternal: function(effect) {
          effect.effects[0].element.hide().undoPositioned().setStyle(oldStyle);
        }
      }, arguments[1] || { }));
};

Effect.Shake = function(element) {
  element = $(element);
  var options = Object.extend({
    distance: 20,
    duration: 0.5
  }, arguments[1] || {});
  var distance = parseFloat(options.distance);
  var split = parseFloat(options.duration) / 10.0;
  var oldStyle = {
    top: element.getStyle('top'),
    left: element.getStyle('left') };
    return new Effect.Move(element,
      { x:  distance, y: 0, duration: split, afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x:  distance*2, y: 0, duration: split*2,  afterFinishInternal: function(effect) {
    new Effect.Move(effect.element,
      { x: -distance, y: 0, duration: split, afterFinishInternal: function(effect) {
        effect.element.undoPositioned().setStyle(oldStyle);
  }}); }}); }}); }}); }}); }});
};

Effect.SlideDown = function(element) {
  element = $(element).cleanWhitespace();
  // SlideDown need to have the content of the element wrapped in a container element with fixed height!
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({
    scaleContent: false,
    scaleX: false,
    scaleFrom: window.opera ? 0 : 1,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().setStyle({height: '0px'}).show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom}); }
    }, arguments[1] || { })
  );
};

Effect.SlideUp = function(element) {
  element = $(element).cleanWhitespace();
  var oldInnerBottom = element.down().getStyle('bottom');
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, window.opera ? 0 : 1,
   Object.extend({ scaleContent: false,
    scaleX: false,
    scaleMode: 'box',
    scaleFrom: 100,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetup: function(effect) {
      effect.element.makePositioned();
      effect.element.down().makePositioned();
      if (window.opera) effect.element.setStyle({top: ''});
      effect.element.makeClipping().show();
    },
    afterUpdateInternal: function(effect) {
      effect.element.down().setStyle({bottom:
        (effect.dims[0] - effect.element.clientHeight) + 'px' });
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping().undoPositioned();
      effect.element.down().undoPositioned().setStyle({bottom: oldInnerBottom});
    }
   }, arguments[1] || { })
  );
};

// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
  return new Effect.Scale(element, window.opera ? 1 : 0, {
    restoreAfterFinish: true,
    beforeSetup: function(effect) {
      effect.element.makeClipping();
    },
    afterFinishInternal: function(effect) {
      effect.element.hide().undoClipping();
    }
  });
};

Effect.Grow = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.full
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var initialMoveX, initialMoveY;
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      initialMoveX = initialMoveY = moveX = moveY = 0;
      break;
    case 'top-right':
      initialMoveX = dims.width;
      initialMoveY = moveY = 0;
      moveX = -dims.width;
      break;
    case 'bottom-left':
      initialMoveX = moveX = 0;
      initialMoveY = dims.height;
      moveY = -dims.height;
      break;
    case 'bottom-right':
      initialMoveX = dims.width;
      initialMoveY = dims.height;
      moveX = -dims.width;
      moveY = -dims.height;
      break;
    case 'center':
      initialMoveX = dims.width / 2;
      initialMoveY = dims.height / 2;
      moveX = -dims.width / 2;
      moveY = -dims.height / 2;
      break;
  }

  return new Effect.Move(element, {
    x: initialMoveX,
    y: initialMoveY,
    duration: 0.01,
    beforeSetup: function(effect) {
      effect.element.hide().makeClipping().makePositioned();
    },
    afterFinishInternal: function(effect) {
      new Effect.Parallel(
        [ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
          new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
          new Effect.Scale(effect.element, 100, {
            scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
            sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
        ], Object.extend({
             beforeSetup: function(effect) {
               effect.effects[0].element.setStyle({height: '0px'}).show();
             },
             afterFinishInternal: function(effect) {
               effect.effects[0].element.undoClipping().undoPositioned().setStyle(oldStyle);
             }
           }, options)
      );
    }
  });
};

Effect.Shrink = function(element) {
  element = $(element);
  var options = Object.extend({
    direction: 'center',
    moveTransition: Effect.Transitions.sinoidal,
    scaleTransition: Effect.Transitions.sinoidal,
    opacityTransition: Effect.Transitions.none
  }, arguments[1] || { });
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    height: element.style.height,
    width: element.style.width,
    opacity: element.getInlineOpacity() };

  var dims = element.getDimensions();
  var moveX, moveY;

  switch (options.direction) {
    case 'top-left':
      moveX = moveY = 0;
      break;
    case 'top-right':
      moveX = dims.width;
      moveY = 0;
      break;
    case 'bottom-left':
      moveX = 0;
      moveY = dims.height;
      break;
    case 'bottom-right':
      moveX = dims.width;
      moveY = dims.height;
      break;
    case 'center':
      moveX = dims.width / 2;
      moveY = dims.height / 2;
      break;
  }

  return new Effect.Parallel(
    [ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
      new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
      new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
    ], Object.extend({
         beforeStartInternal: function(effect) {
           effect.effects[0].element.makePositioned().makeClipping();
         },
         afterFinishInternal: function(effect) {
           effect.effects[0].element.hide().undoClipping().undoPositioned().setStyle(oldStyle); }
       }, options)
  );
};

Effect.Pulsate = function(element) {
  element = $(element);
  var options    = arguments[1] || { },
    oldOpacity = element.getInlineOpacity(),
    transition = options.transition || Effect.Transitions.linear,
    reverser   = function(pos){
      return 1 - transition((-Math.cos((pos*(options.pulses||5)*2)*Math.PI)/2) + .5);
    };

  return new Effect.Opacity(element,
    Object.extend(Object.extend({  duration: 2.0, from: 0,
      afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
    }, options), {transition: reverser}));
};

Effect.Fold = function(element) {
  element = $(element);
  var oldStyle = {
    top: element.style.top,
    left: element.style.left,
    width: element.style.width,
    height: element.style.height };
  element.makeClipping();
  return new Effect.Scale(element, 5, Object.extend({
    scaleContent: false,
    scaleX: false,
    afterFinishInternal: function(effect) {
    new Effect.Scale(element, 1, {
      scaleContent: false,
      scaleY: false,
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping().setStyle(oldStyle);
      } });
  }}, arguments[1] || { }));
};

Effect.Morph = Class.create(Effect.Base, {
  initialize: function(element) {
    this.element = $(element);
    if (!this.element) throw(Effect._elementDoesNotExistError);
    var options = Object.extend({
      style: { }
    }, arguments[1] || { });

    if (!Object.isString(options.style)) this.style = $H(options.style);
    else {
      if (options.style.include(':'))
        this.style = options.style.parseStyle();
      else {
        this.element.addClassName(options.style);
        this.style = $H(this.element.getStyles());
        this.element.removeClassName(options.style);
        var css = this.element.getStyles();
        this.style = this.style.reject(function(style) {
          return style.value == css[style.key];
        });
        options.afterFinishInternal = function(effect) {
          effect.element.addClassName(effect.options.style);
          effect.transforms.each(function(transform) {
            effect.element.style[transform.style] = '';
          });
        };
      }
    }
    this.start(options);
  },

  setup: function(){
    function parseColor(color){
      if (!color || ['rgba(0, 0, 0, 0)','transparent'].include(color)) color = '#ffffff';
      color = color.parseColor();
      return $R(0,2).map(function(i){
        return parseInt( color.slice(i*2+1,i*2+3), 16 );
      });
    }
    this.transforms = this.style.map(function(pair){
      var property = pair[0], value = pair[1], unit = null;

      if (value.parseColor('#zzzzzz') != '#zzzzzz') {
        value = value.parseColor();
        unit  = 'color';
      } else if (property == 'opacity') {
        value = parseFloat(value);
        if (Prototype.Browser.IE && (!this.element.currentStyle.hasLayout))
          this.element.setStyle({zoom: 1});
      } else if (Element.CSS_LENGTH.test(value)) {
          var components = value.match(/^([\+\-]?[0-9\.]+)(.*)$/);
          value = parseFloat(components[1]);
          unit = (components.length == 3) ? components[2] : null;
      }

      var originalValue = this.element.getStyle(property);
      return {
        style: property.camelize(),
        originalValue: unit=='color' ? parseColor(originalValue) : parseFloat(originalValue || 0),
        targetValue: unit=='color' ? parseColor(value) : value,
        unit: unit
      };
    }.bind(this)).reject(function(transform){
      return (
        (transform.originalValue == transform.targetValue) ||
        (
          transform.unit != 'color' &&
          (isNaN(transform.originalValue) || isNaN(transform.targetValue))
        )
      );
    });
  },
  update: function(position) {
    var style = { }, transform, i = this.transforms.length;
    while(i--)
      style[(transform = this.transforms[i]).style] =
        transform.unit=='color' ? '#'+
          (Math.round(transform.originalValue[0]+
            (transform.targetValue[0]-transform.originalValue[0])*position)).toColorPart() +
          (Math.round(transform.originalValue[1]+
            (transform.targetValue[1]-transform.originalValue[1])*position)).toColorPart() +
          (Math.round(transform.originalValue[2]+
            (transform.targetValue[2]-transform.originalValue[2])*position)).toColorPart() :
        (transform.originalValue +
          (transform.targetValue - transform.originalValue) * position).toFixed(3) +
            (transform.unit === null ? '' : transform.unit);
    this.element.setStyle(style, true);
  }
});

Effect.Transform = Class.create({
  initialize: function(tracks){
    this.tracks  = [];
    this.options = arguments[1] || { };
    this.addTracks(tracks);
  },
  addTracks: function(tracks){
    tracks.each(function(track){
      track = $H(track);
      var data = track.values().first();
      this.tracks.push($H({
        ids:     track.keys().first(),
        effect:  Effect.Morph,
        options: { style: data }
      }));
    }.bind(this));
    return this;
  },
  play: function(){
    return new Effect.Parallel(
      this.tracks.map(function(track){
        var ids = track.get('ids'), effect = track.get('effect'), options = track.get('options');
        var elements = [$(ids) || $$(ids)].flatten();
        return elements.map(function(e){ return new effect(e, Object.extend({ sync:true }, options)) });
      }).flatten(),
      this.options
    );
  }
});

Element.CSS_PROPERTIES = $w(
  'backgroundColor backgroundPosition borderBottomColor borderBottomStyle ' +
  'borderBottomWidth borderLeftColor borderLeftStyle borderLeftWidth ' +
  'borderRightColor borderRightStyle borderRightWidth borderSpacing ' +
  'borderTopColor borderTopStyle borderTopWidth bottom clip color ' +
  'fontSize fontWeight height left letterSpacing lineHeight ' +
  'marginBottom marginLeft marginRight marginTop markerOffset maxHeight '+
  'maxWidth minHeight minWidth opacity outlineColor outlineOffset ' +
  'outlineWidth paddingBottom paddingLeft paddingRight paddingTop ' +
  'right textIndent top width wordSpacing zIndex');

Element.CSS_LENGTH = /^(([\+\-]?[0-9\.]+)(em|ex|px|in|cm|mm|pt|pc|\%))|0$/;

String.__parseStyleElement = document.createElement('div');
String.prototype.parseStyle = function(){
  var style, styleRules = $H();
  if (Prototype.Browser.WebKit)
    style = new Element('div',{style:this}).style;
  else {
    String.__parseStyleElement.innerHTML = '<div style="' + this + '"></div>';
    style = String.__parseStyleElement.childNodes[0].style;
  }

  Element.CSS_PROPERTIES.each(function(property){
    if (style[property]) styleRules.set(property, style[property]);
  });

  if (Prototype.Browser.IE && this.include('opacity'))
    styleRules.set('opacity', this.match(/opacity:\s*((?:0|1)?(?:\.\d*)?)/)[1]);

  return styleRules;
};

if (document.defaultView && document.defaultView.getComputedStyle) {
  Element.getStyles = function(element) {
    var css = document.defaultView.getComputedStyle($(element), null);
    return Element.CSS_PROPERTIES.inject({ }, function(styles, property) {
      styles[property] = css[property];
      return styles;
    });
  };
} else {
  Element.getStyles = function(element) {
    element = $(element);
    var css = element.currentStyle, styles;
    styles = Element.CSS_PROPERTIES.inject({ }, function(results, property) {
      results[property] = css[property];
      return results;
    });
    if (!styles.opacity) styles.opacity = element.getOpacity();
    return styles;
  };
}

Effect.Methods = {
  morph: function(element, style) {
    element = $(element);
    new Effect.Morph(element, Object.extend({ style: style }, arguments[2] || { }));
    return element;
  },
  visualEffect: function(element, effect, options) {
    element = $(element);
    var s = effect.dasherize().camelize(), klass = s.charAt(0).toUpperCase() + s.substring(1);
    new Effect[klass](element, options);
    return element;
  },
  highlight: function(element, options) {
    element = $(element);
    new Effect.Highlight(element, options);
    return element;
  }
};

$w('fade appear grow shrink fold blindUp blindDown slideUp slideDown '+
  'pulsate shake puff squish switchOff dropOut').each(
  function(effect) {
    Effect.Methods[effect] = function(element, options){
      element = $(element);
      Effect[effect.charAt(0).toUpperCase() + effect.substring(1)](element, options);
      return element;
    };
  }
);

$w('getInlineOpacity forceRerendering setContentZoom collectTextNodes collectTextNodesIgnoreClass getStyles').each(
  function(f) { Effect.Methods[f] = Element[f]; }
);

Element.addMethods(Effect.Methods);// script.aculo.us dragdrop.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2008 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if(Object.isUndefined(Effect))
  throw("dragdrop.js requires including script.aculo.us' effects.js library");

var Droppables = {
  drops: [],

  remove: function(element) {
    this.drops = this.drops.reject(function(d) { return d.element==$(element) });
  },

  add: function(element) {
    element = $(element);
    var options = Object.extend({
      greedy:     true,
      hoverclass: null,
      tree:       false
    }, arguments[1] || { });

    // cache containers
    if(options.containment) {
      options._containers = [];
      var containment = options.containment;
      if(Object.isArray(containment)) {
        containment.each( function(c) { options._containers.push($(c)) });
      } else {
        options._containers.push($(containment));
      }
    }

    if(options.accept) options.accept = [options.accept].flatten();

    Element.makePositioned(element); // fix IE
    options.element = element;

    this.drops.push(options);
  },

  findDeepestChild: function(drops) {
    deepest = drops[0];

    for (i = 1; i < drops.length; ++i)
      if (Element.isParent(drops[i].element, deepest.element))
        deepest = drops[i];

    return deepest;
  },

  isContained: function(element, drop) {
    var containmentNode;
    if(drop.tree) {
      containmentNode = element.treeNode;
    } else {
      containmentNode = element.parentNode;
    }
    return drop._containers.detect(function(c) { return containmentNode == c });
  },

  isAffected: function(point, element, drop) {
    return (
      (drop.element!=element) &&
      ((!drop._containers) ||
        this.isContained(element, drop)) &&
      ((!drop.accept) ||
        (Element.classNames(element).detect(
          function(v) { return drop.accept.include(v) } ) )) &&
      Position.within(drop.element, point[0], point[1]) );
  },

  deactivate: function(drop) {
    if(drop.hoverclass)
      Element.removeClassName(drop.element, drop.hoverclass);
    this.last_active = null;
  },

  activate: function(drop) {
    if(drop.hoverclass)
      Element.addClassName(drop.element, drop.hoverclass);
    this.last_active = drop;
  },

  show: function(point, element) {
    if(!this.drops.length) return;
    var drop, affected = [];

    this.drops.each( function(drop) {
      if(Droppables.isAffected(point, element, drop))
        affected.push(drop);
    });

    if(affected.length>0)
      drop = Droppables.findDeepestChild(affected);

    if(this.last_active && this.last_active != drop) this.deactivate(this.last_active);
    if (drop) {
      Position.within(drop.element, point[0], point[1]);
      if(drop.onHover)
        drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));

      if (drop != this.last_active) Droppables.activate(drop);
    }
  },

  fire: function(event, element) {
    if(!this.last_active) return;
    Position.prepare();

    if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
      if (this.last_active.onDrop) {
        this.last_active.onDrop(element, this.last_active.element, event);
        return true;
      }
  },

  reset: function() {
    if(this.last_active)
      this.deactivate(this.last_active);
  }
};

var Draggables = {
  drags: [],
  observers: [],

  register: function(draggable) {
    if(this.drags.length == 0) {
      this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
      this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
      this.eventKeypress  = this.keyPress.bindAsEventListener(this);

      Event.observe(document, "mouseup", this.eventMouseUp);
      Event.observe(document, "mousemove", this.eventMouseMove);
      Event.observe(document, "keypress", this.eventKeypress);
    }
    this.drags.push(draggable);
  },

  unregister: function(draggable) {
    this.drags = this.drags.reject(function(d) { return d==draggable });
    if(this.drags.length == 0) {
      Event.stopObserving(document, "mouseup", this.eventMouseUp);
      Event.stopObserving(document, "mousemove", this.eventMouseMove);
      Event.stopObserving(document, "keypress", this.eventKeypress);
    }
  },

  activate: function(draggable) {
    if(draggable.options.delay) {
      this._timeout = setTimeout(function() {
        Draggables._timeout = null;
        window.focus();
        Draggables.activeDraggable = draggable;
      }.bind(this), draggable.options.delay);
    } else {
      window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
      this.activeDraggable = draggable;
    }
  },

  deactivate: function() {
    this.activeDraggable = null;
  },

  updateDrag: function(event) {
    if(!this.activeDraggable) return;
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    // Mozilla-based browsers fire successive mousemove events with
    // the same coordinates, prevent needless redrawing (moz bug?)
    if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
    this._lastPointer = pointer;

    this.activeDraggable.updateDrag(event, pointer);
  },

  endDrag: function(event) {
    if(this._timeout) {
      clearTimeout(this._timeout);
      this._timeout = null;
    }
    if(!this.activeDraggable) return;
    this._lastPointer = null;
    this.activeDraggable.endDrag(event);
    this.activeDraggable = null;
  },

  keyPress: function(event) {
    if(this.activeDraggable)
      this.activeDraggable.keyPress(event);
  },

  addObserver: function(observer) {
    this.observers.push(observer);
    this._cacheObserverCallbacks();
  },

  removeObserver: function(element) {  // element instead of observer fixes mem leaks
    this.observers = this.observers.reject( function(o) { return o.element==element });
    this._cacheObserverCallbacks();
  },

  notify: function(eventName, draggable, event) {  // 'onStart', 'onEnd', 'onDrag'
    if(this[eventName+'Count'] > 0)
      this.observers.each( function(o) {
        if(o[eventName]) o[eventName](eventName, draggable, event);
      });
    if(draggable.options[eventName]) draggable.options[eventName](draggable, event);
  },

  _cacheObserverCallbacks: function() {
    ['onStart','onEnd','onDrag'].each( function(eventName) {
      Draggables[eventName+'Count'] = Draggables.observers.select(
        function(o) { return o[eventName]; }
      ).length;
    });
  }
};

/*--------------------------------------------------------------------------*/

var Draggable = Class.create({
  initialize: function(element) {
    var defaults = {
      handle: false,
      reverteffect: function(element, top_offset, left_offset) {
        var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
        new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur,
          queue: {scope:'_draggable', position:'end'}
        });
      },
      endeffect: function(element) {
        var toOpacity = Object.isNumber(element._opacity) ? element._opacity : 1.0;
        new Effect.Opacity(element, {duration:0.2, from:0.7, to:toOpacity,
          queue: {scope:'_draggable', position:'end'},
          afterFinish: function(){
            Draggable._dragging[element] = false
          }
        });
      },
      zindex: 1000,
      revert: false,
      quiet: false,
      scroll: false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      snap: false,  // false, or xy or [x,y] or function(x,y){ return [x,y] }
      delay: 0
    };

    if(!arguments[1] || Object.isUndefined(arguments[1].endeffect))
      Object.extend(defaults, {
        starteffect: function(element) {
          element._opacity = Element.getOpacity(element);
          Draggable._dragging[element] = true;
          new Effect.Opacity(element, {duration:0.2, from:element._opacity, to:0.7});
        }
      });

    var options = Object.extend(defaults, arguments[1] || { });

    this.element = $(element);

    if(options.handle && Object.isString(options.handle))
      this.handle = this.element.down('.'+options.handle, 0);

    if(!this.handle) this.handle = $(options.handle);
    if(!this.handle) this.handle = this.element;

    if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML) {
      options.scroll = $(options.scroll);
      this._isScrollChild = Element.childOf(this.element, options.scroll);
    }

    Element.makePositioned(this.element); // fix IE

    this.options  = options;
    this.dragging = false;

    this.eventMouseDown = this.initDrag.bindAsEventListener(this);
    Event.observe(this.handle, "mousedown", this.eventMouseDown);

    Draggables.register(this);
  },

  destroy: function() {
    Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
    Draggables.unregister(this);
  },

  currentDelta: function() {
    return([
      parseInt(Element.getStyle(this.element,'left') || '0'),
      parseInt(Element.getStyle(this.element,'top') || '0')]);
  },

  initDrag: function(event) {
    if(!Object.isUndefined(Draggable._dragging[this.element]) &&
      Draggable._dragging[this.element]) return;
    if(Event.isLeftClick(event)) {
      // abort on form elements, fixes a Firefox issue
      var src = Event.element(event);
      if((tag_name = src.tagName.toUpperCase()) && (
        tag_name=='INPUT' ||
        tag_name=='SELECT' ||
        tag_name=='OPTION' ||
        tag_name=='BUTTON' ||
        tag_name=='TEXTAREA')) return;

      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      var pos     = Position.cumulativeOffset(this.element);
      this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });

      Draggables.activate(this);
      Event.stop(event);
    }
  },

  startDrag: function(event) {
    this.dragging = true;
    if(!this.delta)
      this.delta = this.currentDelta();

    if(this.options.zindex) {
      this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
      this.element.style.zIndex = this.options.zindex;
    }

    if(this.options.ghosting) {
      this._clone = this.element.cloneNode(true);
      this._originallyAbsolute = (this.element.getStyle('position') == 'absolute');
      if (!this._originallyAbsolute)
        Position.absolutize(this.element);
      this.element.parentNode.insertBefore(this._clone, this.element);
    }

    if(this.options.scroll) {
      if (this.options.scroll == window) {
        var where = this._getWindowScroll(this.options.scroll);
        this.originalScrollLeft = where.left;
        this.originalScrollTop = where.top;
      } else {
        this.originalScrollLeft = this.options.scroll.scrollLeft;
        this.originalScrollTop = this.options.scroll.scrollTop;
      }
    }

    Draggables.notify('onStart', this, event);

    if(this.options.starteffect) this.options.starteffect(this.element);
  },

  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);

    if(!this.options.quiet){
      Position.prepare();
      Droppables.show(pointer, this.element);
    }

    Draggables.notify('onDrag', this, event);

    this.draw(pointer);
    if(this.options.change) this.options.change(this);

    if(this.options.scroll) {
      this.stopScrolling();

      var p;
      if (this.options.scroll == window) {
        with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
      } else {
        p = Position.page(this.options.scroll);
        p[0] += this.options.scroll.scrollLeft + Position.deltaX;
        p[1] += this.options.scroll.scrollTop + Position.deltaY;
        p.push(p[0]+this.options.scroll.offsetWidth);
        p.push(p[1]+this.options.scroll.offsetHeight);
      }
      var speed = [0,0];
      if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
      if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
      if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
      if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
      this.startScrolling(speed);
    }

    // fix AppleWebKit rendering
    if(Prototype.Browser.WebKit) window.scrollBy(0,0);

    Event.stop(event);
  },

  finishDrag: function(event, success) {
    this.dragging = false;

    if(this.options.quiet){
      Position.prepare();
      var pointer = [Event.pointerX(event), Event.pointerY(event)];
      Droppables.show(pointer, this.element);
    }

    if(this.options.ghosting) {
      if (!this._originallyAbsolute)
        Position.relativize(this.element);
      delete this._originallyAbsolute;
      Element.remove(this._clone);
      this._clone = null;
    }

    var dropped = false;
    if(success) {
      dropped = Droppables.fire(event, this.element);
      if (!dropped) dropped = false;
    }
    if(dropped && this.options.onDropped) this.options.onDropped(this.element);
    Draggables.notify('onEnd', this, event);

    var revert = this.options.revert;
    if(revert && Object.isFunction(revert)) revert = revert(this.element);

    var d = this.currentDelta();
    if(revert && this.options.reverteffect) {
      if (dropped == 0 || revert != 'failure')
        this.options.reverteffect(this.element,
          d[1]-this.delta[1], d[0]-this.delta[0]);
    } else {
      this.delta = d;
    }

    if(this.options.zindex)
      this.element.style.zIndex = this.originalZ;

    if(this.options.endeffect)
      this.options.endeffect(this.element);

    Draggables.deactivate(this);
    Droppables.reset();
  },

  keyPress: function(event) {
    if(event.keyCode!=Event.KEY_ESC) return;
    this.finishDrag(event, false);
    Event.stop(event);
  },

  endDrag: function(event) {
    if(!this.dragging) return;
    this.stopScrolling();
    this.finishDrag(event, true);
    Event.stop(event);
  },

  draw: function(point) {
    var pos = Position.cumulativeOffset(this.element);
    if(this.options.ghosting) {
      var r   = Position.realOffset(this.element);
      pos[0] += r[0] - Position.deltaX; pos[1] += r[1] - Position.deltaY;
    }

    var d = this.currentDelta();
    pos[0] -= d[0]; pos[1] -= d[1];

    if(this.options.scroll && (this.options.scroll != window && this._isScrollChild)) {
      pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
      pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
    }

    var p = [0,1].map(function(i){
      return (point[i]-pos[i]-this.offset[i])
    }.bind(this));

    if(this.options.snap) {
      if(Object.isFunction(this.options.snap)) {
        p = this.options.snap(p[0],p[1],this);
      } else {
      if(Object.isArray(this.options.snap)) {
        p = p.map( function(v, i) {
          return (v/this.options.snap[i]).round()*this.options.snap[i] }.bind(this));
      } else {
        p = p.map( function(v) {
          return (v/this.options.snap).round()*this.options.snap }.bind(this));
      }
    }}

    var style = this.element.style;
    if((!this.options.constraint) || (this.options.constraint=='horizontal'))
      style.left = p[0] + "px";
    if((!this.options.constraint) || (this.options.constraint=='vertical'))
      style.top  = p[1] + "px";

    if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
  },

  stopScrolling: function() {
    if(this.scrollInterval) {
      clearInterval(this.scrollInterval);
      this.scrollInterval = null;
      Draggables._lastScrollPointer = null;
    }
  },

  startScrolling: function(speed) {
    if(!(speed[0] || speed[1])) return;
    this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
    this.lastScrolled = new Date();
    this.scrollInterval = setInterval(this.scroll.bind(this), 10);
  },

  scroll: function() {
    var current = new Date();
    var delta = current - this.lastScrolled;
    this.lastScrolled = current;
    if(this.options.scroll == window) {
      with (this._getWindowScroll(this.options.scroll)) {
        if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
          var d = delta / 1000;
          this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
        }
      }
    } else {
      this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
      this.options.scroll.scrollTop  += this.scrollSpeed[1] * delta / 1000;
    }

    Position.prepare();
    Droppables.show(Draggables._lastPointer, this.element);
    Draggables.notify('onDrag', this);
    if (this._isScrollChild) {
      Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
      Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
      Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
      if (Draggables._lastScrollPointer[0] < 0)
        Draggables._lastScrollPointer[0] = 0;
      if (Draggables._lastScrollPointer[1] < 0)
        Draggables._lastScrollPointer[1] = 0;
      this.draw(Draggables._lastScrollPointer);
    }

    if(this.options.change) this.options.change(this);
  },

  _getWindowScroll: function(w) {
    var T, L, W, H;
    with (w.document) {
      if (w.document.documentElement && documentElement.scrollTop) {
        T = documentElement.scrollTop;
        L = documentElement.scrollLeft;
      } else if (w.document.body) {
        T = body.scrollTop;
        L = body.scrollLeft;
      }
      if (w.innerWidth) {
        W = w.innerWidth;
        H = w.innerHeight;
      } else if (w.document.documentElement && documentElement.clientWidth) {
        W = documentElement.clientWidth;
        H = documentElement.clientHeight;
      } else {
        W = body.offsetWidth;
        H = body.offsetHeight;
      }
    }
    return { top: T, left: L, width: W, height: H };
  }
});

Draggable._dragging = { };

/*--------------------------------------------------------------------------*/

var SortableObserver = Class.create({
  initialize: function(element, observer) {
    this.element   = $(element);
    this.observer  = observer;
    this.lastValue = Sortable.serialize(this.element);
  },

  onStart: function() {
    this.lastValue = Sortable.serialize(this.element);
  },

  onEnd: function() {
    Sortable.unmark();
    if(this.lastValue != Sortable.serialize(this.element))
      this.observer(this.element)
  }
});

var Sortable = {
  SERIALIZE_RULE: /^[^_\-](?:[A-Za-z0-9\-\_]*)[_](.*)$/,

  sortables: { },

  _findRootElement: function(element) {
    while (element.tagName.toUpperCase() != "BODY") {
      if(element.id && Sortable.sortables[element.id]) return element;
      element = element.parentNode;
    }
  },

  options: function(element) {
    element = Sortable._findRootElement($(element));
    if(!element) return;
    return Sortable.sortables[element.id];
  },

  destroy: function(element){
    element = $(element);
    var s = Sortable.sortables[element.id];

    if(s) {
      Draggables.removeObserver(s.element);
      s.droppables.each(function(d){ Droppables.remove(d) });
      s.draggables.invoke('destroy');

      delete Sortable.sortables[s.element.id];
    }
  },

  create: function(element) {
    element = $(element);
    var options = Object.extend({
      element:     element,
      tag:         'li',       // assumes li children, override with tag: 'tagname'
      dropOnEmpty: false,
      tree:        false,
      treeTag:     'ul',
      overlap:     'vertical', // one of 'vertical', 'horizontal'
      constraint:  'vertical', // one of 'vertical', 'horizontal', false
      containment: element,    // also takes array of elements (or id's); or false
      handle:      false,      // or a CSS class
      only:        false,
      delay:       0,
      hoverclass:  null,
      ghosting:    false,
      quiet:       false,
      scroll:      false,
      scrollSensitivity: 20,
      scrollSpeed: 15,
      format:      this.SERIALIZE_RULE,

      // these take arrays of elements or ids and can be
      // used for better initialization performance
      elements:    false,
      handles:     false,

      onChange:    Prototype.emptyFunction,
      onUpdate:    Prototype.emptyFunction
    }, arguments[1] || { });

    // clear any old sortable with same element
    this.destroy(element);

    // build options for the draggables
    var options_for_draggable = {
      revert:      true,
      quiet:       options.quiet,
      scroll:      options.scroll,
      scrollSpeed: options.scrollSpeed,
      scrollSensitivity: options.scrollSensitivity,
      delay:       options.delay,
      ghosting:    options.ghosting,
      constraint:  options.constraint,
      handle:      options.handle };

    if(options.starteffect)
      options_for_draggable.starteffect = options.starteffect;

    if(options.reverteffect)
      options_for_draggable.reverteffect = options.reverteffect;
    else
      if(options.ghosting) options_for_draggable.reverteffect = function(element) {
        element.style.top  = 0;
        element.style.left = 0;
      };

    if(options.endeffect)
      options_for_draggable.endeffect = options.endeffect;

    if(options.zindex)
      options_for_draggable.zindex = options.zindex;

    // build options for the droppables
    var options_for_droppable = {
      overlap:     options.overlap,
      containment: options.containment,
      tree:        options.tree,
      hoverclass:  options.hoverclass,
      onHover:     Sortable.onHover
    };

    var options_for_tree = {
      onHover:      Sortable.onEmptyHover,
      overlap:      options.overlap,
      containment:  options.containment,
      hoverclass:   options.hoverclass
    };

    // fix for gecko engine
    Element.cleanWhitespace(element);

    options.draggables = [];
    options.droppables = [];

    // drop on empty handling
    if(options.dropOnEmpty || options.tree) {
      Droppables.add(element, options_for_tree);
      options.droppables.push(element);
    }

    (options.elements || this.findElements(element, options) || []).each( function(e,i) {
      var handle = options.handles ? $(options.handles[i]) :
        (options.handle ? $(e).select('.' + options.handle)[0] : e);
      options.draggables.push(
        new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
      Droppables.add(e, options_for_droppable);
      if(options.tree) e.treeNode = element;
      options.droppables.push(e);
    });

    if(options.tree) {
      (Sortable.findTreeElements(element, options) || []).each( function(e) {
        Droppables.add(e, options_for_tree);
        e.treeNode = element;
        options.droppables.push(e);
      });
    }

    // keep reference
    this.sortables[element.id] = options;

    // for onupdate
    Draggables.addObserver(new SortableObserver(element, options.onUpdate));

  },

  // return all suitable-for-sortable elements in a guaranteed order
  findElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.tag);
  },

  findTreeElements: function(element, options) {
    return Element.findChildren(
      element, options.only, options.tree ? true : false, options.treeTag);
  },

  onHover: function(element, dropon, overlap) {
    if(Element.isParent(dropon, element)) return;

    if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
      return;
    } else if(overlap>0.5) {
      Sortable.mark(dropon, 'before');
      if(dropon.previousSibling != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, dropon);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    } else {
      Sortable.mark(dropon, 'after');
      var nextElement = dropon.nextSibling || null;
      if(nextElement != element) {
        var oldParentNode = element.parentNode;
        element.style.visibility = "hidden"; // fix gecko rendering
        dropon.parentNode.insertBefore(element, nextElement);
        if(dropon.parentNode!=oldParentNode)
          Sortable.options(oldParentNode).onChange(element);
        Sortable.options(dropon.parentNode).onChange(element);
      }
    }
  },

  onEmptyHover: function(element, dropon, overlap) {
    var oldParentNode = element.parentNode;
    var droponOptions = Sortable.options(dropon);

    if(!Element.isParent(dropon, element)) {
      var index;

      var children = Sortable.findElements(dropon, {tag: droponOptions.tag, only: droponOptions.only});
      var child = null;

      if(children) {
        var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);

        for (index = 0; index < children.length; index += 1) {
          if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
            offset -= Element.offsetSize (children[index], droponOptions.overlap);
          } else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
            child = index + 1 < children.length ? children[index + 1] : null;
            break;
          } else {
            child = children[index];
            break;
          }
        }
      }

      dropon.insertBefore(element, child);

      Sortable.options(oldParentNode).onChange(element);
      droponOptions.onChange(element);
    }
  },

  unmark: function() {
    if(Sortable._marker) Sortable._marker.hide();
  },

  mark: function(dropon, position) {
    // mark on ghosting only
    var sortable = Sortable.options(dropon.parentNode);
    if(sortable && !sortable.ghosting) return;

    if(!Sortable._marker) {
      Sortable._marker =
        ($('dropmarker') || Element.extend(document.createElement('DIV'))).
          hide().addClassName('dropmarker').setStyle({position:'absolute'});
      document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
    }
    var offsets = Position.cumulativeOffset(dropon);
    Sortable._marker.setStyle({left: offsets[0]+'px', top: offsets[1] + 'px'});

    if(position=='after')
      if(sortable.overlap == 'horizontal')
        Sortable._marker.setStyle({left: (offsets[0]+dropon.clientWidth) + 'px'});
      else
        Sortable._marker.setStyle({top: (offsets[1]+dropon.clientHeight) + 'px'});

    Sortable._marker.show();
  },

  _tree: function(element, options, parent) {
    var children = Sortable.findElements(element, options) || [];

    for (var i = 0; i < children.length; ++i) {
      var match = children[i].id.match(options.format);

      if (!match) continue;

      var child = {
        id: encodeURIComponent(match ? match[1] : null),
        element: element,
        parent: parent,
        children: [],
        position: parent.children.length,
        container: $(children[i]).down(options.treeTag)
      };

      /* Get the element containing the children and recurse over it */
      if (child.container)
        this._tree(child.container, options, child);

      parent.children.push (child);
    }

    return parent;
  },

  tree: function(element) {
    element = $(element);
    var sortableOptions = this.options(element);
    var options = Object.extend({
      tag: sortableOptions.tag,
      treeTag: sortableOptions.treeTag,
      only: sortableOptions.only,
      name: element.id,
      format: sortableOptions.format
    }, arguments[1] || { });

    var root = {
      id: null,
      parent: null,
      children: [],
      container: element,
      position: 0
    };

    return Sortable._tree(element, options, root);
  },

  /* Construct a [i] index for a particular node */
  _constructIndex: function(node) {
    var index = '';
    do {
      if (node.id) index = '[' + node.position + ']' + index;
    } while ((node = node.parent) != null);
    return index;
  },

  sequence: function(element) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[1] || { });

    return $(this.findElements(element, options) || []).map( function(item) {
      return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
    });
  },

  setSequence: function(element, new_sequence) {
    element = $(element);
    var options = Object.extend(this.options(element), arguments[2] || { });

    var nodeMap = { };
    this.findElements(element, options).each( function(n) {
        if (n.id.match(options.format))
            nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
        n.parentNode.removeChild(n);
    });

    new_sequence.each(function(ident) {
      var n = nodeMap[ident];
      if (n) {
        n[1].appendChild(n[0]);
        delete nodeMap[ident];
      }
    });
  },

  serialize: function(element) {
    element = $(element);
    var options = Object.extend(Sortable.options(element), arguments[1] || { });
    var name = encodeURIComponent(
      (arguments[1] && arguments[1].name) ? arguments[1].name : element.id);

    if (options.tree) {
      return Sortable.tree(element, arguments[1]).children.map( function (item) {
        return [name + Sortable._constructIndex(item) + "[id]=" +
                encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
      }).flatten().join('&');
    } else {
      return Sortable.sequence(element, arguments[1]).map( function(item) {
        return name + "[]=" + encodeURIComponent(item);
      }).join('&');
    }
  }
};

// Returns true if child is contained within element
Element.isParent = function(child, element) {
  if (!child.parentNode || child == element) return false;
  if (child.parentNode == element) return true;
  return Element.isParent(child.parentNode, element);
};

Element.findChildren = function(element, only, recursive, tagName) {
  if(!element.hasChildNodes()) return null;
  tagName = tagName.toUpperCase();
  if(only) only = [only].flatten();
  var elements = [];
  $A(element.childNodes).each( function(e) {
    if(e.tagName && e.tagName.toUpperCase()==tagName &&
      (!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
        elements.push(e);
    if(recursive) {
      var grandchildren = Element.findChildren(e, only, recursive, tagName);
      if(grandchildren) elements.push(grandchildren);
    }
  });

  return (elements.length>0 ? elements.flatten() : []);
};

Element.offsetSize = function (element, type) {
  return element['offset' + ((type=='vertical' || type=='height') ? 'Height' : 'Width')];
};// script.aculo.us controls.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//           (c) 2005-2008 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
//           (c) 2005-2008 Jon Tirsen (http://www.tirsen.com)
// Contributors:
//  Richard Livsey
//  Rahul Bhargava
//  Rob Wills
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.

if(typeof Effect == 'undefined')
  throw("controls.js requires including script.aculo.us' effects.js library");

var Autocompleter = { };
Autocompleter.Base = Class.create({
  baseInitialize: function(element, update, options) {
    element          = $(element);
    this.element     = element;
    this.update      = $(update);
    this.hasFocus    = false;
    this.changed     = false;
    this.active      = false;
    this.index       = 0;
    this.entryCount  = 0;
    this.oldElementValue = this.element.value;

    if(this.setOptions)
      this.setOptions(options);
    else
      this.options = options || { };

    this.options.paramName    = this.options.paramName || this.element.name;
    this.options.tokens       = this.options.tokens || [];
    this.options.frequency    = this.options.frequency || 0.4;
    this.options.minChars     = this.options.minChars || 1;
    this.options.onShow       = this.options.onShow ||
      function(element, update){
        if(!update.style.position || update.style.position=='absolute') {
          update.style.position = 'absolute';
          Position.clone(element, update, {
            setHeight: false,
            offsetTop: element.offsetHeight
          });
        }
        Effect.Appear(update,{duration:0.15});
      };
    this.options.onHide = this.options.onHide ||
      function(element, update){ new Effect.Fade(update,{duration:0.15}) };

    if(typeof(this.options.tokens) == 'string')
      this.options.tokens = new Array(this.options.tokens);
    // Force carriage returns as token delimiters anyway
    if (!this.options.tokens.include('\n'))
      this.options.tokens.push('\n');

    this.observer = null;

    this.element.setAttribute('autocomplete','off');

    Element.hide(this.update);

    Event.observe(this.element, 'blur', this.onBlur.bindAsEventListener(this));
    Event.observe(this.element, 'keydown', this.onKeyPress.bindAsEventListener(this));
  },

  show: function() {
    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
    if(!this.iefix &&
      (Prototype.Browser.IE) &&
      (Element.getStyle(this.update, 'position')=='absolute')) {
      new Insertion.After(this.update,
       '<iframe id="' + this.update.id + '_iefix" '+
       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
      this.iefix = $(this.update.id+'_iefix');
    }
    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
  },

  fixIEOverlapping: function() {
    Position.clone(this.update, this.iefix, {setTop:(!this.update.style.height)});
    this.iefix.style.zIndex = 1;
    this.update.style.zIndex = 2;
    Element.show(this.iefix);
  },

  hide: function() {
    this.stopIndicator();
    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
    if(this.iefix) Element.hide(this.iefix);
  },

  startIndicator: function() {
    if(this.options.indicator) Element.show(this.options.indicator);
  },

  stopIndicator: function() {
    if(this.options.indicator) Element.hide(this.options.indicator);
  },

  onKeyPress: function(event) {
    if(this.active)
      switch(event.keyCode) {
       case Event.KEY_TAB:
       case Event.KEY_RETURN:
         this.selectEntry();
         Event.stop(event);
       case Event.KEY_ESC:
         this.hide();
         this.active = false;
         Event.stop(event);
         return;
       case Event.KEY_LEFT:
       case Event.KEY_RIGHT:
         return;
       case Event.KEY_UP:
         this.markPrevious();
         this.render();
         Event.stop(event);
         return;
       case Event.KEY_DOWN:
         this.markNext();
         this.render();
         Event.stop(event);
         return;
      }
     else
       if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
         (Prototype.Browser.WebKit > 0 && event.keyCode == 0)) return;

    this.changed = true;
    this.hasFocus = true;

    if(this.observer) clearTimeout(this.observer);
      this.observer =
        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
  },

  activate: function() {
    this.changed = false;
    this.hasFocus = true;
    this.getUpdatedChoices();
  },

  onHover: function(event) {
    var element = Event.findElement(event, 'LI');
    if(this.index != element.autocompleteIndex)
    {
        this.index = element.autocompleteIndex;
        this.render();
    }
    Event.stop(event);
  },

  onClick: function(event) {
    var element = Event.findElement(event, 'LI');
    this.index = element.autocompleteIndex;
    this.selectEntry();
    this.hide();
  },

  onBlur: function(event) {
    // needed to make click events working
    setTimeout(this.hide.bind(this), 250);
    this.hasFocus = false;
    this.active = false;
  },

  render: function() {
    if(this.entryCount > 0) {
      for (var i = 0; i < this.entryCount; i++)
        this.index==i ?
          Element.addClassName(this.getEntry(i),"selected") :
          Element.removeClassName(this.getEntry(i),"selected");
      if(this.hasFocus) {
        this.show();
        this.active = true;
      }
    } else {
      this.active = false;
      this.hide();
    }
  },

  markPrevious: function() {
    if(this.index > 0) this.index--;
      else this.index = this.entryCount-1;
    this.getEntry(this.index).scrollIntoView(true);
  },

  markNext: function() {
    if(this.index < this.entryCount-1) this.index++;
      else this.index = 0;
    this.getEntry(this.index).scrollIntoView(false);
  },

  getEntry: function(index) {
    return this.update.firstChild.childNodes[index];
  },

  getCurrentEntry: function() {
    return this.getEntry(this.index);
  },

  selectEntry: function() {
    this.active = false;
    this.updateElement(this.getCurrentEntry());
  },

  updateElement: function(selectedElement) {
    if (this.options.updateElement) {
      this.options.updateElement(selectedElement);
      return;
    }
    var value = '';
    if (this.options.select) {
      var nodes = $(selectedElement).select('.' + this.options.select) || [];
      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
    } else
      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');

    var bounds = this.getTokenBounds();
    if (bounds[0] != -1) {
      var newValue = this.element.value.substr(0, bounds[0]);
      var whitespace = this.element.value.substr(bounds[0]).match(/^\s+/);
      if (whitespace)
        newValue += whitespace[0];
      this.element.value = newValue + value + this.element.value.substr(bounds[1]);
    } else {
      this.element.value = value;
    }
    this.oldElementValue = this.element.value;
    this.element.focus();

    if (this.options.afterUpdateElement)
      this.options.afterUpdateElement(this.element, selectedElement);
  },

  updateChoices: function(choices) {
    if(!this.changed && this.hasFocus) {
      this.update.innerHTML = choices;
      Element.cleanWhitespace(this.update);
      Element.cleanWhitespace(this.update.down());

      if(this.update.firstChild && this.update.down().childNodes) {
        this.entryCount =
          this.update.down().childNodes.length;
        for (var i = 0; i < this.entryCount; i++) {
          var entry = this.getEntry(i);
          entry.autocompleteIndex = i;
          this.addObservers(entry);
        }
      } else {
        this.entryCount = 0;
      }

      this.stopIndicator();
      this.index = 0;

      if(this.entryCount==1 && this.options.autoSelect) {
        this.selectEntry();
        this.hide();
      } else {
        this.render();
      }
    }
  },

  addObservers: function(element) {
    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
  },

  onObserverEvent: function() {
    this.changed = false;
    this.tokenBounds = null;
    if(this.getToken().length>=this.options.minChars) {
      this.getUpdatedChoices();
    } else {
      this.active = false;
      this.hide();
    }
    this.oldElementValue = this.element.value;
  },

  getToken: function() {
    var bounds = this.getTokenBounds();
    return this.element.value.substring(bounds[0], bounds[1]).strip();
  },

  getTokenBounds: function() {
    if (null != this.tokenBounds) return this.tokenBounds;
    var value = this.element.value;
    if (value.strip().empty()) return [-1, 0];
    var diff = arguments.callee.getFirstDifferencePos(value, this.oldElementValue);
    var offset = (diff == this.oldElementValue.length ? 1 : 0);
    var prevTokenPos = -1, nextTokenPos = value.length;
    var tp;
    for (var index = 0, l = this.options.tokens.length; index < l; ++index) {
      tp = value.lastIndexOf(this.options.tokens[index], diff + offset - 1);
      if (tp > prevTokenPos) prevTokenPos = tp;
      tp = value.indexOf(this.options.tokens[index], diff + offset);
      if (-1 != tp && tp < nextTokenPos) nextTokenPos = tp;
    }
    return (this.tokenBounds = [prevTokenPos + 1, nextTokenPos]);
  }
});

Autocompleter.Base.prototype.getTokenBounds.getFirstDifferencePos = function(newS, oldS) {
  var boundary = Math.min(newS.length, oldS.length);
  for (var index = 0; index < boundary; ++index)
    if (newS[index] != oldS[index])
      return index;
  return boundary;
};

Ajax.Autocompleter = Class.create(Autocompleter.Base, {
  initialize: function(element, update, url, options) {
    this.baseInitialize(element, update, options);
    this.options.asynchronous  = true;
    this.options.onComplete    = this.onComplete.bind(this);
    this.options.defaultParams = this.options.parameters || null;
    this.url                   = url;
  },

  getUpdatedChoices: function() {
    this.startIndicator();

    var entry = encodeURIComponent(this.options.paramName) + '=' +
      encodeURIComponent(this.getToken());

    this.options.parameters = this.options.callback ?
      this.options.callback(this.element, entry) : entry;

    if(this.options.defaultParams)
      this.options.parameters += '&' + this.options.defaultParams;

    new Ajax.Request(this.url, this.options);
  },

  onComplete: function(request) {
    this.updateChoices(request.responseText);
  }
});

// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
//                    text only at the beginning of strings in the
//                    autocomplete array. Defaults to true, which will
//                    match text at the beginning of any *word* in the
//                    strings in the autocomplete array. If you want to
//                    search anywhere in the string, additionally set
//                    the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
//                   a partial match (unlike minChars, which defines
//                   how many characters are required to do any match
//                   at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
//                 Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.

Autocompleter.Local = Class.create(Autocompleter.Base, {
  initialize: function(element, update, array, options) {
    this.baseInitialize(element, update, options);
    this.options.array = array;
  },

  getUpdatedChoices: function() {
    this.updateChoices(this.options.selector(this));
  },

  setOptions: function(options) {
    this.options = Object.extend({
      choices: 10,
      partialSearch: true,
      partialChars: 2,
      ignoreCase: true,
      fullSearch: false,
      selector: function(instance) {
        var ret       = []; // Beginning matches
        var partial   = []; // Inside matches
        var entry     = instance.getToken();
        var count     = 0;

        for (var i = 0; i < instance.options.array.length &&
          ret.length < instance.options.choices ; i++) {

          var elem = instance.options.array[i];
          var foundPos = instance.options.ignoreCase ?
            elem.toLowerCase().indexOf(entry.toLowerCase()) :
            elem.indexOf(entry);

          while (foundPos != -1) {
            if (foundPos == 0 && elem.length != entry.length) {
              ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
                elem.substr(entry.length) + "</li>");
              break;
            } else if (entry.length >= instance.options.partialChars &&
              instance.options.partialSearch && foundPos != -1) {
              if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
                partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
                  elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
                  foundPos + entry.length) + "</li>");
                break;
              }
            }

            foundPos = instance.options.ignoreCase ?
              elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
              elem.indexOf(entry, foundPos + 1);

          }
        }
        if (partial.length)
          ret = ret.concat(partial.slice(0, instance.options.choices - ret.length));
        return "<ul>" + ret.join('') + "</ul>";
      }
    }, options || { });
  }
});

// AJAX in-place editor and collection editor
// Full rewrite by Christophe Porteneuve <tdd@tddsworld.com> (April 2007).

// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
  setTimeout(function() {
    Field.activate(field);
  }, 1);
};

Ajax.InPlaceEditor = Class.create({
  initialize: function(element, url, options) {
    this.url = url;
    this.element = element = $(element);
    this.prepareOptions();
    this._controls = { };
    arguments.callee.dealWithDeprecatedOptions(options); // DEPRECATION LAYER!!!
    Object.extend(this.options, options || { });
    if (!this.options.formId && this.element.id) {
      this.options.formId = this.element.id + '-inplaceeditor';
      if ($(this.options.formId))
        this.options.formId = '';
    }
    if (this.options.externalControl)
      this.options.externalControl = $(this.options.externalControl);
    if (!this.options.externalControl)
      this.options.externalControlOnly = false;
    this._originalBackground = this.element.getStyle('background-color') || 'transparent';
    this.element.title = this.options.clickToEditText;
    this._boundCancelHandler = this.handleFormCancellation.bind(this);
    this._boundComplete = (this.options.onComplete || Prototype.emptyFunction).bind(this);
    this._boundFailureHandler = this.handleAJAXFailure.bind(this);
    this._boundSubmitHandler = this.handleFormSubmission.bind(this);
    this._boundWrapperHandler = this.wrapUp.bind(this);
    this.registerListeners();
  },
  checkForEscapeOrReturn: function(e) {
    if (!this._editing || e.ctrlKey || e.altKey || e.shiftKey) return;
    if (Event.KEY_ESC == e.keyCode)
      this.handleFormCancellation(e);
    else if (Event.KEY_RETURN == e.keyCode)
      this.handleFormSubmission(e);
  },
  createControl: function(mode, handler, extraClasses) {
    var control = this.options[mode + 'Control'];
    var text = this.options[mode + 'Text'];
    if ('button' == control) {
      var btn = document.createElement('input');
      btn.type = 'submit';
      btn.value = text;
      btn.className = 'editor_' + mode + '_button';
      if ('cancel' == mode)
        btn.onclick = this._boundCancelHandler;
      this._form.appendChild(btn);
      this._controls[mode] = btn;
    } else if ('link' == control) {
      var link = document.createElement('a');
      link.href = '#';
      link.appendChild(document.createTextNode(text));
      link.onclick = 'cancel' == mode ? this._boundCancelHandler : this._boundSubmitHandler;
      link.className = 'editor_' + mode + '_link';
      if (extraClasses)
        link.className += ' ' + extraClasses;
      this._form.appendChild(link);
      this._controls[mode] = link;
    }
  },
  createEditField: function() {
    var text = (this.options.loadTextURL ? this.options.loadingText : this.getText());
    var fld;
    if (1 >= this.options.rows && !/\r|\n/.test(this.getText())) {
      fld = document.createElement('input');
      fld.type = 'text';
      var size = this.options.size || this.options.cols || 0;
      if (0 < size) fld.size = size;
    } else {
      fld = document.createElement('textarea');
      fld.rows = (1 >= this.options.rows ? this.options.autoRows : this.options.rows);
      fld.cols = this.options.cols || 40;
    }
    fld.name = this.options.paramName;
    fld.value = text; // No HTML breaks conversion anymore
    fld.className = 'editor_field';
    if (this.options.submitOnBlur)
      fld.onblur = this._boundSubmitHandler;
    this._controls.editor = fld;
    if (this.options.loadTextURL)
      this.loadExternalText();
    this._form.appendChild(this._controls.editor);
  },
  createForm: function() {
    var ipe = this;
    function addText(mode, condition) {
      var text = ipe.options['text' + mode + 'Controls'];
      if (!text || condition === false) return;
      ipe._form.appendChild(document.createTextNode(text));
    };
    this._form = $(document.createElement('form'));
    this._form.id = this.options.formId;
    this._form.addClassName(this.options.formClassName);
    this._form.onsubmit = this._boundSubmitHandler;
    this.createEditField();
    if ('textarea' == this._controls.editor.tagName.toLowerCase())
      this._form.appendChild(document.createElement('br'));
    if (this.options.onFormCustomization)
      this.options.onFormCustomization(this, this._form);
    addText('Before', this.options.okControl || this.options.cancelControl);
    this.createControl('ok', this._boundSubmitHandler);
    addText('Between', this.options.okControl && this.options.cancelControl);
    this.createControl('cancel', this._boundCancelHandler, 'editor_cancel');
    addText('After', this.options.okControl || this.options.cancelControl);
  },
  destroy: function() {
    if (this._oldInnerHTML)
      this.element.innerHTML = this._oldInnerHTML;
    this.leaveEditMode();
    this.unregisterListeners();
  },
  enterEditMode: function(e) {
    if (this._saving || this._editing) return;
    this._editing = true;
    this.triggerCallback('onEnterEditMode');
    if (this.options.externalControl)
      this.options.externalControl.hide();
    this.element.hide();
    this.createForm();
    this.element.parentNode.insertBefore(this._form, this.element);
    if (!this.options.loadTextURL)
      this.postProcessEditField();
    if (e) Event.stop(e);
  },
  enterHover: function(e) {
    if (this.options.hoverClassName)
      this.element.addClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onEnterHover');
  },
  getText: function() {
    return this.element.innerHTML.unescapeHTML();
  },
  handleAJAXFailure: function(transport) {
    this.triggerCallback('onFailure', transport);
    if (this._oldInnerHTML) {
      this.element.innerHTML = this._oldInnerHTML;
      this._oldInnerHTML = null;
    }
  },
  handleFormCancellation: function(e) {
    this.wrapUp();
    if (e) Event.stop(e);
  },
  handleFormSubmission: function(e) {
    var form = this._form;
    var value = $F(this._controls.editor);
    this.prepareSubmission();
    var params = this.options.callback(form, value) || '';
    if (Object.isString(params))
      params = params.toQueryParams();
    params.editorId = this.element.id;
    if (this.options.htmlResponse) {
      var options = Object.extend({ evalScripts: true }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Updater({ success: this.element }, this.url, options);
    } else {
      var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
      Object.extend(options, {
        parameters: params,
        onComplete: this._boundWrapperHandler,
        onFailure: this._boundFailureHandler
      });
      new Ajax.Request(this.url, options);
    }
    if (e) Event.stop(e);
  },
  leaveEditMode: function() {
    this.element.removeClassName(this.options.savingClassName);
    this.removeForm();
    this.leaveHover();
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
    if (this.options.externalControl)
      this.options.externalControl.show();
    this._saving = false;
    this._editing = false;
    this._oldInnerHTML = null;
    this.triggerCallback('onLeaveEditMode');
  },
  leaveHover: function(e) {
    if (this.options.hoverClassName)
      this.element.removeClassName(this.options.hoverClassName);
    if (this._saving) return;
    this.triggerCallback('onLeaveHover');
  },
  loadExternalText: function() {
    this._form.addClassName(this.options.loadingClassName);
    this._controls.editor.disabled = true;
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._form.removeClassName(this.options.loadingClassName);
        var text = transport.responseText;
        if (this.options.stripLoadedTextTags)
          text = text.stripTags();
        this._controls.editor.value = text;
        this._controls.editor.disabled = false;
        this.postProcessEditField();
      }.bind(this),
      onFailure: this._boundFailureHandler
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },
  postProcessEditField: function() {
    var fpc = this.options.fieldPostCreation;
    if (fpc)
      $(this._controls.editor)['focus' == fpc ? 'focus' : 'activate']();
  },
  prepareOptions: function() {
    this.options = Object.clone(Ajax.InPlaceEditor.DefaultOptions);
    Object.extend(this.options, Ajax.InPlaceEditor.DefaultCallbacks);
    [this._extraDefaultOptions].flatten().compact().each(function(defs) {
      Object.extend(this.options, defs);
    }.bind(this));
  },
  prepareSubmission: function() {
    this._saving = true;
    this.removeForm();
    this.leaveHover();
    this.showSaving();
  },
  registerListeners: function() {
    this._listeners = { };
    var listener;
    $H(Ajax.InPlaceEditor.Listeners).each(function(pair) {
      listener = this[pair.value].bind(this);
      this._listeners[pair.key] = listener;
      if (!this.options.externalControlOnly)
        this.element.observe(pair.key, listener);
      if (this.options.externalControl)
        this.options.externalControl.observe(pair.key, listener);
    }.bind(this));
  },
  removeForm: function() {
    if (!this._form) return;
    this._form.remove();
    this._form = null;
    this._controls = { };
  },
  showSaving: function() {
    this._oldInnerHTML = this.element.innerHTML;
    this.element.innerHTML = this.options.savingText;
    this.element.addClassName(this.options.savingClassName);
    this.element.style.backgroundColor = this._originalBackground;
    this.element.show();
  },
  triggerCallback: function(cbName, arg) {
    if ('function' == typeof this.options[cbName]) {
      this.options[cbName](this, arg);
    }
  },
  unregisterListeners: function() {
    $H(this._listeners).each(function(pair) {
      if (!this.options.externalControlOnly)
        this.element.stopObserving(pair.key, pair.value);
      if (this.options.externalControl)
        this.options.externalControl.stopObserving(pair.key, pair.value);
    }.bind(this));
  },
  wrapUp: function(transport) {
    this.leaveEditMode();
    // Can't use triggerCallback due to backward compatibility: requires
    // binding + direct element
    this._boundComplete(transport, this.element);
  }
});

Object.extend(Ajax.InPlaceEditor.prototype, {
  dispose: Ajax.InPlaceEditor.prototype.destroy
});

Ajax.InPlaceCollectionEditor = Class.create(Ajax.InPlaceEditor, {
  initialize: function($super, element, url, options) {
    this._extraDefaultOptions = Ajax.InPlaceCollectionEditor.DefaultOptions;
    $super(element, url, options);
  },

  createEditField: function() {
    var list = document.createElement('select');
    list.name = this.options.paramName;
    list.size = 1;
    this._controls.editor = list;
    this._collection = this.options.collection || [];
    if (this.options.loadCollectionURL)
      this.loadCollection();
    else
      this.checkForExternalText();
    this._form.appendChild(this._controls.editor);
  },

  loadCollection: function() {
    this._form.addClassName(this.options.loadingClassName);
    this.showLoadingText(this.options.loadingCollectionText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        var js = transport.responseText.strip();
        if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check
          throw('Server returned an invalid collection representation.');
        this._collection = eval(js);
        this.checkForExternalText();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadCollectionURL, options);
  },

  showLoadingText: function(text) {
    this._controls.editor.disabled = true;
    var tempOption = this._controls.editor.firstChild;
    if (!tempOption) {
      tempOption = document.createElement('option');
      tempOption.value = '';
      this._controls.editor.appendChild(tempOption);
      tempOption.selected = true;
    }
    tempOption.update((text || '').stripScripts().stripTags());
  },

  checkForExternalText: function() {
    this._text = this.getText();
    if (this.options.loadTextURL)
      this.loadExternalText();
    else
      this.buildOptionList();
  },

  loadExternalText: function() {
    this.showLoadingText(this.options.loadingText);
    var options = Object.extend({ method: 'get' }, this.options.ajaxOptions);
    Object.extend(options, {
      parameters: 'editorId=' + encodeURIComponent(this.element.id),
      onComplete: Prototype.emptyFunction,
      onSuccess: function(transport) {
        this._text = transport.responseText.strip();
        this.buildOptionList();
      }.bind(this),
      onFailure: this.onFailure
    });
    new Ajax.Request(this.options.loadTextURL, options);
  },

  buildOptionList: function() {
    this._form.removeClassName(this.options.loadingClassName);
    this._collection = this._collection.map(function(entry) {
      return 2 === entry.length ? entry : [entry, entry].flatten();
    });
    var marker = ('value' in this.options) ? this.options.value : this._text;
    var textFound = this._collection.any(function(entry) {
      return entry[0] == marker;
    }.bind(this));
    this._controls.editor.update('');
    var option;
    this._collection.each(function(entry, index) {
      option = document.createElement('option');
      option.value = entry[0];
      option.selected = textFound ? entry[0] == marker : 0 == index;
      option.appendChild(document.createTextNode(entry[1]));
      this._controls.editor.appendChild(option);
    }.bind(this));
    this._controls.editor.disabled = false;
    Field.scrollFreeActivate(this._controls.editor);
  }
});

//**** DEPRECATION LAYER FOR InPlace[Collection]Editor! ****
//**** This only  exists for a while,  in order to  let ****
//**** users adapt to  the new API.  Read up on the new ****
//**** API and convert your code to it ASAP!            ****

Ajax.InPlaceEditor.prototype.initialize.dealWithDeprecatedOptions = function(options) {
  if (!options) return;
  function fallback(name, expr) {
    if (name in options || expr === undefined) return;
    options[name] = expr;
  };
  fallback('cancelControl', (options.cancelLink ? 'link' : (options.cancelButton ? 'button' :
    options.cancelLink == options.cancelButton == false ? false : undefined)));
  fallback('okControl', (options.okLink ? 'link' : (options.okButton ? 'button' :
    options.okLink == options.okButton == false ? false : undefined)));
  fallback('highlightColor', options.highlightcolor);
  fallback('highlightEndColor', options.highlightendcolor);
};

Object.extend(Ajax.InPlaceEditor, {
  DefaultOptions: {
    ajaxOptions: { },
    autoRows: 3,                                // Use when multi-line w/ rows == 1
    cancelControl: 'link',                      // 'link'|'button'|false
    cancelText: 'cancel',
    clickToEditText: 'Click to edit',
    externalControl: null,                      // id|elt
    externalControlOnly: false,
    fieldPostCreation: 'activate',              // 'activate'|'focus'|false
    formClassName: 'inplaceeditor-form',
    formId: null,                               // id|elt
    highlightColor: '#ffff99',
    highlightEndColor: '#ffffff',
    hoverClassName: '',
    htmlResponse: true,
    loadingClassName: 'inplaceeditor-loading',
    loadingText: 'Loading...',
    okControl: 'button',                        // 'link'|'button'|false
    okText: 'ok',
    paramName: 'value',
    rows: 1,                                    // If 1 and multi-line, uses autoRows
    savingClassName: 'inplaceeditor-saving',
    savingText: 'Saving...',
    size: 0,
    stripLoadedTextTags: false,
    submitOnBlur: false,
    textAfterControls: '',
    textBeforeControls: '',
    textBetweenControls: ''
  },
  DefaultCallbacks: {
    callback: function(form) {
      return Form.serialize(form);
    },
    onComplete: function(transport, element) {
      // For backward compatibility, this one is bound to the IPE, and passes
      // the element directly.  It was too often customized, so we don't break it.
      new Effect.Highlight(element, {
        startcolor: this.options.highlightColor, keepBackgroundImage: true });
    },
    onEnterEditMode: null,
    onEnterHover: function(ipe) {
      ipe.element.style.backgroundColor = ipe.options.highlightColor;
      if (ipe._effect)
        ipe._effect.cancel();
    },
    onFailure: function(transport, ipe) {
      alert('Error communication with the server: ' + transport.responseText.stripTags());
    },
    onFormCustomization: null, // Takes the IPE and its generated form, after editor, before controls.
    onLeaveEditMode: null,
    onLeaveHover: function(ipe) {
      ipe._effect = new Effect.Highlight(ipe.element, {
        startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
        restorecolor: ipe._originalBackground, keepBackgroundImage: true
      });
    }
  },
  Listeners: {
    click: 'enterEditMode',
    keydown: 'checkForEscapeOrReturn',
    mouseover: 'enterHover',
    mouseout: 'leaveHover'
  }
});

Ajax.InPlaceCollectionEditor.DefaultOptions = {
  loadingCollectionText: 'Loading options...'
};

// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields

Form.Element.DelayedObserver = Class.create({
  initialize: function(element, delay, callback) {
    this.delay     = delay || 0.5;
    this.element   = $(element);
    this.callback  = callback;
    this.timer     = null;
    this.lastValue = $F(this.element);
    Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
  },
  delayedListener: function(event) {
    if(this.lastValue == $F(this.element)) return;
    if(this.timer) clearTimeout(this.timer);
    this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
    this.lastValue = $F(this.element);
  },
  onTimerEvent: function() {
    this.timer = null;
    this.callback(this.element, $F(this.element));
  }
});// script.aculo.us slider.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Marty Haught, Thomas Fuchs
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

if (!Control) var Control = { };

// options:
//  axis: 'vertical', or 'horizontal' (default)
//
// callbacks:
//  onChange(value)
//  onSlide(value)
Control.Slider = Class.create({
  initialize: function(handle, track, options) {
    var slider = this;

    if (Object.isArray(handle)) {
      this.handles = handle.collect( function(e) { return $(e) });
    } else {
      this.handles = [$(handle)];
    }

    this.track   = $(track);
    this.options = options || { };

    this.axis      = this.options.axis || 'horizontal';
    this.increment = this.options.increment || 1;
    this.step      = parseInt(this.options.step || '1');
    this.range     = this.options.range || $R(0,1);

    this.value     = 0; // assure backwards compat
    this.values    = this.handles.map( function() { return 0 });
    this.spans     = this.options.spans ? this.options.spans.map(function(s){ return $(s) }) : false;
    this.options.startSpan = $(this.options.startSpan || null);
    this.options.endSpan   = $(this.options.endSpan || null);

    this.restricted = this.options.restricted || false;

    this.maximum   = this.options.maximum || this.range.end;
    this.minimum   = this.options.minimum || this.range.start;

    // Will be used to align the handle onto the track, if necessary
    this.alignX = parseInt(this.options.alignX || '0');
    this.alignY = parseInt(this.options.alignY || '0');

    this.trackLength = this.maximumOffset() - this.minimumOffset();

    this.handleLength = this.isVertical() ?
      (this.handles[0].offsetHeight != 0 ?
        this.handles[0].offsetHeight : this.handles[0].style.height.replace(/px$/,"")) :
      (this.handles[0].offsetWidth != 0 ? this.handles[0].offsetWidth :
        this.handles[0].style.width.replace(/px$/,""));

    this.active   = false;
    this.dragging = false;
    this.disabled = false;

    if (this.options.disabled) this.setDisabled();

    // Allowed values array
    this.allowedValues = this.options.values ? this.options.values.sortBy(Prototype.K) : false;
    if (this.allowedValues) {
      this.minimum = this.allowedValues.min();
      this.maximum = this.allowedValues.max();
    }

    this.eventMouseDown = this.startDrag.bindAsEventListener(this);
    this.eventMouseUp   = this.endDrag.bindAsEventListener(this);
    this.eventMouseMove = this.update.bindAsEventListener(this);

    // Initialize handles in reverse (make sure first handle is active)
    this.handles.each( function(h,i) {
      i = slider.handles.length-1-i;
      slider.setValue(parseFloat(
        (Object.isArray(slider.options.sliderValue) ?
          slider.options.sliderValue[i] : slider.options.sliderValue) ||
         slider.range.start), i);
      h.makePositioned().observe("mousedown", slider.eventMouseDown);
    });

    this.track.observe("mousedown", this.eventMouseDown);
    document.observe("mouseup", this.eventMouseUp);
    document.observe("mousemove", this.eventMouseMove);

    this.initialized = true;
  },
  dispose: function() {
    var slider = this;
    Event.stopObserving(this.track, "mousedown", this.eventMouseDown);
    Event.stopObserving(document, "mouseup", this.eventMouseUp);
    Event.stopObserving(document, "mousemove", this.eventMouseMove);
    this.handles.each( function(h) {
      Event.stopObserving(h, "mousedown", slider.eventMouseDown);
    });
  },
  setDisabled: function(){
    this.disabled = true;
  },
  setEnabled: function(){
    this.disabled = false;
  },
  getNearestValue: function(value){
    if (this.allowedValues){
      if (value >= this.allowedValues.max()) return(this.allowedValues.max());
      if (value <= this.allowedValues.min()) return(this.allowedValues.min());

      var offset = Math.abs(this.allowedValues[0] - value);
      var newValue = this.allowedValues[0];
      this.allowedValues.each( function(v) {
        var currentOffset = Math.abs(v - value);
        if (currentOffset <= offset){
          newValue = v;
          offset = currentOffset;
        }
      });
      return newValue;
    }
    if (value > this.range.end) return this.range.end;
    if (value < this.range.start) return this.range.start;
    return value;
  },
  setValue: function(sliderValue, handleIdx){
    if (!this.active) {
      this.activeHandleIdx = handleIdx || 0;
      this.activeHandle    = this.handles[this.activeHandleIdx];
      this.updateStyles();
    }
    handleIdx = handleIdx || this.activeHandleIdx || 0;
    if (this.initialized && this.restricted) {
      if ((handleIdx>0) && (sliderValue<this.values[handleIdx-1]))
        sliderValue = this.values[handleIdx-1];
      if ((handleIdx < (this.handles.length-1)) && (sliderValue>this.values[handleIdx+1]))
        sliderValue = this.values[handleIdx+1];
    }
    sliderValue = this.getNearestValue(sliderValue);
    this.values[handleIdx] = sliderValue;
    this.value = this.values[0]; // assure backwards compat

    this.handles[handleIdx].style[this.isVertical() ? 'top' : 'left'] =
      this.translateToPx(sliderValue);

    this.drawSpans();
    if (!this.dragging || !this.event) this.updateFinished();
  },
  setValueBy: function(delta, handleIdx) {
    this.setValue(this.values[handleIdx || this.activeHandleIdx || 0] + delta,
      handleIdx || this.activeHandleIdx || 0);
  },
  translateToPx: function(value) {
    return Math.round(
      ((this.trackLength-this.handleLength)/(this.range.end-this.range.start)) *
      (value - this.range.start)) + "px";
  },
  translateToValue: function(offset) {
    return ((offset/(this.trackLength-this.handleLength) *
      (this.range.end-this.range.start)) + this.range.start);
  },
  getRange: function(range) {
    var v = this.values.sortBy(Prototype.K);
    range = range || 0;
    return $R(v[range],v[range+1]);
  },
  minimumOffset: function(){
    return(this.isVertical() ? this.alignY : this.alignX);
  },
  maximumOffset: function(){
    return(this.isVertical() ?
      (this.track.offsetHeight != 0 ? this.track.offsetHeight :
        this.track.style.height.replace(/px$/,"")) - this.alignY :
      (this.track.offsetWidth != 0 ? this.track.offsetWidth :
        this.track.style.width.replace(/px$/,"")) - this.alignX);
  },
  isVertical:  function(){
    return (this.axis == 'vertical');
  },
  drawSpans: function() {
    var slider = this;
    if (this.spans)
      $R(0, this.spans.length-1).each(function(r) { slider.setSpan(slider.spans[r], slider.getRange(r)) });
    if (this.options.startSpan)
      this.setSpan(this.options.startSpan,
        $R(0, this.values.length>1 ? this.getRange(0).min() : this.value ));
    if (this.options.endSpan)
      this.setSpan(this.options.endSpan,
        $R(this.values.length>1 ? this.getRange(this.spans.length-1).max() : this.value, this.maximum));
  },
  setSpan: function(span, range) {
    if (this.isVertical()) {
      span.style.top = this.translateToPx(range.start);
      span.style.height = this.translateToPx(range.end - range.start + this.range.start);
    } else {
      span.style.left = this.translateToPx(range.start);
      span.style.width = this.translateToPx(range.end - range.start + this.range.start);
    }
  },
  updateStyles: function() {
    this.handles.each( function(h){ Element.removeClassName(h, 'selected') });
    Element.addClassName(this.activeHandle, 'selected');
  },
  startDrag: function(event) {
    if (Event.isLeftClick(event)) {
      if (!this.disabled){
        this.active = true;

        var handle = Event.element(event);
        var pointer  = [Event.pointerX(event), Event.pointerY(event)];
        var track = handle;
        if (track==this.track) {
          var offsets  = Position.cumulativeOffset(this.track);
          this.event = event;
          this.setValue(this.translateToValue(
           (this.isVertical() ? pointer[1]-offsets[1] : pointer[0]-offsets[0])-(this.handleLength/2)
          ));
          var offsets  = Position.cumulativeOffset(this.activeHandle);
          this.offsetX = (pointer[0] - offsets[0]);
          this.offsetY = (pointer[1] - offsets[1]);
        } else {
          // find the handle (prevents issues with Safari)
          while((this.handles.indexOf(handle) == -1) && handle.parentNode)
            handle = handle.parentNode;

          if (this.handles.indexOf(handle)!=-1) {
            this.activeHandle    = handle;
            this.activeHandleIdx = this.handles.indexOf(this.activeHandle);
            this.updateStyles();

            var offsets  = Position.cumulativeOffset(this.activeHandle);
            this.offsetX = (pointer[0] - offsets[0]);
            this.offsetY = (pointer[1] - offsets[1]);
          }
        }
      }
      Event.stop(event);
    }
  },
  update: function(event) {
   if (this.active) {
      if (!this.dragging) this.dragging = true;
      this.draw(event);
      if (Prototype.Browser.WebKit) window.scrollBy(0,0);
      Event.stop(event);
   }
  },
  draw: function(event) {
    var pointer = [Event.pointerX(event), Event.pointerY(event)];
    var offsets = Position.cumulativeOffset(this.track);
    pointer[0] -= this.offsetX + offsets[0];
    pointer[1] -= this.offsetY + offsets[1];
    this.event = event;
    this.setValue(this.translateToValue( this.isVertical() ? pointer[1] : pointer[0] ));
    if (this.initialized && this.options.onSlide)
      this.options.onSlide(this.values.length>1 ? this.values : this.value, this);
  },
  endDrag: function(event) {
    if (this.active && this.dragging) {
      this.finishDrag(event, true);
      Event.stop(event);
    }
    this.active = false;
    this.dragging = false;
  },
  finishDrag: function(event, success) {
    this.active = false;
    this.dragging = false;
    this.updateFinished();
  },
  updateFinished: function() {
    if (this.initialized && this.options.onChange)
      this.options.onChange(this.values.length>1 ? this.values : this.value, this);
    this.event = null;
  }
});// script.aculo.us sound.js v1.8.2, Tue Nov 18 18:30:58 +0100 2008

// Copyright (c) 2005-2008 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
//
// Based on code created by Jules Gravinese (http://www.webveteran.com/)
//
// script.aculo.us is freely distributable under the terms of an MIT-style license.
// For details, see the script.aculo.us web site: http://script.aculo.us/

Sound = {
  tracks: {},
  _enabled: true,
  template:
    new Template('<embed style="height:0" id="sound_#{track}_#{id}" src="#{url}" loop="false" autostart="true" hidden="true"/>'),
  enable: function(){
    Sound._enabled = true;
  },
  disable: function(){
    Sound._enabled = false;
  },
  play: function(url){
    if(!Sound._enabled) return;
    var options = Object.extend({
      track: 'global', url: url, replace: false
    }, arguments[1] || {});

    if(options.replace && this.tracks[options.track]) {
      $R(0, this.tracks[options.track].id).each(function(id){
        var sound = $('sound_'+options.track+'_'+id);
        sound.Stop && sound.Stop();
        sound.remove();
      });
      this.tracks[options.track] = null;
    }

    if(!this.tracks[options.track])
      this.tracks[options.track] = { id: 0 };
    else
      this.tracks[options.track].id++;

    options.id = this.tracks[options.track].id;
    $$('body')[0].insert(
      Prototype.Browser.IE ? new Element('bgsound',{
        id: 'sound_'+options.track+'_'+options.id,
        src: options.url, loop: 1, autostart: true
      }) : Sound.template.evaluate(options));
  }
};

if(Prototype.Browser.Gecko && navigator.userAgent.indexOf("Win") > 0){
  if(navigator.plugins && $A(navigator.plugins).detect(function(p){ return p.name.indexOf('QuickTime') != -1 }))
    Sound.template = new Template('<object id="sound_#{track}_#{id}" width="0" height="0" type="audio/mpeg" data="#{url}"/>');
  else
    Sound.play = function(){};
}/* SWFObject v2.1 <http://code.google.com/p/swfobject/>
	Copyright (c) 2007-2008 Geoff Stearns, Michael Williams, and Bobby van der Sluis
	This software is released under the MIT License <http://www.opensource.org/licenses/mit-license.php>
*/
var swfobject=function(){var b="undefined",Q="object",n="Shockwave Flash",p="ShockwaveFlash.ShockwaveFlash",P="application/x-shockwave-flash",m="SWFObjectExprInst",j=window,K=document,T=navigator,o=[],N=[],i=[],d=[],J,Z=null,M=null,l=null,e=false,A=false;var h=function(){var v=typeof K.getElementById!=b&&typeof K.getElementsByTagName!=b&&typeof K.createElement!=b,AC=[0,0,0],x=null;if(typeof T.plugins!=b&&typeof T.plugins[n]==Q){x=T.plugins[n].description;if(x&&!(typeof T.mimeTypes!=b&&T.mimeTypes[P]&&!T.mimeTypes[P].enabledPlugin)){x=x.replace(/^.*\s+(\S+\s+\S+$)/,"$1");AC[0]=parseInt(x.replace(/^(.*)\..*$/,"$1"),10);AC[1]=parseInt(x.replace(/^.*\.(.*)\s.*$/,"$1"),10);AC[2]=/r/.test(x)?parseInt(x.replace(/^.*r(.*)$/,"$1"),10):0}}else{if(typeof j.ActiveXObject!=b){var y=null,AB=false;try{y=new ActiveXObject(p+".7")}catch(t){try{y=new ActiveXObject(p+".6");AC=[6,0,21];y.AllowScriptAccess="always"}catch(t){if(AC[0]==6){AB=true}}if(!AB){try{y=new ActiveXObject(p)}catch(t){}}}if(!AB&&y){try{x=y.GetVariable("$version");if(x){x=x.split(" ")[1].split(",");AC=[parseInt(x[0],10),parseInt(x[1],10),parseInt(x[2],10)]}}catch(t){}}}}var AD=T.userAgent.toLowerCase(),r=T.platform.toLowerCase(),AA=/webkit/.test(AD)?parseFloat(AD.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,q=false,z=r?/win/.test(r):/win/.test(AD),w=r?/mac/.test(r):/mac/.test(AD);/*@cc_on q=true;@if(@_win32)z=true;@elif(@_mac)w=true;@end@*/return{w3cdom:v,pv:AC,webkit:AA,ie:q,win:z,mac:w}}();var L=function(){if(!h.w3cdom){return }f(H);if(h.ie&&h.win){try{K.write("<script id=__ie_ondomload defer=true src=//:><\/script>");J=C("__ie_ondomload");if(J){I(J,"onreadystatechange",S)}}catch(q){}}if(h.webkit&&typeof K.readyState!=b){Z=setInterval(function(){if(/loaded|complete/.test(K.readyState)){E()}},10)}if(typeof K.addEventListener!=b){K.addEventListener("DOMContentLoaded",E,null)}R(E)}();function S(){if(J.readyState=="complete"){J.parentNode.removeChild(J);E()}}function E(){if(e){return }if(h.ie&&h.win){var v=a("span");try{var u=K.getElementsByTagName("body")[0].appendChild(v);u.parentNode.removeChild(u)}catch(w){return }}e=true;if(Z){clearInterval(Z);Z=null}var q=o.length;for(var r=0;r<q;r++){o[r]()}}function f(q){if(e){q()}else{o[o.length]=q}}function R(r){if(typeof j.addEventListener!=b){j.addEventListener("load",r,false)}else{if(typeof K.addEventListener!=b){K.addEventListener("load",r,false)}else{if(typeof j.attachEvent!=b){I(j,"onload",r)}else{if(typeof j.onload=="function"){var q=j.onload;j.onload=function(){q();r()}}else{j.onload=r}}}}}function H(){var t=N.length;for(var q=0;q<t;q++){var u=N[q].id;if(h.pv[0]>0){var r=C(u);if(r){N[q].width=r.getAttribute("width")?r.getAttribute("width"):"0";N[q].height=r.getAttribute("height")?r.getAttribute("height"):"0";if(c(N[q].swfVersion)){if(h.webkit&&h.webkit<312){Y(r)}W(u,true)}else{if(N[q].expressInstall&&!A&&c("6.0.65")&&(h.win||h.mac)){k(N[q])}else{O(r)}}}}else{W(u,true)}}}function Y(t){var q=t.getElementsByTagName(Q)[0];if(q){var w=a("embed"),y=q.attributes;if(y){var v=y.length;for(var u=0;u<v;u++){if(y[u].nodeName=="DATA"){w.setAttribute("src",y[u].nodeValue)}else{w.setAttribute(y[u].nodeName,y[u].nodeValue)}}}var x=q.childNodes;if(x){var z=x.length;for(var r=0;r<z;r++){if(x[r].nodeType==1&&x[r].nodeName=="PARAM"){w.setAttribute(x[r].getAttribute("name"),x[r].getAttribute("value"))}}}t.parentNode.replaceChild(w,t)}}function k(w){A=true;var u=C(w.id);if(u){if(w.altContentId){var y=C(w.altContentId);if(y){M=y;l=w.altContentId}}else{M=G(u)}if(!(/%$/.test(w.width))&&parseInt(w.width,10)<310){w.width="310"}if(!(/%$/.test(w.height))&&parseInt(w.height,10)<137){w.height="137"}K.title=K.title.slice(0,47)+" - Flash Player Installation";var z=h.ie&&h.win?"ActiveX":"PlugIn",q=K.title,r="MMredirectURL="+j.location+"&MMplayerType="+z+"&MMdoctitle="+q,x=w.id;if(h.ie&&h.win&&u.readyState!=4){var t=a("div");x+="SWFObjectNew";t.setAttribute("id",x);u.parentNode.insertBefore(t,u);u.style.display="none";var v=function(){u.parentNode.removeChild(u)};I(j,"onload",v)}U({data:w.expressInstall,id:m,width:w.width,height:w.height},{flashvars:r},x)}}function O(t){if(h.ie&&h.win&&t.readyState!=4){var r=a("div");t.parentNode.insertBefore(r,t);r.parentNode.replaceChild(G(t),r);t.style.display="none";var q=function(){t.parentNode.removeChild(t)};I(j,"onload",q)}else{t.parentNode.replaceChild(G(t),t)}}function G(v){var u=a("div");if(h.win&&h.ie){u.innerHTML=v.innerHTML}else{var r=v.getElementsByTagName(Q)[0];if(r){var w=r.childNodes;if(w){var q=w.length;for(var t=0;t<q;t++){if(!(w[t].nodeType==1&&w[t].nodeName=="PARAM")&&!(w[t].nodeType==8)){u.appendChild(w[t].cloneNode(true))}}}}}return u}function U(AG,AE,t){var q,v=C(t);if(v){if(typeof AG.id==b){AG.id=t}if(h.ie&&h.win){var AF="";for(var AB in AG){if(AG[AB]!=Object.prototype[AB]){if(AB.toLowerCase()=="data"){AE.movie=AG[AB]}else{if(AB.toLowerCase()=="styleclass"){AF+=' class="'+AG[AB]+'"'}else{if(AB.toLowerCase()!="classid"){AF+=" "+AB+'="'+AG[AB]+'"'}}}}}var AD="";for(var AA in AE){if(AE[AA]!=Object.prototype[AA]){AD+='<param name="'+AA+'" value="'+AE[AA]+'" />'}}v.outerHTML='<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"'+AF+">"+AD+"</object>";i[i.length]=AG.id;q=C(AG.id)}else{if(h.webkit&&h.webkit<312){var AC=a("embed");AC.setAttribute("type",P);for(var z in AG){if(AG[z]!=Object.prototype[z]){if(z.toLowerCase()=="data"){AC.setAttribute("src",AG[z])}else{if(z.toLowerCase()=="styleclass"){AC.setAttribute("class",AG[z])}else{if(z.toLowerCase()!="classid"){AC.setAttribute(z,AG[z])}}}}}for(var y in AE){if(AE[y]!=Object.prototype[y]){if(y.toLowerCase()!="movie"){AC.setAttribute(y,AE[y])}}}v.parentNode.replaceChild(AC,v);q=AC}else{var u=a(Q);u.setAttribute("type",P);for(var x in AG){if(AG[x]!=Object.prototype[x]){if(x.toLowerCase()=="styleclass"){u.setAttribute("class",AG[x])}else{if(x.toLowerCase()!="classid"){u.setAttribute(x,AG[x])}}}}for(var w in AE){if(AE[w]!=Object.prototype[w]&&w.toLowerCase()!="movie"){F(u,w,AE[w])}}v.parentNode.replaceChild(u,v);q=u}}}return q}function F(t,q,r){var u=a("param");u.setAttribute("name",q);u.setAttribute("value",r);t.appendChild(u)}function X(r){var q=C(r);if(q&&(q.nodeName=="OBJECT"||q.nodeName=="EMBED")){if(h.ie&&h.win){if(q.readyState==4){B(r)}else{j.attachEvent("onload",function(){B(r)})}}else{q.parentNode.removeChild(q)}}}function B(t){var r=C(t);if(r){for(var q in r){if(typeof r[q]=="function"){r[q]=null}}r.parentNode.removeChild(r)}}function C(t){var q=null;try{q=K.getElementById(t)}catch(r){}return q}function a(q){return K.createElement(q)}function I(t,q,r){t.attachEvent(q,r);d[d.length]=[t,q,r]}function c(t){var r=h.pv,q=t.split(".");q[0]=parseInt(q[0],10);q[1]=parseInt(q[1],10)||0;q[2]=parseInt(q[2],10)||0;return(r[0]>q[0]||(r[0]==q[0]&&r[1]>q[1])||(r[0]==q[0]&&r[1]==q[1]&&r[2]>=q[2]))?true:false}function V(v,r){if(h.ie&&h.mac){return }var u=K.getElementsByTagName("head")[0],t=a("style");t.setAttribute("type","text/css");t.setAttribute("media","screen");if(!(h.ie&&h.win)&&typeof K.createTextNode!=b){t.appendChild(K.createTextNode(v+" {"+r+"}"))}u.appendChild(t);if(h.ie&&h.win&&typeof K.styleSheets!=b&&K.styleSheets.length>0){var q=K.styleSheets[K.styleSheets.length-1];if(typeof q.addRule==Q){q.addRule(v,r)}}}function W(t,q){var r=q?"visible":"hidden";if(e&&C(t)){C(t).style.visibility=r}else{V("#"+t,"visibility:"+r)}}function g(s){var r=/[\\\"<>\.;]/;var q=r.exec(s)!=null;return q?encodeURIComponent(s):s}var D=function(){if(h.ie&&h.win){window.attachEvent("onunload",function(){var w=d.length;for(var v=0;v<w;v++){d[v][0].detachEvent(d[v][1],d[v][2])}var t=i.length;for(var u=0;u<t;u++){X(i[u])}for(var r in h){h[r]=null}h=null;for(var q in swfobject){swfobject[q]=null}swfobject=null})}}();return{registerObject:function(u,q,t){if(!h.w3cdom||!u||!q){return }var r={};r.id=u;r.swfVersion=q;r.expressInstall=t?t:false;N[N.length]=r;W(u,false)},getObjectById:function(v){var q=null;if(h.w3cdom){var t=C(v);if(t){var u=t.getElementsByTagName(Q)[0];if(!u||(u&&typeof t.SetVariable!=b)){q=t}else{if(typeof u.SetVariable!=b){q=u}}}}return q},embedSWF:function(x,AE,AB,AD,q,w,r,z,AC){if(!h.w3cdom||!x||!AE||!AB||!AD||!q){return }AB+="";AD+="";if(c(q)){W(AE,false);var AA={};if(AC&&typeof AC===Q){for(var v in AC){if(AC[v]!=Object.prototype[v]){AA[v]=AC[v]}}}AA.data=x;AA.width=AB;AA.height=AD;var y={};if(z&&typeof z===Q){for(var u in z){if(z[u]!=Object.prototype[u]){y[u]=z[u]}}}if(r&&typeof r===Q){for(var t in r){if(r[t]!=Object.prototype[t]){if(typeof y.flashvars!=b){y.flashvars+="&"+t+"="+r[t]}else{y.flashvars=t+"="+r[t]}}}}f(function(){U(AA,y,AE);if(AA.id==AE){W(AE,true)}})}else{if(w&&!A&&c("6.0.65")&&(h.win||h.mac)){A=true;W(AE,false);f(function(){var AF={};AF.id=AF.altContentId=AE;AF.width=AB;AF.height=AD;AF.expressInstall=w;k(AF)})}}},getFlashPlayerVersion:function(){return{major:h.pv[0],minor:h.pv[1],release:h.pv[2]}},hasFlashPlayerVersion:c,createSWF:function(t,r,q){if(h.w3cdom){return U(t,r,q)}else{return undefined}},removeSWF:function(q){if(h.w3cdom){X(q)}},createCSS:function(r,q){if(h.w3cdom){V(r,q)}},addDomLoadEvent:f,addLoadEvent:R,getQueryParamValue:function(v){var u=K.location.search||K.location.hash;if(v==null){return g(u)}if(u){var t=u.substring(1).split("&");for(var r=0;r<t.length;r++){if(t[r].substring(0,t[r].indexOf("="))==v){return g(t[r].substring((t[r].indexOf("=")+1)))}}}return""},expressInstallCallback:function(){if(A&&M){var q=C(m);if(q){q.parentNode.replaceChild(M,q);if(l){W(l,true);if(h.ie&&h.win){M.style.display="block"}}M=null;l=null;A=false}}}}}();/*
	CSS Browser Selector v0.2.9
	Rafael Lima (http://rafael.adm.br)
	http://rafael.adm.br/css_browser_selector
	License: http://creativecommons.org/licenses/by/2.5/
	Contributors: http://rafael.adm.br/css_browser_selector#contributors
*/
var css_browser_selector = function() {var ua=navigator.userAgent.toLowerCase(),is=function(t){return ua.indexOf(t) != -1;},h=document.getElementsByTagName('html')[0],b=(!(/opera|webtv/i.test(ua))&&/msie\s(\d)/.test(ua))?('ie ie'+RegExp.$1):is('firefox/2')?'gecko ff2':is('firefox/3')?'gecko ff3':is('gecko/')?'gecko':is('opera/9')?'opera opera9':/opera\s(\d)/.test(ua)?'opera opera'+RegExp.$1:is('konqueror')?'konqueror':is('chrome')?'chrome webkit safari':is('applewebkit/')?'webkit safari':is('mozilla/')?'gecko':'',os=(is('x11')||is('linux'))?' linux':is('mac')?' mac':is('win')?' win':'';var c=b+os+' js'; h.className += h.className?' '+c:c;}();
 

/**
 * @author Ryan Johnson <ryan@livepipe.net>
 * @copyright 2007 LivePipe LLC
 * @package Control.Tabs
 * @license MIT
 * @url http://livepipe.net/projects/control_tabs/
 * @version 2.1.1
 */

if(typeof(Control) == 'undefined')
	var Control = {};
Control.Tabs = Class.create();
Object.extend(Control.Tabs,{
	instances: [],
	findByTabId: function(id){
		return Control.Tabs.instances.find(function(tab){
			return tab.links.find(function(link){
				return link.key == id;
			});
		});
	}
});
Object.extend(Control.Tabs.prototype,{
	initialize: function(tab_list_container,options){
		this.activeContainer = false;
		this.activeLink = false;
		this.containers = $H({});
		this.links = [];
		Control.Tabs.instances.push(this);
		this.options = {
			beforeChange: Prototype.emptyFunction,
			afterChange: Prototype.emptyFunction,
			hover: false,
			linkSelector: 'li a',
			setClassOnContainer: false,
			activeClassName: 'active',
			defaultTab: 'first',
			autoLinkExternal: true,
			targetRegExp: /#(.+)$/,
			showFunction: Element.show,
			hideFunction: Element.hide
		};
		Object.extend(this.options,options || {});
		(typeof(this.options.linkSelector == 'string')
			? $(tab_list_container).getElementsBySelector(this.options.linkSelector)
			: this.options.linkSelector($(tab_list_container))
		).findAll(function(link){
			return (/^#/).exec(link.href.replace(window.location.href.split('#')[0],''));
		}).each(function(link){
			this.addTab(link);
		}.bind(this));
		this.containers.values().each(this.options.hideFunction);
		if(this.options.defaultTab == 'first')
			this.setActiveTab(this.links.first());
		else if(this.options.defaultTab == 'last')
			this.setActiveTab(this.links.last());
		else
			this.setActiveTab(this.options.defaultTab);
		var targets = this.options.targetRegExp.exec(window.location);
		if(targets && targets[1]){
			targets[1].split(',').each(function(target){
				this.links.each(function(target,link){
					if(link.key == target){
						this.setActiveTab(link);
						throw $break;
					}
				}.bind(this,target));
			}.bind(this));
		}
		if(this.options.autoLinkExternal){
			$A(document.getElementsByTagName('a')).each(function(a){
				if(!this.links.include(a)){
					var clean_href = a.href.replace(window.location.href.split('#')[0],'');
					if(clean_href.substring(0,1) == '#'){
						if(this.containers.keys().include(clean_href.substring(1))){
							$(a).observe('click',function(event,clean_href){
								this.setActiveTab(clean_href.substring(1));
							}.bindAsEventListener(this,clean_href));
						}
					}
				}
			}.bind(this));
		}
	},
	addTab: function(link){
		this.links.push(link);
		link.key = link.getAttribute('href').replace(window.location.href.split('#')[0],'').split('/').last().replace(/#/,'');
		this.containers[link.key] = $(link.key);
		link[this.options.hover ? 'onmouseover' : 'onclick'] = function(link){
			if(window.event)
				Event.stop(window.event);
			this.setActiveTab(link);
			return false;
		}.bind(this,link);
	},
	setActiveTab: function(link){
		if(!link)
			return;
		if(typeof(link) == 'string'){
			this.links.each(function(_link){
				if(_link.key == link){
					this.setActiveTab(_link);
					throw $break;
				}
			}.bind(this));
		}else{
			this.notify('beforeChange',this.activeContainer);
			if(this.activeContainer)
				this.options.hideFunction(this.activeContainer);
			this.links.each(function(item){
				(this.options.setClassOnContainer ? $(item.parentNode) : item).removeClassName(this.options.activeClassName);
			}.bind(this));
			(this.options.setClassOnContainer ? $(link.parentNode) : link).addClassName(this.options.activeClassName);
			this.activeContainer = this.containers[link.key];
			this.activeLink = link;
			this.options.showFunction(this.containers[link.key]);
			this.notify('afterChange',this.containers[link.key]);
		}
	},
	next: function(){
		this.links.each(function(link,i){
			if(this.activeLink == link && this.links[i + 1]){
				this.setActiveTab(this.links[i + 1]);
				throw $break;
			}
		}.bind(this));
		return false;
	},
	previous: function(){
		this.links.each(function(link,i){
			if(this.activeLink == link && this.links[i - 1]){
				this.setActiveTab(this.links[i - 1]);
				throw $break;
			}
		}.bind(this));
		return false;
	},
	first: function(){
		this.setActiveTab(this.links.first());
		return false;
	},
	last: function(){
		this.setActiveTab(this.links.last());
		return false;
	},
	notify: function(event_name){
		try{
			if(this.options[event_name])
				return [this.options[event_name].apply(this.options[event_name],$A(arguments).slice(1))];
		}catch(e){
			if(e != $break)
				throw e;
			else
				return false;
		}
	}
});
if(typeof(Object.Event) != 'undefined')
	Object.Event.extend(Control.Tabs);generic = {     
    init: function() {  
        this.env.debug = this.env.query("debug"); 
        if (this.env.debug && this.env.isIE) console.drawWin(); 
    },
    env: { 
        isIE : !!(typeof(ActiveXObject) == 'function'),
        isIE6 : !!(!!(typeof(ActiveXObject) == 'function') && (/MSIE\s6\.0/.test(navigator.appVersion))),
        isFF : Prototype.Browser.Gecko,
        isFF2 : !!(typeof(navigator.product) != 'undefined' && navigator.product == 'Gecko' && !((document.childNodes) && (!navigator.taintEnabled)) && navigator.userAgent.toLowerCase().split(' firefox/')[1].split('.')[0] == '2'),
        isFF3 : !!(typeof(navigator.product) != 'undefined' && navigator.product == 'Gecko' && !((document.childNodes) && (!navigator.taintEnabled)) && navigator.userAgent.toLowerCase().split(' firefox/')[1].split('.')[0] == '3'),
        isMac    : !!(/macppc|macintel/.test(navigator.platform.toLowerCase())),
        isSafari : !!(/Safari/.test(navigator.userAgent)),
        
        domain : window.location.protocol + "//" + window.location.hostname,
        
        query: function(key) {
            if (typeof generic.env.parsedQuery == "undefined") {
                generic.env.parsedQuery = window.location.href.toQueryParams();
            }
            var result = generic.env.parsedQuery[key] || null;
            return result; 
        },
        
        debug: false
        
    }, 
    helpers: { 
        div: new Element("div") //used by Widget class, Prototype way is needed for IE
    },
    events: {
        target: document,
        fire: function(args) {
            //console.log("generic.events.fire: "+args.event + " / " + args.msg);  
            if (!args) return;
            var e = args.event; 
            var msg = (typeof args.msg == "undefined") ? null : args.msg; 
            generic.events.target.fire(e, {msg:msg});
        },
        observe: function(evt, func) {
            if (!evt || !func) return; 
            generic.events.target.observe(evt, function(e){   
                func(e.memo.msg);
            });
        }   
    },
    forms: {
        select: {
            addOption:  function(args) {
                if (!args || !args.menuNode) return;
                var val = args.value;
                var label = args.label || val;
                var options = args.menuNode.options;
                options[options.length] = new Option(label, val);
            },
            setValue: function(args) {
                var idx = 0;
                for (var i=0, len=args.menuNode.options.length; i<len; i++) {
                    if (args.value == args.menuNode.options[i].value) {
                        idx = i;
                        break;
                    }
                }
                args.menuNode.selectedIndex = idx;
            }
        }
    }
}; 

    
// debug    
if (typeof console === "undefined") {
    if (generic && generic.env.debug) {
        console = {
            tracen : 0,
            win : {},
            drawWin: function() {  
                outp = document.createElement("DIV");
                outp.id = "console-window";
                outp.style.cssText = "position:absolute;top:10px;right:10px;width:400px;height:200px;padding:5px;overflow-x:hidden;overflow-y:scroll;background-color:#ffffff;color:#000000;font-size:12px;border:1px solid red;z-index:9999";
                document.body.appendChild(outp);  
                this.win = $(outp.id);
            },
            log: function(s) {
                if ((typeof this.win != "undefined") && (typeof this.win.innerHTML != "undefined")) {
                    this.tracen++;
                    s = (typeof(s) == "undefined") ? "undefined" : s.toString().replace(/\</gi, "&lt;").replace(/\>/gi, "&gt;");
                    this.win.innerHTML = this.win.innerHTML + "<b>" + this.tracen + "</b>. " + s + "<br/>";
                }
            }    
        }   
    } else {
        console = {
            tracen : 0,
            win : {},
            drawWin: function() {  
                return;
            },
            log: function(s) {
                return;
            }    
        }
    }
};
var generic = generic || {};
generic.rb = generic.rb || {};
var rb = rb || {};

/**
	* This method provides access to resource bundle values that have been 
	* written to the HTML in JSON format. The file that outputs these values
	* must be included in the .html as a script tag with the desired RB name
	* as a query string paramter.
	* It returns an object that exposes one method: get()
	* @example
	* // var myBundle = generic.rb("account");
	* // myBundle.get("err_please_sign_in");
	* @example
    * <script src="/js/shared/v2/internal/resource.tmpl?rb=account"></script>
	* @param {String} rbGroupName name of resource bundle needed
	* @methodOf generic
*/
generic.rb = function(rbGroupName) {
	var findResourceBundle = function(groupName) {
		if (groupName && rb) {
			var rbName = groupName;
			var rbHash = $H(rb[rbName]);
			if (rbHash) {
				return rbHash;
			} else {
				return $H({});
			}
		} else {
			return $H({});
		}
	};
	
	var resourceBundle = findResourceBundle(rbGroupName);
	
	var returnObj = {
    	/**
        * This method will return the value for the requested Resource Bundle key.
        * If the key is not found, the key name will be returned.
    	* @example
    	* // var myBundle = generic.rb("account");
    	* // myBundle.get("err_please_sign_in");
    	* @param {String} keyName key of desired Resource Bundle value
    	*/
		get: function(keyName) {
		    if ( ! Object.isString(keyName) ) {
		        return null;
		    }
			var val = resourceBundle.get(keyName);
			if (val) {
				return val;
			} else {
				return keyName;
			}
		}
	};
	
	return returnObj;
	
};var generic = generic || {};

generic.RediTemplate = Class.create( Template, {
    initialize: function ( template, pattern ) {
        this.template = template?template:'';
        this.readyState = template?1:0;
        this.pattern = pattern?pattern:Template.Pattern; 
        this.queue = new Array();       
        return;
    },
    load: function(template) {
        this.template = template.toString(); 
        this.readyState = 1; 
        this.onReadyState();
    }, 
    evaluateCallback: function (options) {  
        this.options = {
          object:       {},
          callback:     function () {}
        };
        Object.extend(this.options, options || { });  
        if (this.readyState) {   
            this.options.callback(this.evaluate(this.options.object));
        } else { 
            this.queue.push({
                qtype: 'callback',
                obj: this.options.object,
                fnc: this.options.callback
            });
         }
         return;        
    },
    onReadyState: function () {
        while (q = this.queue.shift()) {
            var object = q.obj;
            var qtype = q.qtype;
            var callback = q.fnc;
            var elm;
            callback(this.evaluate(object)); 
        }
    }
}); 

/*
 WDR:
 Some new options to the get() method:
 
 urlparams -- a js hash of stuff to be added to the query string. 
    simple example:

    generic.templatefactory.get({
        path: '/templates/edit-address-jsdata.tmpl',
        urlparams: {
            ADDRESS_ID: options.addrid
        }
    }).evaluateCallback({
        ...blah blah... 
    });

    this results in the url 'http://(domain)/templates/edit-address-jsdata.tmpl?ADDRESS_ID=88888'
    (assuming 88888 is the value of "options.addrid")
    
    From there, the perl side is plain old $request->param('ADDRESS_ID') or whatever...
    
 method -- HTTP method to use.  default is "GET", but you can override as "POST" if desired.
 
 NOTE - The default get() params use the "GET" http method and no query string.
 This leverages the browser cache to save the gotten file for subsequent calls.
 Therefore, understand that using the urlparams and method options may bypass
 the browser cache, which may or may not be your intent.

*/

generic.TemplateFactory = Class.create( Hash, {  
    templatesHash: false,
    get: function (params) {
        var key = params.key || params.path;
        var query = params.query;
        var forceReload = params.forceReload || false; 
        var templateString = params.templateString || false;

        // RediTemplate previously created
        if (typeof this._object[key] != "undefined" && !forceReload && !query) {  
            return this._object[key];
        }
        
        // create RediTemplate 
        this._object[key] = new generic.RediTemplate();  
        
        // check if the path is a key in a hash
        if (this.templateHash) { 
            try { if (key.indexOf(this.templateHash)==0) templateString = eval(key); } catch(e) {}
        }
        
        // check if using a string (either via params.templateString or via the hash)
        if (templateString) {
            this._object[key].load(templateString);
            return this._object[key];
        } 
        
        // get template via Ajax
        var url = key;
        if (query) {
            var q = $H(query);
            var queryString = q.toQueryString();
            url += "?" + queryString;
        }
        var tAjax = new Ajax.Request(url, {
            method: params.method || 'get',
            parameters: params.urlparams,
            onSuccess: function(transport) {   
                this._object[key].load(transport.responseText);
            }.bind(this)
        });
        return this._object[key];
    }   
});
generic.templatefactory = new generic.TemplateFactory();Widget = Class.create({
    setProperties: function(args) {
        Object.extend(this, args);
    }, 
    initialize: function(o) {  
        if (o) this.setProperties(o);  
        //console.log("Widget.init: "+this.id); 
        this.domNode = false;
        this.children = []; 
        
        if (this.templatePath||this.templateString) {
            this.mixInProperties();
        } else { //widget has no template    
            if ($(this.id))  $(this.id).widget = this; 
            this.create();  
        } 
    },
    mixInProperties: function() { 
        //console.log("this.mixInProperties "+this.id+"/"+this.templatePath);
        var key = (this.templateKey ? this.templateKey : this.templatePath);
        var forceReload = (this.forceReload ? this.forceReload : false);
        var params = {key:key, forceReload:forceReload, query: this.query}; 
        if (this.templateString) params.templateString = this.templateString; 
        generic.templatefactory.get(params).evaluateCallback({
            object: this,
            callback: this.handleMixIn.bind(this)
        }); 
    },
    handleMixIn: function(html) { 
        html = html.strip();  
        generic.helpers.div.update(html); 
        this.domNode = generic.helpers.div.firstDescendant();  
    
        var existingContent = false; 
        if ($(this.id)) this.nodeToReplace = $(this.id);
        if (this.nodeToReplace) { //obj with same id might exist, or a different container passed in to be replaced with this node
            //console.log("handleMixIn: this.nodeToReplace "+this.nodeToReplace.id + " /check: "+$("gnav_products"));
            existingContent = this.nodeToReplace.innerHTML; 
            if (this.reinsertNode) {
                //console.log("this.handleMixIn: this.reinsertNode");
                this.nodeToReplace.parentNode.removeChild(this.nodeToReplace); //e.g. psubnav_my_mac
                this.nodeToReplace = false;
            }
        } 
    
        if (this.nodeToReplace) {
            this.updateMixIn();
        } else {
            this.insertMixIn();
        }
        
        var self = this;
        this.domNode.widget = self;     
        this.attachPoints();  
        this.attachEvents();  
        this.containerNode = this.containerNode ? this.containerNode : this.domNode;
        
        if (existingContent) { 
            this.containerNode.insert(existingContent);
        }
        
        this.create(); 
    },
    updateMixIn: function() { 
        //console.log("Widget.updateMixIn: "+this.domNode+" "+this.nodeToReplace.id + " /check: "+$("gnav_products"));
        this.nodeToReplace.parentNode.replaceChild(this.domNode, this.nodeToReplace); 
    },
    insertMixIn: function() {
        if (this.domInsertionMethod) { 
            this.domInsertionMethod(this);
        } else { 
            try {  
                var container = this.domParent ?  this.domParent : $(this.parentId).widget.containerNode;
                if (typeof container == "string") container = $(container); 
                //console.log("this.handleMixIn "+this.id+" // "+container.id);    
                container.insert(this.domNode);   
            } catch(e) {
                console.log("Widget.insertMixIn e: "+this.id+"/"+this.parentId);
            }
        }  
    },
    create: function() {   
        //console.log("Widget.create: "+this.id);   
        if ($(this.parentId) && $(this.parentId).widget) {
            this.parent = $(this.parentId).widget;
            $(this.parentId).widget.children.push(this); 
        } else {
            //console.log("!! Widget.create: "+this.id +" / "+ this.domParent +" / "+ this.parentId +" / "+ $(this.parentId));   
        }       
        
        if (this.postCreate) {
            this.postCreate();   
        } 
    },
    attachPoints: function() { 
        //console.log("Widget.attachPoints "+this.id);    
        var self = this; 
        try {
            var elemsWithPoints = this.domNode.select("[attachPoint]");   
            if (null !== this.domNode.getAttribute("attachPoint")) elemsWithPoints.push(this.domNode);  
            var attachPoint;
        
            elemsWithPoints.each(function(elem) {   
                attachPoints = String(elem.getAttribute("attachPoint")).split(",");  
                attachPoints.each(function(attachPoint){
                    //console.log("Widget.attachPoints: "+elem.id+"/"+attachPoint);  
                    self[attachPoint] = elem; 
                    self["domNode"].widget[attachPoint] = elem;  
                }); 
        
            });
        } catch(e) {
            console.log("Widget.attachPoints e "+ this.domNode + " " +e.description);   
            return;
        }  
    },
    attachEvents: function() { 
        //console.log("Widget.attachEvents: "+this.id);    
        var self = this; 
        try {
            var elemsWithEvents = this.domNode.select("[attachEvent]"); 
            if (null !== this.domNode.getAttribute("attachEvent")) elemsWithEvents.push(this.domNode);  
            var events, etype, functionName, func; 
        
            elemsWithEvents.each(function(elem) { 
                events = String(elem.getAttribute("attachEvent")).split(",");  
                events.each(function(event){
                    event = event.split(":");
                    etype = event[0];
                    functionName = event[1];
                    func = self[functionName];
                    //console.log('Widget.attachEvents: ' + elem.id + '  attached e ' + functionName + ' e type = '+etype+" func = "+func);   
                    //if (func) elem.observe(etype, func.bind(self)); // ie bug
                    if (func) elem["on"+etype] = func.bind(self);
                }); 
            });
        } catch(e) {
            console.log("Widget.attachEvents e "+ this.domNode + " " +e.description);   
            return;
        }  
    }
});var generic = generic || {};

/**
 * This singleton class provides an interface to the Perl Gem JSON-RPC methods via AJAX.
 * @memberOf generic
 */
generic.jsonrpc = ( function() {
    var jsonRpcObj = {

	    id: 0, 
	    url: generic.env.domain + "/rpc/jsonrpc.tmpl",
	    errorCodes: {
	        101: "The data type of this method is not supported.",
	        102: "The data type of the request parameters is not supported.",
	        103: "Your request did not return any results.",
	        104: "Response is not in the expected format."
	    },

		/**
		 * This is the method to use when calling a JSON-RPC method.
		 * @example
		generic.jsonrpc.fetch({
		    method: 'rpc.form',
			"params": [
				{
					"_SUBMIT": "address",
					"COUNTRY_ID": "46",
					"ADDRESS_ID": "9342012",
					"LAST_NAME": "Don",
					"FIRST_NAME": "William"
				}
			]
		    onSuccess: function(jsonRpcResponse) {
		        var responseData = jsonRpcResponse.getData();
				console.dir(responseData);
		    },
		    onFailure: function (jsonRpcResponse) {
		        var errorObjectsArray = jsonRpcResponse.getMessages();
		        var errListNode = addressForm.select("ul.error_messages")[0];
		        generic.showErrors(errorObjectsArray, errListNode, addressForm);
		    }
		}); // end jsonRpcWrapper.fetch
		
         * @param {Array} *Optional* args.params an Array of hashes of method parameters.
         * @param {string|Node} args.method *Optional* name of JSON-RPC method to call. Default value is 'rpc.form'
         * @param {function} args.onSuccess *Optional* callback function. It is invoked with a JsonRpcResponse object as a parameter if the AJAX call returns with HTTP status of 200 AND without a JSON-RPC error.
         * @param {function} args.onFailure *Optional* callback function. It is invoked with a JsonRpcResponse object if the AJAX call returns with HTTP status other than 200. It is also invoked if a 200 response contains a JSON-RPC error.
         * @param {function} args.onBoth *Optional* callback function. If provided, it will override any other callback function passed as a parameter and it will be invoked by any JSON-RPC response.
		 * @memberOf generic.jsonrpc
		 */
	    fetch: function(/* Object*/args) {
	        var self = this;
	        this.id++;

	        var options = {method:'post'};

	        if (args.onBoth) {
	            options.onSuccess = args.onBoth;
	            options.onFailure = args.onBoth;
	        } else {
	            options.onSuccess = args.onSuccess || function (response) {
	                console.log('JSON-RPC success');
	                console.log(Object.toJSON(response.getValue()));
	            };
	            options.onFailure = args.onFailure || function (response) {
	                console.log('JSON-RPC failure');
	                console.log(Object.toJSON(response.getMessages()));
	            };
	        }

	        options.onSuccess = options.onSuccess.wrap(
	            function(proceed, response) {
	                if (!response||!response.responseText) { // empty response
	                    errorHandler(this.createErrorResponse(103));
	                    return;
	                }
	                // Analytics general event for RPC..
	                generic.events.fire({event:'RPC:RESULT',msg:response});

	                var responseArray = response.responseText.evalJSON(true);

	                if (Object.isArray(responseArray)) {
	                    var resultObj = responseArray[0];
	                    if (resultObj) {
	                        var jsonRpcResponse = generic.jsonRpcResponse(resultObj);
	                        if (resultObj.error) { // server returns an error
	                            errorHandler(jsonRpcResponse);
	                        } else if (resultObj.result) { // successful response in expected format
	                            //console.log("generic.jsonrpc.onSuccess");
	                            proceed(jsonRpcResponse);
	                        }
	                    } else { // top-level response array is empty
	                        errorHandler(self.createErrorResponse(103));
	                    }
	                } else { // response is not in expected format (array) 
	                    errorHandler(self.createErrorResponse(104));
	                }
	            });

	        options.onFailure = options.onFailure.wrap( function(proceed, response) {
                var resp = response;
                //server returned failure, i.e. onFailure was not triggered by this class
                if (typeof response.responseText != "undefined") {
                    //console.log("generic.jsonRPC onFailure: server error");
                    try { //server returns an error in json
                        var responseArray = response.responseText.evalJSON(true);
                        var resultObj = responseArray[0];
                        resp = generic.jsonRpcResponse(resultObj);
                    } catch(e) { //server response is not json
                        //console.log("generic.jsonRPC onFailure: server error, result is not json");
                        resp = self.createErrorResponse(response.status,response.responseText);
                    }
                }

                proceed(resp);
            });

	        var errorHandler = options.onFailure;
	        var method = args.method || 'rpc.form';
	        var params = args.params || [];

	        var postObj = {
	            method: method,
	            id: self.id
	        };

	        // make sure a method was passed
	        if ( !Object.isString(method) || method.length <= 0 ) {
	            errorHandler(self.createErrorResponse(101));
	            return null;
	        }

	        //make sure that the params type is an obj
	        if (typeof params === 'string') {
	            postObj.params = params.evalJSON();
	        } else if (typeof params === 'object') {
	            postObj.params = params;
	        } else {
	            errorHandler(self.createErrorResponse(102));
	            return null;
	        }

	        var postString = "[" + Object.toJSON(postObj) + "]";

	        options.parameters = $H({JSONRPC:[postString]}).toQueryString();
	        var url = this.url + '?dbgmethod=' + method;
	        new Ajax.Request( url, options );
	        return this.id;
	    },

	    createErrorResponse: function(errorCode, errorMsg) {
	        errorMsg = errorMsg || this.errorCodes[errorCode];
	        var errorObj = new generic.jsonRpcResponse({
	            "error" : {
	                "code": errorCode,
	                "data": {
	                "messages" : [{
	                    "text" : errorMsg,
	                    "display_locations" : [],
	                    "severity" : "MESSAGE",
	                    "tags" : [],
	                    "key" : ""
	                }]
	                }
	            },
	            "id" : this.id
	        });
	        return errorObj;
	    }
	};
    return jsonRpcObj;
} )(); 


 /**
 * A JsonRpcResponse object is instantiated and returned to the onSuccess and onError
 * callback functions that are passed to the fetch() method. It exposes the contents
 * of the response through its getData, getError, and getMessages methods.
 */
generic.jsonRpcResponse = function (resultObj) {
	var jsonRpcResponseObj = {};
    var rawResponse = resultObj; // raw response data is kept in a private variable

	var CartResult = function(responseData) {
	    var data = responseData;
	    var cartItem = {
	        product: { 
	            sku: {}
	        }
	    };
	    var cartMethod;

	    if (data.ac_results &&
	            Object.isArray(data.ac_results) && 
	            data.ac_results[0]) {
	        if (data.ac_results[0].result &&
	                data.ac_results[0].result.CARTITEM) {
	            var item = data.ac_results[0].result.CARTITEM;
	            var prodRegEx = /^prod\.(.+)$/;
	            var skuRegEx = /sku\.(.+)$/;
	            var prodObj = { sku: {} };
	            for (var prop in item) {
	                var newPropName = null;
	                var prodResult = prop.match(prodRegEx);
	                if (prodResult && prodResult[1]) {
	                    newPropName = prodResult[1];
	                    cartItem.product[newPropName] = item[prop];
	                }
	                if (!newPropName) {
	                    var skuResult = prop.match(skuRegEx);
	                    if (skuResult && skuResult[1]) {
	                        newPropName = skuResult[1];
	                        cartItem.product.sku[newPropName] = item[prop];
	                    }
	                }
	                if (!newPropName) {
	                    cartItem[prop] = item[prop];
	                }
	            }
	        }
	        if (data.ac_results[0].action) {
	            cartMethod = data.ac_results[0].action;    
	        }
	    }
	    this.getItem = function() {
	        return cartItem;
	    };
	    this.getMethod = function() {
	        return cartMethod;
	    }
	};


    jsonRpcResponseObj.getId = function() {
        if (rawResponse) {
            return rawResponse.id;
        }
        return null;
    };
    jsonRpcResponseObj.getError = function() {
        if (rawResponse &&
            rawResponse.error) {
            return rawResponse.error;
        }
        return null;
    };
    jsonRpcResponseObj.getData = function() {
        if (rawResponse &&
            rawResponse.result &&
            rawResponse.result.data) {
            return rawResponse.result.data;
        }
        return null;
    };
    jsonRpcResponseObj.getValue = function() {
        if (rawResponse &&
            rawResponse.result &&
            typeof rawResponse.result.value != "undefined") {
            return rawResponse.result.value;
        }
        return null;
    };
    /**
     * This method returns the contents of the response's error property.
     * It first checks the result property, then checks the error property.
     */        
    jsonRpcResponseObj.getMessages = function() {
        if (rawResponse) {
            if (rawResponse.result &&
                rawResponse.result.data &&
                rawResponse.result.data.messages) {
                return rawResponse.result.data.messages;
            } else if (rawResponse.error &&
                       rawResponse.error.data &&
                       rawResponse.error.data.messages) {
                return rawResponse.error.data.messages;
            }
        }
        return null;
    };
    jsonRpcResponseObj.getCartResults = function() {
		var data = this.getData();
		if (!data) {
			return null;
		}
		var returnObj = new CartResult(data);
		return returnObj;	
    };

    return jsonRpcResponseObj;
};


generic.cookie = function(/*String*/name, /*String?*/value, /*.__cookieProps*/props){  
	var c = document.cookie; 
	if (arguments.length == 1) { 
		var matches = c.match(new RegExp("(?:^|; )" + name + "=([^;]*)"));
		if (matches) {
			matches = decodeURIComponent(matches[1]);
			try {
			     return matches.evalJSON(true); //Object
			} catch(e) {
			     return matches; //String
			}
			
		} else {
			return undefined;
		} 
	} else {
		props = props || {};
// FIXME: expires=0 seems to disappear right away, not on close? (FF3)  Change docs?
		var exp = props.expires;
		if (typeof exp == "number"){ 
			var d = new Date();
			d.setTime(d.getTime() + exp*24*60*60*1000);
			exp = props.expires = d;
		}
		if(exp && exp.toUTCString){ props.expires = exp.toUTCString(); } 
		
		value = encodeURIComponent(value);
		var updatedCookie = name + "=" + value;
		
		for(propName in props){
			updatedCookie += "; " + propName;
			var propValue = props[propName];
			if(propValue !== true){ updatedCookie += "=" + propValue; }
		}
		 
		document.cookie = updatedCookie;
	}
};
	  var generic = generic || {};
generic.flash = {
    abort: false,
    swfObject : swfobject,
    defaults : {
        defaultAlt : { 
            href: "http://www.adobe.com/go/getflashplayer"
        },
        attributes : { 
            playerversion: "9.0.28",
            width: "100%",
            height: "100%", 
                    hspace: 0,
                    vspace: 0,
                    align: "top"
        },
        params : {
            wmode: "transparent", 
                    quality: "high",
                menu: "true",
                    swliveconnect: "true",
                    allowscriptaccess: "always", 
                    scale: "noScale",
                    allowfullscreen: "true"
                } 
        },
    
    embed: function (attributes, params, placeholderId) { 
        if (generic.flash.abort) return;
     
        if (!$(placeholderId)) { 
            console.log("generic.flash.embed: Element doesnt exist"); 
            return; 
        }    
        
        /**
        //BUG: if two swfs are embedded at the same time (with no delay) & there is a "this" reference,
        //the second one is embedded twice.
                attributes = generic.mixin(generic.flash.defaults.attributes,attributes);
                params = generic.mixin(generic.flash.defaults.params,params);
                **/     
                   var defaults = {
        defaultAlt : { 
            href: "http://www.adobe.com/go/getflashplayer"
        },
        attributes : { 
            playerversion: "9.0.28",
            width: "100%",
            height: "100%", 
                    hspace: 0,
                    vspace: 0,
                    align: "top"
        },
        params : {
            wmode: "transparent", 
                    quality: "high",
                menu: "true",
                    swliveconnect: "true",
                    allowscriptaccess: "always", 
                    scale: "noScale",
                    allowfullscreen: "true"
                } 
                 };
                 
                attributes = Object.extend(defaults.attributes, attributes);
                params = Object.extend(defaults.params, params);
                 
            if (typeof params.flashvars != "string") { 
                params.flashvars = Object.toQueryString(params.flashvars); 
            }  
     
        // version check
        if (generic.flash.swfObject.hasFlashPlayerVersion(attributes.playerversion)) {   
            generic.flash.swfObject.addDomLoadEvent(function() { 
                generic.flash.swfObject.createSWF(attributes,params,placeholderId); 
            }); 
            return;
        }
         
        //content if flash doesn't embed
        var altid = attributes.altcontentid; 
        if (altid && $(altid)) {
            altid.style.visibility = "visible";
                    altid.style.display = "block";
        } else { 
            var defaultalt = $(placeholderId).select(".noflash")[0]; 
            if (!defaultalt) return; 
            if (!defaultalt.getAttribute("href")) {
                     defaultalt.observe("click", function() {
                window.open(defaults.defaultAlt.href);
               });
            }
            defaultalt.style.visibility = "visible";
                    defaultalt.style.display = "block";
          
        }     
    },

    /**
     * @namespace favorites contains favorites-related methods that are called by Action Script.
     * @memberOf generic.flash
     */
    favorites: {
        /* Used by Action Script to add items to a collection. Response data
         * is returned via a callback function.
         * @param args {object}
         * @param args.movieName {String} the value of the embed/object tag's name attribute
         * @param args.callback {String} the name that the container (the browser) uses to access the callback Function
         * @param args.skuBaseId {String} the value of the SKU_BASE_ID field for the SKU that is to be added
         * @methodOf generic.flash.favorites
         */
        add: function (args) {
            var options = Object.extend( {
                movieName : "",
                callback: "",
                skuBaseId: ""
            }, args);

            if ( options.skuBaseId.length < 1 ) {
                return null;
            }
            if ( !generic || !generic.checkout || !generic.checkout.cart ) {
                return null;
            }
            var cartObj = generic.checkout.cart;            
            var callbackFn = function(options, responseObj) {
                // console.log(responseObj.getMessages());
                if ( options.movieName.length > 1 &&
                    document[options.movieName] &&
                    document[options.movieName][options.callback] &&
                    typeof document[options.movieName][options.callback] === "function" ) {
                    if (responseObj.getMessages() || responseObj.getError()) {
                        document[options.movieName][options.callback](responseObj.getMessages());                        
                    }
                }
            }.curry(options);
            cartObj.updateCart({
                params: {
                    skus: [options.skuBaseId],           
                    itemType: "favorites",
                    action: "add"
                },
                onSuccess: callbackFn,
                onFailure: callbackFn
            });        
        }
    },

    /**
     * @namespace cart contains cart-related methods that are called by Action Script.
     * @memberOf generic.flash
     */
    cart: {
        /* Used by Action Script to add items to user's shopping cart. Response data
         * is returned via a callback function.
         * @param args {object}
         * @param args.movieName {String} the value of the embed/object tag's name attribute
         * @param args.callback {String} the name that the container (the browser) uses to access the callback Function
         * @param args.skus {Array} the value of the SKU_BASE_ID field for each SKU that is to be added
         * @param args.quantity: {Number} the quantity of items that will be added to the cart.             
         * @methodOf generic.flash.cart
         */
        add: function (args) {
            var options = Object.extend( {
                movieName : "",
                callback: "",
                skus: [],
                quantity: 1
            }, args);
            if ( options.skus.length < 1 ) {
                return null;
            }
            if ( !generic.checkout || !generic.checkout.cart ) {
                return null;
            }
            var cartObj = generic.checkout.cart;            
            var callbackFn = function(options, responseObj) {
                if ( options.movieName.length > 1 &&
                        document[options.movieName] &&
                        document[options.movieName][options.callback] &&
                        typeof document[options.movieName][options.callback] === "function" ) {
                    if (responseObj.getData()) {
                        document[options.movieName][options.callback](responseObj.getData().ac_results);                        
                    } else if (responseObj.getError()) {
                        document[options.movieName][options.callback](responseObj.getMessages());
                    }
                }
            }.curry(options);
            cartObj.updateCart({
                params: {
                    skus: options.skus,            
                    INCREMENT: 1
                },
                onSuccess: callbackFn,
                onFailure: callbackFn
            });         
        }
    }
};generic.checkout = {};

/**
 * generic.checkout.cart 
 * - depends on: generic.cookie, generic.jsonrpc
 */ 
generic.checkout.cart = ( function() {

    return {
        setCookie: false, 
    
        order: new Hash(),
        payments: new Array(),
        carts: new Hash(), 
        items: new Array(),
        samples: new Array(),
     
        totalShoppedItems: 0, 
        totalItems: 0,
     
        transactionParams: {
        transactionFields: {
                    "trans_fields" : ["TRANS_ID", "payments"]
            },
        paymentFields: {
                    "payment_fields" : ["address", "PAYMENT_TYPE", "PAYMENT_AMOUNT", "TRANS_PAYMENT_ID"]
            },
        orderFields: {
                    "order_fields" : ["items", "samples", "address", "TRANS_ORDER_ID"]
            }
        },
    
        itemTypes: { 
            "cart" : {
                "id": "SKU_BASE_ID",
                "_SUBMIT" : "cart"
            },
            "giftcard" : {
                "id": "CART_GIFTCARD_ID",
                "_SUBMIT" : "giftcard"
            },
            "collection" : {
                "id": "SKU_BASE_ID",
                "_SUBMIT" : "collection.items"
            },
            "favorites" : {  
                "id": "SKU_BASE_ID",
                "_SUBMIT" : "alter_collection"
            }
        },

        initialize: function(args) { 
            Object.extend(this, args); // copy args to obj
        },
    
        getCartTotals: function() {
            var cookie = generic.cookie("cart");
            if (cookie && cookie!==null) {
               // console.log("generic.cart.getCartTotals cookie: "+Object.toJSON(cookie)); 
               Object.extend(this, cookie);
           
               generic.events.fire({event:'cart:countsUpdated'});
            } else {
               // console.log("generic.cart.getCartTotals !cookie");  
               this.getCart();
            }  
        },

        // MERGE NOTE: MAC-specific
        //update Cookie, in case Cart has been updated via form submission instead of generic.cart 
        updateCartTotals: function(args) { 
             //console.log("generic.cart.updateCartTotals: "+Object.toJSON(args));  
             Object.extend(this, [args]);
             if (this.ifSetCookie) this.setCookie();    
             generic.events.fire({event:'cart:countsUpdated'});
        },
    
        setCookie: function() { 
            console.log("generic.cart.setCookie "+this.totalItems);
            var s  = {
                totalItems: this.totalItems 
            }
            s = Object.toJSON(s);
            generic.cookie("cart",s, {path:"/"}); 
        },
    
        getCart: function(args) {
            //console.log("generic.cart.getCart");
            var self = this; 

            if (args != null && args.pageDataKey) {
                var pageData = generic.page_data(args.pageDataKey);
                if (pageData.get("rpcdata")) {
                    // console.log( "cart page data found!" );
                    self._updateCartData(pageData.get("rpcdata"));
                    return;
                }
            }

            var params = {};
            params = Object.extend ( params, self.transactionParams.transactionFields );
            params = Object.extend ( params, self.transactionParams.paymentFields);
            params = Object.extend ( params, self.transactionParams.orderFields);
        
             var id = generic.jsonrpc.fetch({
                method : 'trans.get',
                params: [params],
                onSuccess:function(jsonRpcResponse) {
                    self._updateCartData(jsonRpcResponse.getValue()); 
                },
                onFailure: function(jsonRpcResponse){
                    //jsonRpcResponse.getError();
                    console.log('Transaction JSON failed to load');
                }
            });
            return id;
        },
   
         //sets up internal representation of cart state
        // Assuming one order per transaction
        _updateCartData: function(data){ 
            // console.log("generic.checkout.cart._updateCartData");
            var self = this;
            this.data = data;
            if (!data.order) return; // MAC: trans_data can exist for transactions where trans_data.order does not, such as favorites where free shipping message is returned in trans_data
            this.totalItems = data.items_count; 
            this.defaultCartId = data.default_cart_id;
            this.payments = (data.trans && data.trans.payments) ? $A(data.trans.payments) : null;  
            this.order = data.order; 
         
            // contents and sample_contents mirror the sku by qty hashes
            this.order.contents = new Hash();
            this.order.sample_contents = new Hash();

            if (this.order.items != null) {
                this.order.items = this.order.items.reject(function(item){ // filter out nulls
                    return item === null;
                });
            }
        
            var items = this.order.items || null;
            var totalShoppedItems = 0;
            if (items != null) {  
                items.each(function(item){   
                    if (!item) { return; }
                    totalShoppedItems+=item.ITEM_QUANTITY;
                
                    // set up contents by cart hashes 
                    var cartID = item.CART_ID;
                    var cart = self.carts.get(cartID);
                    if (!cart) {
                        self.carts.set(cartID, new Hash()); 
                        cart = self.carts.get(cartID); 
                        cart.set('contents', new Hash());
                    } 
                    var id = item['sku.SKU_BASE_ID'] ? item['sku.SKU_BASE_ID'] : item.COLLECTION_ID; 
                    cart.get('contents').set(id, item.ITEM_QUANTITY);
                
                    // compute per-unit tax (replace this with field from JSONRPC result when available)
                    var unitTax = item.APPLIED_TAX/item.ITEM_QUANTITY;
                    item.UNIT_TAX = unitTax;

                    // set up order contents hash (spans carts)
                    if (item.itemType.toLowerCase() == 'skuitem') {
                        var key = item['sku.SKU_BASE_ID'];
                        var qty = item.ITEM_QUANTITY; 
                        //error self.order.contents.set(key, qty);
                        self.order.contents[key] = qty; 
                    } else if (item.itemType.toLowerCase() == 'kititem') {
                        var key = item.COLLECTION_ID;
                        var qty = item.ITEM_QUANTITY;
                        self.order.contents.set(key,qty);
                    } else {
                        // FUTURE: other cart item types (e.g. kits)
                    } 
                
                });
            }
        
            this.totalShoppedItems = totalShoppedItems;
        
            var samples = this.order.samples;
            if (samples != null) {
                samples.each(function(item){
                    // set up contents by cart hashes
                    var cartID = item.CART_ID;
                    var cart = self.carts.get(cartID);
                 
                    if (!cart) {
                        self.carts.set(cartID, new Hash());
                        cart = self.carts.get(cartID);
                        cart.set('contents', new Hash());
                    } 
               
                    var id = item['sku.SKU_BASE_ID'] ? item['sku.SKU_BASE_ID'] : item.COLLECTION_ID; 
                    cart.get('contents').set(id, item.ITEM_QUANTITY);

                    // set up order contents hash (spans carts)
                    if (item.itemType.toLowerCase() == 'sampleitem') {
                        var key = item['sku.SKU_BASE_ID'];
                        var qty = item.ITEM_QUANTITY;
                        self.order.sample_contents.set(key,qty); 
                    } else {
                        // other item types (are likely errors)
                    } 
                });
            }
        
            // if (self.setCookie) self.setCookie();        
            generic.events.fire({event:'cart:countsUpdated'});
            // generic.events.fire({event:'cart:updated'}); 
        },
   
        /* args is a hash
        required keys: args.params.skus array, either args.params.INCREMENT or args.QTY with args.INCREMENT overriding args.QTY
        optional keys: args.itemType, args.OFFER_CODE, args.CART_ID*/ 
        updateCart: function(args){
            console.log("cart.updateCart: "+Object.toJSON(args.params));
            if (!args.params) return null; 
            var self = this;   
            var onSuccess = args.onSuccess || Prototype.emptyFunction;  
            var onFailure = args.onFailure || Prototype.emptyFunction; 
            var itemType = args.params.itemType || "cart"; //e.g. cart, collection, giftcard etc
            var id = self.itemTypes[itemType].id;
            var qty = args.params.QTY;
            var action = args.params.action;
            var increment = args.params.INCREMENT;
            var skus = args.params.skus;
         
            var params = {
                '_SUBMIT': self.itemTypes[itemType]["_SUBMIT"] 
            };  
        
            //id 
            params[id] = (skus.length == 1) ? skus[0] : skus; //MK collections array syntax correct?
            //qty   
            if (increment && increment>=0) {
               //currently +1 will be added regardless of INCREMENT's actual value
               //backend requires QTY property to exist but it will not be used
             params["INCREMENT"] = increment; 
             params["QTY"] = 1; 
            } else if (increment && increment<0) {
                //decrements qty by -1
            } else if (typeof(qty) !== "undefined" && qty>=0) { 
                params["QTY"] = qty;  
            }
                     
            //offer code
            if (args.params.OFFER_CODE && args.params.OFFER_CODE.length>0) {
                params['OFFER_CODE'] = args.params.OFFER_CODE;
            }
            
            // SS: for some reason this was set to always be "add"
            // need "delete" option for removing favorites
            if (action && (action.length > 0)) {
                params['action'] = action;
            }

            // targeting of the correct cart is still missing (and important to get right) 
            // cart id if we are adding to something other than the default cart
            if (args.params.cart_id && (args.params.cart_id != self.defaultCartId)) {
                params['CART_ID'] = args.params.cart_id;
            }
        
            var id = generic.jsonrpc.fetch({
                "method" : 'rpc.form',
                "params" : [params],
                "onSuccess": function(jsonRpcResponse){
                    var data = jsonRpcResponse.getData();
                    //load data
                    if (data && data["trans_data"]) { 
                        self._updateCartData(data["trans_data"]);
                    }
                    if (itemType === 'cart') {
                        var cartResultObj = jsonRpcResponse.getCartResults();
                        document.fire("cart:updated", cartResultObj);
                    };
                    if (itemType == 'favorites') {
                        document.fire("favorites:updated", jsonRpcResponse);
                    };
                    onSuccess(jsonRpcResponse);
                },
                "onFailure": function(jsonRpcResponse){
                    onFailure(jsonRpcResponse);
                }
            });
 
            return id;
        }, 
    
        getItemQty : function(baseSkuId) {  
            if (!this.order.items) return 0;
            var lineItem = this.order.items.find( function (line) {
                return line['sku.SKU_BASE_ID'] ==  baseSkuId;
              });  
            if (!lineItem) {
                return 0; 
            }                
            return lineItem.ITEM_QUANTITY;
        },
    
        getBaseSkuIds: function() {  //MK: what is this used for?
            //console.log("generic.cart.getBaseSkuIds: "+this.order.items);
            if (!this.order.items) return new Hash();
            var baseSkuIds = this.order.items.pluck( 'sku.SKU_BASE_ID' ); //MK what about giftcards/collections?  
            return baseSkuIds; 
        },  
    
        getSubtotal: function() {
            var lineItems = this.order.items;
            if (!this.order.items) return 0;
            var subtotal = 0;
            for (var i=0, len = lineItems.length; i<len; i++) {
                var lineItem = lineItems[i];
                subtotal += (lineItem.UNIT_PRICE + lineItem.UNIT_TAX) * lineItem.ITEM_QUANTITY;
            }
            return subtotal;
        },
     
        getTotalShoppedItems: function(){ //products and gift cards
           /** var ttl = 0;
            var items = this.order.items;
            if (items != null) {
                items.each(function(item){
                    if (item && item.ITEM_QUANTITY) {
                        ttl += item.ITEM_QUANTITY;
                    }
                });
            } 
            return ttl;**/
            return this.totalShoppedItems;
        },         
        
        getTotalSamples: function() {
             var ttl = 0;
             var samples = this.order.samples;
                if (samples != null) {
                    samples.each(function(item){
                        ttl += item.ITEM_QUANTITY;
                    });
            } 
            return ttl;
        }, 
    
        getTotalItems: function(){ 
           // return this.getTotalShoppedItems() + this.getTotalSamples();
           return this.totalItems;
         }    
     };
}() ) ;
generic.popup = function(/*Object*/args) {
    var activatorNode = $(args.activator);
    if (!activatorNode) { return false; }
        
    //console.log("creating popup for "+activatorNode); 
    var specs = Object.toQueryString(Object.extend(generic.popup.defaults, args));
    var specs = specs.replace(/\&/g, ",");
     
    var open = function() {
        var win = window.open(args.url, args.name, specs);
        if (!win) generic.popup.errorAction(); 
    };

    activatorNode.observe("click", open); 
    
    return true;
}

generic.popup = Object.extend(generic.popup, {
    defaults: {  
        height: 500,
        width: 500,
        top: 25,
        left: 25,
        resizable: "yes",
        scrollbars: "yes",
        status: "no",
        toolbar: "no",
        menubar: "no",
        location: "no"
    },
    errorAction: function() {
         var msg = global.rb.popup_error_message;
         if (msg) alert(msg);
    }
});var extendElement = {
     /* gets element's ancestor with specified tagname/classname **/
     getAncestor: function(element, s){
       	var ancestors = element.ancestors();
		var type = "tagName";
		if (s.substring(0,1)==".") {
			type = "className";
			s = s.substring(1,s.length);
		} 
		var oRegExp = new RegExp("(\\b)" + s + "(\\b)");
		var result = false;
		find(result); 
		return result;
		
		function find(o) { 
			for (var i=0; i<ancestors.length; i++) { 
				if (oRegExp.test((type=="tagName")? ancestors[i].tagName : ancestors[i].className)) {  
					result = ancestors[i]; 
					break;
				}  
			}  
		}
    },
    
    /* gets element's direct children with specified tagname/classname **/	
    getChildren: function(element, s) {
		var children = element.childElements();
		var type = (s.substring(0,1)==".") ? "className" : "tagName";
		s = s.replace(/\./,"");
		var oRegExp = new RegExp("(\\b)" + s + "(\\b)");
		var results = [];
	
		children.each(function($_) {
        	if (oRegExp.test((type=="tagName")? $_.tagName : $_.className)) results.push($_);
       	}); 
		 
		return results;
	}
}
Element.addMethods(extendElement);

 
	 
/**
 * generic.user
 * - depends on: generic.jsonrpc
 */
generic.user = (function(){

    return {
        signed_id : false,

        timeoutLength : 15 * 60 * 1000,

        initialize: function(args) {
            generic.updateProperties.apply(this, [args]);
        },

        getUser: function(args) {
            var self = this;

            if (args != null && args.pageDataKey) {
                var pageData = generic.page_data(args.pageDataKey);
                if (pageData.get("rpcdata")) {
                    console.log( "user page data found!");
                    self._updateUserData(pageData.get("rpcdata"));
                    return;
                }
            }

            var id = generic.jsonrpc.fetch({
                method : 'user.json',
                params: [],
                onSuccess: function(jsonRpcResponse) {
                    self._updateUserData(jsonRpcResponse.getValue());
                },
                onFailure: function(jsonRpcResponse) {
                    console.log('User JSON failed to load');
                }
            });
            return id;
        },

        // until we better parameterise this...
        _updateUserData: function(data) {
            var seld = this;
            if (data != null && data[this.userinfo_rpc_key] != null) {
                Object.extend( this, data[this.userinfo_rpc_key] );
            } else {
                Object.extend( this, data );
            }
            generic.events.fire({event:'user:updated'});
        },

        isSignedIn: function() {
            return ( this.signed_in ? true : false );
        }

    };
}() );

// log out user after 15 minutes on secure pages
if (document.location.protocol == 'https:') {
    var logout = function() {
//        alert(generic.user.timeoutLength);
        document.location.href = '/account/signin.tmpl?timeout=1';
    };
    window.setTimeout( logout, generic.user.timeoutLength );
}
var generic = generic || {};

generic.errorStateClassName = 'error';

/**
 * This method displays error messages. It takes an array of error objects and a UL node
 * as parameters. If the UL is not spuulied, it will attempt to find a <UL class="error_messages">
 * in the DOM. It will then attempt to insert one directly after a <DIV id="header"> (If no header
 * is found, the method exits.) All the LI child nodes (previous messages) of the UL are hidden.
 * The text property of each error Hash is then displayed as an LI.
 * This method can also alter the style of the input elements that triggered the error.
 * The tags property in an error hash must be an array that contains a string starting with
 * "field." If the optional formNode parameter is supplied, this form node will be
 * searched for the field, and that field will be passed to the generic.showErrorState method. 
 * @example 
 * var errArray = [
 *      {
 *          "text": "Last Name Alternate must use Kana characters.",
 *          "display_locations": [],
 *          "severity": "MESSAGE",
 *          "tags": ["FORM", "address", "field.last_name_alternate"],
 *          "key": "unicode_script.last_name_alternate.address"
 *      },
 *      {
 *          "text": "First Name Alternate must use Kana characters.",
 *          "display_locations": [],
 *          "severity": "MESSAGE",
 *          "tags": ["FORM", "address", "field.first_name_alternate"],
 *          "key": "unicode_script.first_name_alternate.address"
 *      }
 *  ];
 * var listNode = $$("ul.error_messages")[0];
 * generic.showErrors(errArray, listNode);
 * @param {Array} errorObjectsArray Array of error hashes.
 * @param {DOM Node UL} errListNode UL element in which the error messages will be displayed.
 * @param {DOM Node} formNode Form element (or any container node) that contains the inputs
 * to be marked as being in an error state. 
 */
generic.showErrors = function(errorObjectsArray, errListNode, formNode) {
    var ulNode = errListNode || $$("ul.error_messages")[0];
    if (!ulNode) {
        ulNode = new Element("ul", {"class":"error_messages"});
        var header = $$("div#header")[0];
        if (!header) {
            return null;
        } else {
            header.insert({after:ulNode});
        }
    }
    var errListItemNodes = ulNode.select("li");
    errListItemNodes.each(function(li){
        li.hide();
    });
    ulNode.addClassName("errors-no-messages");
    if (errorObjectsArray.length > 0 && Object.isElement(formNode)){
        // hide all error states on fields
        var inputNodes = formNode.select("input");
        inputNodes = inputNodes.concat(formNode.select("select"));
        inputNodes = inputNodes.concat(formNode.select("label"));
        inputNodes.each(function(inputNode) {
            generic.hideErrorState(inputNode);
        });
    }
    errorObjectsArray.each(function(errObj){
        var errKey = errObj.key;
        var errListItemNode = null;
        if (errKey) {
            var regEx = new RegExp(errKey);
            // try to find LI whose ID matches the error key 
            errListItemNode = errListItemNodes.find(function(node) {
                return regEx.test(node.id);
            });
        }
        if (errListItemNode) {
            errListItemNode.show();
        } else {
            errListItemNode = new Element("li").insert(errObj.text);
            ulNode.insert(errListItemNode);
        }
        if (errObj.displayMode && errObj.displayMode === "message") {
            errListItemNode.addClassName("message");
        }
        if (errObj.tags && Object.isArray(errObj.tags) && formNode) {
            // search through error objects, show error state for any tagged with "field.[NAME]"
            var fieldPrefixRegEx = /^field\.(\w+)$/;
            errObj.tags.each( function(tag) {
                var reResults = tag.match(fieldPrefixRegEx);
                if(reResults && reResults[1]) {
                    var fieldName = reResults[1].toUpperCase();
                    var inputNode = formNode.select("input[name=" + fieldName + "]")[0] || formNode.select("select[name=" + fieldName + "]")[0];
                    if (inputNode) {
                        generic.showErrorState(inputNode);
                        var labelNode = formNode.select("label[for=" + inputNode.id + "]")[0];
                        generic.showErrorState(labelNode);                      
                    }
                }
            });
        }

    });
    ulNode.show();
    if (errorObjectsArray.length > 0){
        ulNode.removeClassName("errors-no-messages");
    }
};
generic.showErrorState = function(inputNode) {
    if (!inputNode || !Object.isElement(inputNode)) {
        return null;
    }
    inputNode.addClassName(generic.errorStateClassName);
}

generic.hideErrorState = function(inputNode) {
    if (!inputNode || !Object.isElement(inputNode)) {
        return null;
    }
    inputNode.removeClassName(generic.errorStateClassName); 
}
var brand = {};

/**
 * Brand extensions of generic.flash
 */
generic.flash.playerversion = (generic.env.isMac) ? "10.0.0" : generic.flash.playerversion;
// generic.flash.cart.add Brand-specific feature(s): 
// - Send result.data.trans_data & result.data.ac_results back to flash (vs. just ac_results)
generic.flash.cart.add = function (args) {
    var options = Object.extend( {
        movieName : "",
        callback: "",
        skus: [],
        quantity: 1
    }, args);
    if ( options.skus.length < 1 ) {
        return null;
    }
    if ( !generic.checkout || !generic.checkout.cart ) {
        return null;
    }
    //console.log("args passed from flash: "+Object.toJSON(options));
    var cartObj = generic.checkout.cart;
    var catId = options.catId;
    if (catId) {
        var match = catId.split("CAT");
        catId = (match ? match[1] : catId);
    }
    var callbackFn = function(options, responseObj) {
        if ( options.movieName && (options.movieName.length > 1) && $(options.movieName) ) {
            var flashObject = $(options.movieName);
            var externalCallback = options.callback;
            if (externalCallback && typeof flashObject[externalCallback] === "function" ) {
                // send result.data.trans_data & result.data.ac_results
                var responseData = responseObj.getData();
                if (responseData) {
                    flashObject[externalCallback]({trans_data: responseData.trans_data, ac_results: responseData.ac_results});
                } else if (responseObj.getError()) {
                    //console.log("generic.flash.cart.add callback: sending error/messages "+Object.toJSON(responseObj.getMessages()) );
                    externalCallback(responseObj.getMessages());
                }
            } else {
                console.log("generic.flash.cart.add callback: failure on callback argument = ",flashObject[externalCallback]);
            }
        }
    }.curry(options);
    cartObj.updateCart({
        params: {
            skus: options.skus,            
            INCREMENT: 1,
            CAT_BASE_ID: catId
        },
        onSuccess: callbackFn,
        onFailure: callbackFn
    });         
};


/**
 * Takes a hex value and returns equivalent rgb value
 * @param {String} hex  
 * @memberOf brand
 * @example
    generic.hexToRGB("#FFFDF4");
 */
brand.hexToRGB = function(hex) {
    var rgb = [];
    if(!hex) return [0, 0, 0];
    var h = cutHex(hex);
    rgb.push( parseInt( h.substring(0,2),16) );
    rgb.push( parseInt( h.substring(2,4),16) );
    rgb.push( parseInt( h.substring(4,6),16) ); 
    //console.log("hexToRGB: "+hex+" = "+rgb[0]+" "+rgb[1]+" "+rgb[2]);
    return rgb;
    
    function cutHex(h) {return (h.charAt(0)=="#") ? h.substring(1,7):h} 
}

/**
 * brand.updateProperties.apply
 * Needed for cases where Object.extend doesn't return what's expected
 * @memberOf brand
 * @example
    brand.updateProperties.apply(this, [args]);
 */
brand.updateProperties = function(obj) {   
    if (!obj) return;
    for (prop in obj) {  
        this[prop] = obj[prop];
    }  
}


/**
 * brand.tabs
 * Tabbed Container
 * Use this class IF you have created the html manually
 * and you just want to wrap it with the class object.
 * @memberOf brand
 */
brand.tabs = Class.create(Control.Tabs,
{
    // default Control.Tab options
    options: {
        activeClassName: 'tab-active',
        setClassOnContainer: true
    },
       
    initialize: function($super, containerId, args) {
        var options = this.options;
        Object.extend(options, args || {});
        Object.extend(this, args || {});

        if (args.scrollbar) this.initScrolling();        
        $super(containerId, options);
        if (args.useImageHeaders) this.initHeaders();
    },
  
    setActiveTab: function($super, link) {
        if (link.id === this.activeLink.id) return;
        if (this.beforeShow) this.beforeShow(link);
        if (this.scrollbar && this.tabContainer) {
            this.tabContainer.removeClassName(this.scrollbar.enabledClass); // start w/ scrollbar hidden
        }
        if (this.imgHeaders) {
            var linkImg = this.imgHeaders[link.id];
            if (linkImg) linkImg.changeSrc("on");
            var activeLinkImg = this.imgHeaders[this.activeLink.id];
            if (activeLinkImg) activeLinkImg.changeSrc("off");
        }
        
        $super(link);
        this.resetScrolling();
    },

    onContentRefresh: function() {
        this.resetScrolling();
    },

    initHeaders: function() {
        var imgs = {}
        this.links.each(function(link) {
            var imgNode = link.select('img')[0];
            if (imgNode) imgs[link.id] = new brand.img(imgNode, ["on", "off"]);
        });
        this.imgHeaders = imgs;
    },
    
    initScrolling: function() {
        var contentNode = this.scrollbar.contentNode;
        var handleId = this.scrollbar.handleId;
        var trackId = this.scrollbar.trackId;
        if (!contentNode || !handleId || !trackId) return;

        if (!$(handleId) || !$(trackId)) return;
        
        // scroll the element vertically based on its width and the slider maximum value
        var scroll= function(value) {
            contentNode.scrollTop = Math.round(value / scrollbar.maximum * (contentNode.scrollHeight - contentNode.offsetHeight) );
        }
        
        var scrollbar = new Control.Slider(handleId, trackId, {
            axis: 'vertical',
            onSlide: scroll,
            onChange: scroll
        });
        
        this.scrollbarObj = scrollbar;
        this.scrollbarNode = this.scrollbar.containerNode;
    },
    
    resetScrolling: function() {
        var scrollbarObj = this.scrollbarObj;
        if (!this.scrollbar || !scrollbarObj) return;
        var contentNode = this.scrollbar.contentNode;
        var scrollbarNode = this.scrollbar.containerNode;
        // disable/enable scrolling depending on overflow/height of scrollable content
        scrollbarObj.setValue(0);
        if (contentNode.scrollHeight <= contentNode.offsetHeight) {
            scrollbarNode.hide();
            //scrollbarObj.setDisabled();
        } else {
            //scrollbarObj.setEnabled();
            scrollbarNode.show();
            //this.tabContainer.addClassName(this.scrollbar.enabledClass);
        }
    },
    
    updateTab: function(tabId, html) {
        var containerNode = $(tabId);
        if ( containerNode ) {
            containerNode.update ( html );
        }
    }

});

/**
 * brand.bottomFixed
 * simulate fixed bottom position
 * @memberOf brand
 */ 
brand.bottomFixed = Class.create({  
    node: null,  // node: DOM element to position 
    minTop: 0, // minTop: Number minimum pixels for node.style.top 
    isLoaded: false,

    initialize: function(args) {
        if (!args.node) return false;
        this.node = args.node;
        this.s = this.node.style;
        var observeResize = (args.observeResize == false ? false : true);
        if (args.bottom) {
            this.fromBottom = args.bottom;
        } else {
            this.fromBottom = parseInt(this.node.getStyle("bottom"), 10);
        }
        if (isNaN(this.fromBottom)) { 
            console.log("brand.bottomFixed: bottom is NaN");
            return;
        }        
        if (args.minTop) {
            this.minTop = args.minTop;
            this.hasMinTop = true;
        } else {
            this.hasMinTop = false;
        }
        
        // initial top position
        if (args.startingTopPosition == 0 || args.startingTopPosition) {
            this.s.top = args.startingTopPosition + "px";
        } else {
            this.position();
            this.s.bottom = '';
            this.s.visibility = "visible";
        }
        
        // set scroll events
        var self = this;
        Event.observe(window, 'scroll', function() {
            self.onScroll();
        });
        if (observeResize) {
            Event.observe(window, 'resize', function() {
                self.onScroll();
            });
        }

        this.isLoaded = true;
    },
    position: function() {  
        var h = window.pageYOffset ||
        document.documentElement.scrollTop;
        h = (h ? h : 0);
        var shiftY = ((h + document.documentElement.clientHeight) - (this.node.offsetHeight + this.fromBottom));
        if (isNaN(shiftY)) return;
        if (this.currentY != shiftY) {  
            var changeBy = shiftY;      
            if (this.hasMinTop && (changeBy <= this.minTop)) {
                changeBy = this.minTop;
            }
            this.currentY = changeBy;
            this.s.top = (changeBy + 'px');  
        }   
    },
    onScroll: function() {   
        this.position();
    }
});
var site = brand;

site.init = function() {  
    /** testing **/
    //generic.flash.abort = true;  
    //site.view.colorNav.abort = true; 
    //site.globalnav.abort = true;
    //site.checkout.abort = true;  
    //site.product.abort = true;
    //site.customerService.abort = true;
       
    /** temporary changes **/
    //turn off features not ready for launch

    generic.init(); 
    generic.templatefactory.templateHash = "jsTemplates";
    
    getGlobalRBKeys();
    site.setGlobalParams();
    
    site.forms.init();
    
    try {
    var id = page_data.panel_nav["default"].id;
    } catch(e) {
    console.log("page_data.panel_nav missing");
    return;
    }

    site.globalnav.init();
    site.view.init();
    site.account.init();
    site.checkout.init();
    site.product.init();
    site.customerService.init();    
    site.livechat.init();
    
    site.overlay.initLinks();
}     


document.observe("dom:loaded", site.init );

//---------------------------------------------global settings
var global = global || {};

global.isipad = false;

site.setGlobalParams = function () {
    if (page_data) {
        if (page_data.is_ipad_user_agent && page_data.is_ipad_user_agent == 1) {
          global.isipad = true;
        }
    }
};

var getGlobalRBKeys = function () {    
    generic.rb.language = generic.rb("language");
    global.rb = global.rb || {};
    global.rb.popup_error_message = generic.rb.language.get("popup_error_message");
    generic.rb.language.rb_close = generic.rb.language.get('close');
    //global.rb.download_flash = generic.rb.language.get("download_flash"); // needed?
}
      

//--------------------------------------------------extensions
generic.overlay = brand.overlay;
var mac = mac || {};
mac.productView = mac.productView || {};
brand.product = brand.product || {};
mac.productView.addButton = brand.product.addButton;


//-----------------------------------------------------hacks
//cms-output html panel_navs sometimes have this ancient js
//eg. Artists panel or Customer Service panel
var el = function() { return legacy};
var legacy = { addBehavior:function() {}} 
var BehaviorRollover = false;  


//-----------------------------------------------------legacy    
generic.flash.Api = {};
generic.flash.Api.jsCall = function(method, args) { 
    if (generic.flash.ApiMethods[method]) {
        var resp = generic.flash.ApiMethods[method](args);
        return resp;
    } else {}  
};

generic.flash.ApiMethods = { 
    cuePoint: function() {  
        var args = arguments[0];
        generic.events.fire({event:"videoPlayer:cuePoint", msg:args}); 
    //  var inc = Object.toJson(args);
    //  return {"results": args} 
    },

    cuePointProduct: function(args) {
        var passthru = args[0].actions[0];
        this.cuePoint(passthru);
    },

    alterCart: function(args) {   
        console.log("generic.flash.apiMethods.alterCart "+Object.toJSON(args));
        site.checkout.alterCart.alter(args); 
    },

    pageData: function(nargs) { alert("generic.flash.ApiMethods.pageData");
        var args = nargs[0];
        var result;
        var pd = parent.page_data;
        if (!pd) return;
        if (args && args.query) {
            //console.log("retrieving page_data with query: " + args.query);
            var path = args.query.split(".");
            var length = path.length;
            var value = pd;
            for (var i = 0; i < length; i++) {
                var key = path.shift();
                value = value[key];
            }
            result = value;
        } else {
            result = pd;
        }
        return { "results": result } 
    }, 
    
    notifyEvent: function(nargs) {    
        site.view.colorNav.setWidth(nargs.event);
    },
    
    setElementSize: function(nargs) {  
        //brand.view.setElementSize(nargs[0]);
        site.view.productBrowser.resizeEmbedContainer(nargs[0]);
    }
}


// cms inline functions
//-----------------------------------------------------

function openFullWindow(url, name, w, h) {
    var w = window.open(url, name, "menubar=1, toolbar=1, resizable=1, scrollbars=1, width=" + w + ", height=" + h);
}
/**
 * brand.menu
 * Class: Creates a menu whose display is triggerd by mouse events over targetId
 * @memberOf brand
 */
brand.menu = Class.create( {

    // targetId: String
    // id of dom node that triggers display of menu
    targetId: "",
    
    // menuId: String
    // id of menu dom node
    menuId: "",
    
    timer: null,    
    timerDuration: 3,

    /**
     * brand.menu.initialize
     * Method for creating an instance of brand.menu
     * @param {String} args.menu  Menu node Id
     * @param {String} args.target  Id of node which triggers display of menu 
     * @example
        var footerMenu = new brand.menu({
            menu: "countries_container",
            target: "countries_hd"
        });
     * @methodOf brand.menu
     */
    initialize: function(/* Object */args) { 
        this.menuId = args.menu;
        var target = $(args.target);
        var menu = $(this.menuId);
        if (menu && target) {
        this.handlers = [
            target.observe('mouseover', this.show.bind(this)),
            target.observe('mouseout', this.startHide.bind(this)),
            menu.observe('mouseover', this.keepMenu.bind(this)),
            menu.observe('mouseout', this.startHide.bind(this))
        ];
    }
    },
    show: function(e) {
            //console.log("calling  show from "+e.target);
        this.keepMenu(e);
        $(this.menuId).removeClassName("hidden");
    },
    startHide: function(e) {
            //console.log("calling startHide from "+e.target); 
        this.timer = setTimeout(this.hide.bind(this), this.timerDuration);
        Event.stop(e);
        //dojo.stopEvent(e);
    },    
    keepMenu: function(e) {
        //console.log("calling keepMenu from "+e.target);
        clearTimeout(this.timer);
        Event.stop(e);
        //dojo.stopEvent(e);        
    },
    hide: function() { 
        $(this.menuId).addClassName("hidden");
    }
});


/**
 * brand.menuItem
 * line item in a menu
 * @memberOf brand.view
 */
brand.menuItem = Class.create({
    // domNode: Node Object
    // menu item dom node
    domNode: null,
    
    // rolloverClass: String
    // class to apply on mouseover
    rolloverClass: "",

    initialize: function(/* Object */args) { 
        this.domNode = args.domNode;
        this.rolloverClass = args.rolloverClass;
        if (this.domNode) {
            this.domNode.observe('mouseover', this._onMouseOver.bind(this));
            this.domNode.observe('mouseout',  this._onMouseOut.bind(this));
        }
    },
    _onMouseOver: function(e) {
        this.domNode.addClassName(this.rolloverClass);
    },
    _onMouseOut: function(e) {
        this.domNode.removeClassName(this.rolloverClass);
    }
});/**
 * brand.slideshow  
 * @memberOf brand
 */
brand.slideshow = Class.create({
    loop: false, 
    autoStart: false,  
    hasNav: false,  
    interval: 2, 
    looks: null,
    currentSlideIndex: -1,
    totalSlides: 0,
    slides: [],
    header: null,
    slide: null,
    link: null,
      
    initialize: function (args) { 
        if (!args.looks||!args.slide) return;
        //console.log("brand.slideshow");
     
        Object.extend(this, args || {});
        var self = this;   
     
        this.totalSlides = this.looks.length;
        var slide, look, header;
        for (var i=0;i<this.totalSlides;i++) {
            slide = {};
            slide.title = this.looks[i].title;
            //preload slides
            slide.slide = new Image();
            slide.slide.src = this.looks[i].image;
            slide.slide.link = this.looks[i].link;  
            //preload headers 
            slide.header = new Image();
            slide.header.src = this.looks[i].header;
            this.slides.push(slide); 
        };
     
        this.hasNav = (this.nav && this.nav.left && this.nav.right);
        if (this.hasNav) {
            if (this.looks.length==1) {
                if (this.nav && this.nav.left) this.nav.left.style.visibility = "hidden";
                if (this.nav && this.nav.right) this.nav.right.style.visibility = "hidden";  
            } else { 
                if (this.nav.left) this.nav.left.observe("click", function() {self.changeSlide(1)} );
                if (this.nav.right) this.nav.right.observe("click",function() { self.changeSlide(-1)} );
            } 
        } 
     
        this.changeSlide(1); 
    }, 
    changeSlide: function(n) {
        this.currentSlideIndex += n;
        if (this.currentSlideIndex > (this.totalSlides-1)) {
            if (this.loop) {
                this.currentSlideIndex = 0;
            } else {
               //hide next btn?
               return;  
            } 
        } else if (this.currentSlideIndex < 0) {
            if (this.loop) {
                this.currentSlideIndex = this.slides.length - 1;
            } else {
               //hide prev btn?
               return;  
            } 
        }
           
        this.setSlide();
    },
    setSlide: function() { 
        console.log("brand.slideshow.changeSlide: "+this.currentSlideIndex);
        this.slide.src = this.slides[this.currentSlideIndex].slide.src;
        if (this.header) this.header.style.backgroundImage = "url("+ this.slides[this.currentSlideIndex].header.src + ")"; 
        if (this.link) this.link.href = this.slides[this.currentSlideIndex].slide.link; 
    }
});/**
 * brand.progress
 * Class: Swap content with progress indicator while something loads
 * @memberOf brand
 */ 
brand.progress = Class.create(
{
    progressNode: null, // node to show on progress start
    containerNode: null, // node to hide on progress start 

    /**
     * brand.progress.initialize
     * Method for creating an instance of brand.progress
     * @returns An instance of brand.progress
     * @param {DOM Node} args.containerNode  Node object of content to hide during progress (Required: either args.containerNode or args.containerId)
     * @param {String} args.containerId  Id of content node to hide during progress
     * @param {DOM Node} args.progressNode  Node object containing progress indicator to show during progress (Required: either args.progressNode or args.progressId)
     * @param {String} args.progressId  Id of node containing progress indicator to show during progress
     * @param {Boolean} *Optional* args.matchDimensions Set to true if progress indicator content should automatically set its dimensions to match the content being hidden
     * @example
        var progress = new brand.progress({
            containerNode: swatchSet.domNode,
            progressNode: searchNodes.container.select('.progress')[0]
        }); 
     * @methodOf of brand.progress
     */    
    initialize: function(/* Object */args) {
        brand.updateProperties.apply(this, [args]);
        this.containerNode = (args.containerNode ? args.containerNode : $(args.containerId));
        this.progressNode = (args.progressNode ? args.progressNode : $(args.progressId));
        if (args.matchDimensions) {
            this._setDimensions();
        }
    },
    
    start: function() {
        if (!this.progressNode || !this.containerNode) { return; }
        this.containerNode.style.display = "none";
        this.progressNode.style.display = "block";
    },
    
    clear: function() {
        if (!this.progressNode || !this.containerNode) { return; }
        this.containerNode.style.display = "block";
        this.progressNode.style.display = "none";
    },

    // for errors and user messages
    showMessage: function(args) {
        if (!this.progressNode) { return; }
        this.progressNode.style.display = "none";
        var messageNode = args.messageNode;
        if (messageNode && args.message) {
            messageNode.update(args.message);
            messageNode.show();
        }
        if (!args.hideContainer && this.containerNode) {
            this.containerNode.style.display = "block";
        }
    },
    
    _setDimensions: function() { 
        this.progressNode.style.width = this.containerNode.getWidth() + "px";
        this.progressNode.style.height = this.containerNode.getHeight() + "px";    
    }
});

/**
 * brand.progressOverlay
 * Class: Display progress indicator as an overlay
 * @memberOf brand
 */
brand.progressOverlay = Class.create (brand.progress, 
{

    // summary:
    //      Progress overlay
    
    offset: {w: 0, h: 0},

    /**
     * brand.progressOverlay.initialize
     * Method for creating an instance of brand.progressOverlay
     * @returns An instance of brand.progressOverlay
     * @param {String} args.containerId  Id of content node to hide during progress
     * @param {String} args.progressId  Id of node containing progress indicator to show during progress
     * @param {Object} args.offset  width & height offset for dimensions of overlay
     * @example
        var progress = new generic.progressOverlay({
            containerId: "address-container",
            progressId: "progress-address-container",
            offset: {w: 10, h: 10}
        }); 
     * @methodOf of brand.progress
     */    
    initialize: function(/* Object */args) {
        this.containerNode = $(args.containerId);
        this.progressNode = $(args.progressId);
        if (args.offset) {
            this.offset = args.offset;
        }
        
        // set dimensions 
        this.progressNode.style.width = (this.containerNode.getWidth() + this.offset.w) + "px";
        this.progressNode.style.height = (this.containerNode.getHeight() + this.offset.h) + "px";
    },
    
    start: function() {
        if (!this.progressNode) { return; }
        this.progressNode.style.display = "block";
    },
    
    clear: function() {
        if (!this.progressNode) { return; }
        this.progressNode.style.display = "none";
    }
});/**
 * @namespace
 */
brand.forms = {
    init: function() {
        brand.forms.form.init();
        brand.forms.input.init();
    }
}

/**
 * @namespace
 */
brand.forms.form = {
    init: function() {
        // apply form submission behavior for specified forms
        $$("FORM.no-submit").each(function($_) { 
            $_.observe("submit", function(e) {
                Event.stop(e);
            }) 
        })
    } 
}

/**
 * @namespace
 */
brand.forms.input = {
    init: function() { 
        // apply inline label behavior for specified input fields
        $$("INPUT.inline-label-field").each(function($_) { 
            var fieldlabel = new brand.forms.inlineLabelField({ field: $_ });         
        });
    }
}


/**
 * This function tests a string against the expected pattern for an email address
 * @return {Boolean}
 * @param {String} Email address string to validate
 * @methodOf brand.forms
 */
brand.forms.isEmailAddress = function(s) {
    var filter  = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
    return (filter.test(s)); 
}

/* USED?
brand.forms.limitTextLength = function(args) {
    var field = args.field;
    var max_length = args.max_length;
    var display_element = args.display_element;
    
    if (field.value.length > max_length) {
        field.value = field.value.slice(0, max_length);
    }   
    if (!display_element) return;
    
    var remaining_chars = max_length - field.value.length; 
    if (remaining_chars < max_length) {
        // TODO: need RB/language solution here
        (remaining_chars != 1) ? plural = 's' : plural = '';
        display_element.innerHTML =  remaining_chars + ' character' + plural + ' left.';
    } else {
        display_element.innerHTML = '';
    }
}
*/


/**
 * brand.forms.inlineLabelField 
 * This class attaches behavior to a text or password input field where the label
 * is displayed inside the field and toggles on and off in response to focus/blur
 * @memberOf brand.forms
 */
brand.forms.inlineLabelField = Class.create({
    // summary:
    //      Handles toggling of label display for fields that show label
    //      in the field itself (via value attribute)

    // field: DOM node
    field: null,
    
    // fieldPswdDisplay: DOM node
    // field which displays password label
    // (workaround for IE's inability to toggle field's "type" attribute)
    fieldPswdDisplay: null,
    
    // label: String
    // field label
    label: "",
    
    // ftype: String
    // field type attribute (text, password)
    ftype: "text",
    
    // hasValue: Boolean
    // flag is true when user has entered a value in field
    hasValue: false,

    // _hasMaxlengthDisplay: Boolean
    // item's label char count exceeds field maxlength property
    _hasMaxlengthDisplay: false,
    
    // _maxlength: Object
    // if hasMaxlengthDisplay, _maxlength.data = maxlength property of input data, _maxlength.display = maxlength count for label
    _maxlength: null,

    /**
     * Creates an instance of brand.forms.inlineLabelField
     * @example
        var fieldlabel = new brand.forms.inlineLabelField({ field: fieldNode });

     * @param {Object} args.field  Input field node object   
     * @param {String} *Optional* args.label  Label string
     * @methodOf brand.forms
     */    
    initialize: function(/* Object */args) {
        this.field = args.field;
        
        // optional label arg
        if (args.label) {
            this.label = args.label;
        } else if (this.field.title && this.field.title.length > 0) {
            this.label = this.field.title;
        } else {
            this.label = "";
        }
        
        var displayField = this.field;
        this.ftype = this.field.getAttribute("type");
        
        if (this.ftype === "password") {
            this.fieldPswdDisplay = $(this.field.id+".label");
            if (!this.fieldPswdDisplay) { return };
            displayField = this.fieldPswdDisplay;
        }
        
        // check for maxlength restrictions
        var flength = this.field.maxLength;
        var labelLength = this.label.length;
        if ((flength > 0) && (flength < labelLength)) {
            this._maxlength = {};
            this._hasMaxlengthDisplay = true;
            this._maxlength.data = flength;
            this._maxlength.display = labelLength;
        }
        var self = this;
        this.handlers = [
            displayField.observe("focus", self._onFocus.bind(self)),
            this.field.observe("blur",self._onBlur.bind(self))
        ];
        
        this.setLabelState();
    },

    setLabelState: function() {
        this.checkHasValue();
        // only clear field if there is no user-set value in field
        if (!this.hasValue) {
            this.showLabel();
        }
    },

    checkHasValue: function() {
        var val = this.field.value;
        // field has user-set value
        if ((val.length > 0) && (val !== " ") && (val !== this.label)) {
            this.hasValue = true;
        } else {
            this.hasValue = false;
        }
    },
    
    beforeSubmit: function() {
        // if value is still populated by label rather than user input, clear it out before form submission
        if (!this.hasValue) {
            this.checkHasValue();
            if (!this.hasValue) {
                this.field.value = "";
            }
        }
    },
    
    _onFocus: function() {
        // display label if there is no user-set value in field
        if (!this.hasValue) {
            this.clearField();
        }
    },
    
    _onBlur: function() {
        this.setLabelState();
    },
    
    showLabel: function() {
        if (this.ftype === "password") {
            this.field.style.display = "none";
            this.fieldPswdDisplay.style.display = "block";
        } else {
            // set maxlength to accomodate label
            if (this._hasMaxlengthDisplay) {
                this.field.maxLength = this._maxlength.display;
            }
            // show label as value
            this.field.value = this.label;
        }
    },
    
    clearField: function() {
        if (this.ftype === "password") {
            this.field.style.display = "block";
            this.field.focus(); // set focus before currently focused field is hidden to avoid FF cursor disappearance
            this.fieldPswdDisplay.style.display = "none";
        } else {
            // clear field
            this.field.value = "";
            // set maxlength for expected data
            if (this._hasMaxlengthDisplay) {
                this.field.maxLength = this._maxlength.data;
            }
        }
    }
});

/**
 * brand.forms.contextualOptions 
 * This class attaches behavior to 2 select menus where the contents of the 2nd menu
 * are contextual based on the selected value of the 1st menu
 * @memberOf brand.forms
 */
brand.forms.contextualOptions = Class.create({ 
    srcSelect: null,
    targetSelect: null, 
    valueKey: null,
    labelKey: null, 
    targetSelectOptions: {}, 
    _selectsAreWidgets: false,

    /**
     * Creates an instance of brand.forms.contextualOptions
     * @example
        var contextualStoreSelect = new brand.forms.contextualOptions({
            srcSelectId: "select-menu-state",
            targetSelectId: "select-menu-stores",
            targetSelectOptions: {
               "AL" : [
                  {
                     "DOOR_NAME" : "The Summit",
                     "DOOR_ID" : "77790"
                  }
               ],
               "AK" : [
                  {
                     "DOOR_NAME" : "Lakeside",
                     "DOOR_ID" : "77771"
                  }
               ]
            }
            valueKey: "DOOR_ID",
            labelKey: "DOOR_NAME"
        });

     * @param {String} args.srcSelectId  ID of source select menu where initial selection will be made   
     * @param {String} args.targetSelectId  ID of target select menu whose options will be populated based on the selected value of args.srcSelectId 
     * @param {Object} args.targetSelectOptions  A JSON list of options for targetSelectId, keyed by the values listed the source select menu
     * @param {String} args.valueKey  Key ID for the option value in each item in the args.targetSelectOptions data
     * @param {String} args.labelKey  Key ID for the option label in each item in the args.targetSelectOptions data 
     * @memberOf brand.forms
     */     
    initialize: function(/* Object */args) {
        srcSelect = $(args.srcSelectId);
        targetSelect = $(args.targetSelectId);
        if (srcSelect && targetSelect) {
            srcSelect.observe("change", this.onChange.bind(this));
        } else {
            console.log("brand.form.contextualOptions: select element not found");
            return false;
        } 
        this.srcSelect = srcSelect;
        this.targetSelect = targetSelect;
        this.targetSelectOptions = args.targetSelectOptions; 
        if (args.valueKey) { this.valueKey = args.valueKey; }
        if (args.labelKey) { this.labelKey = args.labelKey; }
        if(this.srcSelect.selectedIndex) {
            this.onChange();
        }
    },
    
    onChange: function() {
        var srcval = this.srcSelect.value;
        var targetSelect = this.targetSelect;
        if (srcval === "" || srcval === null) { return false; } 
        this.removeOptions(targetSelect);
        this.addOptions(targetSelect);
    },
    
    getNewOptions: function() {
        // summary:
        // 
        // Defines default path to list of new options
        // Redefined to pass list from different array/object heirarchy
        var selected = this.srcSelect.value;
        return this.targetSelectOptions[selected];
    },

    addOptions: function(targetSelect) {
        var isWidget = this._selectsAreWidgets;
        var options = this.getNewOptions();
        
        // expect value, label pair stored in data, either as object (key id'ed) or in array (first 2 positions)        
        var labelKey = (this.labelKey ? this.labelKey : 0);
        var valueKey = (this.valueKey ? this.valueKey : 1);

        options.each(function(option, ix) {
            targetSelect.options[ix] = new Option(option[labelKey], option[valueKey]); 
        });

    },
    
    removeOptions: function(targetSelect) {
        var l = (targetSelect.options.length - 1);
        var i;
        for (i = l; i >= 0; i--) {
            if (targetSelect.options[i].value !== "") {
                targetSelect.options[i] = null;         
            }
        }    
    }
    
});
/**
 * @class brand.overlay 
 * Singleton class offers pop-over display functionality.
 * It supports one visible window at a time.
 *
 * Extension of brx.overlay. Additional features for Mac:
 * - showing pre-constructed popover (vs. creating a container for the content dynamically & inserting that into the DOM)
 * - args.foregroundNode
 * - args.removeOnHide
 * - args.displayDuration
 * - args.displayInline 
 */
brand.overlay = function() {
    var isVisible = false;
    var isCentered = true;
    var backgroundNode = null;
    var foregroundNode = null;
    var containerNode = null;
    var anchorNode = null;
    var offsetLeft = 0;
    var offsetTop = 0;
    var removeOnHide = true;
    var displayDuration = null;
    var durationTimer = null;
    var onClose = null;

    var scaleElementToPage = function(ele) {
        if (!Object.isElement(ele)) {
            return;
        }
        var docsize = $(document.body).getDimensions();
        ele.setStyle({
            height: docsize.height+'px',
            width: docsize.width+'px'
        });        
    };

    var centerElement = function(ele) {
        if (!Object.isElement(ele)) {
            return;
        }        
        var contentDimensions = ele.getDimensions();
        var windowScrollOffsets = document.viewport.getScrollOffsets();
        var windowDimensions = document.viewport.getDimensions();
        var yPosition;
        if (windowDimensions.height < contentDimensions.height) {
            yPosition = 0;
        } else {
            yPosition = (windowDimensions.height/2) - (contentDimensions.height/2) + (windowScrollOffsets.top);
        }
        ele.style.top = yPosition + "px";
        var xPosition = (windowDimensions.width/2) - (contentDimensions.width/2) + (windowScrollOffsets.left);
        ele.style.left = xPosition + "px";
    };

    var scrollHandler = function(evt) {
        if (isCentered) {
            centerElement(foregroundNode);            
        } else if (anchorNode) {
            foregroundNode.clonePosition(anchorNode, {setWidth: false, setHeight: false, offsetLeft: offsetLeft, offsetTop: offsetTop});
        }
    };
    var insertCloseLink = function(containerEle) {
        var closeLink = new Element("a", {"class": "close-link"});
        closeLink.insert(generic.rb.language.rb_close);
        var closeDiv = new Element("div", {"class": "close-container"});
        closeDiv.insert(closeLink);
        containerEle.insert({"top": closeDiv});
        closeLink.observe("click", function (closeClickEvt) {
            closeClickEvt.preventDefault();
            brand.overlay.hide();
        });
        return closeLink;
    };
    var hideSelects = function () {
        var selectNodes = $$("select");
        selectNodes.each( function(node) {
            node.addClassName("overlay-hidden");
        });
    };
    var restoreSelects = function() {
        var selectNodes = $$("select.overlay-hidden");
        selectNodes.each( function(node) {
            node.removeClassName("overlay-hidden");
        });
    };

    return {
        /**
         * This function displays a pop-over window. If a pop-over is already showing, the launch()
         * function will replace it with the new one.
         * @example 
         * // brand.overlay.launch({
         * //     content: htmlNode,
         * //     cssStyle: {
         * //         border: #000000 1px solid,
         * //         backgroundColor: #ffffff,
         * //         left: "100px",
         * //         top: "350px",
         * //         width: "250px",
         * //         height: "350px"
         * //     },
         * //     cssClass: 'product-overlay',
         * //     lockPosition: true,
         * //     includeBackground: false
         * // });
         * @param {Object} args.cssStyle Hash of CSS style definitions for the window. Uses JS notation (i.e., "marginLeft").
         * @param {Object} args.cssClass Name of CSS class that is optional to add to the overlay window.
         * @param {String|Node} args.content HTML, node, or text that will display in the window.
         * @param {Boolean} lockPosition if true, the overlay layer will remain anchored at the same x,y coords
         * when the user scrolls or resizes.
         * @param {Boolean} includeBackground if true, a background Node will cover the page directly behind the
         * overlay contents.
         * @param {Boolean} args.center, no value (undefined) centers the content, setting this to 'false' allows 
         * a top and left css value. 'undefined' was used to prevent having to add a 'true' value to every overlay
         * @param {Node} args.lockToDomNode node whose position will be inherited by the overlay element.
         * @param {Number} args.offsetTop vertical offset from lockToDomNode node. Only applies if lockToDomNode is included.
         * @param {Number} args.offsetLeft horizontal offset from lockToDomNode node. Only applies if lockToDomNode is included.
         * @param {String|Node} args.foregroundNode overlay container node, if it already exists in the DOM and doesn't need to be created 
         * @param {Boolean} args.displayInline keep overlay content in its specified place in the DOM. Default is false.
         * @param {Boolean} args.removeOnHide if true, remove the overlay foreground Node from DOM on hide. Default is true. 
         * @param {Number} args.displayDuration milliseconds to show ovleray, after which the overlay the overlay is closed/hidden
         */
        launch : function(args) {
            if (isVisible) { // check internal flag
                this.hide();
            }
            
            // MERGE NOTE:
            if (typeof args.removeOnHide !== "undefined") {
                removeOnHide = args.removeOnHide;
            }
            if (Object.isFunction(args.onClose)) {
                onClose = args.onClose;
            }
            displayDuration = args.displayDuration;
            
            foregroundNode = (args.foregroundNode ? args.foregroundNode : foregroundNode);
//console.log("foregroundNode.hasClassName(overlay-created) = "+foregroundNode.hasClassName("overlay-created")+" foregroundNode = "+foregroundNode.id);
            
            if (!removeOnHide && foregroundNode) {
                // if overlay is not set to be destroyed on hide, check or set class name that 
                // identifies whether overlay behavior has been hooked up
                if (foregroundNode.hasClassName("overlay-created")) {
                    this.show(args);
                } else {
                    this.create(args);
                    foregroundNode.addClassName("overlay-created");
                }
            } else {
                this.create(args);
            }
            
            hideSelects();
            isVisible = true; // set internal flag
            
            if (displayDuration) {
                var self = this;
                var reset = function() {
                    clearTimeout(durationTimer);
                    self.hide();
                }
                durationTimer = setTimeout(reset, displayDuration);
            } else {
                clearTimeout(durationTimer);
            }
        },
        create : function(args) {
            var displayInline = (args.displayInline ? args.displayInline : false);
            
            if (!containerNode && !displayInline) { // can't retrieve body variable in function declaration b/c it hasn't finished loading
                containerNode = $(document.body);
            }
            if (args.includeBackground) {
                if (!backgroundNode) { // create background node, if necessary
                    backgroundNode = new Element('div', {"class":"overlay-background", style:"display:none"});
                    containerNode.insert(backgroundNode);
                }
                backgroundNode.style.display = "block";
                scaleElementToPage(backgroundNode);
            }
            if (!foregroundNode) { // create foreground node, if necessary
                foregroundNode = new Element('div', {"class":"overlay-container", style:"display:none"});
                containerNode.insert(foregroundNode);
            }
            // insert elements into DOM if needed and adjust layout
            if (args.content) {
                foregroundNode.insert(args.content);
            }
            foregroundNode.style.display = "block";
            if (args.cssStyle) {
                foregroundNode.setStyle(args.cssStyle);
            }
            if (args.cssClass) {
                foregroundNode.addClassName(args.cssClass);
            }
            var closeLinks = foregroundNode.select(".close-link"); // look for a close link
            if (closeLinks.length < 1) {
                var newCloseLink = insertCloseLink(foregroundNode); // Insert link if one is not found
                closeLinks.push(newCloseLink);
            }
            var self = this;
            closeLinks.each( function(link) { // attach event handler to close links
                link.observe("click", function(clickEvt) {
                    self.hide();
                });
            });
            // attach events for scroll & resize
            if (!args.lockPosition && !displayInline) {
                Event.observe( window, 'resize', scrollHandler );
                Event.observe( window, 'scroll', scrollHandler );
            }
            if (args.center == undefined && !displayInline) {
                isCentered = true;
            } else {
                isCentered = !!args.center;
            }
            if (isCentered) {
                centerElement(foregroundNode);
            }
            if (Object.isElement(args.lockToDomNode)) {
                if (args.offsetLeft) {
                    offsetLeft = args.offsetLeft;
                } else {
                    offsetLeft = 0;
                }
                if (args.offsetTop) {
                    offsetTop = args.offsetTop;
                } else {
                    offsetTop = 0;
                }
                anchorNode = args.lockToDomNode;
                foregroundNode.clonePosition(anchorNode, {setWidth: false, setHeight: false, offsetLeft: offsetLeft, offsetTop: offsetTop});
            } else {
                anchorNode = null;
            }
        }, 
        /**
         * This function shows a pop-over that's already been created and 
         * just needs hide/show toggling
         */
        show: function() {
            if (Object.isElement(foregroundNode)) {
                foregroundNode.style.display = "block";
            }
        },
        /**
         * This function "closes" the pop-over window. If this.recreateOnShow 
         * is set to true: It completely removes the foreground node and its children
         * from the DOM.  Otherwise, foregroundNode is just hidden.
         * The background element is set to display: none.
         */
        hide: function() {
            isVisible = false; // set internal flag
            restoreSelects();
            // clean up DOM and layout
            if (Object.isElement(foregroundNode)) {
                // MERGE NOTE: want option of removing or just hiding overlay
                if (removeOnHide) {
                    // remove events for scroll & resize
                    Event.stopObserving( window, 'resize', scrollHandler );
                    Event.stopObserving( window, 'scroll', scrollHandler );
                    // remove overlay node
                    foregroundNode.remove();
                    foregroundNode = null;
                } else {
                    foregroundNode.style.display="none";
                }
            }
            if (Object.isElement(backgroundNode)) {
                backgroundNode.style.display="none";
            }
            if (onClose) onClose();
        },
        /**
         * This function scans the DOM for <a class="overlay-links"> elements. It takes the href attribute
         * from those links and preloads that URL via AJAX into a hidden DIV. This div is used
         * as the content for an overlay window when the link is clicked.
         */
        initLinks: function() {
            var linksToModify = $$("a.overlay-link");
            linksToModify.each( function(link) {
                if (link.hasClassName("overlay-ready")) {
                    return;
                }
                var styleObj = {};
                var linkClassNames = link.className;
                var cssClass = "";
                var widthRegexResults = linkClassNames.match(/overlay-width-(\d+)/);
                if (widthRegexResults) {
                    styleObj.width = widthRegexResults[1] + "px";
                }
                var heightRegexResults = linkClassNames.match(/overlay-height-(\d+)/);
                if (heightRegexResults) {
                    styleObj.height = heightRegexResults[1] + "px";
                }
                var cssClassRegexResults = linkClassNames.match(/overlay-addclass-([a-z\-\_]+)/);
                if (cssClassRegexResults) {
                    cssClass = cssClassRegexResults[1];
                }
                
                var containerDiv = new Element("div");
                containerDiv.style.display = "none";
                document.body.appendChild(containerDiv);
                containerDiv.addClassName("overlay-content-container");
                
                var req = new Ajax.Request(link.href, {
                    method:'get',
                    onSuccess: function(transport) {
                        var response = transport.responseText || "no response text";
                        containerDiv.update(response);
                    },
                    onFailure: function(){
                        var errMsg = "Error loading " + link.href
                        containerDiv.update(errMsg);
                    }
                });
                link.observe("click", function(clickEvt) {
                    clickEvt.preventDefault();
                    containerDiv.style.display = "block";
                    brand.overlay.launch({
                        content: containerDiv,
                        includeBackground: true,
                        cssStyle: styleObj,
                        cssClass: cssClass
                    });
                });
                link.addClassName("overlay-ready");
            }); // end linksToModify.each()
            
        },
        /**
         * This function is used to fetch specific rb keys needed for the
         * overlay. It is called on dom::loaded so each key is only 
         * fetched once.  
         * The specific language bundle is needed to be included in each page 
         * template header.  If not, the key name called will be returned.
        */
        getRBKeys: function() {
            generic.rb.language = generic.rb("language");
            generic.rb.language.rb_close = generic.rb.language.get('close');
        }
    };
}();
var brand = brand || {};

/**
 * brand.img 
 * This class creates and preloads an image object for rollovers, src switching, etc...
 * @memberOf brand
 */
brand.img = Class.create({
     /**
     * @return {Object} instance of brand.img with the following methods available: changeSrc
     * @example
            var imgNode = link.select('img')[0];
            var imgObj = new generic.img(imgNode, ["on", "off"]);
            imgObj.changeSrc("on");
     * @param {Node} imgNode   
     * @param {Array} states  array of states/versions of the image, by standardized suffix, such as "on"
     * @methodOf brand.img
     */ 
    initialize: function(imgNode, states) {
        if (!imgNode) {
            console.log("brand.img: imgNode UNDEFINED");
            return false;
        }
        //console.log("brand.img: called on imgNode "+imgNode.id);
        this.node = imgNode;
        this.preloaded = {};
        // get filename bits 
        var src = this.node.src;
        var bits = src.match(/^(.*)_(on|off|sel|dis)\.(.*?)$/);
        this.suffix = "";

        if ( bits == null ) {
            var bits = src.match(/^(.*)_(on|off|sel|dis)_(.*?)\.(.*?)$/); // look for suffix between on|off & extension, ex: base_on_fr_ca.gif
            if ( bits == null ) { return false; }
            this.suffix = "_" + bits[3];
        }

        this.srcBase = bits[1];
        this.srcExt = bits[bits.length-1];
        if (!this.srcExt) return false;
        this.states = states;

        // preload img
        // note: version arg must correspond to suffix of image, i.e.:
        // if default image name is image_name_off.jpg, arg value "on" looks for image_name_on.jpg
        if (!this.states) return;
        for (var i=0;i<this.states.length;i++) {
            var state = this.states[i];
            var separator = (state !== "" ? "_" : "");
            var preloadSrc = this.srcBase + separator + state + this.suffix + '.' + this.srcExt;
            var pl = new Image();               
            pl.src = preloadSrc; 
            this.preloaded[state] = pl;
        }
    },
    changeSrc: function(state) {
        var p = (this.preloaded ? this.preloaded[state] : null);
        if (!this.srcBase) return;
        // use preloaded src ref
        if (p) {
            this.node.src = p.src;
            
        // else load img via path
        } else {
            this.node.src = this.srcBase + '_' + state + this.suffix + '.' + this.srcExt;
        }
    }
});

/**
 * brand.rollover 
 * Applies image switching behavior to an image node on mouseover/out of the specified
 * image node or (if specified) to a different trigger node
 * @memberOf brand
 */
brand.rollover = Class.create({
     /**
     * Creates instances of brand.rollover
     * @example
            var rollover = new generic.rollover($("my-image"), $("my-button")); 
     * @param {Node} imgNode   image whose src will be changed on mouseover/out
     * @param {Node} triggerNode  *Optional* target node of mouse event.  Default is imgNode.
     */
    initialize: function(/* Object */imgNode,/* Object */triggerNode){
        var trigger = (triggerNode == null ? imgNode : trigger);
        this.img = new brand.img(imgNode, ["off","on"]);
        trigger.observe("mouseover", this.onMouseOver.bind(this));
        trigger.observe("mouseout", this.onMouseOut.bind(this));
    },
    onMouseOver: function(e) {
        if (!e.target.hasClassName("disable_rollover")) {
            this.img.changeSrc("on");
        }
    },
    onMouseOut: function(e) {
        if (!e.target.hasClassName("disable_rollover")) {
            this.img.changeSrc("off");
        }
    }
});

/**
 * Preloads and stores a batch of images for use with image src switching 
 * @return {Object} Hash of preloaded images
 * @param {DOM Node} args.node  Image node
 * @param {String} args.imagePath  Image file path to preload and store 
 * @param {Object} args.imageStore  Previously saved image hash to append to
 * @param {String} args.imageId  ID key for image in hash 
 * @memberOf brand
 */
brand.loadImage = function(args) {
    var imgStore = args.imageStore;
    var imgId = args.imgId;
    var imgPath = args.imagePath;
    var node = args.node;
    if (!node || (typeof(imgId) === "undefined") || !imgStore || !imgPath) {
        return imgStore;
    }
    imgId.toString();
    var preloadedImg = imgStore[imgId];
    
    // if first request for image, store it
    if (!preloadedImg) {        
        imgStore[imgId] = new Image();
        imgStore[imgId].src = imgPath;
        node.src = imgStore[imgId].src;
    
    // if 2nd+ request get preloaded image
    } else {
        node.src = preloadedImg.src;
    }
    return imgStore;
}/**
 * brand.pager
 * @memberOf brand
 */
brand.pager = Class.create( {

	list: [],
	current: 1,
	per_page: 10,

	initializer: function(args) {
		if (!args.list) { return; }
		Object.extend(this, [args]);
		this._init();
	},

	_init: function() {
		this.pages = Math.ceil(this.list.length / this.per_page);
		this.first = 1;
		this.last = this.pages;

		var self = this;
		this.from = (function() {
			return (self.current - 1) * self.per_page;
		})();
		this.to = (function() {
			var tmp = self.from + self.per_page;
			return (tmp <= self.list.length) ? tmp : self.list.length;
		})();

		this.prev = (function() {
			return self.hasPrev() ? self.current - 1 : self.current;
		})();
		this.next = (function() {
			return self.hasNext() ? self.current + 1 : self.current;
		})();
	},
	next: function() {
	    self = this; 
	    return self.hasNext() ? self.current + 1 : self.current;
	},
	previous: function() {
	     self = this;
	     return self.hasPrev() ? self.current - 1 : self.current;
	},
	setPage: function(p) {
		if (p && typeof(p) == "string") {
			p = parseInt(p);
		}
		if (p && p !== this.current) {
			this.current = p;
			this._init();
			return this.getPage();
		}
	},

	getPage: function() {
		return this.list.slice(this.from, this.to);
	},

	hasNext: function() {
		return (this.current < this.pages);
	},

	hasPrev: function() {
		return (this.current > 1);
	}

});
brand.livechat = {
    livechatPopArgs : {
        url: "/includes/live_chat_popup.tmpl",
        resizable: "no",
        scrollbars: "no",
        width: 611,
        height: 450,
        menubar: "no",
        location: "no"
    },
    init: function() {
        // multiple instances of live chat popup buttons
        brand.livechat.initLiveChatPopup("#gnav_popup_live_chat"); 
    },
    initLiveChatPopup: function(btnId) {
        var params = this.livechatPopArgs;
        params["activator"] = btnId;
        var lc = new generic.popup(params);
        
        var uri = window.location;
        var URL_DOMAIN = uri.protocol + "//" + uri.host;

        // Load any livechat links with the button above
    	$$(btnId).each(function(el){
            var liveChatButtonImageObj = new Image();
            var timestamp = Number(new Date());
            liveChatButtonImageObj.src = uri.protocol+'//service.liveperson.net/hc/24631554/?cmd=repstate&site=24631554&channel=web&&ver=1&imageUrl='+URL_DOMAIN+'/images/liveperson&skill=MAC%20France&timestamp='+timestamp;
            liveChatButtonImageObj.name="hcIcon";
            liveChatButtonImageObj.style.border="0";
            el.update(liveChatButtonImageObj);
    	});
    },
    openLiveChatPopup : function() { // for cms inline reference
        var lc = window.open(this.livechatPopArgs.url, 'live_chat',"resizable=0, scrollbars=0, width=" + this.livechatPopArgs.width +", height=" + this.livechatPopArgs.height); 
        generic.events.fire("livepopup:click", {type:"cms"} );

    }  
} 
 
brand.search = Class.create({

    // config: Object
    // section data from site.globalnav.config[section]
    config: null,
    
    // isDefaultPanel: Boolean
    // true if search panel is the open/page panel
    isDefaultPanel: false,
    
    // resultsNode: DOM node
    // node container for results html
    resultsNode: null,

    // progressNode: DOM node
    // progress node to show while search is in progress    
    progressNode: null,

    // _hasContent: Boolean
    // false before initial search, true once search has run & there are child widgets to destroy
    _hasContent: false,
    
    // children: Array
    // widget ids for each search result     
    _children: [""],
        
    _defaultState: null,    
    _isSearching: false,
           
    initialize: function(args) {
        //console.log("brand.search.init");
       
        if (!args.config || !args.config.search) return;
        
        for (var arg in args) {
            this[arg] = args[arg];
        } 
        
        this.formField = $(this.config.search.formFieldId);
        var formSubmit = $(this.config.search.formSubmitId);
        if (!this.formField || !formSubmit) return;
        
        this._defaultState = page_data.panel_nav["default"];
      
        var psubnav = $("psubnav_" + this.config.id).widget;
        this.resultsNode = psubnav.resultsNode;
        this.contentResultsNode = psubnav.contentResultsNode;
        this.progressNode = (this.progressNode ? this.progressNode : psubnav.progressNode);
        
        this.formField.observe("keypress", this._onkeypress.bind(this));
        formSubmit.observe("click", this._onclick.bind(this));

        if (this.isDefaultPanel && this._defaultState.query) {
            this.submit({ query: this._defaultState.query });
        }
    },

    _onkeypress: function(event) { 
        if (event.keyCode != Event.KEY_RETURN) {
            return false;
        }                
        this.submit(event);      
    },
    
    _onclick: function(e) {
        this.submit(e);
    },
    
    submit: function(args) {
        if (this._isSearching) { return; }
        var query = (args && args.query) ? args.query : this.formField.value;
       
        // on error: show popover
        if ( !query || query === this._defaultState.searchDefault ) {
            var popid = this.config.search.errorPopup;
            if (popid) {
                brand.overlay.launch({
                    foregroundNode: $(popid),
                    displayInline: true,
                    removeOnHide: false,
                    displayDuration: 5000
                }); 
            }
            
            return false;
        }
         
        // if no error: execute search
        this._execute({ query: query });
    },

    _execute: function(args) {
        this._isSearching = true;
        var psubnav = $("psubnav_" + this.config.id).widget;        
        var defaultId = null;
        //console.log("brand.search._execute: "+this.parentId + " / " + $(this.parentId));
        this.reset();
        
        if (!$(this.parentId)) return; //TESTING
        
        // show progress, hide content
        this._showProgress(true);
        psubnav.resultsMessageNode.innerHTML = "";
        if (psubnav.contentResultsContainer) {
            psubnav.contentResultsContainer.addClassName("hidden");
        }
        // if triggered by form submit vs. panel click
        if (this.panelManagerId) {
            var gset = $(this.parentId).widget; 
            var pnavManager = gset.getChild(this.panelManagerId);
        }
  
        // check if default state        
        if (this.isDefaultPanel) {            
            defaultId = (this._defaultState.item.item ? this._defaultState.item.item.id : null); 
            if (this.panelManagerId) {
                this._showDefault();
            } 
        // search: open sliding panel
        } else if (pnavManager) {
            pnavManager.onTrigger(true);      
            window.parent.scrollTo(0,0); //scroll to top of screen
        }
                
        // ajax request
        var self = this;
        var onLoadArgs = {
            query: args.query,
            psubnav: psubnav,
            defaultId: defaultId
        }
        
        // wait for the panel to open before starting load (avoids animation stutters)
        // Panel.durationOpen = 400
        // TODO: create callback in Panel.js to know when panel has opened instead of using timeout
        var doRequest = function() {
            var c = self.config.content;
            var url = c.url + "?" + c.param + "=" + encodeURIComponent(args.query); 
            //var url = "/js/brand/globalnav/data/" + self.config.id + "_"+ args.query + ".txt";  
            new Ajax.Request(
                url, {
                method: 'get',
                onSuccess:  function(transport) {  
                     self.onLoad(transport.responseText, onLoadArgs)
                }.bind(this) 
             }); 
        }              
        setTimeout(doRequest, 400);
             
    },
    
    onLoad: function(data, args) {
        //console.log("brand.search.onLoad");

        this._isSearching = false;
        var self = this;
        var psubnav = args.psubnav; 
        
        if (typeof data == "string") data = data.evalJSON(true); 
        var detailItems = data.products;
        
        var hasItemInDefaultCategory = false; 
        var contentResults = data.content_results || [];
        var hasResults = (detailItems.length > 0 || contentResults.length > 0); 
        
        // reset previously rendered content/state
        if (this._hasContent) {
            this.reset();
        }
        // headers for results and no results
        //var hdNode = $(this.id + "_hd");
        var hdNode = $("psubnav_search_hd");
        if (hdNode && data.header_img) {
            hdNode.setAttribute("src", data.header_img);
            hdNode.setAttribute("alt", data.header_alt);
        }
        
        // show results count/message
        if (data.results_message) {
            psubnav.resultsMessageNode.innerHTML = data.results_message;
        } else if (!hasResults && data.no_results_message) {
            psubnav.resultsMessageNode.innerHTML = data.no_results_message;
        }
        
        if (!hasResults) {
            this._showProgress(false);
            return false;
        }
        
        // SS note: move this to where "/globalnav/event/getcontent/onload" used to fire?
        generic.events.fire({event:"search:results", msg:{ pageid: "Search", keywords: args.query, count: data.count, cat: "2200"}});
              
        // check if results contain default item 
        if (this.isDefaultPanel) {
           if (this._defaultState.query === args.query) {
                hasItemInDefaultCategory = detailItems.any(function(item){ 
                    return (item.id === args.defaultId);
                });
            }
  
            if (hasItemInDefaultCategory) {
                this.resultsNode.addClassName("panelnav_category_default");
            } else {
                 this.resultsNode.removeClassName("panelnav_category_default");
            }
        }
        
        // output content results, if they exist
        if (data.content_results) { 
             data.content_results.each( function(item, idx) {
                var detailArgs = {
                    idx: idx,
                    item: item
                };
                var detail = self._initContentSearchDetail(detailArgs);
                self._children.push(id);
                psubnav.addSubItem(detail.domNode, self.resultsNode);
                
                if (detail.startup) detail.startup(); 
            });
        }       

        detailItems.each( function(item, idx) {
            //console.log("brand.search.onLoad detailItems "+idx + "/" +  psubnav.id);
            // set if default
            var isdefault = false;
            if (!item.id) item.id = item.sku.path
            
            if (hasItemInDefaultCategory && (item.id === args.defaultId)) {
                isdefault = true;
            }
            var id = "psubitem_" + self.config.id + "_" + item.id;

            var detailArgs = {
                id: id,
                item: item,
                isdefault: isdefault,
                isInDefaultCategory: hasItemInDefaultCategory, 
                parentId: psubnav.id, 
                domParent: $(psubnav.id).widget.resultsNode
            }
           // discontinued search results
            if (self.config.id === "discontinued") {
                var detail = self._initDiscontinuedDetail(detailArgs);
                
            // main search results
            } else {
                var detail = self._initSearchDetail(detailArgs);         
            }

            self._children.push(id);
            
            // place detail node in DOM & start it up:
            if (detail.startup) detail.startup();  
        });
        
        // broadcast that search content has loaded/rendered:
        var pid;
        if (this.config.id === "search") {
            pid = "pnav_search_panel";
        }
        //JSTest dojo.publish("/globalnav/event/getcontent/onload", [{ type: "panel", id: pid, parentId: psubnav.parentId }]);
            
        this._hasContent = true; // nodes exist which will have to be destroyed before next search
        
        // hide progress, show content
        this._showProgress(false);   
    },

    _initSearchDetail: function(args) {
        //console.log("brand.search._initSearchDetail");
        var item = args.item;      
                
        var detailArgs = Object.extend(args, { 
            url: item.uri, 
            product: item,
            hdPath: item.header,
            //hdHeight: item.header_image_height,
            displayName: item.name,
            description: item.short_desc,
            thumbPath: item.thumb,
            hex: (item.shade_result ? item.sku.color[0] : ""),
            shadename: (item.shade_result ? item.sku.shade_name : "") 
        });
        
        if ( item.is_giftcard == 1 )  {
            detailArgs.templatePath = "jsTemplates.globalnav.SearchGiftcard";
            var detail = new brand.globalnav.SearchProductDetail(detailArgs);
        } else if ( item.is_custom_palette == 1 )  {
            detailArgs.templatePath = "jsTemplates.globalnav.SearchCustomPalette";
            var detail = new brand.globalnav.SearchProductDetail(detailArgs);        
        } else if ( item.shade_result || !item.shaded ) {
            var detail = new brand.globalnav.SearchQuickBuyDetail(detailArgs);
        } else {
            try {
                var detail = new brand.globalnav.SearchProductDetail(detailArgs);    
            } catch(err) {
                console.log("brand.search._initSearchDetail error " + err.description);
            }
        }           
            
        return detail;
    },

    _initContentSearchDetail: function(args) {
        // TODO: mac jp version has not been tested.  may be missing necessary args
        var id = "psubitem_" + this.config.id + "_content_" + args.idx;
        var item = args.item;        
        var detail = new brand.globalnav.Detail({
            id: id,
            templatePath: "jsTemplates.globalnav.SearchContentDetail",
            url: item.url,
            description: item.short_desc,
            isdefault: false,
            isInDefaultCategory: false
        });    

        return detail;
    },
    
    _initDiscontinuedDetail: function(args) {
        var item = args.item;   
        var detail = new brand.globalnav.DiscontinuedProductDetail({
            id: args.id,
            url: item.uri,
            displayName: item.name,
            hdPath: item.header,
            thumbPath: item.thumbnail,
            description: item.description,
            sku: item.sku,
            shadedResult: (item.shade_result == 1 ? true : false),            
            isdefault: args.isdefault,
            isInDefaultCategory: args.isInDefaultCategory
        });  
        
        return detail;
    },

    _showProgress: function(/* Boolean */state) {
        // toggle display of progress & content
        var s = this.progressNode.style;
        if (state) {
            s.display = "block";
            this.resultsNode.style.display = "none"; 
        } else {
            s.display = "none";
            this.resultsNode.style.display = "block";
        }
    },
    
    reset: function() {
        //console.log("brand.search.reset");
        this.resultsNode.innerHTML = "";
     
        this._showProgress(true);
        this._hasContent = false;
    },
    
    _showDefault: function() {
        var parent = $("globalnav_container").widget;
        // tell parent set to bring focus back to default
        if (parent.onChildClick && (parent.activeItemId !== "")) {
            parent.onChildClick(this.config.id, true);
        }
    }
    
});brand.customerService = {}

brand.customerService.faq = {
    activeId: null,
    
    init: function() {
        // get drop down                
        var menu = $("faq-questions");
        var self = this;
        self.showAnswer(menu.value);
        menu.onchange = function() {
            self.showAnswer(menu.value);
        }
    },
    showAnswer: function(questionId) {
        var activeNode = $(this.activeId);
        if (activeNode) {
            activeNode.style.display = "none";
        }
        var answerNode = $(questionId);
        if (answerNode) {
            answerNode.style.display = "block";
        }
        this.activeId = questionId;
    }
}
brand.product = brand.product || {};

// check for duos, trios quads, etc...
brand.product.getShadeType = function(args) {
    var type = "solo";
    var ismulti = false;
    var sku = args.product.skus[0];
    
    // check properties in page data
    var pdtype = args.product.product_multicolor_type;
    if (!pdtype && sku.sku_multicolor_type) {
        pdtype = sku.sku_multicolor_type;
    }

    // finally, check for multiple hexes
    if (pdtype && (sku.color.length >= args.multicolor_min)) {
        type = pdtype;
        ismulti = true;
    }
      
    return { type: type, ismulti: ismulti };
}


// inventory status display
// SPP: for shaded prods with cards: handles inventory message in card & display 
//      toggling of bag button in card  & spp page
// SPP: for prods w/out cards: handles inventory message in page & display
//      toggling of bag button in page
// MPP cartAdd popover: handles inventory message in popover & display toggling 
//      of bag button in popover
// arguments: 
//      shoppable: page_data sku "shoppable"
//      message: page_data sku "inventory_status_message"
//      messageNode: node in which to insert message string
//      buttonNode: specific bag button node to suppress (optional)  By default
//          brand.product.inventoryStatus hides all nodes w/ class "inventory-status-conditional"
//      containerNode: node to add "visible-inventory-status" css class to (optional)
brand.product.inventoryStatus = function(args) {
    var msgNode = args.messageNode;
    var statusKey = args.statusKey;
    var suppressAddToBag = (statusKey == 1 || statusKey === "1" || args.shoppable === "1" || args.shoppable == 1 ? false : true);
    var btnNodes = [];
    // use specified buttonNode
    var btn = $(args.buttonNode);
    if (btn) {
        btnNodes[0] = btn;
    } else {    
        // get all bag buttons with class="inventory-status-conditional"
        var btnNodes = $$(".inventory-status-conditional");
    }
    
    if (suppressAddToBag) {
        btnNodes.each(function(btn) { 
            btn.hide();
        });
    } else {
        btnNodes.each(function(btn) {
            btn.show();
        });
    }

    var containerNode = args.containerNode; // set css class on container for easier manipulation of inventory display states in css instead of in js
    // get & display message if any
    var message = args.message;
    if ((!message || message.length < 2) && statusKey) {
        if (site.product.rb["inventory_status_message_" + statusKey]) message = site.product.rb["inventory_status_message_" + statusKey];
    }

    if (message && message.length > 1) {
        if (msgNode) msgNode.update(message);
        if (containerNode) {
            containerNode.addClassName("visible-inventory-status"); // for spp
        } else if (msgNode) {
            msgNode.style.display = "block";
        }
    } else {
        if (msgNode) msgNode.update("");
        if (containerNode) {
            containerNode.removeClassName("visible-inventory-status");
        } else if (msgNode) {
            msgNode.style.display = "none";
        }
    }

}


// Shoppables on content pages (i.e. news, faves)
brand.product.shoppableContent = {
    cartConfirmProps: {},
    
    init: function(args) {
        //console.log("brand.product.shoppableContent.init");
        var self = this;
        
        var is_shaded = false; 
        var containerId = (args.containerId ? args.containerId : "main_content");
        this.cartConfirmProps = (args.cartConfirmProps ? args.cartConfirmProps : this.cartConfirmProps);
        
        if (args.positionPopup) {
            this.positionPopup = args.positionPopup;
        }
         
        args.products.each(function( product ) {
            var skus = product.skus;
            skus.each(function( sku ) {
    
                var hasLinkNodes = false; 
          
                // check for link nodes as single instances (element  id) or multiple instances (css class)
                var linkNodes = [];
                linkNodes = $(containerId).select("."+sku.path);
                if (linkNodes[0]) {
                    hasLinkNodes = true;
                } else {
                    linkNodes[0] = $(sku.path);
                    if (linkNodes[0]) {
                        hasLinkNodes = true;
                    }
                }
                
                //console.log("brand.product.shoppableContent.init hasLinkNodes = "+hasLinkNodes+" sku = ",sku);
                            
                if (hasLinkNodes) {
                    self.initPopover(product, sku, linkNodes);
                }
            });            
        });        
    },
    
    initPopover: function(product, sku, linkNodes) {
        //console.log("brand.product.shoppableContent.initPopover");
        
        var self = this;
        
        if ((sku.shade_name.length > 0) && (sku.color[0].length > 2)) {
            is_shaded = true;
        } else {
            is_shaded = false;
        }
        
        this.cartConfirmProps.type = "order";
        
        var cartConfirmPlaceholder = $("cart_confirm_placeholder-" + sku.path);
        var cartAddPlaceholder = $("cart_add_placeholder-" + sku.path);
        if (!cartConfirmPlaceholder || !cartAddPlaceholder) return; // placeholder popover nodes not found

        var cartConfirmMsg = new brand.product.cartConfirm({
            id: "cart_confirm-" + sku.path,
            is_shaded: false,
            prodName: product.name,
            nodeToReplace: cartConfirmPlaceholder
        });
        
        cartConfirmMsg.setDisplayProperties(this.cartConfirmProps);
        
        // init quick buy popover
        var cartAddArgs = {
            id: "cart_add-"+sku.path,
            is_shaded: is_shaded,
            prodName: product.name,
            skuFieldId: "prod_sku_cart_add-" + sku.path,
            price: product.price,
            cartConfirm: cartConfirmMsg,
            sku: sku,
            nodeToReplace: cartAddPlaceholder
        }
        
        if (is_shaded) {
            cartAddArgs.smooshId = "smoosh_img_cart_add-" + sku.path // pass smoosh img id to set template to use swatch smoosh instead of product thumb
        } else {
            cartAddArgs.smooshPath = product.thumb;
        }
        
        var cartAddMsg = new brand.product.cartAdd(cartAddArgs);
        
        if ($("prod_sku_cart_add-" + sku.path)) $("prod_sku_cart_add-" + sku.path).value = sku.path; 
        
        var show = function(evt) {
            self.show(evt, cartAddMsg, cartConfirmMsg);
        }
        
        // connect each link node to popover action
        linkNodes.each(function( node ) {
            node.observe("click", show );  
        });
    },
    
    show: function(evt, cartAddMsg, cartConfirmMsg) {
        if (this.positionPopup) { 
            this.positionPopup(evt, cartAddMsg, cartConfirmMsg);
        }
        cartAddMsg.show();            
    }
}


/* brand.product.addButton
 * Adapted from brx.product.addButton
 */
brand.product.addButton = function (args) { 
    var addButtonNode = args.addButtonNode;
    if (!addButtonNode) return;
    var progressNode = args.progressNode;
    var skuData = args.skuData;
    var skus = [];
    var enabled = true;
    var callback = args.callback || function() {};
    var onFailure = args.onFailure || callback;
    var skuField;

    // sku values can be set before click for non-shaded or "add to all buttons"
    if (args.skuData && args.skuData.SKU_BASE_ID) {
        skus = [args.skuData.SKU_BASE_ID];
    } else if (args.skuBaseId) {
        skus = [args.skuBaseId];
    } else if (args.skus) {
        skus = args.skus;
    } else if (args.skuField) {
        skuField = args.skuField;
    } else if (addButtonNode.nodeName === "INPUT") {
        skuField = args.addButtonNode;
    } else {
        return null;
    }
    var itemType = args.itemType || 'cart';
    var action = args.action || 'add';
    
    if (progressNode) {
        var progress = new brand.progress({
            containerNode: addButtonNode,
            progressNode: progressNode
        });
    }
        
    addButtonNode.observe("click", function(clickEvt) {
        clickEvt.preventDefault();
        if (!enabled) return;
        var item_params = {};
        
        // set sku value for buttons that don't know sku value until selection (ex: shaded)
        if (skuField) {
            var sku = skuField.value;
            if (sku.indexOf("SKU") > -1) { // get sku base id from full path
                sku = sku.split("SKU")[1];
            }
            skus = [sku];
        }
        if (!skus || !skus[0]) return;

        if (progress) {
            progress.start();
        }
        enabled = false;
        
        // Send individual parameters based on type
        item_params = {
            skus: skus,
            itemType: itemType
        }
        // MERGE NOTE: if correct syntax is mac JP, then adding multiple skus looks like it shouldn't use increment or action
        if (itemType !== 'favorites' && skus.length == 1) {
            item_params.INCREMENT = 1;
        }
        if (skus.length == 1) {
            item_params.action = action;
        }
        // MERGE NOTE: not sure if removal calls should be using qty = 0, or "delete" as action.  If the former, then when creating instance of addButton, pass qty instead of action
        //if (action === "delete") {
            //item_params.QTY = 0;
            //item_params.INCREMENT = -1;
        //}
        
        generic.checkout.cart.updateCart({
            params: item_params,
            onSuccess: function(r) {
                var resultData = r.getData();
                callback(r);
                if (progress) progress.clear();
                enabled = true;
                addButtonNode.fire("cartButton:success", resultData);
            },
            onFailure: function(r) {
                onFailure(r);
                if (progress) progress.clear(); 
                enabled = true;
            }
        });
    });
    return {
        getItemType: function() {
            return itemType;
        },
        setSkuBaseId: function (newSkuBaseId) {
            skuBaseId = newSkuBaseId;
        },
        setSkuData: function (data) {
            skuData = data;
            this.setSkuBaseId(data.SKU_BASE_ID);
        },
        setShoppable: function() {
            if (!skuData) {
                return null;
            }
            if (brx.productData.isShoppable(skuData)) {
                addButtonNode.removeClassName("hidden");                
            } else {
                addButtonNode.addClassName("hidden");                               
            }
        },
        setEnabled: function(state) {
            enabled = state;
        }
    };
};


/**
 * brand.product.waitlist
 * basic handling for waitlist popovers/content where present
 * @memberOf brand.product
 */
brand.product.waitlist = {
    
    // note: currently very simple, but placeholder for waitlist instances that have more fancy behavior as some have had in the past
    init: function() {
        //console.log("brand.product.waitlist.init");
        var waitListOverlays = $$("div.overlay-container-waitlist");
        var count = waitListOverlays.length;
        var self = this;
        for (i=0; i<count; i++) {
            self.initOverlay({ overlayContainerNode: waitListOverlays[i] });
        }
    },
    
    initOverlay: function(args) {
        if (!args) return;
        var overlayContainerNode = args.overlayContainerNode;
        if (!overlayContainerNode) return;
        var closeLink = overlayContainerNode.select(".close-link")[0]; // look for a close link
        if (!closeLink) return;
        // attach event handler to close links
        closeLink.observe("click", function(clickEvt) {
            clickEvt.preventDefault();
            overlayContainerNode.hide();
        });
        
        /*
        // close overlay after N milliseconds
        var reset = function() {
            clearTimeout(durationTimer);
            overlayContainerNode.hide();
        }
        var durationTimer = setTimeout(reset, 5000);
        */
    }
};
// SPP functions
brand.spp = brand.spp || {};

// for non-shaded prods & shaded prods that are supposed to display like non-shaded
brand.spp.setSkuSelection = function(args) {
    var sku = args.sku;
    // inventory status
    site.product.inventoryStatus({
        shoppable: sku.shoppable,
        message: sku.inventory_status_message,
        messageNode: site.spp.inventoryStatusNode,
        containerNode: $("prod-details")
    });
   
    site.spp.skuField.value = sku.path;
    site.spp.skuFavField.value = sku.path;
    args.cartConfirmMsg.sku = sku;
    
    // shaded: product photo per-sku
    if (args.isShaded) {
        hasPhotosBySku = site.spp.photoBySku.init();
        if (hasPhotosBySku) {
            site.spp.photoBySku.onSkuSelect(sku, "0");
        }
    }
};

    
// swatch display for thumbnail grid & name list
brand.spp.initSwatches = function(args) {
    if (!args.node) { return; }
    var swatchSet;
    var swatchArgs = args.swatchArgs;
    swatchArgs.parentId = args.node;
    var prodBrowserSkuField = args.pageArgs.prodBrowserSkuField;
    var product = swatchArgs.product;
    var skuField = swatchArgs.skuField;
    var favField = args.pageArgs.favField;
    var filters = {};
    var cartConfirm = args.pageArgs.cartConfirm;
    swatchArgs.multiShaded = args.pageArgs.multiShaded; // multi-shaded prods that display separate swatch images for each color value
    var hasPhotosBySku = false;

    // swatch cards for all shades
    var swatchCard = new brand.product.swatchCard({
        id: "swatchcards",
        nodeToReplace: $("swatchcard-placeholder"),
        skus: product.skus,
        price: product.price,
        cartConfirm: cartConfirm,
        multiShaded: swatchArgs.multiShaded,
        closeOnClickOutside: { 
            enable: true,
            nodesToExclude: [ $(swatchArgs.domParent) ] // don't allow clicks in thumbs area to bubble up to body (at which point it would cancel out the opening of the swatch card)
        }
    });

    // single sku multi-shaded prod
    if (swatchArgs.multiShaded && swatchArgs.multiShaded.isSingleSkued) {
        //console.log("brand.spp.swatchSet.init: multiShaded single sku");
        // prods that display multi-colored sets of swatches & only display 1 sku per page        
        var sku = product.skus[0];
        
        // inventory status
        site.product.inventoryStatus({
            shoppable: sku.shoppable,
            message: sku.inventory_status_message,
            messageNode: site.spp.inventoryStatusNode,
            containerNode: $("prod-details")
        });
        
        // on swatch select
        swatchArgs.onSelectCallback = function(selectedChild, args) {
            //console.log("swatchArgs.onSelectCallBack selectedChild = ",selectedChild);
                        
            // open card on selection if not triggered by load              
            if (swatchCard && (!args || args.event !== "load")) {
                swatchCard.onSwatchSelect({ sku: sku, swatchNode: selectedChild.domNode, swatchIdx: selectedChild.idx });
            }

        }
        swatchSet = new brand.product.thumbSwatchSet(swatchArgs, args.node);
        skuField.value = sku.path;
        favField.value = skuField.value;
        cartConfirm.sku = sku;
        
    // multi-skued prods    
    } else {
        //console.log("brand.spp.swatchSet.init: multiple skus");
        var multiShaded = (swatchArgs.multiShaded.isMultiSkued ? true : false);

        // set up main image swapping by sku
        hasPhotosBySku = brand.spp.photoBySku.init();
        
        // on swatch select
        swatchArgs.onSelectCallback = function(selectedChild, args) {
            //console.log("swatchArgs.onSelectCallBack selectedChild = ",selectedChild);
            //console.log("swatchArgs.onSelectCallBack args = ",args);
            
            var sku, skuIdx, swatchNode;
            if (selectedChild && selectedChild.sku) {
                sku = selectedChild.sku;
                skuIdx = selectedChild.idx;
            } else if (args) {
                sku = args.sku;
                skuIdx = args.skuIdx;
            }
            var swatchIdx = (multiShaded && multiShaded.isMultiSkued && args.swatchIdx ? args.swatchIdx : selectedChild.idx);
            
            // if selection wasn't triggered from name menu or initial load open the card
            // Note: swatch card internally handles the setting of its cart button 
            // open card on selection if not triggered by load or name menu
            if (!args || args.event !== "name-menu") {              
                if (swatchCard && (!args || args.event !== "load")) { // can't do on load, since menu might not have been created
                    if (!multiShaded) brand.spp.swatchSorters.updateNameMenu(skuIdx); // multi-shaded, multi-skued prods don't update menu on swatch click, since visibility of current set of swatches is only set via name menu
                    if (!multiShaded || (multiShaded && args.event !== "subset-select")) swatchCard.onSwatchSelect({ sku: sku, swatchNode: selectedChild.domNode, swatchIdx: swatchIdx });
                 }
            }

            // handle page/main inventory status node display
            // (swatch card status node handled by swatchCard class)
            site.product.inventoryStatus({
                shoppable: sku.shoppable,
                message: sku.inventory_status_message,
                messageNode: site.spp.inventoryStatusNode
            });
                
            // tell page favorites button, cart confirm & color play button which sku has been selected
            // Note: swatchSet class internally handles setting skuField
            // multi-shaded multi-skued prods would only set this on selection of whole sku set, rather than on selection of individual swatches
            if (!multiShaded || (multiShaded && args.event === "subset-select")) {
                if (favField) favField.value = sku.path;
                cartConfirm.sku = sku;
                if (prodBrowserSkuField) prodBrowserSkuField.value = selectedChild.hex;
                // swap main Image
                if (hasPhotosBySku) brand.spp.photoBySku.onSkuSelect(sku, skuIdx);
            }
        }
        
        //console.log("brand.product.initSwatchSet calling SC: " ,swatchArgs); 
        swatchSet = new brand.product.thumbSwatchSet(swatchArgs, args.node);
        
        // init swatch sorting/filtering logic
        brand.spp.swatchSorters.init({
            swatchSet: swatchSet,
            product: product,
            shadedType: swatchArgs.shadedType, // shaded type set for all prods
            multiShaded: swatchArgs.multiShaded, // multiShaded params only set for true multiShade display types (single-skued and multi-skued with multiple swatches per sku)
            hasTabs: args.pageArgs.hasTabs
        });
        
        // now that both swatches & menus have initialized, update name menu
        // to reflected the sku selection made at initial load time
        var selectedIndex;
        if (swatchSet.selectedChildWidget && typeof swatchSet.selectedChildWidget.idx !== "undefined") {
            selectedIndex = swatchSet.selectedChildWidget.idx;
        } else if (multiShaded && swatchSet.selectedChildNode && typeof swatchSet.selectedChildNode.idx !== "undefined") {
            selectedIndex = swatchSet.selectedChildNode.idx;
        }
        if (selectedIndex) brand.spp.swatchSorters.updateNameMenu(selectedIndex);
    }
   
    // make swatch card container available on multi-shaded spp
    if (swatchArgs.multiShaded) $("swatch-pop-container").removeClassName("hidden");

    return swatchSet;
};

// sorting/filtering drop-downs
brand.spp.swatchSorters = {

    _currentFilter: "all", // "all" = showing all shades
    _savedFilterStates: {},
    _settingFilter: false,
    
    finishesKey: "finishes",
    nameKey: "name",
    searchKey: "search",
    
    init: function(args) {
        //console.log("brand.spp.swatchSorters");  
       
        var swatchSet = args.swatchSet;
        var product = args.product;
        this.filters = {}; // filter hash to pass to swatchSet
        var finishFilters = {};
        
        this.multiShaded = args.multiShaded;
        
        // dom nodes
        var nameMenuNode = $("menu-swatches-byname");
        this.nameMenuContainer = $("menu-swatches-byname-container");
        this.finishMenuContainer = $("menu-filter-byfinish");
        var searchNodes = {
            container: $("product-search"),
            fieldId: "product-search-field",
            button: $("product-search-button"),
            message: $("product-search-message")
        }
        
        this.nameMenu = this._initNameMenu(swatchSet, product, nameMenuNode);
        this.searchForm = this._initSearch(swatchSet, product, searchNodes);

        // get/set filters
        if (args.shadedType !== "duo") {
            // by finish
            finishFilters = this._getFinishFilters(product);
            if (finishFilters) {
                this.filters = finishFilters.filters; // save each finish is a sibling of other filters (i.e. "matte" & "limited")
                this.finishMenu = this._initFinishFiltering(swatchSet, finishFilters);
            }
        }

        // get best sellers data or pro products data but not both
        var showProProducts = (page_data.pro_member == 1 ? true : false);
        var otherFilters = ["limited_life"]; // page data keys
        if (showProProducts) {
            otherFilters.push("pro_products"); // page data key
        } else if (product.has_top_sellers == 1) {
            // top sellers: reformat data into common filters format
            var topsellerFilter = this._getFilterBySkuId({ product: product, skuArray: product["top_seller_skus"] });
            // page data may say "has_top_sellers == 1", but top_seller skus listed may not match any that are displayable on page
            if (topsellerFilter && topsellerFilter.length > 0) this.filters["top_sellers"] = topsellerFilter;
        }
        
        // get/set one-dimensional filter data (e.g. not top sellers or finishes)
        this._getFilters(product, otherFilters);

        // set filter property for swatchSet instance
        swatchSet.filters = this.filters;
                    
        // tab changes trigger changes in swatch display/sorting
        if (args.hasTabs && brand.spp.tabContainer) {
            this._enableTabs({ swatchSet: swatchSet });
        }
    },

    _initNameMenu: function(swatchSet, product, menu) { 
        // set up callback for sorting menu
        if (!menu) { return false; }
        
        var self = this;        
        var names = this._getNames(product);
        var optionCount = menu.options.length; // if there's a default label we want to add to it rather than starting options at 0
        this._savedFilterStates[this.nameKey] = "all";
        var count = 0;
        
        // if only 1 name, hide menu and container
        if (names.length <= 1) {
            if (this.nameMenuContainer) {
                this.nameMenuContainer.hide();
            } else {
                menu.hide();
            }
            return false;
        }
        
        names.each(function(opt, idx) {
            count = (optionCount + idx);
            menu.options[count] = new Option(opt.label, opt.num); 
        });

        var onChange = function() {  
            //console.log("brand.spp.swatchSorters._initNameMenu onChange ");
            var value = this.getValue();
            //console.log("value = "+value);
            if (typeof(value) === "undefined") { return; }
            if (self.multiShaded && self.multiShaded.isMultiSkued) {
                var sku = product.skus[value];
                swatchSet.setSwatchSubset({ sku: sku, event: "name-menu", skuIdx: value });
            } else {
                var swatch = $("swatch_" + product.skus[value].sku_id).widget;
                if (!swatch || swatchSet.selectedChildWidget == swatch) { return; }
                swatchSet.setSwatch(swatch, { event: "name-menu" });
                // if swatch seleted is not in current filter, set filter back to "all"
                var filter = self._currentFilter;
                if (filter !== "all") {
                    var hasSwatchInFilter = self.filters[filter].any(function(swatchIdx) {
                        return (swatchIdx === value);
                    });

                    // reset filter display state to "all"
                    if (!hasSwatchInFilter) {
                        self._setFilter(swatchSet, { value: "all", type: self.nameKey, event: "name-menu" });
                    }
                }
            }
        }
        menu.observe("change", onChange);
        
        return menu;
    },
        
    _initFinishFiltering: function(swatchSet, finishFilters) {
        //console.log("_initFinishFiltering: "+menu);
        // populate filtering menu 
        if (!this.finishMenuContainer || !finishFilters) return false;
        
        var self = this;
        var labels = finishFilters.labels;
        var filters = finishFilters.filters;
        var count = 0;
        var type = this.finishesKey;
        var menu = this.finishMenuContainer.select("select")[0];
        var optionCount = menu.length;
        if (!menu) return false;

        for (var filter in filters) {
            menu.options[optionCount] = new Option(labels[filter], filter);
            optionCount++;
            count++;
        }
        
        // hide menu if no prod has no finishes
        if (count == 0) {
            this.finishMenuContainer.hide();
            return false;
        }
        
        // set up callback
        var onChange = function() {
            var value = this.getValue();
            //console.log("brand.spp.swatchSorters._initFinishFiltering onChange value = "+value+" self._currentFilter = "+self._currentFilter+" self._settingFilter = "+self._settingFilter);
            if (value !== self._currentFilter && !self._settingFilter) {
                self._setFilter(swatchSet, { value: value, type: type });
                swatchSet.domNode.show();
                self._savedFilterStates[type] = value; // save state
            }
        }
        menu.observe("change", onChange);
        
        return menu;
    },

    _initSearch: function(swatchSet, product, searchNodes) {
        if (!searchNodes || !searchNodes.container) return false;
        var self = this;
        var msgNode = searchNodes.message;
        msgNode.hide();
        var query = "";
        var isSearching = false;
        var progress = new brand.progress({
            containerNode: swatchSet.domNode,
            progressNode: searchNodes.container.select('.progress')[0]
        }); 
        
        var showSkus = function(response) {
            var data = response.getValue();

// LOCAL TESTING ONLY
/*
try {
var tmp = response.responseText.evalJSON(true);
var data = tmp[0].result.value;
}
catch(err) { console.log("eval: "+err) }
*/         
            
            if (!data || !data.skus || data.skus.length < 1) {
                // empty results
                onNoResult(response);
                
            } else {
                var resultCount = data.skus.length;
                isSearching = false;
                // set filter of sku indices
                self.filters[self.searchKey] = self._getFilterBySkuId({ product: product, skuArray: data.skus });
                self._setFilter(swatchSet, { value: self.searchKey, type: self.searchKey, nocache: true });
                progress.clear();
                var resultMessage = site.product.rb.search_results.replace("QUERY", query);
                resultMessage = resultMessage.replace("RESULTS", resultCount);
                msgNode.update(resultMessage);
                msgNode.show();
            } 
        }

        var onNoResult = function(response) {
            console.log('RPC call successful, but no results returned. response = ',response);
            //var resultMessage = site.product.rb.search_no_results.replace("QUERY", query); // replace "QUERY" with search input
            var resultMessage = site.product.rb.search_no_results;
            onMessage({ message: resultMessage });
        }
        
        var onFailure = function(response) {
            var message = response.getMessages();
            console.log('Product Search JSON failed to load. response = ',message);
            onMessage({ message: "Error: "+message[0].text });
        }

        var onMessage = function(args) {
            isSearching = false;
            progress.showMessage({ messageNode: msgNode, message: args.message, hideContainer: true });
            self.filters[self.searchKey] = null;
        }
        
        var getSkus = function() {
            if (isSearching) return;
            isSearching = true;
            progress.start();
            query = $F(searchNodes.fieldId);
            // LOCAL TESTING ONLY
/*
            new Ajax.Request("/js/brand/product/data/pot_" + query + ".txt", {
                method: "get",
                onSuccess:  showSkus,
                onFailure: onFailure
            });
            return;
*/
            generic.jsonrpc.fetch({
                method:    'search',
                params:    [{ 'KEYWORDS': query, 'PRODUCT_ID': product.product_id, 'sku_fields': ['SKU_ID'] }],
                onSuccess: showSkus,
                onFailure: onFailure
            });
        }
        searchNodes.button.observe("click", getSkus);
        
        return searchNodes.container;
    },
    
    updateNameMenu: function(idx) {
        //console.log("brand.product.updateNameMenu "+typeof idx+" this.nameMenu = "+this.nameMenu); 
        if (this.nameMenu) {
            this.nameMenu.value = idx;
        }
    },
    
    _getNames: function(product) {
        var names = [];
        var sortedSkus = product.sorters.names;
        var count = sortedSkus.length;
        for (var i=0; i<count; i++) {
            var name = {};
            var num = sortedSkus[i];
            name.num = num.toString();
            name.label = product.skus[num].shade_name;
            names[i] = name;
        }
        return names;            
    },

    _setFilter: function(swatchSet, args) {
        var prev = this._currentFilter;
        var value = args.value;
        this._currentFilter = value;
        if ((prev === this._currentFilter && !args.nocache) || this._settingFilter) { return; }
        this._settingFilter = true;
        
        // send sort/filter value to swatchSet instance
        //console.log("swatchSet filters = ",swatchSet.filters);
        swatchSet.processData("filter", value, args.nocache);
        //console.log("swatchSet.processData: _setFilter - to: "+value+" type = "+args.type);

        // name menu selection is forcing a re-display of all swatches so show its tab
        if (args.event === "name-menu" && value === "all") {
            brand.spp.tabContainer.setTabRemotely({ sortingKey: args.type });
        }
        
        this._settingFilter = false;
    },
    
    _getFinishFilters: function(product) {
        var filters = {};
        var labels = {};
        var skusByFinish = product.filters[this.finishesKey];
        var sortedFinishNames = product.filters.sorted_finishes;
        hasFinishes = false;
        if (!sortedFinishNames) return false;
        var finishCount = sortedFinishNames.length;  

        for (i=0; i<finishCount; i++) {
            var finishName = sortedFinishNames[i]; // finishId = id value for menu
            // convert indices to strings
            var skus = skusByFinish[finishName].toString();
            skus = skus.split(",");
            hasFinishes = true;
            var finishId = finishName.replace(/ /, ""); // finishId = id value for menu (lowercased, white-space, removeed, etc...)
            finishId = finishId.toLowerCase();
            var label = finishName; // label = display label for menu (entities stripped)
            // replace html entities
            var entitiesToUnicode = site.spp.entitiesToUnicode;
            if (entitiesToUnicode && (label.indexOf("&") > -1)) {
                for (var entity in entitiesToUnicode) {
                    var entityString = new RegExp(entity, 'g');
                    label = label.replace(entityString, entitiesToUnicode[entity]);
                }
            }
            
            labels[finishId] = label;
            filters[finishId] = skus;  
        } 

        if (!hasFinishes) return false;
        return { filters: filters, labels: labels };
    },

    // get skus when data is a set of sku_ids instead of a list of sku indices
    _getFilterBySkuId: function(args) {
        var filter = [];
        if (!args.product || !args.skuArray) return false;
        var product = args.product;
        var skuArray = args.skuArray;
        this.skuIndices = (this.skuIndices ? this.skuIndices : this._getSkuIndices(product.skus));
        if (!skuArray || skuArray.length == 0) return false;
        var resultIdx = i = 0;
        var count = skuArray.length;
        
        // loop thru skus to find index of top seller sku ids
        for (i=0; i<count; i++) {
            var skuId = skuArray[i];
            // json-rpc results return obj as: {SKU_ID="SKU1688"}
            if (typeof skuId === "object") {
                if (skuId.SKU_ID) skuId = skuId.SKU_ID;
            }
            var skuIdx = this.skuIndices[skuId];
            if (!skuIdx) continue;
            filter[resultIdx] = skuIdx;
            resultIdx++;
        }        
        return filter;             
    },
    
    _getFilters: function(product, dataKeys) {
        if (!product.filters) return;
        var self = this;
        
        dataKeys.each(function(key){
            var filterIndices = product.filters[key];
            var count = filterIndices.length;
            if (filterIndices && count > 0) {
                // convert int indices into strings
                var filter = [];
                for (var i=0; i<count; i++) { 
                    var num = filterIndices[i];
                    filter.push(num.toString());
                }
                
                // add filter to master filter hash
                if (filter.length > 0) {
                    self.filters[key] = filter;
                }
            }
        });             
    },
    
    // store position of skus in sku array, keyed by sku ids
    _getSkuIndices: function(skus) {
        if (!skus) return false;
        var indices = {};
        var skuCount = skus.length;
        // loop thru skus to find index       
        for (i=0; i<skuCount; i++) {
            var skuId = skus[i].sku_id;
            indices[skuId] = i.toString(); // convert int indices into strings
        }
        return indices;    
    },

    _enableTabs: function(args) {
        if (!brand.spp.tabContainer.tabs) return;
        var self = this;
        generic.events.observe("tabs:beforeshow", function(eventArgs) {
            self.onTabChange(args.swatchSet, eventArgs);
        });

        // display tabs with sorting/filtering data
        var sortingKeys  = brand.spp.tabContainer.sortingKeys;
        for (var key in sortingKeys) {
            var tabId = sortingKeys[key];
            var tab = $(tabId);
            if (!tab) continue;
            var parent = tab.parentNode;
            if (!parent) continue;
            
            if (key === this.finishesKey && this.finishMenu) {
                // finishes tab
                parent.removeClassName("hidden");
            } else if (this.filters[key]) {
                // other tabs
                parent.removeClassName("hidden");
            }
        }
        
        // reset scrolling state now that content has loaded
        brand.spp.tabContainer.tabs.resetScrolling();
    },
    
    onTabChange: function(swatchSet, args) {
        //console.log("onTabChange args = ",args);
        //console.log("onTabChange, this._savedFilterStates = ",this._savedFilterStates);
        var selectedLink = args.selectedLink;
        if (!selectedLink) return;
        var selectedSort = (args.selectedSort ? args.selectedSort : this.nameKey);
        var activeSort = (args.activeSort ? args.activeSort : this.nameKey);
        var finishMenu = this.finishMenu;
        var searchForm = this.searchForm;
        var sortValue = (this._savedFilterStates[selectedSort] ? this._savedFilterStates[selectedSort] : selectedSort);

        // toggle display of swatches, filter menu & search form when tabbing away from their tab
        if (finishMenu && activeSort === this.finishesKey) {
            this.finishMenuContainer.hide();
            swatchSet.domNode.show();
        } else if (searchForm && activeSort === this.searchKey) {
            searchForm.hide();
            swatchSet.domNode.show();           
        }
            
        // case: filter by finish        
        if (finishMenu && selectedSort === this.finishesKey) {
            // toggle display of filter menu
            this.finishMenuContainer.style.display = "block";
            if (finishMenu.selectedIndex == 0) {
                swatchSet.domNode.hide();
            } else {
                // show the state of the finish filter with last known selected finish
                this._setFilter(swatchSet, { value: sortValue, type: selectedSort });
            }

        // case: search results
        } else if (searchForm && selectedSort === this.searchKey) {
            // toggle display of search form
            searchForm.style.display = "block";
            // show swatches if there's a previous result set to show
            var searchResults = this.filters[this.searchKey];
            if (searchResults && searchResults.length > 0) {
                this._setFilter(swatchSet, { value: sortValue, type: selectedSort, nocache: true });
            } else {
                swatchSet.domNode.hide();
            }
          
        // case: other tabs
        } else {
            
            // set display of swatches based on selected tab
            this._setFilter(swatchSet, { value: sortValue, type: selectedSort });
        }
    
    }
};


// spp tab handler
brand.spp.tabContainer = {

    // tab names as paired with swatch sorting types 
    sortingKeys: {
        "name": "tab-shades",
        "pro_products": "tab-pro",
        "limited_life": "tab-limited",
        "top_sellers": "tab-bestsellers",
        "finishes": "tab-byfinish",
        "search" : "tab-search"
    },
    
    init: function() {
        var self = this;
        this.tabs = new brand.tabs("prod-tabs-nav",
            {
                tabContainer: $("prod-tabs"),
                activeClassName: "tab-active",
                beforeShow: function(link) {
                    self.beforeShow(link);
                },              
                useImageHeaders: true,
                scrollbar: {
                    contentNode: $('scroll-content-container'),
                    containerNode: $("scrollbar-container"),
                    handleId: "scrollbar-handle",
                    trackId: "scrollbar-track",
                    enabledClass: "scrollbar-enabled"  
                }
            }
        );
        
        // save sorting types by tab
        var tabKeys = {};
        for (var key in this.sortingKeys) {
            var value = this.sortingKeys[key];
            tabKeys[value] = key;
        }
        this.tabKeys = tabKeys;
    },
    
    beforeShow: function(link) {
        var activeLink = (this.tabs ? this.tabs.activeLink : null);
        if (!link || !activeLink) return;
        if (link.id === this.tabs.activeLink.id) return;
        
        // broadcast tab change for swatch sorting based on tab
        generic.events.fire({event: "tabs:beforeshow", msg: { selectedLink: link, selectedSort: this.tabKeys[link.id], activeSort: this.tabKeys[activeLink.id] } });    
    },
    
    setTabRemotely: function(args) {
        var sortingKey = args.sortingKey;
        var linkId = args.linkId;
        if (!sortingKey && !linkId) return;
        var link = (sortingKey ? $(this.sortingKeys[sortingKey]) : $(linkId));
        if (!link || (link.id === this.tabs.activeLink.id)) return;
        this.tabs.setActiveTab(link);
    }
    
};

// "more" description
brand.spp.initDescription = function(args) {
    var linkNode = args.linkNode;
    var descriptionNode = args.descriptionNode;
    if (!linkNode || !descriptionNode || !args.hasDescription) return;
    
    linkNode.removeClassName("hidden");
    
    // event handlers
    linkNode.observe("mouseover", function() {
        descriptionNode.style.visibility = "visible";
        brand.spp.toggleFormSelectForIE6(false);
    });
    descriptionNode.observe("mouseout", function() {
        descriptionNode.style.visibility = "hidden";
        brand.spp.toggleFormSelectForIE6(true);
    });
};


// color play link
brand.spp.initColorPlayButton = function(field) {        
    var url = "/flash/color_play/index.tmpl";
    var param = "?colorplaysample=";
    if (!field) return false;
        
    var _onClick = function(e) {
        var hex;
        hex = field.value.split("#")[1];
        if(hex) {
            hex = "0x" + hex;
            location.href = url + param + hex;
        } else {
            location.href = url;
        }
    }
    field.observe("click", _onClick);
}

// main image rollover
brand.spp.photoRollover = {
    hasRollover: false,
    overImg: null,
    outImg: null,
    init: function(outImg, overImg) {
        var node = $("prod-image");
        if (!node) {
            return;
        }
        // if first set of image paths is passed then set to true
        // otherwise will start off as false, but can be set to true if brand.spp.photoBySku
        // passes valid per-sku image paths on sku selection
        if (outImg && overImg) {
            this.hasRollover = true;
            this.outImg = outImg;
            //this.overImg = overImg;
            this.overImg = this.defaultOverImg = overImg; // Note: assumes that brand wants a single sku's over/_alt image to act as the default overImg for any skus that don't have one
        } else {
            return; // SS NOTE: for now ignore if no default product.image_medium_rollover since many skus with sku.image_medium_rollover seem to be 404
        }

        var self = this;
        
        var over = function(e) {
            if (!self.hasRollover) return;
            e.target.src = self.overImg;
        }
        
        var out = function(e) {
            if (!self.hasRollover) return;
            e.target.src = self.outImg;
        }
        node.observe("mouseover", over);
        node.observe("mouseout", out);
    }
};

// main image swap on shade selection
brand.spp.photoBySku = {
    init: function() {
        this.node = $("prod-image");
        if (!this.node) return false;
        this.preloaded = {};
        return true;
    },
    
    onSkuSelect: function(sku, swatchIdx) {
        //console.log("brand.spp.photoBySku = "+swatchIdx);
        var img = sku.sku_image; // sku-specific image that shows on swatch click
        if (!img) return;
        var overImg = (sku.image_medium_rollover ? sku.image_medium_rollover : null); // sku-specific "on" state of outImg
        
        // preloads image (of not already pre-loaeded) & sets new src
        var preloaded = brand.loadImage({
            node: this.node,
            imagePath: img,
            imageStore: this.preloaded,
            imgId: swatchIdx
        });
        this.preloaded = preloaded;
  
        // if photo rollover, set on & off state to currently selected "special" image
        var pr = brand.spp.photoRollover;
        if (pr.hasRollover) {
            pr.outImg = img;
            if (overImg) {
                pr.overImg = overImg;
            } else {
                pr.overImg = pr.defaultOverImg;
            }
        }
        /* SS note: use this if sku.image_medium_rollover is meant to be reliable
        if (overImg) {
            pr.hasRollover = true;
            pr.outImg = img;
            pr.overImg = overImg;
        } else {
            pr.hasRollover = false;
        }
        */
    } 
};

// products with multiple skus based on size (non-shaded)
brand.spp.initSized = function(args) {
    var menu = args.menuNode;
    var skus = args.skus;
    var skuCount = skus.length;
    if (skuCount < 1) return;
    var firstSkuId = 0;

    // populate menu
    if (!args.menuNode) {
        site.spp.setSkuSelection({ sku: skus[0], cartConfirmMsg: args.cartConfirmMsg });
        return; // if returning here, then this method was somehow applied to a single-skued product in which case we still want to do sku selection
    }

    var skuIndices = {};
    var skuIdArray = new Array();
    for (var i=0; i<skuCount; i++) {
        var skuId = skus[i].sku_base_id;
        skuIdArray.push(skuId);
        skuIndices[skuId] = i; // keep track of sku index in data
    }

    // Sort by sku_base_id to get Small, Medium, Large order
    skuIdArray = skuIdArray.sort();
    for (var i=0; i<skuCount; i++) {
    	var skuId = skuIdArray[i];
    	var skuIndex = skuIndices[skuId]
        var label = skus[skuIndex].product_size;
        menu.options[i] = new Option(label, skuIndex);
        if (i == 0) {
            firstSkuIdx = skuIndex;
        }
    }

    // pre-select first sku
    site.spp.setSkuSelection({ sku: skus[firstSkuIdx], cartConfirmMsg: args.cartConfirmMsg });
    
    // menu event callback
    var onChange = function() {  
        //console.log("brand.spp sized onChange ");
        var value = this.getValue();
        //console.log("brand.spp sized onChange value = "+value);
        if (typeof(value) === "undefined") { return; }
        // set selected sku
        site.spp.setSkuSelection({ sku: skus[value], cartConfirmMsg: args.cartConfirmMsg });
    }
    menu.observe("change", onChange);
};

// IE6 workaround: toggle display of form select(s) when popovers are showing
brand.spp.toggleFormSelectForIE6 = function(show) {
    if (generic.env.isIE6) {
        var container = $("prod-container");
        if (container) {
            if (show) {
                container.removeClassName("popup-visible");
            } else {
                container.addClassName("popup-visible");
            }
        }
    }
};
brand.product.swatchSet = Class.create(Widget, {    
    sortType: "",    
    isContainer: true,
    isActive: false,
    _started: false,
    _loaded: null,
    _dataMethod: "sort",
    _dataParam: "",
    _activeSet: "",
    _initialized: 0,
    
    // _swatchSelected: Boolean
    // flag true when a sku has been selected
    _swatchSelected: false,

    // initDefault: Boolean
    // start with first swatch selected by default
    initDefault: false,
    
    shadedType: "solo",
    isDiscontinued: false,
    selectedSku: "",

    // productType: String (optional)
    // additional identifier to distinguish btwn products appearing more than once on page.
    // ex: cross-sell vs. main product
    productType: "",
    
    skus: "",
    smooshImg: "",
    video_prod: false,

    sorters: {},
    filters: {},
    
    initialize: function($super, arguments, node) {
        this.setProperties(arguments);
        this.skus = this.product.skus;
        if (this.product.video_prod) this.video_prod = true;
        // clear out & reference as subset instead
        // some extra attributes added here for external use (e.g. shop together)
        this.productData = {
            id: this.product.product_id,
            sorters: this.product.sorters,
            name: this.product.name,
            image_small: this.product.image_small,
            uri: this.product.uri
        }
        this.product = {};
        this.id = node.id; 
        var self = this;
        this._loaded = {};
 
        this.sortType = (this.sortType !== "" ? this.sortType : "color");
        this._dataParam = this.sortType;

        if (this.multiShaded && this.multiShaded.isSingleSkued) {
            //console.log("brand.product.swatches.initialize: this.multiShaded.isSingleSkued");
            var sku = this.skus[0];
        
            // check for multiple values (arrays) in single sku
            if (Object.isArray(sku.smoosh) && sku.smoosh.length > 0) {
                this.smooshes = {};        
            }
            // save shade info (alternate to sku.smoosh, etc...)
            var idbase = this.productData.id + "_" + this.productType;
            //console.log("this.multiShaded.isSingleSkued " + this.productData.name + " idbase = "+idbase);

            for (var i = 0; i < sku.color.length; i++) {
                var id = "swatch_" + idbase + i.toString();
                if (this.smooshes) {
                    this.smooshes[id] = sku.smoosh[i];        
                }          
            }            
        } else {        
            // template has a sort order
            // multiShaded.isMultiSkued we're not using sorters
            if (this.productData.sorters && !this.multiShaded) {
                this.sorters = this.productData.sorters;
            } else {
                // creates default sort order from order of skus in page_data
                var set = []; 
                this.skus.each(function(sku, idx) { 
                    set[idx] = [idx];
                });
                this.sorters[this._dataParam] = set;
            }
        }
         
        $super();
    },
    
    setSwatch: function(child, args) { 
        //console.log("setSwatch: "+child.id+" / child = ",child);
        //console.log("setSwatch: child idx ",child.idx);       
        if (!child || this.selectedChildWidget == child) { 
            this.onSelectCallback(child, args);
            return;
        }
        var sku = child.sku;
        
        // toggle state of previous & current selected swatches
        if (this.selectedChildWidget) {
            this.selectedChildWidget.selected = false;
            this.toggleSelectedState(this.selectedChildWidget, false);
        }
        child.selected = true;
        this.toggleSelectedState(child, true);
        this.selectedChildWidget = child;
        if ((!this.multiShaded || (this.multiShaded && !this.multiShaded.isSingleSkued)) && this.skuField) {
            this.skuField.value = child.sku.path;
        }
        this.onSelectCallback(child, args);
        this._swatchSelected = true;
            
        // on user click event
        if (args && args.event !== "load") {
           // fire coremetrics event
           generic.events.fire({event:"swatch:click", msg:{ name: child.name, product: child.parent.product}});
        }
    },
    
    onSelectCallback: function(child, args) {
        // for external callback use
    },

    processData: function(method, param, nocache) {
        //console.log("brand.product.swatchSet.processData");
        if (!nocache && this.started && (this._dataMethod === method && this._dataParam === param)) { return; }
        this._dataMethod = method;
        this._dataParam = param;

        var set;
        if (method === "sort") {
            if (param === "status") {
                var sorters = this.sorters[param];
                for (var i = 1; i <= 4; i++) {
                    var sub = sorters[i.toString()];
                    if (!set) {
                        set = sub;
                    } else {
                        set.concat(sub);
                    }
                }
            } else {
                set = this.sorters[param];
            }
        } else {
            if (param === "all") {
                this._dataMethod = "sort";
                set = this.sorters[this.sortType];
            } else {
                var filters = this.filters;
                if (filters[param]) {
                    set = filters[param];
                }
            }
        }
        
        this._activeSet = set;
        this._updateSet();
    },

    _updateSet: function() { 
        var children = this.children;
        if (children && this._initialized > 0) {
            var childrenCount = children.length;
            for (var i = 0; i < childrenCount; ++i) {
                var child = children[i];
                $(child.id).style.display = "none";
            }
        }
        
        this._initialized++;
        var ids = this._activeSet;
        //console.log("_updateSet "+ids.length+" this._started = "+this._started);
        if (!ids) return;
        
        var idsCount = ids.length;
        for (var i = 0; i < idsCount; ++i) {
            var idx = ids[i];
            var swatch = this._loaded[idx];
            //console.log("add/show swatch: "+idx+" / "+swatch.sku.shade_name);
            if (!swatch) {
                console.log("idx = "+idx+" ids = ",ids);
                continue;
            }
            var swatchNode = $(swatch.id);
            if (!swatchNode) continue;
            swatchNode.style.display = "block";
            this.domNode.appendChild(swatchNode);
            if (this._addClassByColumn) {
                this._addClassByColumn(swatch, i);
            }
            
            // display with specified starting swatch, if any
            if (!this._started) {
                if (this.selectedSku && (this.selectedSku === swatch.sku.sku_id )) {
                    this.setSwatch(swatch, { event: "load" });
                }

            }
        }
              
    }  
});


brand.product.hexSwatchSet = Class.create(brand.product.swatchSet, 
{
    templateKey : "hexSwatchSet",
    templateString: '<div id="#{id}" class="swatchset-hex-container"></div>',
    
    // skuField: DOM node
    // input field for storing selected sku
    skuField: null,
    
    selectedClass: "swatch_hex_container_selected",
    selectedHexClass: "swatch_hex_selected", 
    
    postCreate: function() {
        //console.log("brand.product.hexSwatchSet.postCreate "+this.id); 
        var skuCount = this.skus.length;
        for (var i = 0; i < skuCount; ++i) { 
            var sku = this.skus[i];
            var cid = "swatch_" + sku.sku_id; 
            if (this.video_prod) cid = "video_" + cid;
            
            var swatch = new brand.product.hexSwatch({
                id: cid,
                sku: sku,
                containerId: this.id,
                parentId: this.id,
                idx: i
            });                
            this._loaded[i] = swatch;
            
        }
        
        this.processData(this._dataMethod, this._dataParam);
        this._started = true;
    },
    
    toggleSelectedState: function(child, state) {
        if (!child || !this.selectedClass) return;
        if (state) {
            child.domNode.addClassName(this.selectedClass);
            child.hexNode.addClassName(this.selectedHexClass);
        } else {
            child.domNode.removeClassName(this.selectedClass);
            child.hexNode.removeClassName(this.selectedHexClass);
        }
    }
    
});


brand.product.thumbSwatchSet = Class.create(brand.product.swatchSet, 
{

    templateKey : "thumbSwatchSet",
    templateString: '<div id="#{id}" class="swatchset-thumbs-container"></div>',

    selectedClass: "swatch-thumb-selected",
    subsetClass: "swatchsubset-thumbs-container",
    selectedSubsetClass: "swatchsubset-selected",
    
    // skuField: DOM node
    // input field for storing selected sku
    skuField: null,

    // arrays for multiple values associated w/ each sku (ex: multi-colored skus)
    smooshes: null,
    
    // # of thumbs on each horizontal line
    columns: 7,
                        
    postCreate: function() {
        //console.log("brand.product.thumbSwatchSet.postCreate "+this.id); 
        var idbase = this.productData.id + "_" + this.productType;
        //console.log("brand.product.thumbSwatchSet.postCreate idbase = "+idbase);
        
        // multi-shaded, 1 sku per SPP
        if (this.multiShaded.isSingleSkued) {
            var sku = this.skus[0];
            var itemCount = sku.shade_name.length;
            for (var i = 0; i < itemCount; i++) { 
                var cid = "swatch_" + idbase + i.toString();
                var name = (sku.shade_name[i] ? sku.shade_name[i] : "");
                var hex = (sku.color[i] ? sku.color[i].toString() : null);
                var swatch = new brand.product.thumbSwatch({
                    id: cid,
                    sku: sku,
                    idx: i,
                    name: name,
                    smooshThumb: sku.smoosh_thumb[i],
                    hex: hex,
                    parentId: this.id,
                    parentIsMultiShaded: this.multiShaded                   
                });
                if (this._addClassByColumn) {
                    this._addClassByColumn(swatch, i);
                }
            }
        // multiple swatches per sku, multiple skus per SPP
        } else if (this.multiShaded.isMultiSkued) {
            var skuCount = this.skus.length;
            var subsetNode;
            for (var i = 0; i < skuCount; ++i) {
                var sku = this.skus[i];
                var itemCount = sku.color.length; // count swatches per sku by color array

                // create subset container for multi-shued swatch sets that need to display separately
                if (this.multiShaded.isMultiSkued) {
                    subsetNode = document.createElement("div");
                    subsetNode.id = "swatchsubset_" + sku.sku_id;
                    this.containerNode.appendChild(subsetNode);
                    $(subsetNode.id).addClassName(this.subsetClass);
                }

                for (var j = 0; j < itemCount; j++) {
                    var cid = "swatch_" + sku.sku_id + "_" + j.toString();
                    var hex = (sku.color[j] ? sku.color[j].toString() : null);
                    var swatch = new brand.product.thumbSwatch({
                        id: cid,
                        sku: sku,
                        //containerId: subsetNode.id,
                        idx: j,
                        smooshThumb: sku.smoosh_thumb[j],
                        hex: hex,
                        domParent: subsetNode,
                        parentId: this.id,
                        parentIsMultiShaded: this.multiShaded
                    });
                }
                // display with specified starting swatchsubset, if any
                if (this.selectedSku && (this.selectedSku === sku.sku_id)) {
                    this.setSwatchSubset({ sku: sku, event: "load", skuIdx: i });
                }
            }

            // set first sku as selected if none selected yet
            if (this.initDefault && !this._swatchSelected) {
                this.setSwatchSubset({ sku: this.skus[0], event: "load", skuIdx: 0 });  // set first sku as selected
            }

        // default shaded prod: 1 swatch per sku, multiple skus per SPP   
        } else {        
            var skuCount = this.skus.length;
            for (var i = 0; i < skuCount; ++i) { 
                var sku = this.skus[i];
                var cid = "swatch_" + sku.sku_id;           
                var swatch = new brand.product.thumbSwatch({
                    id: cid,
                    sku: sku,
                    containerId: this.id,
                    idx: i,
                    parentId: this.id
                });                
                this._loaded[i] = swatch;
    
            }
            
            this.processData(this._dataMethod, this._dataParam);
            
            // set first sku as selected if this.selectedSku wasn't already selected in _updateSet method
            if (this.initDefault && !this._swatchSelected && this._loaded[0]) {
                this.setSwatch(this._loaded[0], { event: "load" });
            }
        }
        
        this._started = true;
    },
    
    setSwatchSubset: function(args) {
        var sku = args.sku;
        if (!sku) return;
        var subsetNode = $("swatchsubset_"+sku.sku_id);
        var skuIdx = args.skuIdx; // sku position in sku array (not shade idx within sku)
        //console.log("setSwatchSubset: args.skuIdx = "+args.skuIdx+" id: "+subsetNode.id+" / subsetNode = ",subsetNode);
        if (subsetNode && sku) { // send event even on initial load
           document.fire("swatch:click", { sku: sku, shadeIdx: 0, product: this.productData });
        }

        // toggle state of previous & current selected subsets
        if (this.selectedChildNode) {
            this.selectedChildNode.selected = false;
            this.selectedChildNode.hide();
        }
        subsetNode.style.display = "block";
        this.selectedChildNode = subsetNode;
        this.selectedChildNode.idx = skuIdx;

        if (this.skuField) this.skuField.value = sku.path;
        this._swatchSelected = true;
        this.onSelectCallback(subsetNode, { sku: sku, event: "subset-select", skuIdx: args.skuIdx });
    },
    
    toggleSelectedState: function(child, state) {
        if (this.multiShaded) return;
        if (state) {
            child.shadeNode.addClassName(this.selectedClass);
        } else {
            child.shadeNode.removeClassName(this.selectedClass);
        }
    },
    
    _addClassByColumn: function(swatch, positionIdx) {        
        // get column position
        var columns = this.columns;
        var col = 1;
        if (columns) {
            var num = (positionIdx + 1); // start from 1 instead of 0
            if (num > columns) {
                var multiplyby = Math.floor(positionIdx / columns);
                col = (num - (columns * multiplyby));
            } else {
                col = num;
            }
            // save class name to assign on mouseover
            // NOTE: saving for later since removing/adding classnames at this stage for all swatches would affect perfomance on prods w/ lots of skus
            swatch.columnClass = "thumb-col"+col;
            swatch.resetColumnClass = true;
        }
    }
});


/** Individual Swatch Classes **/

brand.product.swatch = Class.create(Widget,
{

    sku: null,
    name: null,
    type: "solo",
    selected: false,
    hex: null,
    rgb: "",
    idx: 0, // sku index, or for multi-shaded prods, position of shade in arrays for shade name, hex, etc...
    
    // smooshThumb: String
    // thumb jpg path: required for thumbSwatch, optional for hexSwatch
    smooshThumb: null, 

    initialize: function($super, arguments) { 
        this.setProperties(arguments);
        
        var sku = this.sku;
        this.smooshThumb = arguments.smooshThumb || sku.smoosh_thumb;
        this.hex = arguments.hex || (sku.color[0] ? sku.color[0].toString() : null);
        this.name = arguments.name || sku.shade_name;
        this.rgb = brand.hexToRGB(this.hex);
        
        $super();
    },
    
    postCreate: function() { 
        if ( this.rgb ) { 
            this._setTextColor(this.rgb);
        }
    },

    _onClick: function(e) {
        if (e) e.preventDefault();
        this.parent.setSwatch(this, { event: e });
    },
    
    _setTextColor: function(rgb) {
        if (!rgb || !this.tooltipNode) { return; }
        // if color is light, set text to black for appropriate contrast
        if (this._isBright(rgb)) {
            this.tooltipNode.style.color = "#000";
        }
    },
    
    _getBrightness: function(rgb) {
        if (!rgb) { return; }
        var total = rgb[0] + rgb[1] + rgb[2];
        return total;
    },

    _isBright: function(rgb) {
        if (!rgb) { return; }
        var isBright = this._getBrightness(rgb) > 450;
        return isBright;
    }
    
});


brand.product.hexSwatch = Class.create(brand.product.swatch,
{
    _swatchPath: "jsTemplates.product.hexSwatch",
    _swatchImagePath: "jsTemplates.product.hexSwatchImage",
     
    initialize: function($super, arguments) {
        this.setProperties(arguments);       
        this.templatePath = this._swatchPath;

        if ( this.sku.sku_multicolor_type ) { 
            this.type = this.sku.sku_multicolor_type;
        }
        if ( this.type === "duo" || ( this.type === "solo" && this._isDark(this.rgb) ) ) {
            this.templatePath = this._swatchImagePath;
        }
        
        $super(arguments);
    },
    
    _onMouseOver: function(e) {
        if (this.name && this.tooltipNode) {
            this.tooltipNode.style.visibility = "visible";
            this.domNode.style.zIndex = "10"; // fix for IE position+layering bug
        }
    },

    _onMouseOut: function(e) {
        if (this.name && this.tooltipNode) {
            this.tooltipNode.style.visibility = "hidden";
            this.domNode.style.zIndex = "1";
        }
    },

    _isDark: function(rgb) {
        if (!rgb) { return; }
        var isDark = this._getBrightness(rgb) < 100;
        return isDark;
    }
});


brand.product.thumbSwatch = Class.create(brand.product.swatch,
{

    templatePath: "jsTemplates.product.thumbSwatch",
    
    resetColumnClass: true,

    initialize: function($super, arguments) {
        this.setProperties(arguments);
        
        // rb keys for template
        if (!this.parentIsMultiShaded) {
            this.text_select = site.product.rb["select"];        
            this.text_toshop = site.product.rb.to_shop;
        }
        
        $super(arguments);
    },
    
    postCreate: function($super) {
        this.shadeNode = this.domNode.select("A")[0];
        this.containerClasses = this.domNode.className || "";
        
        // attach event handler here instead of via widget class (workaround
        // for widget class not passing event & event needed for preventDefault)
        this.domNode.observe("click", this._onClick.bind(this));
        
        if (this.name) {
            // tooltip workaround for IE (handled via CSS for other browsers)
            if (generic.env.isIE && this.tooltipNode) {
                this.domNode.observe("mouseover", this._showToolTip.bind(this));
                this.domNode.observe("mouseout", this._hideToolTip.bind(this));
            } else {
                this.domNode.observe("mouseover", this._addClass.bind(this));
            }
        }
        
        $super();
    },

    _addClass: function(e) {
        if (!this.resetColumnClass || !this.columnClass) return;
        this.domNode.className = (this.containerClasses + " " + this.columnClass);
        this.resetColumnClass = false;
    },
    
    _showToolTip: function(e) {
        this._addClass(e);
        this.tooltipNode.style.visibility = "visible";
        this.domNode.style.zIndex = 50; // fix for IE position+layering bug
        this.shadeNode.style.zIndex = 50;
    },

    _hideToolTip: function(e) {
        this.tooltipNode.style.visibility = "hidden";
        this.domNode.style.zIndex = 1;
        this.shadeNode.style.zIndex = 1;
    }
    
}); 
brand.product.videoPlayer = {
  config: {},
  lastCuePoint: 0,
  totalCuePoints: 0,
   
  init: function() {   
    //console.log("brand.product.videoPlayer.init"); 

    this.text_step = site.product.rb.step;
    this.text_of = site.product.rb.of;
    
    if (!this.config.cue_points) return; // in case corresponding video file hasn't loaded
    
    if(this.config.cue_points.length) {
        this.totalCuePoints = this.config.cue_points.length - 1;
        this.lastCuePointTime = Number(this.config.cue_points[this.totalCuePoints - 2].time);
    }
 
    this.videoPlaceholder =  $("flash_placeholder");
    if (!this.videoPlaceholder) {
        return;
    }
    this.productContainer = $("video_prod_container");
    
    this.drawVideoPlayer();
    var self = this;
    generic.events.observe("videoPlayer:cuePoint", self.setCuePoint.bind(self));
    var timerCount = 0;

    // make sure flash has loaded before trying to set cue points
    var playerIsReady = function() {
        timerCount++;
        //console.log("playerIsReady: checking");
        var vp = $("howToVideoPlayer");
        try {
            var queryTime = vp.queryTime();
        }
        catch(err) { console.log("video player vp.queryTime error: "+err) }
        if (vp && typeof queryTime !== "undefined") {
            self.playerFlashObject = vp;
            clearInterval(timer);
            self.processRelatedProducts();
            self.setInitialView();
        } else if (timerCount > 10) {
            clearInterval(timer);
            console.log("getting method 'queryTime' of flash object failed.  giving up.");
        }
    }   
    var timer = setInterval(playerIsReady, 500);
    
  }, 
  
  drawVideoPlayer: function() {
    //console.log("brand.product.videoPlayer.drawVideoPlayer");
    var placeholderNode = this.videoPlaceholder;
    if (!placeholderNode) return;
    var params = { 
       bgcolor: "#000000",
       flashvars: { 
        conf_uri: placeholderNode.getAttribute("conf_uri"),
        showTitle: placeholderNode.getAttribute("show_title") || "true",
        pageName : placeholderNode.getAttribute("page_name")
       }  
    };

    if (placeholderNode.getAttribute("color") === "white") {
        params.flashvars.color = "white"; 
    } 
   
    var attr = {
        id: "howToVideoPlayer",
        name: "howToVideoPlayer",
        data: "/flash/_video_player/howToVideoPlayer_query.swf" 
    };
     
     generic.flash.embed(attr, params, "flash_placeholder"); 
  },
  
  processRelatedProducts: function() { 
     //console.log("brand.product.videoPlayer.processRelatedProducts");
     if (!$("vid_prods")) return; 
               
     if (this.productContainer) this.productContainer.addClassName("hidden");  
     $$("#vid_prods .rel_prod").invoke('addClassName', "hidden");
     $$("#vid_prods .swatch_hex_container").invoke("show");
//console.log('$$("#vid_prods .swatch_hex_container") = ',$$("#vid_prods .swatch_hex_container"));
      
     brand.mpp.item.init({
        data: page_data.video_products,
        initButtons: true,
        video_prod: true,
        type: "video"
     }); 
     
  },
   
  setInitialView: function() {
     if (this.productContainer) {
        this.productContainer.addClassName("hidden");
     }
     $("tip_title").innerHTML = (this.config.title) ? this.config.title : "&nbsp;"; 
     $("tip_copy").innerHTML = (this.config.intro) ? this.config.intro : "&nbsp;";  
  },
  
  getTimeCode: function() {
    //console.log("brand.products.videoPlayer.getTimeCode");
    var vp = this.playerFlashObject;
    if (!vp) { return "0000"; }
    //console.log("Flash Player time: " + vp.queryTime() + " " + Math.round(vp.queryTime()));
    var time = Math.round(vp.queryTime()); 
    return time;
  },
  
  showCue: function(cidx) { 
    //console.log("brand.products.videoPlayer.showCue cidx: " + cidx); 
    if (cidx==-1) {
        this.setInitialView(); 
        return;
    } 
     
    var cue = this.config.cue_points[cidx];    
    var completed = (cidx == this.totalCuePoints);
      
    $("tip_title").innerHTML = (completed) ? "&nbsp;" : this.text_step + " " + (cidx+1) + " " + this.text_of + " " + this.totalCuePoints; 
    $("tip_copy").innerHTML = cue.copy;  
    
    var hasProducts = false;
    $$("#vid_prods .rel_prod").invoke('addClassName', "hidden");
    $$("#vid_prods .swatch_hex_container").invoke("hide");
     
    var str, productNode, productId, skuId, swatchNode;   
    cue.prods.each(function(catprodsku, idx) {
        //console.log("sku path: "+catprodsku); 
        catprodsku = catprodsku.strip();
        str = catprodsku.split("PROD")[1];
        productId = "PROD" + str.split("SKU")[0];
        skuId = str.split("SKU")[1];
        productNode = $("video_" + productId);
        swatchNode = $("video_swatch_SKU" + skuId);
           
        if (!!productNode) { 
            hasProducts = true;
            //$("vid_prods").appendChild(product); // NEEDED?
            productNode.removeClassName("hidden");    
            if (!!swatchNode) swatchNode.style.display = "block"; 
        } 
    });
    
    if (hasProducts && this.productContainer) this.productContainer.removeClassName("hidden");  
  },
  
  setCuePoint: function(idx) { 
    //console.log("brand.products.videoPlayer.setCuePoint "); 
    var time = this.getTimeCode();
    if (time == "0000") { return; }
  
    var cuePoint = -1;  
    
    for (var i=0;i<=this.totalCuePoints;i++) { 
        if (time >= this.config.cue_points[i].time) {
             if (i==this.totalCuePoints) cuePoint = i; //outro 
        } else { 
            cuePoint = i-1;   
            break;
        }
    }  
     
    this.showCue(cuePoint); 
  }  
};
// MPP functions
brand.mpp = brand.mpp || {};


// for mpp product modules
brand.mpp.item = {
   
    is_shaded: false,

    // input field (as add to bag btn) that holds sku path 
    // value of currently selected color sku
    skuField: "", 
    
    // page/content type
    type: "mpp",
    
    // this can be used when products which contain the same product
    // id are going to appear on the same page. Setting to true,
    // will append the path of the first sku instead of a product id to nodes
    altNodeId: false,
    
    // this is used in the case that products have the same id.
    // the type used if this is true is the time to ensure that
    // ids are unique for swatches
    altType: false,
    
    init: function(args) {
        //console.log("brand.mpp.item.init: "+args.type);
    
        var products = args.data;
        var cartConfirmMsg;  
        var self = this;
        this.altNodeId = (args.altNodeId ? args.altNodeId : false );
        this.altType = (args.altType ? args.altType : false );
        if (args.type) { this.type = args.type; }
        var video_prod = args.video_prod || false;
        if (!products) return;
        
        products.each(function(prod, idx) {
            var is_shaded = (prod.shaded == 1);
            var nodeId = prod.product_id;
            if (self.altNodeId) {
                nodeId = prod.skus[0].path;
            } else if (video_prod) {
                // use "video_PRODXXX" for nodeId
                nodeId = self.type +'_'+ nodeId;
            }
            prod.video_prod = video_prod;

            // proceed if node exists in html
            if ($(nodeId)) {
                
                // init cart confirm
                var created = false;
                if (!$("cart_confirm-" + nodeId)) {
                    created = true;
                    cartConfirmMsg = new brand.product.cartConfirm({
                        id: "cart_confirm-" + nodeId,
                        is_shaded: is_shaded,
                        prodName: prod.name,
                        nodeToReplace: $("cart_confirm_placeholder-" + nodeId)
                    });
                } 
                
                var msgProps;
                if (is_shaded && args.shadedMessageProps) {
                    msgProps = args.shadedMessageProps;                    
                } else if (args.messageProps) {
                    msgProps = args.messageProps;                    
                }

                // set up product photo rollover
                if (prod.image_small_rollover) {
                    self.initPhotoRollover(prod, nodeId);
                }
                    
                if (is_shaded && created) {
                    // set up swatches
                    var type = (self.altType ? (new Date()).getTime() : self.type);
                    self.initSwatch(prod, cartConfirmMsg, type, msgProps, nodeId);
                } else {
                    // non-shaded: 1st sku is default add-to-bag button value
                   self.initButton(prod, cartConfirmMsg, msgProps, nodeId);
                }
                
            }
        }); // end forEach products
       
    },
    
    initSwatch: function(prod, cartConfirmMsg, type, msgProps, nodeId) {
        //var nodeId = (this.altNodeId ? prod.skus[0].path : prod.product_id);
        //console.log("initSwatch: "+nodeId + " " + $("swatch_colors_placeholder-" + nodeId));
        if (!$("swatch_colors_placeholder-" + nodeId)) return; //page_data has extra wrongful content
        
        var smooshImgLg = prod.skus[0].smoosh;
        var skuFieldId = "prod_sku_cart_add-" + nodeId;        
         
        // init quick buy popover
        var cartAddMsg = this.initCartPopover(prod, nodeId, {
            is_shaded: true,
            skuFieldId: skuFieldId,
            smooshPath: (smooshImgLg.replace ? smooshImgLg.replace(/168x168/g, "56x56") : "/images/common/blank.gif"),
            smooshId: "smoosh_img_cart_add-" + nodeId,
            cartConfirm: cartConfirmMsg             
        });
        
        // mixin alternate properties for text values, favorites remove, etc...
        if (msgProps) {
            cartAddMsg = Object.extend(cartAddMsg, msgProps);
        }
        
        // sku field is in CartAdd template, so now it's available
        var skuField = $(skuFieldId);
      
        // init swatch color squares
        var swatch = new brand.product.hexSwatchSet({
            product: prod,
            skuField: skuField,
            productType: type,
            
            // on swatch select:
            onSelectCallback: function(selectedChild) {
                // quick buy
                cartAddMsg.sku = selectedChild.sku;
                cartAddMsg.show();
                // tell cartConfirm which sku has been selected
                cartConfirmMsg.sku = selectedChild.sku;
            }
            
        }, $("swatch_colors_placeholder-" + nodeId));
    },
    
    initButton: function(prod, cartConfirmMsg, msgProps, nodeId) {  
        //console.log("brand.mpp.item.initButton"); 
        //var nodeId = (this.altNodeId ? prod.skus[0].path : prod.product_id);        
        var skuFieldId = "prod_sku-" + nodeId;
        var skuField = $(skuFieldId);
        
        // if shoppable, init add button
        if ( skuField ) {        
            cartConfirmMsg.sku = prod.skus[0];
            skuField.value = prod.skus[0].path;
            
            var cartAddBtn = brand.product.addButton({
                addButtonNode: skuField,
                callback: function(response) {
                    //console.log("brand.product.initButton cartAddBtn: cartConfirmMsg");  
                    cartConfirmMsg.show({ response: response });
                }
            });
    
            // mixin alternate properties for text values, favorites remove, etc...
            if (msgProps) {
                cartConfirmMsg.setDisplayProperties(msgProps.confirm);
            }
        }
        
        // handle remove button for favorites
        if (this.type === "favorites") {
            var removeNode = $("btn_favorites_remove-" + nodeId);
            if (removeNode) {
                var removeBtn = brand.product.addButton({
                    addButtonNode: removeNode,
                    itemType: "favorites",
                    skuField: skuField,
                    action: "delete",
                    callback: function(response) {
                        if (msgProps.callbackRemoveButton) {
                            msgProps.callbackRemoveButton({ removeNodeId: removeNode.id, skuFieldValue: removeNode.id });   
                        }
                    }
                }); 
            }
        }
    },

    initCartPopover: function(product, nodeId, args) {
       //console.log("brand.mpp.item.initCartPopover: "+this.type);
       
        var cartAddClass = brand.product.cartAdd;
        var popArgs = {
            id: "cart_add-" + nodeId,
            is_shaded: false,
            prodName: product.name,
            price: product.price,
            product_price_with_tax: product.price_with_tax,
            nodeToReplace: $("cart_add_placeholder-" + nodeId)
        }
        popArgs = Object.extend(popArgs, args);

        if (this.type === "favorites") {
            cartAddClass = brand.product.cartAddFromFavorites;
            popArgs.isRemovable = true;
        }
        var cartAddMsg = new cartAddClass(popArgs); 
        
        return cartAddMsg;
    },
    
    initPhotoRollover: function(product, containerId) {
        var containerNode = $(containerId);
        if (!containerNode) return;
        var imgNode = containerNode.select("a img.thumb")[0];
        var img = product.image_small;
        var altImg = product.image_small_rollover;       
        if (!img || !altImg || !imgNode) return;
        var preloaded = new Image();
        preloaded.src = altImg; 
       
        var over = function(e) {
            e.target.src = preloaded.src;
        }
    
        var out = function(e) {
            e.target.src = img;
        }
        imgNode.observe("mouseover", over);
        imgNode.observe("mouseout", out);   
    }
}


/**
 * favorites cart & swatch functionality
 */
brand.mpp.initFavorites = function() {
    //console.log("site.product.favoritesMpp");
    var data = page_data.catalog.mpp.products;
    var itemContainerNode = $("favorites-product-container");

    var removeFavorite = function(args) {
       //console.log("site.product.favorites.removeButton "+args.removeNodeId+"/"+args.skuFieldValue);  
       if (!args||!args.removeNodeId) return; 
       var catprodsku = args.removeNodeId.split("-")[1];
       if (!catprodsku) return; 
       
       var productNode = $(catprodsku); 
       var swatches = productNode.select(".swatchset-hex-container")[0];
       var removeNode;
       
       // if product is unshaded or has only 1 swatch, remove the product
       if (!swatches || swatches.select(".swatch_hex_container").length==1) { 
           removeNode = productNode;
       }
       // else just remove the swatch
       else if (args.skuFieldValue) {
           var skuId = args.skuFieldValue.split("SKU")[1]; 
           removeNode = $("swatch_SKU"+skuId);
       }
       
       //console.log("removeNode: "+removeNode.id);
       if (removeNode) {
           removeNode.remove();
           
           // check for empty list
           var items = itemContainerNode.select("div.rel_prod");
           if (items.length < 1) {
               var noItemNode = $("no-favorites-message");
               if (noItemNode) noItemNode.removeClassName("hidden");
               if (itemContainerNode) itemContainerNode.hide();
           }
       }
    };
    
    site.mpp.item.init({
        data: data,
        altNodeId: true,
        altType: true,
        initButtons: true,
        type: "favorites",
        
        // configure confirm messages for cart or favorites remove
        messageProps: {
            //confirm: confirm_props,
            callbackRemoveButton: removeFavorite 
        },
        shadedMessageProps: {
            //callback: function(response) {
                //this.setConfirmProperties(confirm_props); // MERGE NOTE: not sure this was correct
            //},
            callbackRemoveButton: removeFavorite  
        }

    });  
}
brand.product.productOverlay = Class.create(Widget, { 

    isOpen: false,
    position: {},
    lockToNode: null,
    templateString: "",
    baseClass: "",
    widgetsInTemplate: false,
    itemCount: 0,

    initialize: function($super, arguments) {
        //console.log("brand.product.productOverlay.init: "+arguments.id); 
        $super(arguments); 
    },
    
    open: function(args) {
        if (this.isOpen) { return; }
        var self = this;

        brand.overlay.launch({
            foregroundNode: this.domNode,
            displayInline: true,
            removeOnHide: false,
            onClose: function() {
                self.onClose();
            }
        });

// MERGE: TODO: convert these settings to overlay call where possible/needed
        if (this.lockToNode) {
            // calculate height to get top offset 
            var t, h = 0;
            h = this.domNode.getHeight();
            t = (h * -1);
            this.position = {offsetTop: t};
            // position in relation to specified node
            var offsetTop = (this.position.offsetTop ? this.position.offsetTop : 0);
            var offsetLeft = (this.position.offsetLeft ? this.position.offsetLeft : 0);
            this.domNode.clonePosition(this.lockToNode, { setWidth: false, setHeight: false, offsetLeft: offsetLeft, offsetTop: offsetTop });
        } else {
            if (this.position.top) this.domNode.style.top = this.position.top+"px";
            if (this.position.left) this.domNode.style.left = this.position.left+"px";
        }       
        this.isOpen = true;
    },
    
    show: function(args) {
        //console.log("brand.product.productOverlay.show ",args);  
        //if (this.isOpen) { return; }
        if (this.isOpen) {
            this.close();
        }
        var response = (args && args.response ? args.response : null);
        this._updateDisplay(response);
        this.open(args);
    },
    
    getErrors: function(response) {
        var errors;
        var key;
        if (response && response.getMessages()) {
            errors = {};
            var messages = response.getMessages();
            for (var i = 0; i < messages.length; i++) {
                if (messages[i].text) {
                    key = messages[i].key;
                    errors[key] = messages[i];
                }
            }
        }
        return errors;
    },
    
    close: function() {
        //node.style.left = "-5000px";
        //this.domNode.style.display = "none";
        //this.isOpen = false;
        brand.overlay.hide();
        this.onClose();
    },
    
    // NEEDED?
    onClose: function() { 
        this.isOpen = false;
        //if (this.closeCallback) this.closeCallback();
    }
    
});


brand.product.cartAdd = Class.create( brand.product.productOverlay,
{
    //templatePath: "/js/brand/product/templates/cartAdd.html",
    templatePath: "jsTemplates.product.cartAdd",
    
    is_shaded: false,
    _enabled: true,
    
    // sku: Object
    // sku object from page_data
    sku: null,
    
    // template vars
    prodName: "",
    smooshPath: "/images/common/blank.gif",
    hex: "",
    price: "",
    smooshImgStore: {},
    
    initialize: function($super, arguments) {
        //console.log("brand.product.cartAdd init, args = ",arguments);
        this.templateString = false; //overrides productOverlay  
        $super(arguments);
    },
    
    postCreate: function() {
        //console.log("brand.product.cartAdd postCreate "+this.id);
        if (this.smooshId) this.smooshNode = $(this.smooshId);
        var button = (this.skuField ? this.skuField : $(this.skuFieldId));
        var self = this;
        if (button) {
            // add to cart button
            var prodBtn = brand.product.addButton({
                addButtonNode: button,
                callback: function(response) {
                    if (self.callback) {
                        self.callback(response);
                    }
                    self.close();
                    self.cartConfirm.show({ response: response });
                }
            });
        }
    },
    
    setConfirmProperties: function(args) {
        // used to set display properties of confirm on CartAdd popover callback (ex: for favorites vs. checkout message)
        this.cartConfirm.setDisplayProperties(args);
    },
    
    show: function($super, arguments) {
        generic.events.fire({event:"productmessage:cartadd/show", msg:this.sku});
        //console.log("brand.product.cartAdd.show: ", this.prodName); 
        $super(arguments); 
    },
    
    _updateDisplay: function() {
        var sku = this.sku;
        //console.log("CartAdd._updateDisplay: sku = "+this.sku);
        if ( this.smooshNode ) {
            this.smooshNode.style.backgroundColor = sku.color[0];
            this.smooshNode.src = this.smooshPath; // sets to blank to reset before loading next
            var smooshImg = sku.smoosh;
            if (typeof smooshImg === "object") {
                smooshImg = sku.smoosh[0];
            }
            var imgStore = brand.loadImage({
                node: this.smooshNode,
                imagePath: smooshImg.replace(/168x168/g, "56x56"),
                imageStore: this.smooshImgStore,
                imgId: sku.sku_id
            });
            this.smooshImgStore = imgStore;
        }
        this.swatchTitleNode.innerHTML = sku.shade_name;
        
        // set sku value
        var button = (this.skuField ? this.skuField : $(this.skuFieldId));
        button.value = sku.path;

        // inventory status
        site.product.inventoryStatus({
            shoppable: sku.shoppable,
            message: sku.inventory_status_message,
            messageNode: this.inventoryStatusNode,
            buttonNode: this.addToBagNode,
            containerNode: this.domNode
        });
        
        // "finish" name
        this.finishNameNode.innerHTML = (sku.finish ? "("+sku.finish+")" : "");
    }

});


brand.product.cartAddFromFavorites = Class.create( brand.product.cartAdd, {

    // isRemovable: Boolean
    // Use/show remove button
    isRemovable: true,
    
    postCreate: function($super, arguments) {    
        $super(arguments); 
        //console.log("brand.product.cartAddFromFavorites.postCreate");
           
        // set up remove button
        if (this.isRemovable) {     
            var self = this;      
            var removeBtn = brand.product.addButton({
                addButtonNode: self.removeNode,
                skuField: $(self.skuFieldId),
                itemType: "favorites",
                action: "delete",
                callback: function(response) {
                    if (self.callbackRemoveButton) {
                        self.callbackRemoveButton({ removeNodeId:self.removeNode.id, skuFieldValue:$(self.skuFieldId).value });
                    }
                    self.close();
                    //self.cartConfirm.show({ response: response });
                }
            });        
            this.removeNode.removeClassName("hidden");
        }
    }

});


brand.product.cartConfirm = Class.create( brand.product.productOverlay,
{
    //templatePath: "/js/brand/product/templates/cartConfirm.html",
    templatePath: "jsTemplates.product.cartConfirm",
     
    is_shaded: false,
    prodName: "",
    useLeftAlign: false,
    
    // sku: Object
    // sku object from page_data
    sku: null,
    
    // type: String
    // item type for count in cookie (optional) ex: "favorites"
    type: "cart",
    showingErrors: false,
    
    initialize: function($super, arguments) {
        // default text
        this.text_addedMessageCheckout = site.product.rb.added_to_shopping_bag;
        this.text_addedMessageFavorites = site.product.rb.added_to_favourites;
        this.text_add_to_bag = site.product.rb.add_to_bag;
        this.text_thank_you = site.product.rb.thank_you;
        this.text_favorites = site.product.rb.favorites;
        this.text_checkout = site.product.rb.checkout;
        this.text_sorry = site.product.rb.sorry;
        this.text_continue_shopping = site.product.rb.continue_shopping;
    
        $super(arguments);
    },
  
    postCreate: function() {
        //this.cartHandler = generic.checkout.cart;
        this.shadeNameDash = this.shadeNameNode.innerHTML;
    },
    
    show: function($super, arguments) {
        var confirmNode = this.domNode;
        if (this.useLeftAlign) {
            confirmNode.addClassName("pop-confirm-align-left");
            confirmNode.removeClassName("pop-confirm-align-default");
        } else {
            confirmNode.addClassName("pop-confirm-align-default");
            confirmNode.removeClassName("pop-confirm-align-left");
        } 
        $super(arguments); 
    },
    
    setDisplayProperties: function(args) {
       // used to set display properties of confirm (ex: for favorites vs. checkout message) 
       brand.updateProperties.apply(this, [args]);
    },
    
    _updateDisplay: function(response) {
        //console.log("brand.product.cartConfirm._updateDisplay "+response.getError()); 
        
        // check for error
        var hasErrors = response ? response.getError() : false;
        var hasFssMessage = false;
        var hasPreviousErrors = this.showingErrors;
                 
        // show error
        if (hasErrors) {  
            this.errorMessageNode.innerHTML = response.getMessages() ? response.getMessages()[0].text : ""; 
            this.cartConfirmErrorNode.removeClassName("hidden");
            this.cartConfirmDisplayNode.addClassName("hidden");  
            this.showingErrors = (hasErrors ? true : false); // save error state
        } 
        
        // show confirm message 
        else {
            // reset error display in case last state was showing errors
            if (hasPreviousErrors) {
                this.cartConfirmErrorNode.addClassName("hidden"); 
                this.cartConfirmDisplayNode.removeClassName("hidden");
            }
            //this.itemCount = this.cartHandler.getItemCount(this.type);
            //this.itemCount = this.cartHandler.getTotalItems();   
            //this.itemCountNode.innerHTML = this.itemCount.toString();
            this.prodNameNode.innerHTML = this.prodName;
            // populate shadeName node with shade name or size info
            if ((this.sku.shade_name && !Object.isArray(this.sku.shade_name)) || (this.isSized && this.sku.product_size)) {
                var skuInfo = this.sku.shade_name;
                if (this.isSized && this.sku.product_size) skuInfo = this.sku.product_size;
                this.shadeNameNode.innerHTML = this.shadeNameDash + skuInfo;
            } else {
                this.shadeNameNode.innerHTML = "";
            }
            
            if (this.type === "favorites") {
                this.domNode.addClassName("cart-confirm-overlay-container-favorites");
                this.addedMessageNode.innerHTML = this.text_addedMessageFavorites;
            } else {      
                this.domNode.removeClassName("cart-confirm-overlay-container-favorites");
                this.addedMessageNode.innerHTML = this.text_addedMessageCheckout;
                var data = response.getData();
                var fssMessage = "";
                var fssMessageNode = this.cartConfirmDisplayNode.select(".cart-confirm-fss-message")[0];
                if (fssMessageNode && data && data.trans_data) {
                    fssMessage = data.trans_data.free_shipping_message;
                    if (fssMessage && fssMessage.length > 1 && fssMessage != null) {
                        hasFssMessage = true;
                        fssMessageNode.innerHTML = fssMessage;
                        fssMessageNode.style.display = "block";
                    }
                }
                if (!hasFssMessage) fssMessageNode.hide();
            }
        }
    }

});


brand.product.swatchCard = Class.create( brand.product.cartAdd,
{

    templatePath: "jsTemplates.product.swatchCard",
    //templatePath: "/js/brand/product/templates/swatchCard.html",
    
    skuPath: "",
    closeOnClickOutside: null,
    swatchIdx: null, // idx to id single sku shades
    
    initialize: function($super, arguments) {
        this.setProperties(arguments);
        this.price = this.price;
        var sku = this.skus[0];
        this.text_limited = site.product.rb.limited;
        this.text_macpro = site.product.rb.macpro;
    
        $super(arguments, true);
        
        if (this.multiShaded) {
            this.domNode.addClassName("swatchcard-container-multishaded");
        }
    },

    postCreate: function($super) {
        // set nodes specfic to this class
        var id = this.id;
        this.skuField = $("prod-sku-"+id);
        this.shadeNameNode = $("shade-name-"+id);
        this.smooshNode = $("smoosh-img-"+id);
        this.descriptionNode = $("shade-description-"+id);
        this.finishNode = $("shade-finish-"+id);
        this.finishDescriptionNode = $("shade-finish-description-"+id);
        this.limitedNode = $("limited-flag");    
        this.proNode = $("pro-flag");
        this.inventoryStatusNode = $("inventory-status-"+id);
        $super();
        
        // if popover should close when user clicks outside of it
        if (this.closeOnClickOutside && this.closeOnClickOutside.enable) {
            var self = this; 
            $(document.body).observe("click", function(e) {        
                if (!self.isOpen) return;
                self.close();
            });
    
            var stopPropagation = function(e) {
                e.stopPropagation();
            }
    
            this.domNode.observe("click", stopPropagation);
    
            var nodesToExclude = this.closeOnClickOutside.nodesToExclude;
            if (nodesToExclude) {
                nodesToExclude.each(function(node) {
                    node.observe("click", stopPropagation);
                });
            }
        }
    },

    onSwatchSelect: function(args) {
        //console.log("brand.product.swatchCard.onSwatchSelect ",args);
        this.sku = args.sku;
        var swatchNode = args.swatchNode;
        this.skuField.value = this.sku.path;
        var useLeftAlign = false;
        this.swatchIdx = (args.swatchIdx >= 0 ? args.swatchIdx : null);
        
        // get child of swatchNode: swatchNode doesn't return proper values
        // from clonePosition (because of float?)
        this.lockToNode = swatchNode.select(".swatch-thumb")[0];
        
        // get column position of swatch thumb
        var getRightColumn = function() {
            var classes = $w(swatchNode.className)
            var classCount = classes.length;
            if (classCount.length < 1) return false;
            var hasClass = false;
            
            var searchIn = ["thumb-col5", "thumb-col6", "thumb-col7"];
            for (var i=0; i<classCount; i++) {
                if (searchIn.indexOf(classes[i]) != -1) {
                    hasClass = true;
                    break;
                }               
            }
            
            return hasClass;
        }
        useLeftAlign = getRightColumn();
        
        // add class by column position
        var node = this.domNode;
        if (useLeftAlign) {
            node.addClassName("pop-card-align-left");
            node.removeClassName("pop-card-align-default");
        } else {
            node.addClassName("pop-card-align-default");
            node.removeClassName("pop-card-align-left");
        }
        // pass these settings to the cart confirm obj
        var cartConfirm = this.cartConfirm;
        cartConfirm.setDisplayProperties({ type: "cart", lockToNode: this.lockToNode, useLeftAlign: useLeftAlign });
        
        // close other popovers if open
        if (cartConfirm.isOpen) {
            cartConfirm.close();
        }      
        this.show();
    },
    
    _updateDisplay: function() {
        //console.log("swatchCard._updateDisplay: sku = "+this.sku);
        var sku = this.sku;
        var swatchIdx = this.swatchIdx;
        var smoosh, imgId, shadeName, description;
        
        // data per single sku item array (multi-shaded)
        if (this.multiShaded && (swatchIdx >= 0)) {
            smoosh = sku.smoosh[swatchIdx];
            imgId = (sku.sku_id + swatchIdx);
            shadeName = (Object.isArray(sku.shade_name) ? sku.shade_name[swatchIdx] : sku.shade_name);
            description = sku.shade_description[swatchIdx];
            
        } else {
        // data per individual sku
            smoosh = sku.smoosh;
            imgId = sku.sku_id;
            shadeName = sku.shade_name;
            description = sku.shade_description;
            
            // content that displays for solo/duo skus
            // mac pro & limited flags
            if (this.limitedNode) {
                if (sku.limited_life == 1) {
                    this.limitedNode.show();
                } else {
                    this.limitedNode.hide();
                }
            }
            if (this.proNode) {
                if (sku.pro_product == 1) {
                    this.proNode.show();
                } else {
                    this.proNode.hide();
                }
            }
            
            // update inventory status
            site.product.inventoryStatus({
                shoppable: sku.shoppable,
                message: sku.inventory_status_message,
                messageNode: this.inventoryStatusNode,
                containerNode: this.domNode
            });         
        }
        
        this.smooshNode.src = this.smooshPath; // sets to blank to reset before loading next
        var imgStore = brand.loadImage({
            node: this.smooshNode,
            imagePath: smoosh,
            imageStore: this.smooshImgStore,
            imgId: imgId
        });
        this.smooshImgStore = imgStore;
        
        this.shadeNameNode.update(shadeName);
        // finish node content suppressed due to duplicate finish name in most shade name data
        //this.finishNode.innerHTML = (sku.finish && sku.finish_description ? "("+sku.finish+")" : "");
        //this.finishDescriptionNode.update(sku.finish_description);

        this.descriptionNode.update(description);
    }
    
});brand.checkout = brand.checkout || {};

brand.checkout = {
    abort: false,
    
    /* MERGE NOTE: needed?
    isCartView: false,  
    hasErrors : false,
    
    canContinueCheckout: function(state) {
        console.log("brand.checkout.canContinueCheckout: " + state);
        var style = state ? "visibility:visible" : "visibility:hidden";
        $$(".btn-checkout").invoke("setStyle", style); 
    },     
     
    makeAdditionalInfoBtns: function() {  
        //console.log("brand.checkout.makeAdditionalInfo Btns");
        // popups
        var popargs = {
            w: 480,
            h: 660,
            resizable: "no",
            scrollbar: "yes"
        }   
        var popup_shipping = new generic.popup({        
            activator: "btn_popup_shipping",
            url: "/cms/checkout/popup/shipping_popup.tmpl", 
            resizable: popargs.resizable,
            scrollbars: popargs.scrollbars,
            width: popargs.w,
            height: popargs.h       
        });    
        var popup_tax = new generic.popup({     
            activator: "btn_popup_tax",
            url: "/cms/checkout/popup/tax_popup.tmpl",  
            resizable: popargs.resizable,
            scrollbars: popargs.scrollbars,
            width: popargs.w,
            height: 200   
        });
        var popup_returns = new generic.popup({ 
            activator: "btn_popup_returns",
            url: "/cms/checkout/popup/return_popup.tmpl",  
            resizable: popargs.resizable,
            scrollbars: popargs.scrollbars,
            width: popargs.w,
            height: 500   
        });     
        var popup_security = new generic.popup({    
            activator: "btn_popup_security",
            url: "/cms/checkout/popup/security_popup.tmpl", 
            resizable: popargs.resizable,
            scrollbars: popargs.scrollbars,
            width: popargs.w,
            height: popargs.h   
        }); 
    },
    */
  
    makeExitBtn: function() {
        //console.log("btn_exit_checkout");
        // exit checkout button: sends user to last page before entering checkout
        var btn_exit_checkout = $("btn_exit_checkout"); 
        
        if (btn_exit_checkout) {
            btn_exit_checkout.observe("click", function() {   
                var loc = "/"; // home page default 
                var ref = document.referrer; 
                if (ref && (ref.indexOf(".maccosmetics")>-1) && (ref.indexOf("checkout")==-1)) {
                    loc = ref;
                }
                window.location = loc;
            });
        } 
    } 
};

        
brand.checkout.cartStatus = {  
    countNodeId: "global_cart_count",
    countContainerId: "shopping_bag_items", 
    
    init: function() {
        //console.log("brand.checkout.cartStatus.init");
        this.countNode = $(this.countNodeId); 
        var self = this;
          
        generic.events.observe("cart:countsUpdated", function(args) { 
            self.updateCount();
        });
          
        var countContainer = $(this.countContainerId);
        if (countContainer) {
            countContainer.removeClassName("hidden");
        } 
         
    }, 
    updateCount: function() {
        //console.log("brand.checkout.cartStatus.updateCount");
        if (this.countNode) { 
            this.countNode.innerHTML = generic.checkout.cart.getTotalItems();
        }
    }
}
brand.account = brand.account || {};

brand.account.panel = {
    // summary: 
    //      Sets default states for "my account" link headers
    hasSetPanelLinks: false,
    accountConfig: null,

    init: function(accountConfig) { 
        //console.log("brand.account.panel.init, accountConfig = ",accountConfig);  
        if (!$("accountnav")) { return; }
        var self = this;
        this.accountConfig = accountConfig;
        this.setState();
         
        this.setPanelLinks(); // in case the links are in panel_open
     
        //subscribe to "my mac" left nav onclick for non-account/checkout pages
        generic.events.observe("globalnav:getcontent/my_mac", function(event) {
            self.handlePanelRefresh({event: "show_panel"});
        });   
    },
    
    handlePanelRefresh: function(args) { 
        brand.account.panel.setPanelLinks();    
    }, 
    
    setPanelLinks: function() {
        //console.log("brand.account.setPanelLinks "+this.hasSetPanelLinks);
        if (this.hasSetPanelLinks) return;
    
        $$(".signout_link").each(function(s) { 
            this.hasSetPanelLinks = true;
            s.observe("click", brand.account.panel.signoutSubmit ); 
        });  
    }, 
    
    signoutSubmit: function() {
         ///console.log("brand.account.signoutSubmit"); 
         var onSuccess = function() {
             generic.events.fire({event:"cartCount:reset",msg:0});  
             location.replace('/account/signin.tmpl'); 
         }
         var onFailure = function() {
             console.log("brand.account.signoutSubmit: SIGNOUT failure");
         } 
         generic.jsonrpc.fetch({method:'rpc.form', params:[{'_SUBMIT':'signout'}], onSuccess:onSuccess, onFailure:onFailure});
    },
    
    setState: function() { 
        var pn = page_data.panel_nav["default"]; 
        var section, subsection, activeSection;
        var sections = this.accountConfig.sections
        if (pn && pn.item) {
            try {
                section = pn.id;
                subsection = pn.item.id;           
            }
            catch (err) { 
                console.log("setState: ",err);  
            }
        }
        if (section !== "account") return;
        
        if (subsection === "index") subsection = "account_index"; // account landing id
         
        // find "active" subsection 
        for (i=0; i<sections.length; i++) {
            //console.log("test: "+sections[i] + " / " + section + " / " + subsection); 
            if (sections[i] === section || sections[i] === subsection) {
                activeSection = sections[i];  
                break;
            }
        }                
         
        // set section/subsection image states 
        var activeImg = $("hd_"+activeSection);
       
        // set section/subsection image states
        this.setImages(activeSection);           
    },
    
    setImages: function(activeSection) {
        // apply rollovers to each non-active section image
        // show sel state for active section image        
        var activeImg = "hd_"+activeSection;
        $$("#accountnav img.accountnav_hd").each( function(node) {
            if (activeImg !== node.id) {
                var r = new brand.rollover(node, null);
            } else {
                var aImg = new brand.img(node, ["sel"]);
                aImg.changeSrc("sel");
            }
        });
    }
} 

/**
 * Email sign up for utility nav
 */
brand.account.emailSignup = {
    emailSignupJsonRpcPath: "email.signup",
     
    init: function(args) { 
        var self = this;
        var submitNode = args.submitNode;
        this.fieldNode = args.fieldNode;
        if (!submitNode || !this.fieldNode) return;
        submitNode.observe('click', self.validateEmail.bind(self));
        this.fieldNode.observe('keypress', self.validateEmail.bind(self));
    },
    
    validateEmail: function(event) { 
        if (!this.fieldNode) { return; } 
        
        if ( event.type === "keypress" && (event.keyCode != Event.KEY_RETURN) ) { 
            return false;           
        }
        var email = this.fieldNode;
        var popup; 
        var onSuccess = function() {
            brand.overlay.launch({
                foregroundNode: $("pop_email_valid"),
                displayInline: true,
                removeOnHide: false,
                displayDuration: 5000
            });
        }
        var onFailure = function() {
            brand.overlay.launch({
                foregroundNode: $("pop_email_invalid"),
                displayInline: true,
                removeOnHide: false,
                displayDuration: 5000
            });
        }
        var requestArgs = [{
            EMAIL_ADDRESS: email.value 
        }]; 
            
        if (brand.forms.isEmailAddress(email.value)) { 
            var d = generic.jsonrpc.fetch({method:this.emailSignupJsonRpcPath, params:requestArgs, onSuccess:onSuccess, onFailure:onFailure});
        } else { 
            onFailure();
        }
       
        Event.stop(event);

        return false;
    }
}
brand.view = {};

/**
 * Add css classnames to form elements for IE6
 * @memberOf brand.view
 */
brand.view.setFormSelectors = function() {
    //console.log("brand.setFormSelectors");
    $$("INPUT[type=text]").invoke("addClassName","input-text");
    $$("INPUT[type=password]").invoke("addClassName","input-password"); 
    $$("INPUT[type=image]").invoke("addClassName","input-image"); 
}

/**
 * Apply rollover behavior to images w/ class "rollover"
 * @memberOf brand.view
 */
brand.view.initRollovers = function() {
    $$("img.rollover").each(function(el) { 
        var rollover = new brand.rollover(el, null); 
    }); 
}


/**
 * brand.view.colorNav
 * swf bordering globalnav that displays a clickable gradient of colors
 * Navigates to color browser
 * @memberOf brand.view
 */
brand.view.colorNav = {
   abort: false,
   placeholder: "color_nav_placeholder",
   flashid: "color_nav",
   offW: 8,
   onW: 90,
   timer: null,
 
   embed: function() { 
        if (brand.view.colorNav.abort) return;
        
        if (generic.env.isSafari) {
            var cn = $("color_nav_container");
            if (cn) { cn.style.display = "none"; }
            this.placeholder = "color_nav_placeholder_standalone";
            this.flashid = "color_nav_standalone";
        } 
        
        var params = {
            menu: "false",
            movie: "/flash/color_nav/color_nav.swf",
            flashvars: {
                gradient_uri: "/flash/color_nav/assets/color_gradient.png",
                application_uri: "/flash/color_play/index.tmpl",
                application_query_string: "?colorplaysample="
            }
        };
    
        var attr = {
            id: this.flashid,
            name: this.flashid,
            data: "/flash/color_nav/color_nav.swf",
            width: this.offW
        };
        
        if (generic.flash.playerversion) {
            attr.playerversion = generic.flash.playerversion;
        }
 
        if ($(this.placeholder)) generic.flash.embed(attr, params, this.placeholder);  
    },
    setWidth: function(e) {    
        if (e=="mouseover") { 
             clearTimeout(brand.view.colorNav.timer);  
             $(this.flashid).style.width = this.onW + "px";  
        } else {     
             brand.view.colorNav.timer = setTimeout(function(){    
                 $(brand.view.colorNav.flashid).style.width = brand.view.colorNav.offW + "px";     
             }, 600);
        }
    
    }    
}

/*
 * brand.view.utilityNav
 * sets up visual toggling of utility nav button fields
 * hooks up email & locator fields to emailSignup & locator classes if applicable
 * NOTE: search initialized from brand.search Class
 * @memberOf brand.view
 */
brand.view.utilityNav = {
    
    formelements: {},    
    buttonsets: {},
    minTop: 400, // minimum num of pixels from top of body to beginning of utility nav
    
    init: function(args) {

        // button/form field toggling
        var container = $("utilitynav");
        if (!container) return false;
        var nodes = container.select('.utilitynav_button');
        var self = this;
        if (args.minTop) this.minTop = args.minTop;

        nodes.each(function(node) {
            // button/form pair toggling relies on naming convention: utilitynav_[button or form]_[pair name]
            if (!node.id) return;
            var name = node.id.replace(/utilitynav_button_/g, "");
            var formnode = $("utilitynav_form_" + name);

            // get form element id's being used (not all locale's may have same elements)
            //var ftext = dojo.query("input[type=text]", formnode);
            var fieldnode = formnode.select("input[type=text]")[0];            
            var fieldsubmit = formnode.select("input[type=image]")[0];
            
            if (name && formnode && fieldnode) {
                self.buttonsets[node.id] = name;
                self.buttonsets[fieldnode.id] = name;
                self.buttonsets[fieldsubmit.id] = name;
                
                self.formelements[name] = { field: fieldnode, submit: fieldsubmit };
                node.observe("click", function() {
                    self.showForm(node);
                });
                fieldnode.observe("blur", function() {
                    self.showButton(fieldnode);
                });
                fieldsubmit.isfocused = false;
                fieldsubmit.onfocus = function() { 
                    this.isfocused = true;
                }
                fieldsubmit.observe("blur", function() {
                    self.showButton(fieldsubmit);
                });
            }       
        });

        // form submits: email signup & locator     
        if (this.formelements.email) {
            this.initEmailSignup(this.formelements.email);
        }
        if (this.formelements.locator) {
            this.initLocator(this.formelements.locator);
        }

    },

    showForm: function(node) {
        var shownode, hidenode;
        var hidenode = node;
        var name = this.buttonsets[node.id];
        var shownode = $("utilitynav_form_" + name);
        this.toggle(hidenode, shownode);
        
        // focus on field
        var focusnode = (this.formelements[name] ? $(this.formelements[name].field) : null);
        if (!focusnode) { return; }
        focusnode.focus();
        focusnode.value = "";
        focusnode.isfocused = true;
    },

    showButton: function(node) {
        var shownode, hidenode;
        node.isfocused = false;
        var name = this.buttonsets[node.id];
        var self = this;
        var submitbtn = $(this.formelements[name].submit);
        var field = $(this.formelements[name].field);
        var hidenode = $("utilitynav_form_" + name);
        var shownode = $("utilitynav_button_" + name);
        
        // wait for animation to finish
        var pause = function() {
            if (!submitbtn.isfocused && !field.isfocused) {
                self.toggle(hidenode, shownode);           
            }
        }
        setTimeout(pause, 200);
    },
    
    toggle: function(hidenode, shownode) {
        if (hidenode && shownode) {
            hidenode.style.display = "none";
            shownode.style.display = "block";
        }
    },
    
    initEmailSignup: function(formelements) {
        if (site.account && site.account.emailSignup) {
            site.account.emailSignup.init({
                submitNode: formelements.submit,
                fieldNode: formelements.field
            });
        } else {
            console.log("(site || brand).account.emailSignup not found");
        }

    },
    
    initLocator: function(formelements) {
        if (site.locator) {
            site.locator.init({
                submitNode: formelements.submit,
                fieldNode: formelements.field
            });
        } else {
            console.log("(site || brand).locator not found");
        }

    }
}


brand.view.flashPopover = { 
    params: {  
        wmode: "transparent"
    },
    attr: {
        id : "page_overlay",
        name: "page_overlay",
        height: "84",
        width:"245",  
        bgcolor: "#000" 
    },
    embed: function() {
        if (!$("page_overlay_div")) return; 
        var params = this.params;
        var attr = this.attr;
        params.movie = $("page_overlay_div").getAttribute("swf");
        attr.data = $("page_overlay_div").getAttribute("swf");
        generic.flash.embed(attr, params, "page_overlay_div");  
    },
    close: function() {  //called by shipping swf
        $("page_overlay_container_div").addClassName("page_overlay_container_div_closed"); 
        //console.log("brand.view.flashPopover.close()");
    }   
}
      

/*
 * brand.view.footer
 * Applies behavior to footer container and country chooser menu
 * @memberOf brand.view
 */
brand.view.footer = {  
    adjust: function() { 
        if (generic.env.isIE6) {
            var footerNode = $("footernav");
            this.initIELayerFix(footerNode);
            var footer = new brand.bottomFixed({
                node: footerNode
            });
        }

        if (!global.isipad) {      
            var utilityNav = new brand.bottomFixed({
                node: $("utilitynav"),
                minTop: brand.view.utilityNav.minTop
            });
        } 
        
        //country chooser drop-up
        var footerMenu = new brand.menu({
            menu: "countries_container",
            target: "countries_hd"
        }); 
    
    },
    
    // IE workaround:
    // Puts an iframe behind the footer in IE so windowed elements (<select>'s) don't show through
    initIELayerFix: function(footer) {
        if (!footer) return;
        var iframe = new Element('iframe', { "id": "footer_iframe", "src": "/includes/blank.html", "frameborder": "0",  "marginwidth": "0", "marginheight": "0", "scrolling": "no" });
        footer.insert({ "top": iframe });
        var footerContent = $("footernav_content");
        var offset = iframe.offsetHeight; 
        // move the footer content up so it's on top of the iframe
        if (footerContent) {
            footerContent.style.marginTop = "-" + offset + "px";
        }
    }
};
    

/*
 * brand.view.heightHandler
 * manage height changes for/caused by absolutely positioned elements
 * @memberOf brand.view
 */
brand.view.heightHandler = { 
    pagetype: null,
    min: 620,
    winh: 0, // re-saved everytime window height changes
    bodyh: 0,
    bodyhOriginal: 0, // onload is saved as original height of body
    bodyhWithoutPanel: 0, // current body height w/out additional height of sliding panel
    offset: 0, // height as offset by footer (calculated on init)
    isCMS: false,
    spacer: null, // node to resize to affect body height
    isLoading: false,
    isResizing: false,
    timer: null,
    isIE6: false,
    hasCMSLayers: false,
    
    pagetypeAttributes: {
        "checkout" : { exclude: true },
        "flash_landing" : { fixedScrolling: true, fillsWindow: true }, // ex: home
        "full_window_flash" : { fillsWindow: true }, // ex: custom palette
        "full_window" : { fillsWindow: true }, // ex: full window image
        "flash_browser" : { excludeOnload: true, excludeOnResize: true }, // contain their own resize functions // NOTE: excludeOnResize USED TO JUST be color_play, now we're trying OTHER PRODUCT BROWSERS HERE
        "locator" : { excludeOnload: true }
    },
    
    init: function() {
        // get page type
        if (this.pagetype) { 
            type = this.pagetype;
        } else {
            try {
                this.pagetype = page_data.panel_nav["default"].id;
            }
            catch (e) { };
        }
        var pageAttributes = this.pagetypeAttributes[this.pagetype];
//console.log("heighthandler: this.pagetype = "+this.pagetype);
        
        this.isCMS = (page_data ? page_data.cms_generated : null);
        var cmslayers = $$("#main_content_td .cms_layer");
        this.hasCMSLayers = !!cmslayers.length;
        var useSetCMSHeightContainer = $$("#main_content_td .set-cms-container-height")[0];
        if (this.hasCMSLayers && useSetCMSHeightContainer) {
            this.setCMSHeight(useSetCMSHeightContainer, cmslayers); // ex: newsworthy
        } else if (this.isCMS || this.hasCMSLayers) {
            this.cmsCleanup(cmslayers);
        }
        
        if (pageAttributes && pageAttributes.exclude) { return; }
        if (!$("globalnav_container")) { return; } // ignore pages that don't have the global nav
        
        var spacer = $("column_spacer");        
        if (generic.env.isIE6) {
            this.isIE6 = true;
            var colorNav = $("color_nav_td");
            spacer = (colorNav ? colorNav : spacer); // if no color nav, don't set its height 
        }
        this.spacer = spacer;

        // get offset for footer
        var fnnode = $("footernav");
        this.offset = (fnnode ? fnnode.offsetHeight : this.offset); 
        
        // ensure that winh doesn't result in negative value later
        this.winh = this.getWindowHeight();
        if (this.winh <= this.offset) {
            this.winh = (this.offset * 2);
        }
        
        // SS NOTE: home was being excluded here, so put in equivalents, but not sure why it was being excluded
        if (!pageAttributes || (pageAttributes && !pageAttributes.fillsWindow)) {
            this.bodyhOriginal = document.body.scrollHeight; 
            this.bodyh = this.bodyhOriginal; 
        }
        
        this.onLoad(); 

        // handlers for events that can change window/body size
        var self = this;
        generic.events.observe("accordion:open", function(event) {
            self.onNavChange("show", event);
        });
        generic.events.observe("panelnav:show", function(event) {
            self.onNavChange("show", event);
        });
        generic.events.observe("panelnav:hide", function(event) {
            self.onNavChange("hide", event);
        });
        if (!pageAttributes || (pageAttributes && !pageAttributes.excludeOnResize)) {
            Event.observe(window, "resize", function() {
                self.onResize();
            });
        }
        
        this.isLoading = false;        
    },

    onLoad: function() {
        //console.log("heighthandler.onLoad");
        var pageAttributes = this.pagetypeAttributes[this.pagetype];
        if (pageAttributes && pageAttributes.excludeOnload) {
            if (!this.isIE6) {
                //console.log("heighthandler.onLoad: excluding");
                return;
            } else if (!$("color_nav_container")) {
                return;
            }
        }
        this.isLoading = true;        
        var h = (this.winh > this.min) ? this.winh : this.min; 
        var spacer = this.spacer;
  
        // home page: set height of spacer column to match window
        if (pageAttributes && pageAttributes.fillsWindow) {  
            h = (this.winh - this.offset);
            this.spacer = spacer = $("main_content");
            spacer.style.height = h + "px";
            
        // default: for sub pages
        } else {  
       
            // if body is shorter than window, set height to match window
            if (!this.hasCMSLayers && (h >= this.bodyh)) {    
                //console.log("body shorter than window");
                spacer.style.height = h + "px";  
                //cms content handled by CMSCleanup
                
            // IE6: cases where taller body requires explicitly setting spacer height 
            } else if (this.isIE6) {
                var colorNav = $("color_nav");  
                if (colorNav) { 
                    var maxh = (2800 > this.bodyh) ? 2800 : this.bodyh;
                    colorNav.style.height = this.bodyh + "px";
                    setTimeout(function() {
                        colorNav.style.height=maxh+"px";
                    }, 2000);  
                          
                }
                
                spacer.style.height = this.bodyh + "px"; 
            
            // for flash browsers, set spacer height to at least window initially, since flash could resize itself to shorter than window w/out triggering the resize event
            //} else if (this.pagetype === "flash_product_browser") {
            //    spacer.style.height = h + "px";
            }
        }

        this.bodyhOriginal = this.bodyh = this.bodyhWithoutPanel = h;
    },

    onNavChange: function(action, args) {
        var pageAttributes = this.pagetypeAttributes[this.pagetype];
        //if (pageAttributes && pageAttributes.fixedScrolling) return;
        //console.log("brand.view.heightHandler: onNavChange");
        
        if (this.isResizing) { return; }
        var type = args.type;
        var parentId = args.parentId;
        // ignore gnav accordion
        if (parentId === "globalnav_container" && type === "accordion") {
            return;
        }              
        var spacer = this.spacer;
        var id = args.id;
        if (type === "panel") {
            this.activePanelId = id;
        }
        
        var panel = $(this.activePanelId);
        if (!panel) { return; } // ex: open panels
        this.isResizing = true; // set isResizing after all non-returnable conditions met
        var self = this;
                
        //console.log("/panelnav/event/"+action+", id = "+id+" panel to check = "+this.activePanelId+" type = "+type+" panel node = "+panel+" parent id = "+parentId);
    
        // wait for animation to finish
        var pause = function() {
            if (action === "hide" && type === "panel") {
                var h = self.bodyhWithoutPanel;
                // compare pre-panel open body height against window height
                if (h < self.winh) {
                    h = self.winh;
                }
                //console.log("CLOSE: h = "+h+" self.bodyhWithoutPanel = "+self.bodyhWithoutPanel);
                spacer.style.height = h + "px";
                self.bodyh = h;
            } else if (action === "show") {
                var panelh = panel.offsetHeight;
                // compare against window & last known body height
                // (getting current body height here not consistent across browsers)
                if (panelh > self.winh && panelh > self.bodyh) {
                    spacer.style.height = panelh + "px";
                    self.bodyh = panelh;
                }           
            }
            self.isResizing = false;
        }
        setTimeout(pause, 600); // note: should be equal to or greater than duration of open/close Panel sliding       
    },
    
    onResize: function() {
        if ((this.isResizing || this.isLoading) && !this.isIE6) { return; }
        var winh = this.getWindowHeight();
        var bodyh = this.bodyh;        
        var page = this.pagetype;
        var pageAttributes = this.pagetypeAttributes[page];
        if (winh > bodyh) {  
            this.doResize(page, winh);
        } else if (pageAttributes && pageAttributes.fillsWindow) { // page is supposed to fill window & winh is less than bodyh
            if ((winh < this.min) && (this.min > bodyh)) { 
                this.doResize(pageAttributes, this.min);
            }
            
        // workaround for IE6: when contents in main body change dynamically (ex: elements load/display after page load), then we have to get/set height again
        } else if (this.isIE6) {            
            bodyh = document.body.scrollHeight;
            if (bodyh != this.bodyh) {
                this.doResize(page, bodyh);
                this.bodyh = bodyh;
            }
        }
        this.winh = winh;
    },
    
    doResize: function(pageAttributes, elementh) {
        var timer = this.timer;
        var spacer = this.spacer;
        var offset = this.offset;
        var h = elementh;
        var self = this;
        var resize = function() {
            if (pageAttributes && pageAttributes.fillsWindow) { h = (elementh - offset); }
            spacer.style.height = h + "px";
            self.bodyh = self.bodyhWithoutPanel = h;
        };      
    
        // avoid IE resize recursion
        if (this.isIE6) {
            if (timer) clearTimeout(timer);
            timer = setTimeout(resize, 300);
        } else {
            resize();
        }     
    },
    
    getWindowHeight: function() {
        var h;
        if (typeof window.innerHeight !== 'undefined') {
            h = window.innerHeight;
        } else {
            h = document.documentElement.clientHeight;
        }
        return h;
    },
    
    setCMSHeight: function(container, cmslayers) {
        var h = 0;
        var nodes = cmslayers;
        if (cmslayers.length==0) { return 800 }; /** cms_generated flag inaccurately set **/
        var thisNodeBottom;
        var lastNode = cmslayers[0];
        var lastNodeBottom = 100; // arbitrary non-zero min
        // figure out which node is last on page
        cmslayers.each(function(node, ix) { 
            thisNodeBottom = parseInt(node.style.top) + parseInt(node.style.height);
            if (lastNode && lastNodeBottom) {
                if (lastNodeBottom < thisNodeBottom) {
                    lastNodeBottom = thisNodeBottom;
                    lastNode = node; 
                }
            }
        });
        // use top+height value of last node to set height for whole container
        container.style.height = (lastNodeBottom + 40 + "px"); // add 40 slop
    },
    
    cmsCleanup: function(cmslayers) { 
        var cmsBlocks = []; 
        cmslayers.each(function($_) {  
            pushNew(cmsBlocks,$_.ancestors()[0]);
        });
        cmsBlocks.each(function($_) { 
            if (!$_.hasClassName("noCMSCleanup")) {
                parseCMSLayers($_);
            }
        });
        console.log("heightHandler.cmsCleanup");
      
        function parseCMSLayers(parentDiv) { 
            var moved = false; 
            if (parentDiv.hasClassName("hidden")) {  
                moved = true;
                parentDiv.origLeft = parentDiv.style.left;  
                parentDiv.style.left = "-5000px";
                parentDiv.removeClassName("hidden");
            }
            var input = parentDiv.select(".cms_layer");  
            var output = input.sort(function(a,b){return parseInt(a.style.top) - parseInt(b.style.top)}); 
            
            var outputRows = [];
            var outputFinal = [];
            outputRows[0] = [];
            var oIndex = 0;
            var previousTop = 0;
        
            for (var i=0;i<output.length;i++) { 
                output[i].style.height = "auto";  
                output[i].cmsTop = parseInt(output[i].style.top); 
                output[i].actualHeight = parseInt(output[i].clientHeight);   
                output[i].impliedTopMargin = (i==0) ? output[i].cmsTop : output[i].cmsTop - output[i-1].cmsTop - output[i-1].actualHeight; 
                //console.log(output[i].id + " " + output[i].actualHeight);
                previousTop = (i==0) ? output[i].style.top : output[i-1].style.top;
                
                if (output[i].style.top==previousTop) {//same row
                     
                } else {//next row
                    outputRows[oIndex] = outputRows[oIndex].sort(function(a,b){return  parseInt(a.style.left) -  parseInt(b.style.left)});    
                    oIndex++; 
                    outputRows[oIndex] = [];
                }
                outputRows[oIndex].push(output[i]);
            }
            outputRows[oIndex] = outputRows[oIndex].sort(function(a,b){return  parseInt(a.style.left) -  parseInt(b.style.left)}); 
      
            var o = {}; var css = ""; 
            var adjust = (parentDiv.id == "main_content_td" ) ? 476 : 0;
            for (var i=0;i<outputRows.length;i++) { 
                for (var j=0;j<outputRows[i].length;j++) { 
                    o = outputRows[i][j]; 
                    css = "position:relative;"
                    css += "width:" + o.style.width + ";";  
                    css += "height:" + o.actualHeight + "px;";
                    css += "margin-left:" + (parseInt(o.style.left)-adjust) + "px;";
                    css += "margin-top:" + o.impliedTopMargin + "px;"; 
                    o.style.cssText = css;  
                    parentDiv.appendChild(o);
                } 
            }   
     
            if (moved) { 
                 parentDiv.addClassName("hidden")
                 parentDiv.style.left = parentDiv.origLeft; 
            }  
        }
    
        function pushNew(arr,o) {  
            var n = true;
            for (var i=0;i<arr.length;i++) {
                if (arr[i]==o) {n = false;break;}
            }
            if (n) arr.push(o);   
        }      
       
        try {
            var colorNav = (generic.env.isSafari) ? $("color_nav_standalone") : $("color_nav");  
            colorNav.style.height = ($("main_table").scrollHeight + $("footernav").clientHeight + 50) +  "px"; 
        } catch(e) {}
    }   
};
brand.view.home = {
	leftoffset: 194,
    init: function() {
        var flashPlaceholder = $("main_bkg_div");
        if(generic.env.isIE6)
           var containerNode = $("homepage_flash_container");
        else
           this.containerNode = $("homepage_flash_container");
        
        // ipad version of home
        if (global.isipad) {
            this.initJSSlideshow();
            return;
        } // end: ipad version of home

        if (!flashPlaceholder) return;
        brand.view.heightHandler.pagetype = "flash_landing"; // assumes 'position: fixed' scrolling for regular home page
        var leftoffset = (generic.env.isIE6 ? 196 : 194); // IE6 Hack: leave room to add invisible borders
        // alternate type for content that needs to scroll normally
        //brand.view.heightHandler.pagetype = "flash_browser";
                
        if (generic.env.isFF) { 
            // hack: force main flash to wait for color strip to load
            brand.view.home.embedSplash.delay(0.5);
        } else {
            brand.view.home.embedSplash();

            // mimic position:fixed for ie6
            var setBottom = function() {
                var fixedFlashTest = new site.bottomFixed({
                    node: brand.view.home.containerNode,
                    bottom: 18, // height of footer
                    startingTopPosition: 0,
                    observeResize: false
                }); 
            };
            if (generic.env.isIE6 && brand.view.heightHandler.pagetype === "flash_landing") {
                setBottom.delay(1);
            }
        }
    },
    embedSplash: function() {   
        var containerNode = brand.view.home.containerNode;
        var movieId = "main_bkg";
        var params = { 
            wmode: "transparent", 
            flashvars: {
               "assetDomain"  : "/flash/home_page/assets/",
               "assetsDomain" : "/flash/home_page/assets/",
               "conf_uri" : "/flash/home_page/xml/config.xml",
               "movieName" : movieId
               // "waitlistPath" : "swf/optIn.swf",
               // "optinXmlAddr" : "swf/xml/optIn.xml"
            }
        };

        var attr = {
            id : movieId,
            name: movieId,
            data: "/flash/HomepageSlideshow.swf",
            bgcolor: "#000"
        };

        //generic.flash.embed(attr, params, "main_bkg_div");
        
        // check for flash embed status
        var timerCount = 0; 
        var checkFlashLoading = function() {
            timerCount++;
            // if flash embed worked, set resizing
            if ($("main_bkg")) {
                clearInterval(timer);
                // scrollable home page content        
                //site.view.sizeContainer({ container: containerNode, setHeight: true, //minimumHeight: minh, setWidth: false }); // Venomous Villains
                //Event.observe(window, "resize", function() {
                //    site.view.sizeContainer({ container: containerNode, setHeight: true, minimumHeight: minh, setWidth: false });
                //});
                
                // regular full-window slideshow
                site.view.sizeContainer({ container: containerNode, leftoffset: site.view.home.leftoffset, setWidth: true, setHeight: false });
                Event.observe(window, "resize", function() {
                    site.view.sizeContainer({ container: containerNode, leftoffset: site.view.home.leftoffset, setWidth: true, setHeight: false });
                });
            } else if (timerCount > 2) {
                // if flash embed failed, use js slideshow
                clearInterval(timer);
                brand.view.home.initJSSlideshow();
                console.log("loading hp slideshow instead of flash, timerCount");
            }  
        }
        
        // give flash embed JS time to modify the DOM before checking for the re-rendered html
        var timer = setInterval(checkFlashLoading, 100);
        checkFlashLoading();
        
    },
    
    // non-flash version of slideshow
    initJSSlideshow: function() {
        brand.view.heightHandler.pagetype = (global.isipad ? "full_window" : "image_landing");
        var overlayMessage = $("fss-overlay-container");
        var noFlashPlaceholder = $("homepage-noflash-content");
        var previousScaleState;

        var scaleSlideshow = function(slidesNode, noFlashContainer, event) {
            if (generic.env.isIE6) { // workaround for IE6 loading contents at 0
                if (generic.env.isIE6 && event === "load") {
                    var mainContentHeight = $("main_content").style.height;
                    if (mainContentHeight) mainContentHeight = parseInt(mainContentHeight, 10);
                    noFlashContainer.style.height = (mainContentHeight && (!isNaN(mainContentHeight)) ? mainContentHeight+"px" : "100%");
                } else {
                    noFlashContainer.style.height = "100%";
                }
            }
            var viewportSize = document.viewport.getDimensions();
            var viewportWidth = (viewportSize.width - site.view.home.leftoffset);
            var viewportHeight = viewportSize.height;
            var scaleState = "full-width";
            var classToRemove = "slide-images-full-height";
            if (viewportWidth < (viewportHeight * 1.5)) { // 1.5 = image w:h ratio
                classToRemove = "slide-images-full-width";
                scaleState = "full-height";
            }
            //alert("viewportWidth = "+viewportWidth+" viewportHeight = "+viewportHeight+" scaleState = "+scaleState + " previousScaleState = "+previousScaleState+" event = "+event);
            if (scaleState !== previousScaleState) {
                slidesNode.addClassName("slide-images-" + scaleState);
                slidesNode.removeClassName(classToRemove);
                //console.log("adding class "+scaleState);
            }
            previousScaleState = scaleState;
        }

        var displayContent = function() {
            var noFlashContainer = $("front-slideshow-noflash"); // html container for pc (non-ipad) version
            var slidesNode = $("front-slides");
            if (!slidesNode) return;

            if (noFlashContainer) {
                noFlashContainer.style.display = "block"; 
                var containerNode = brand.view.home.containerNode;
                if (containerNode) containerNode.style.width = "100%"; // for non-flash slideshow, set width 100% on parent container
            }        
            // init slideshow
            if (typeof Slideshow !== "undefined" && slidesNode) {

                if (slidesNode) {
                  var progress = new brand.progress({
                      containerNode: slidesNode,
                      progressNode: $('slideshow-loading')
                  });
                  progress.start();
                }

                new Slideshow('front-slides', {
                    fadeDuration: 1,
                    delay: 7,
                    pauseBeforeNext: 1, // pause before next frame loads. gives time for last frame to fade completely or partially out
                    childrenToAnimate: {
                        className: "slide-image-logo",
                        delayShow: 2.2, // amount of time (in seconds) to delay fade in after main slide fades in
                        delayHide: 2, // amount of time (in seconds) to delay fade out before main slide fades out
                        fadeDuration: 1
                    },
                    onEnd: function() {
                      if(progress) {
                        progress.clear();
                      }

                      // display fss overlay
                      if(overlayMessage) {
                        brand.overlay.launch({ foregroundNode: overlayMessage, displayInline: true, removeOnHide: false });
                      }
                    }
                    
                });
            } else {
                // display fss overlay
                if(overlayMessage) {
                  brand.overlay.launch({ foregroundNode: overlayMessage, displayInline: true, removeOnHide: false });
                }
            }
            
            // scale images for variable pc window size
            if (!global.isipad) {
                Event.observe(window, "resize", function() {
                    scaleSlideshow(slidesNode, noFlashContainer, "resize");
                });
                scaleSlideshow(slidesNode, noFlashContainer, "load");
            }
        }
        
        // check if slideshow html needs to be loaded
        if (global.isipad || $("front-slideshow-noflash")) {
            // pc/non-flash: tmpl wouldn't already be loaded if the page's default/1st choice is to use the flash
            // ipad: tmpl include already loaded via perl
            displayContent(); // go ahead and display slideshow if html is ready    
        } else if (noFlashPlaceholder) {
            // grab slideshow html (Note: deferring here avoids loading slideshow content in background if flash is in effect)
            var linkNode = $("homepage-noflash-content-path");
            var tmplPath = (linkNode ? linkNode.href : null);
            if (!tmplPath) return;
            var req = new Ajax.Request(tmplPath, {
                method:'get',
                onSuccess: function(transport) {
                    var response = transport.responseText || "no response text";
                    noFlashPlaceholder.style.display = "block";
                    noFlashPlaceholder.update(response);
                    displayContent();
                },
                onFailure: function() {
                    console.log("initJSSlideshow: Error loading " + tmplPath);
                }
            });
        }
    }
};  

brand.view.collectionBrowser = {
    init: function() {  
        //Looks Collection
        //brand.view.collectionBrowser.embedLooksSwf(); 

        //Picks Collection
        brand.view.collectionBrowser.embedPicksSwf();
        
        //What's New Collections
        brand.view.collectionBrowser.embedWhatsNewMedia();
        
        brand.view.collectionBrowser.embedLooksSlideshow();
    }, 
    embedLooksSwf: function() { 
        if (!$("looks_flash")) return;
        
        var placeholderNode = $("flash_placeholder");
        if (!placeholderNode) return;
        var movieId = "looksBrowser"; 
        var params = { 
			bgcolor: "#000000",
			flashvars: {
				conf_uri: "/flash/looks/xml/" + placeholderNode.getAttribute("conf_uri") + ".xml",
				json_cat_id: placeholderNode.getAttribute("json_cat_id"),
                movieName : movieId
            } 
        };
    
        var attr = {
            id: movieId,
            name: movieId,
            data: "/flash/_looks_browser/looksBrowser.swf",
            width: 460,
            height: 370 
        };
    
        generic.flash.embed(attr, params, "flash_placeholder");
    }, 
    embedPicksSwf: function() { 
        if (!$("picks_flash")) return;

        var placeholderNode = $("flash_placeholder");
        if (!placeholderNode) return;
        var movieId = "picksBrowser";
		var params = {  
			bgcolor: "#000000",
			flashvars: {
				conf_uri: "/flash/picks/xml/" + placeholderNode.getAttribute("conf_uri") + ".xml",
				json_cat_id: placeholderNode.getAttribute("json_cat_id"),
				movieName: movieId
			}
        };

        var attr = {
            id: movieId,
            name: movieId,
            data: "/flash/_picks_browser/picksBrowser.swf",
            width: 460,
            height: 370
        };

		generic.flash.embed(attr, params, "flash_placeholder");
    },
    embedLooksSlideshow: function() {  
        //console.log("brand.view.collectionBrowser.embedLooksSlideShow: "+page_data.catalog.mpp.media.looks);  
        var looksContainer = $("collection_looks");
        if (!looksContainer || !page_data.catalog.mpp.media.looks) return;   
         
        var slideshow = new brand.slideshow({ 
            loop: true,
            looks: page_data.catalog.mpp.media.looks,
            slide: $("slideshow_slide"), 
            header: $("slideshow_header"),
            link: $("slideshow_link"),
            nav: { left: $("slideshow_prev"), right: $("slideshow_next")} 
        });
            
        looksContainer.removeClassName("hidden"); 
    },
    
    embedWhatsNewMedia: function() {
        //check for flash_p for cases when img instead of swf
        var placeholderNode = $("flash_placeholder");
        if (!placeholderNode) {
            // init slideshow if relevant html found
            if (typeof Slideshow !== "undefined" && $("collection-slides")) {
                new Slideshow('collection-slides', 2);
            }        
            return;
        }
        if (!page_data.catalog.flash_display_order) return;

        var movieId = "collectionBrowser";
        var params = {  
            bgcolor: "#000000",
            flashvars: {
                conf_uri: "/flash/collection_browser_example/xml/config.xml",
                json_cat_id: placeholderNode.getAttribute("json_cat_id"),
                display_order: page_data.catalog.flash_display_order.join(","),
                movieName: movieId
            }
        };

        var videoHeightAttribute = placeholderNode.getAttribute("videoHeight");
        var videoAttribute = placeholderNode.getAttribute("video");
        if (videoHeightAttribute && videoAttribute) {
            params.flashvars.videoHeight = videoHeightAttribute;
            params.flashvars.video = videoAttribute;
        }
    
        var attr = {
            id: movieId,
            name: movieId,
            data: "/flash/_collection_browser/collectionBrowser.swf",
            width: 475,
            height: 375
        };  
        
        generic.flash.embed(attr, params, "flash_placeholder"); 
    }
};
    
brand.view.productBrowser = {
	attr: {
		id : "productBrowser",
		name: "productBrowser",
		bgcolor: "#000000" 
	},
	init : function() {   
		// specify this to keep global height handler from changing height 
		brand.view.heightHandler.pagetype = "flash_browser";
        var leftoffset = 210;
        var useSizeContainer = true;
        
        var container = $("productBrowser_resize");
        if (container.hasClassName("colorPlay")) this.embedColorPlay();
        if (container.hasClassName("brushPlay")) this.embedBrushFinder();
        if (container.hasClassName("mascaraFinder")) this.embedMascaraFinder();
        
        if (container.hasClassName("foundationFinder")) {
            this.embedFoundationFinder({ container: container });
            useSizeContainer = false;
        }  
        
        if (container.hasClassName("backstage")) {
            this.embedBackstage({ container: container });
            useSizeContainer = false;
        }
          
        if (useSizeContainer) {
            brand.view.sizeContainer({ container: container, leftoffset: leftoffset });
            
            Event.observe(window, "resize", function() {
                site.view.sizeContainer({ container: container, leftoffset: leftoffset });
            }); 
        } 
	}, 
    embedColorPlay: function() {    
		var params = { 
			flashvars: {
				conf_uri : "/flash/color_play/xml/config.xml",
				colorplaysample: generic.env.query("colorplaysample") || "",
				filters: generic.env.query("filters")  || "",
				v: "20090402",
                movieName: this.attr.id
			} 
		};
		var attr = this.attr;
		attr.data = "/flash/_product_browser/productBrowser.swf"; 
        generic.flash.embed(attr, params, "flash_placeholder");  
	}, 
    embedBrushFinder: function() {   
		var params = { 
			flashvars: {
				"conf_uri" : "xml/config.xml",
                "movieName": this.attr.id
        	}
        };
		var attr = this.attr;
        attr.data = "/flash/_guide_browser/guideBrowser.swf";
        generic.flash.embed(attr, params, "flash_placeholder"); 
	}, 
	embedMascaraFinder: function() { 
		var params = { 
			wmode: "opaque", 
			flashvars: {
				"conf_uri" : "/flash/mascara_finder/xml/config_mascara.xml",
                "movieName": this.attr.id
			}
		};
		var attr = this.attr;
        attr.data = "/flash/_guide_browser/guideBrowser.swf";
        generic.flash.embed(attr, params, "flash_placeholder"); 
	}, 
    embedFoundationFinder: function(args) {
        var params = {
            allowScriptAccess: "always",
            flashvars: {
                "conf_uri" : "/flash/foundation_finder/xml/config.xml",
                "style" : 1,
                "movieName": this.attr.id
            }
        };
        var attr = this.attr;
        attr.data = "/flash/foundation_finder/foundation_finder.swf";
        generic.flash.embed(attr, params, "flash_placeholder");
        
        this.setHeight({ h: 600, container: args.container });
        Event.observe(window, "resize", function() {
            site.view.productBrowser.setHeight({ h: 600, container: args.container });
        });
    },
    embedBackstage: function(args) {
        var params = {
            flashvars: {
                "conf_uri" : "/flash/backstage/xml/config.xml",
                "startCity": generic.env.query("city"),
                "movieName": this.attr.id
            }
        };
        var attr = this.attr;
        attr.data = "/flash/backstage/index.swf";
        generic.flash.embed(attr, params, "flash_placeholder");
        
        this.setHeight({ h: 670, container: args.container });
        Event.observe(window, "resize", function() {
            site.view.productBrowser.setHeight({ h: 670, container: args.container });
        });
    },
    setHeight: function(args) {   
        //console.log("productBrowser.setHeight args = ",args);
        //console.log("brand.view.heightHandler offset = "+brand.view.heightHandler.offset);
        try { 
            var contentHeight = args.h;
            var container = args.container;
            var containerMinHeight = args.containerMinHeight;
            var viewportHeight = document.viewport.getHeight();  
            var newHeight = (contentHeight > viewportHeight) ? contentHeight : viewportHeight;
            if (containerMinHeight) {
                newHeight = (containerMinHeight > newHeight) ? containerMinHeight : newHeight;
            }
            newHeight = (newHeight - site.view.heightHandler.offset) + "px"; 
            //console.log("productBrowser.setHeight: "+args.h + "/" + containerMinHeight +"/"+ viewportHeight + "/ " + newHeight);
            
            container.style.height = newHeight;    
        } catch(e) {
            console.log("productBrowser.setHeight e: "+e.description);
        }
    }
};

brand.view.artists = { 
    createRollOvers: function() {
        //console.log("brand.view.artists.createRollOvers");
        var last_popup;
        $$("img.artists-rollover").each( function(elem) {
            elem.observe("mouseover", function(e) {
                var popup_image = new Element("img");
                popup_image.className = "artist_pic";
                var popup_image_name = e.target.src;
                popup_image_name = popup_image_name.replace('114x114', '140x130_on');
                popup_image_name = popup_image_name.replace('jpg', 'png');
                popup_image.src = popup_image_name;
                popup_image.style.position = "absolute";
                if ( last_popup ) {
                    last_popup.style.display = "none";
                }
                popup_image.style.display = "block";
                
                // var img_coords = dojo.coords(e.target);
                var img = e.target; 
                /* 130 = height of replacement, 140 = width of replacement */
                popup_image.style.top = (img.positionedOffset().top - ((130 - img.getHeight())/2) + 2) + "px";
                popup_image.style.left = (img.positionedOffset().left - ((140 - img.getWidth())/2)) + "px";
                
                popup_image.observe("mouseout", function(e) {
                    e.target.style.display = "none";
                    e.target.parentNode.removeChild(popup_image);
                });
                
                e.target.parentNode.appendChild(popup_image);
                last_popup = popup_image;
            });
        });
    } 
};

brand.view.fromourlips = {
    init: function() {
        brand.view.heightHandler.pagetype = "flash_landing";

        // hack: force main flash to wait for color strip to load 
        if (generic.env.isFF) {
            brand.view.fromourlips.embedSplash.delay(3);
        } else {
            brand.view.fromourlips.embedSplash();
        }

    },
    embedSplash: function() {
        var params = {
            wmode: "opaque",
            flashvars: {
				"assetsDomain" : "/flash/vivaglam_201002/assets/",
				"conf_uri" : "/flash/vivaglam_201002/xml/config.xml",
                "movieName": this.attr.id
            }
        };

        var attr = {
            id : "main_bkg",
            name: "main_bkg",
            data: "/flash/vivaglam_201002/index.swf",
            bgcolor: "#FFFFFF"
        };

        generic.flash.embed(attr, params, "main_bkg_div");
    }
};


// Handle width setting for flash containers where needed
// (Height handled from site-wide brand.view.heightHandler unless specified to set height from here)
brand.view.sizeContainer = function(args) {
    var leftoffset = args.leftoffset || 190;
    var container = args.container;
    var setWidth = args.setWidth || false;
    var setHeight = args.setHeight || false;
    try {
        var viewportSize = document.viewport.getDimensions();
        if (setWidth) {
            var viewportWidth = viewportSize.width - leftoffset;
            container.style.width = viewportWidth + "px";
        }
        if (setHeight) {
            var viewportHeight = viewportSize.height;
            var minimumHeight = args.minimumHeight || 400; // arbitrary
            if (viewportHeight < minimumHeight) {
                container.style.height = "" + minimumHeight + "px";
            } else {
                container.style.height = "100%";
                $("main_content").style.height = (viewportHeight - site.view.heightHandler.offset) + "px";
            }
        }        
    } catch(e) {
        console.log("sizeContainer e: "+e);
    }
};
/** 
MK: some of this should move to /js/site/coremetrics/, especially the parts with hardcoded catprodskus  
**/

brand.coremetrics = {
 	abort: false,
 	init: function() {
 		if (this.abort) return; 
 		this.liveperson.track();
 		this.panelNav.track();
 		//this.livepopupTrack();
 		
 	},
 	livepopupTrack: function() {
 	   document.observe("livepopup:click", function(event) {
  		cmCreateConversionEventTag("Live Chat", "1", "ASK AN ARTIST", "10");
	   });	
 	}
 }
 /**
 MK: should be initialized from /js/site/init.js 
 document.observe("dom:loaded", brand.coremetrics.init );    
 **/
 /**
 brand.coremetrics.liveperson = {  
          tagConfig : {},
	track: function() { 
	 this.tagConfig = {
              'lpServer' : "service.liveperson.net",
              'lpNumber' : "24631554",
              'lpProtocol' : pageVar.reporting["scheme"]
            }, 
            this.addMonitorTag();
            
            if (typeof(this.tagConfig.sessionVar) == "undefined"){ 
                   this.tagConfig.sessionVar = [];
        	  }
            this.tagConfig.sessionVar.push('skill=MAC');
 	},
	addMonitorTag: function(src) {
            if (typeof(src)=='undefined'||typeof(src)=='object') {
                src = this.tagConfig.lpMTagSrc ? this.tagConfig.lpMTagSrc : '/hcp/html/mTag.js';
            }
            if (src.indexOf(pageVar.reporting["scheme"])!=0) {
                src = this.tagConfig.lpProtocol+"://"+this.tagConfig.lpServer+src+'?site='+this.tagConfig.lpNumber;
            }
            else {
                if(src.indexOf('site=')<0) {
                   src = (src.indexOf('?')<0) ? src+'?' : src+'&';src=src+'site='+this.tagConfig.lpNumber;
                }
             } 
             var s=document.createElement('script');
             s.setAttribute('type','text/javascript');
             s.setAttribute('charset','UTF-8');
             s.setAttribute('src',src);
             //document.getElementsByTagName('head').item(0).appendChild(s);  
	}   
 }
 **/
 
 brand.coremetrics.panelNav = {
    cm_map : {}, // combined map of all cmcats on all nav categories
    cm_corrected_cat : {}, // map of ids to cmcats
    
    track: function() {  
        //this.createMap();
 
        document.observe("panelnav:show", function(event) {
  		console.log("brand.coremetrics.panelNav.track: show / " + Object.toJSON(event.memo));
  		if (event.memo.type === "panel"){
                       //brand.coremetrics.panelNav.panelClickOpen(event.memo); 
                	} else if (event.memo.type === "accordion") {
                       //brand.coremetrics.panelNav.open(event.memo); 
                	} 
	});
 
        document.observe("panelnav:hide", function(event) {
  		console.log("brand.coremetrics.panelNav.track: hide / " + Object.toJSON(event.memo));
  	//	brand.coremetrics.panelNav.panelClickClose(event.memo); 
	});
         
		
         /*  
         // nav event listener 
        dojo.subscribe("/panelnav/event/show", this, function(args) {
            // time out to give panel animation chance to run
            var self = this;
            var t = function() {
                if (args.type === "panel"){
                       self.panelClickOpen(args);
                } else if (args.type === "accordion") {
                       self.open(args);
                }
            }               
            
             setTimeout(t, 600);                      
        });
       if needed for close events
        dojo.subscribe("/panelnav/event/hide", this, function(args) {
            //console.log("/panelnav/event/hide, is passing = "+dojo.toJson(args, true));
            this.panelClickClose(args);
        }); 
        */       
    }/**,  
    createMap: function() {
    	  
    var skipcount = 0;
    var sections = site.globalnav.config.items;
    try {
    for (x in sections) { 
      for (y in sections[x]){
        if (y === "items"){       
           var items = sections[x].items;
          for (var element in items){
            if (typeof(items[element].id)  != "undefined" && typeof(items[element].cmcat) != "undefined"){
              cm_map[items[element].id] = items[element].name;
            }
            if ( !(items[element].id.match("CAT[0-9]*")) ){
              cm_corrected_cat[items[element].id] = items[element].cmcat;   
            }
          }
        }else{
          if (typeof(sections[x].id)  != "undefined" && typeof(sections[x].cmcat) != "undefined"){
            if ( !(sections[x].id.match("CAT[0-9]*")) ){
                  cm_corrected_cat[sections[x].id] = sections[x].cmcat;
                }else{
                    cm_map[sections[x].id] = sections[x].name;
                }
           }
         } 
       } 
    }
     } catch(e) {}
    },  
    open: function(args){
        if (typeof args.parentId != "undefined") {
            var ParentCatID = args.parentId.match("CAT[0-9]*");
            var SubCat = args.id.match("CAT[0-9]*");
            if (cm_map[ParentCatID]){
                cmCreatePageviewTag('MPP : ' + cm_map[ParentCatID]+ ' : '+args.displayName,'',SubCat,'');
            }else{
                ParentCatID = args.id.match("CAT[0-9]*");
                if (typeof cm_map[ParentCatID] != "undefined"){ // this supresses top level Products and Artistry
                    cmCreatePageviewTag('MPP : ' +cm_map[ParentCatID],'',SubCat,'');
                }
            }
        }
        this.ElementTag(args,"open"); 
    },

    close: function(){
        //console.log("Accordian close on ",this.id,this.displayName," called in connected close");
        //site.globalnav.Coremetrics.ElementTag(this,"close");
    },
    
    //top level panel tags other than accordian calls
    panelClickOpen: function(args){

        var s = args.sectionId;
        var d = args.itemId;
        if (s ==="makeup_artistry"){ //inconsitent data don't know what else to do
             s="Makeup Artistry";
        }
        
        if (typeof s != "undefined" && typeof d != "undefined"){
            if (d.match("CAT[0-9]*")){
                cmCreatePageviewTag(s + ' : ' + args.displayName, '',d,'');
            }else{
                cmCreatePageviewTag(s + ' : '+args.displayName, '',cm_corrected_cat[d],'');
            }
        }
        this.ElementTag(args,"open");    
    },

    panelClickClose: function(args){
        //console.log("Panel.js CLICK close panel ",this.id);
        //this.ElementTag(this,"close");
    },

    SubNavChildClick: function(sectionId){
        var d = this.activeItemId.match("CAT[0-9]*");
        var s = sectionId.match("CAT[0-9]*");
        var ParentCatID = this.parentId.match("CAT[0-9]*");
        if (typeof cm_map[d] != "undefined"){
            cmCreatePageviewTag(cm_map[d] + ' : ' + this.id, '',s,'');
        }
    },
    ElementTag: function(args, action){ 
        if (!args.parentId || !args.id) {
            return;
        }
        var ParentCatID = args.parentId.match("CAT[0-9]*");
        var SubCatid = args.id.match("CAT[0-9]*");
        
        var EL_ARGS = 'NAV ';
        if (typeof args.sectionId != "undefined" && args.sectionId !=""){
            EL_ARGS = EL_ARGS +  args.sectionId + ' ';}
        if (typeof cm_map[ParentCatID] != "undefined" && cm_map[ParentCatID] !=""){
            EL_ARGS = EL_ARGS + cm_map[ParentCatID] + ' '; }
        if (typeof  args.displayName != "undefined" && args.displayName  !=""){
            EL_ARGS = EL_ARGS + args.displayName.replace(/\s/g,"_") + ' '; }
        if (typeof cm_map[args.id] != "undefined" && cm_map[args.id] !=""){
            EL_ARGS = EL_ARGS + cm_map[args.id].replace(/\s/g,"_") + ' '; }
        
        EL = EL_ARGS.match(/[\w-'\+]+/g);
        var cmargs="";     
        var inc=2;
        if (EL.length > 1){ 
        inc = 2;}else{
        inc = 1;}
        
        for (i=0;i<=EL.length - inc;i++){
            cmargs = cmargs + EL[i] + ' : ';
        }
        cmargs = cmargs.slice(0,-2);
        cmCreatePageElementTag(EL[EL.length-1],cmargs);
    }**/
}


	 
		
/** generic.flash.ApiMethods
	cmCreatePageviewTag: function(args) {
		//console.log("creating pageview tag: ", args);
		var resp = dojo.global.cmCreatePageviewTag(args[0],args[1],args[2],args[3]);
		//console.log("cm response:");
		return this.response.createResponse(1, "pageview tag created");
	},

	cmCreateManualLinkClickTag: function(args) {
		dojo.global.cmCreateManualLinkClickTag(args[0],args[1],args[2]);
		return this.response.createResponse(1, "link click tag created");
	},
	
	cmCreatePageElementTag: function(args) {
		//console.log("creating pageelement tag: ", args);
		var resp = dojo.global.cmCreatePageElementTag(args[0],args[1],args[2],args[3],args[4]);
		//console.log("cm response:");
		return this.response.createResponse(1, "Page Element tag created");
	},
	cmCreateProductElementTag: function(args) {
		dojo.global.cmCreatePageElementTag(args[0],args[1],args[2],args[3],args[4]);
		return this.response.createResponse(1, "Product Element tag created");
	},
	
	cmCreateProductviewTag: function(args) {
		dojo.global.cmCreateProductviewTag(args[0],args[1],args[2]);
		return this.response.createResponse(1, "productview tag created");
	},

	cmCreateConversionEventTag: function(args) {
		dojo.global.cmCreateConversionEventTag(args[0],args[1],args[2],args[3]);
		return this.response.createResponse(1, "conversion event tag created");
	}, **/

/**
brand.product.SwatchContainer.setSwatch
 // coremetrics page view tag only on add to bag showing 
            var cat = sku.path.match("CAT[0-9]*");
            var prod = sku.path.match("PROD([0-9]*)");               
            // only do product view on mpp page
            var pdcatalog = page_data.catalog;
            if (typeof(pdcatalog) !== "undefined") {
                if (typeof(pdcatalog.spp) === "undefined") {
                    cmCreateProductviewTag(prod[1],this.product.name,cat);
                }
            }
            if (typeof(page_data.featured_goodbyes) !== "undefined"){
                cmCreateProductviewTag(prod[1],this.product.name,cat);
            }
            if (typeof(page_data.reorder) !== "undefined"){
                cmCreateProductviewTag(prod[1],this.product.name,cat);
           }
 **/
 
 /** search 
 
        
        // important, CM search tag 
        var cmcount = data.count || "0";
        cmCreatePageviewTag('search : search results', data.query ,'2000', cmcount);
        **/
     var Analytics = Class.create({
    subscribers : {},
    listeners : {},
    seenMemoWithTag : {},
    isEnabled: false,
    enabledModules: new Array(),
    cm_map : {},
    cm_corrected_cat : {},
    localPath : '',
    macSkipCount : 0,
    prodCatData : {},
    swatchSkip : false,


    RPC_METHODS_ALLOWED: new Hash({
        "prodcat" : 1,
        "generic" : 1,
        "cart" : 1,
        "rpc.form" : 1,
        "search" : 1,
        "email.signup" : 1
    }),

    initialize: function(modules,enabled){
        this._addStaticListeners();
    },
    addPendingTags: function(newTags ){
        this.pendingTags = newTags;
        this.execTags();
        return this;
    },
    addElementEvents: function(newEvents){
        this.elementEvents = newEvents;
        
    },
    _addStaticListeners: function(){
        var self = this;
        document.observe("dom:loaded", function(){
            self.isEnabled = (typeof ANALYTICS_ENABLED != "undefined") ? ANALYTICS_ENABLED : false;
            self.enabledModules = (typeof ANALYTICS_MODULES != "undefined" ) ? ANALYTICS_MODULES : [];

            // CONVERSION EVENTS - If conversion events exist add handler as directed and form input for location
            self.conversionEvents = (typeof CONVERSION_EVENTS != "undefined" ) ? CONVERSION_EVENTS : [];
            if (typeof self.conversionEvents != "undefined"){
                //console.log(self.conversionEvents);
                self.conversionEvents.each(function(CEVENT){
                    console.log("event on ", CEVENT);
                    //_addFrontendEvent: function(myElement,myEvent,tagBlocks){
                      self._addFrontendEvent(CEVENT);

                });

            }
            // console.log( "Analytics enabled: ", self.isEnabled );
            // console.log( "Analytics registered modules: ", self.enabledModules );
            self.localPath = document.location.pathname;

            // I loath this, but events fire user generated or not and something has to supress them on direct urls to SPP, for example

            if ( self.localPath.match("product") ){
                self.macSkipCount = 3; 
            }
            if (self.localPath.match("account") ){
                self.macSkipCount = 1;
            }
            if (self.localPath.match("looks") ){
                self.macSkipCount = 3;
            }
            if (self.localPath.match("artists")){
                self.macSkipCount = 1;
            }
            if (self.localPath.match("category")){
                self.macSkipCount = 3;
            }
            if (self.localPath.match("whats_new")){
                self.macSkipCount = 2;
            }

            if (typeof site != "undefined"){
                if (typeof site.globalnav != "undefined"){
                    self.createMap();
                }
            }
        });


    // Mac special start
    document.observe("panelnav:show", function(event) {
     
        var open_data = event.memo.msg;
       console.log("brand.coremetrics.panelNav.track: show / ",open_data);

        if (open_data.type === "panel"){
          console.log("pannel show");            
           self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":["products : "+ open_data.displayName  ,null,open_data.itemId,null,null],"tag":"cmCreatePageviewTag"}]}});
        } else if (open_data.type === "accordion") {
          console.log("accordion show");
        }
    });

    document.observe("accordion:open", function(event) {
console.log(event);

        if ( self.macSkipCount ){
          self.macSkipCount--;
          return;
        } 
        var open_data = event.memo.msg;
        console.log("brand.coremetrics.Accordion.track: open / ", open_data);
        var parentCat ='' 
        var parentCatName = '';
        var curCat = '';
        var prefix = 'MPP : ';
        if (open_data.parentId === 'globalnav_container'){
            prefix = '';
        }else{
            parentCat = open_data.parentId.match("CAT[0-9]*");
            parentCatName = self.cm_map[parentCat] + " : ";
            curCat = open_data.id.match("CAT[0-9]*")
            console.log("CURRENT CAT ",curCat); 
            console.log("Parent cat",parentCat,"Match ",parentCatName);
        }
        // remove Shop Products and Makeup Artistry because they are special cases 
        if (open_data.displayName != "Nos Produits" && open_data.displayName != "L'Art Du Maquillage"){
            self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[prefix + parentCatName + open_data.displayName,null,curCat,null,null],"tag":"cmCreatePageviewTag"}]}});
        }
    });



    // The below might be removable, but just commented out the tag line for now. 
    // SPECIAL HOOK for Mac catalog.tmpl work around, also note, page_data doesn't have any English names so we have to use results from the RPC not in page_data, saving here.
    document.observe("panel:prodcat", function(event){


       // Save for possible later use 
           self.prodCatData = event.memo.msg.data;
       if ( self.macSkipCount ){
          self.macSkipCount--;
          return;
        }
       var prodcatRequest = event.memo.msg.request;
       var thisCat = '';
       if (self.cm_corrected_cat[prodcatRequest.itemId] != "undefined"){
           thisCat = self.cm_corrected_cat[prodcatRequest.itemId];
 console.log("corrrected cat ",thisCat);
       }else{
           thisCat = prodcatRequest.itemId;
       } 
       //catID = page_data.catalog.collection.match("CAT[0-9]*"); 
       //self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":["products : "+ prodcatRequest.parent.displayName  ,null,prodcatRequest.itemId,null,null],"tag":"cmCreatePageviewTag"}]}});
    
    });



    // this is a lot more specific to when we need a product view tag on a swatch click vs the generic swatch:click event. 
    document.observe("productmessage:cartadd/show", function(event){
       var cur_cat = event.memo.msg.path.match("CAT[0-9]*");
       var cur_prod = event.memo.msg.path.match("PROD([0-9]*)")[0];
       var pdcatalog = page_data.catalog;
       var productName = '';
       if (self.swatchSkip){
           return;
       }
            if (typeof(pdcatalog) != "undefined") {
                if (typeof(pdcatalog.mpp) != "undefined") {
                    if (typeof pdcatalog.mpp.products != "undefined"){
                        for (var i=0; i < pdcatalog.mpp.products.length; i++){
                            if (pdcatalog.mpp.products[i].category_id.match(cur_cat) && pdcatalog.mpp.products[i].product_id.match(cur_prod) ){
                                //console.log("GOT NAME ",pdcatalog.mpp.products[i].cm_name, cur_cat, pdcatalog.mpp.products[i].category_id); 
                                productName = pdcatalog.mpp.products[i].cm_name;
                            } 
                        }    
                    }else{
                        Object.keys(pdcatalog.mpp).each(function(CATEGORY){    
                           var CAT = pdcatalog.mpp[CATEGORY];
                           if (typeof CAT.products != "undefined"){
                               for (var i=0; i < CAT.products.length; i++){
                                   if (CAT.products[i].category_id.match(cur_cat) && CAT.products[i].product_id.match(cur_prod) ){
                                       //console.log("GOT NAME ",CAT.products[i].cm_name);
                                       if (typeof CAT.products != "undefined"){
                                          productName = CAT.products[i].cm_name;
                                       }
                                   }
                               }
                           }
                        });
                    }
                }

            }

           self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[cur_prod,productName,cur_cat ,null,null,null],"tag":"cmCreateProductviewTag"}]}});
       

   });
   
    // Mac special end 

   //Search hook for frontend and soon Endeca
   document.observe("search:results", function(event){
    var res = event.memo.msg;
    self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[res.pageid,res.keywords,res.cat,'"' + res.count +'"',null],"tag":"cmCreatePageviewTag"}]}});
  });

        // our RPC hook
        document.observe('RPC:RESULT', function(obj){
     
            var rpcRequestArray, rpcResponseArray;
            var requestMethod, requestId;
            if (typeof obj.memo.msg.request != "undefined") {
                rpcRequestArray = (obj.memo.msg.request.parameters.JSONRPC != null) ?
                    obj.memo.msg.request.parameters.JSONRPC.evalJSON() :
                    null;

                if (rpcRequestArray) {
                    rpcResponseArray = obj.memo.msg.responseText.evalJSON();
                    if (rpcResponseArray) {
                        rpcRequestArray.each(function(rpcRequest){
                            requestMethod = rpcRequest.method;
                            requestId = rpcRequest.id;
                            // console.log("Analytics handling RPC request:  ", requestMethod, " with id: ", requestId);

                            // We can do special handlers for requests here or filter out non handled requests... 
                            if (!self.RPC_METHODS_ALLOWED.get(requestMethod)) {
                                 //console.log("Analytics skipped ", requestMethod);
                            } else {
                                // Make sure we have the response for this request (id's must match).
                                var myRpcResponse = rpcResponseArray.find(function(rpcResponse){
                                    return rpcResponse.id == requestId
                                });
                                if (myRpcResponse && myRpcResponse.result != null) {
                                    //console.log("Analytics will handle ", myRpcResponse.result.data.Analytics);
                                    var newTags = myRpcResponse.result.data.Analytics;
                                    self.addPendingTags(newTags);
                                }
                            }
                        });
                    }
                }
            }
        });
   

    //Page data already exists hook
        document.observe('PAGEDATA:RESULT', function(obj){
            console.log("GOT PAGE DATA ",obj);
            if (typeof obj.memo != "undefined"){
                var catalog_path = obj.memo.msg;
                if (typeof eval ('page_data.' + catalog_path) != "undefined"){
                    if (prodList =  eval ('page_data.' + catalog_path ) ){
                            console.log("page data from products ", prodList);
                            if (obj.memo.msg.match("mpp") ){
                              for (var i=0; i < prodList.length; i++){
                                if( prodList[i].shaded == 0){
                                 console.log("pending tags ",prodList[i].cm_name,prodList[i].category_id,prodList[i].product_id,prodList[i].shaded ); 
                                 self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[prodList[i].product_id,prodList[i].cm_name,prodList[i].category_id ,null,null,null],"tag":"cmCreateProductviewTag"}]}});
                                }
                              }
                            }    
                            if ( catalog_path.match("spp")){
                                console.log("pending tags SPP"); 
                                console.log("pending tags ",prodList.cm_name,prodList.category_id,prodList.product_id,prodList.shaded,prodList.cross_sell);
                                self.swatchSkip = true;
                                self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[prodList.product_id,prodList.cm_name,prodList.category_id ,null,null,null],"tag":"cmCreateProductviewTag"}]}});
                                if (prodList.cross_sell.length > 0){
                                    for (var i=0; i < prodList.cross_sell.length; i++){
                                        if (prodList.cross_sell[i].shaded==0){
                                             p = prodList.cross_sell[i];
                                             console.log("pending tags cross sell ",p.cm_name,p.category_id,p.product_id);
                                            self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[p.product_id,p.cm_name,p.category_id ,null,null,null],"tag":"cmCreateProductviewTag"}]}});

                                        }
                                    }
                                }
                            }
                    }else{
                        console.log("NO TAG DATA FROM PAGE DATA - catalog_path");
                    }
                }else{
                    console.log("NO TAG DATA FROM PAGE DATA - catalog_path doesn't exist");
                }
            }else{
                console.log("NO TAG DATA FROM PAGE DATA - obj.memo undef");
            }

        });
     },
    


    addDynamicListener: function(taggingModule, myEvent, TagBlocks){
        var self = this;
        var id = "default";
        TagBlocks.each(function(tagBlock){
            if (typeof tagBlock.memo != "undefined") {
                id = tagBlock.memo;
            }
            if (!self.subscribers[id]) { self.subscribers[id] = {}; }
            if (!self.subscribers[id][taggingModule]) { self.subscribers[id][taggingModule] = {}; };
            if (!self.subscribers[id][taggingModule][myEvent]) {
                self.subscribers[id][taggingModule][myEvent] = new Array();
            } else {
                 //console.log( "Event is defined ", self.subscribers[id][taggingModule][myEvent] );
            }

            if (!self.seenMemoWithTag[id]) { self.seenMemoWithTag[id] = {}; }
            if (!self.seenMemoWithTag[id][tagBlock.tag]) {
                self.seenMemoWithTag[id][tagBlock.tag] = 1;
                self.subscribers[id][taggingModule][myEvent].push(tagBlock);
                //console.log( "pushed Event for: id: ", id, " taggingModule: ", taggingModule, " event: ", myEvent, " -> ", self.subscribers[id][taggingModule][myEvent] );
            }

        });

        // Attach the listener for this event type.
        if (!self.listeners[myEvent]) {
            self.listeners[myEvent] = 1;
            //Event.observe(window, myEvent, function(evt){
            document.observe(myEvent, function(evt){
                //console.log("running window event: ", myEvent, " with memo: ", evt.memo);

                var myId = evt.memo;
                self.enabledModules.each(function(taggingModule){
                     //console.log("for tagging module: ", taggingModule);
                    if (typeof self.subscribers[myId][taggingModule] != "undefined"){
                       if (self.subscribers[myId][taggingModule][myEvent]) {
                          self.execEventTagBlocks( self.subscribers[myId][taggingModule][myEvent] );
                       }
                    }
                });
            });
        }
    },
    execTags: function (){
        var self = this;
        if (typeof self.pendingTags == "object") {
            Object.keys(self.pendingTags).each(function(taggingModule){
                if (typeof taggingModule != "object" && self.pendingTags[taggingModule] == 'notag') {
                        return;
                }
                Object.keys(self.pendingTags[taggingModule]).each(function(myEvent){
                    // console.log("Analytics: module / event ", taggingModule, myEvent);
			        if (myEvent != 'dom:loaded'){
                        // register this event with our collection of listeners.
                        self.addDynamicListener(taggingModule, myEvent, self.pendingTags[taggingModule][myEvent]);
                        // and then execute it
                        //self.execEventTagBlocks( self.pendingTags[taggingModule][myEvent] );
                    } else {
                        // incoming tagging events under the 'dom:loaded' label can be executed straightaway.
                        self.execEventTagBlocks( self.pendingTags[taggingModule][myEvent] );
                    } 
                }); 
            });
        }
    },
    execEventTagBlocks: function(tagBlocks){
        tagBlocks.each(function(tagBlock){
            if (!tagBlock.params || !tagBlock.tag) { return; }
                 //console.log( "Analytics.execEventTagBlocks about to execute tag: ", tagBlock.tag, " with params: ", tagBlock.params );
            if (typeof window[tagBlock.tag] == "undefined") {
                 //console.log( "The Tagging Module function is not found: ", tagBlock.tag );
                return;
            }
            window[tagBlock.tag].apply(this, tagBlock.params);
        });
    },

     _addFrontendEvent: function(EVENT){
        var self = this;

        if ( $(EVENT.domID) == null && EVENT.event != "dom:loaded" ){
            console.log("CM ELEMENT does not EXIST! ",EVENT.domID);
            return;
        }
        if (EVENT.event == "dom:loaded"){
            if (EVENT.type == 'conversion_event'){
                 if (EVENT.points < 1){
                      EVENT.points = '"0"';
                 }
                 self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[EVENT.eventID, EVENT.actionType, EVENT.cat, EVENT.points,EVENT.attributes],"tag":"cmCreateConversionEventTag"}]}});
             }
           // other event types
        }else{

            Event.observe(EVENT.domID,EVENT.event, function(evt){
                var cevents;
                console.log("CM Conv events ",evt.target.id);
                 self.conversionEvents.each(function(CEVENT){
                       if (CEVENT.domID == evt.target.id){
                           cevents = CEVENT;
                       }
                    });

                if (cevents.type == 'conversion_event'){
                     if (cevents.points < 1){
                          cevents.points = '"0"';
                     }
                     self.addPendingTags({"CoreMetrics":{"dom:loaded":[{"params":[cevents.eventID, cevents.actionType, cevents.cat, cevents.points,cevents.attributes],"tag":"cmCreateConversionEventTag"}]}});
                     Event.stopObserving(cevents.domID,cevents.event);

                }

            });
        }
    },



    // Possible special funtions for front end (reserved for future use)
    onDivShow: function() {},
    onFrameUpdate: function() {},
    onJsRedirect: function() {},





// More just for Mac, but the createMap func itself or something like it could become more standard for Navs. 
createMap: function() {

    var sections = site.globalnav.config.items;
    
    for (x in sections) {
      for (y in sections[x]){
        if (y === "items"){
            var items = sections[x].items;
            for (var element = 0; element < items.length; element++){
              if (typeof(items[element].id)  != "undefined" && typeof(items[element].cmcat) != "undefined"){
                this.cm_map[items[element].id] = items[element].name;
              }
              if ( !(items[element].id.match("CAT[0-9]*")) ){
                this.cm_corrected_cat[items[element].id] = items[element].cmcat;
              }
            }
        }else{
            if (typeof(sections[x].id)  != "undefined" && typeof(sections[x].cmcat) != "undefined"){
              if ( !(sections[x].id.match("CAT[0-9]*")) ){
                    this.cm_corrected_cat[sections[x].id] = sections[x].cmcat;
                  }else{
                      this.cm_map[sections[x].id] = sections[x].name;
                  }
              }
            }
       }
    }

}




});
Analytics = new Analytics();

/*
 * File: brand.globalnav.GlobalNav
 * Auth: Steph Shindler & Mina Kumar
 */
                                         
/*

Communication up/down hierarchy:
    - GlobalSet: onChildClick called from children: Accordion
    - PanelNavSet: onChildClick called from children: PanelSubNav
    - PanelNavSet & GlobalSet: setActiveItem called from children: PanelNav._setActive (sets ActiveItem for PanelNavSet & GlobalSet AND SETS Accordion.ActiveSubItem)
    - PanelSubNav/ProductSubNav: onParentClick called from parent: PanelNav


1st LEVEL:
-----------------------
2 Accordions & 2 Panel Navs:
    - gnav_products, gnav_artistry
    - pnav_givingback, pnav_mymac ("pnav" instead of "gnav" because these are PanelNav trigger/headers, so their panel gets named after them; pnav_givingback_panel

2nd LEVEL (GENERAL): 
-----------------------
This level can either be a single PanelNav or multiple PanelNavs owned by a PanelNavSet:
    - Option A: 1 PanelNavSet + 1 Panel
        - multiple PanelNavs
    - Option B: 1 PanelNav + 1 Panel
    
Attachment notes:
    - PanelNavSet gets instantiated as a child of the Accordion above it.
    - PanelNav attachment requires 2 things: 
        (1) place the panelnav header/trigger node in the correct place in the DOM = pass PanelNav node to Accordion's addSubItem method which does a DOM insertion
        (2) place the panelnav widget object in its place in the widget hierarchy = pass parentId prop on instantiation which sets widget parent
    - Panels get created by either PanelNavSet._addPanel (if Panel is associated with a set of PanelNavs) or PanelNav._addPanel (if Panel is associated w/ only 1 PanelNav)

3rd LEVEL (GENERAL): 
-----------------------
Each PanelNav (2nd level) has 1 PanelSubNav or an extension of PanelSubNav:
    - PanelSubNav: Div container for panel content associated w/ a single PanelNav link
    - ProductSubNav: container template that includes nodes for ProductCategoryDetail & a category Accordion     
    
*/ 

brand.globalnav = { abort: false };

brand.globalnav.GlobalNav = Class.create(
{
    // summary:
    //      Iterates through site.loader.config hierarchy and applies correct classes
    //      to each level of the nav tree (section, super-category, category and detail/product). 
    //        Compares data with page_data.panel_nav to check for default state of the nav.
    
    config: null,
    _configKeys: {},
    defaultState: {}, // item hierarchy of default panel
    defaultNavCreated: false,
    globalNavSetId: "",

    initialize: function(args) {
        this.config = args.config;
        this.defaultState = args.defaultState;
        this.globalNavSetId = args.globalNavSetId; 
        var self = this;
        
        // fired from panelManager._onClick
        generic.events.observe("panelnav:click", function(event) {
            self.getPanelContent(event);
        }); 
        
        // start loading: iterates through config hierarchy
        this.initSections();
    },
    
    initSections: function() {
        // summary:
        //      Iterates through top-level sections (as passed from config)
       
        var sections = this.config.items;
        var defaultSection = this.defaultState.id;
        var self = this; 
        
        sections.each(function(section, idx) {
            if (!section) return;
            // check for default section
            var isDefaultSection = false; // reset for every loop instance
            if (section.id === defaultSection) {
                isDefaultSection = true;
            }
            self._configKeys[section.id] = { idx: idx, items: {} };
                        
            // section has a set of panel navs
            if (section.items && (section.items.length > 0)) {
                self.initPanelNavSet(section, isDefaultSection);

            // item is a direct link from left nav
            } else if (section.uri && !section.content) {
                self.initHeader({type: "pnav", item: section, isdefault: isDefaultSection, parentId: self.globalNavSetId, domParent: section.domNode, domParentType: "GlobalSet"});
                
            // section has only 1 panel nav
            } else {                
                if (section.id === "search") {
                    self.initSearch(section);
                    
                } else {                  
                    // console.log("GlobalNav.initSections -> initPanelNav");
                    self.initPanelNav({item: section, isdefault: isDefaultSection, domParentType: "GlobalSet"});
                }
            }
                    
        });
    
    },
    
    initPanelNavSet: function(section, isDefaultSection) {  
        
        // summary:
        //      1 Accordion + PanelNavSet to 1 Panel with N num of PanelNavs (i.e. Products)
        //      Displays (a set of) PanelNav links in an Accordion container as top-level/left-hand links
 
        // init Accordion for set
        var accordionContainer = new site.globalnav.Accordion({
            id: "gnav_"+section.id, 
            parentId: section.parentId, 
            domParent: section.domParent,
            pnavsetChild: "pnavset_"+section.id,
            displayName: section.name,
            hdPath: section.header  
        });

        // init PanelNavSet
        var pnavset = new brand.globalnav.PanelNavSet({
            id: "pnavset_"+section.id, 
            parentId: "gnav_"+section.id
        });

        var defaultPnav = this.defaultState.item;
        var defaultPnavId = (defaultPnav ? defaultPnav.id : null);
       
        var self = this;
        // init PanelNavs for this PanelNavSet
        section.items.each(function(pnavItem, idx) {
            if (!pnavItem) return;
            self._configKeys[section.id].items[pnavItem.id] = { idx: idx };
            self.initPanelNav({item: pnavItem, defaultId: defaultPnavId, section: section, domParent: accordionContainer, domParentType: "PanelNavSet"});
        });

        // now that all the left-hand supercategories have been populated, set the state of the gnav accordion
        if (isDefaultSection) {
            accordionContainer.open();
        }
           
    },

    initPanelNav: function(args) {
        var pnavItem = args.item;
        var section = (args.section ? args.section : pnavItem); // if no parent section, this item is a top-level section
        var domParent, pnavDomParent, parentId, headerParentId;

        // get parent child context for top-level vs. second level panel navs
        if (args.domParentType === "GlobalSet") { // top-level item (e.g. giving_back)
            domParent = pnavDomParent = pnavItem.domParent;
            parentId = pnavItem.parentId;
        } else if (args.domParentType === "PanelNavSet") { // second-level item (e.g. whats new) parent is a PanelNavSet/Accordion
            domParent = args.domParent;
            pnavDomParent = domParent.containerNode;
            parentId = "pnavset_" + section.id;
        }
        
        // isDefaultPanel: is PanelSubNav instance associated w/ default category
        var isDefaultPanel = false;
        if (args.isdefault || (!this.defaultNavCreated && (pnavItem.id === args.defaultId))) {
            isDefaultPanel = true;
        }

        // default header args: for regular/href link or isDefaultPanel
        var headerArgs = {type: "pnav", item: pnavItem, isdefault: isDefaultPanel, parentId: parentId, domParent: domParent, domParentType: args.domParentType}
        
        // find cases of items with panel nav content but suppressed gnav links (e.g. studio)
        var showGnavLink = (!pnavItem.suppressGnavLink ? true : false);
        var createPanelNav = ((showGnavLink || isDefaultPanel) ? true : false);
        
        // item is an href link from left nav
        if (pnavItem.uri && !pnavItem.content && showGnavLink) {
            this.initHeader(headerArgs);
        
        //  item has a panel to populate w/ content
        } else {
            
            if (showGnavLink) {
                if (isDefaultPanel) {
                    this.initHeader(headerArgs);
                    
                } else { 
                    //console.log("GlobalNav.initPanelNavSet: create PanelNav "+ "pnav_"+pnavItem.id + " / " +domParent.id);
                    //create PanelNav for the set
                    var pnav = new brand.globalnav.PanelNav({
                        id: "pnav_"+pnavItem.id, 
                        parentId: parentId, // widget parent *NOT* DOM parent
                        domParent: pnavDomParent, // DOM parent 
                        displayName: pnavItem.name,
                        hdPath: pnavItem.header,
                        sectionId: section.id,
                        item: pnavItem
                    });               
                    // now that pnav has a place in the DOM, start it up:
                    pnav.startup(); 
                }
            }
          
            // continue to subnav level
            if (createPanelNav) {
                this.initPanelSubNav(pnavItem, isDefaultPanel, section);
            }

        }
    },    
            
    initPanelSubNav: function(pnavItem, isDefaultPanel, section) { 
        //console.log("GlobalNav.initPanelSubNav "); 
        
        // summary:
        //      Creates each instance of either a PanelSubNav or ProductSubNav
        //      associated with parent PanelNav
        
        var hasLoaded = false;

        // default PanelSubNav args
        var psubnavArgs = {
            id: "psubnav_"+pnavItem.id,
            parentId: "pnav_"+pnavItem.id,  
            isDefaultPanel: isDefaultPanel,
            itemId: pnavItem.id
        }
        // get config for item
        // content/widget config settings will either be at item level (i.e. whats new) or at parent level (i.e. supercategories like eyes have shared settings as a group)
        if (pnavItem.content) {
            psubnavArgs.content = pnavItem.content; // individual level settings
        } else if (section.itemsConfig) {
            psubnavArgs.content = section.itemsConfig.content; // has shared settings
        }
        
        if (pnavItem.hasLoaded) psubnavArgs.hasLoaded = true;
 
        if (pnavItem.id === "discontinued") {
            psubnavArgs.content = pnavItem.content;
            psubnavArgs.templatePath = "jsTemplates.globalnav.DiscontinuedSubNav";
            var psubnav = new brand.globalnav.PanelSubNav(psubnavArgs);
            
        // prodcat sub-sections & looks
        } else if (psubnavArgs.content && psubnavArgs.content.widget === "ProductSubNav") {
            //console.log("GlobalNav.PanelSubNav - prodcat sub-sections & looks"); 
            // create ProductSubNav (extension of PanelSubNav) for the PanelNav
            var psubnav = new site.globalnav.ProductSubNav(psubnavArgs);

        // cms-generated & other nav types
        } else {
            //console.log("GlobalNav.PanelSubNav - cms-generated & other nav types : "+pnavItem.id);          
            var content = pnavItem.content;
            var isCMS = false;
            var isDefaultCMS = false;

            // get psubnav class to use
            var psubnavClass = "PanelSubNav"; // use PanelSubNav by default
            if (content && content.widget) {
                psubnavClass = content.widget;
            }

            // global nav tmp writes out cms html navs for default state (non-prod cat only)
            if (content && content.cms && content.handleAs === "html") {
                //console.log("GlobalNav.PanelSubNav - global nav tmp writes out cms html navs");  
             
                hasLoaded = true;
                isCMS = true;
                
                if (isDefaultPanel) {
                    isDefaultCMS = true;                    
                    // set rollovers & default/detail states in html/cms nav content
                    this.initCMSDisplay({isdefault: isDefaultCMS});
                } 
            }
            
            if (!isDefaultCMS) { 
                //console.log("GlobalNav.PanelSubNav - !isDefaultCMS : psubnavArgs = ",psubnavArgs); 
                psubnavArgs.content = content;
                var psubnav = new site.globalnav[psubnavClass](psubnavArgs);                
            }
        }


        // render default panel content
        if (isDefaultPanel && !hasLoaded) {
            //console.log("GlobalNav.PanelSubNav - render default content for "+pnavItem.id); 
            this.getPanelContent({psubnav: psubnav, sectionId: section.id, item: pnavItem});
        }
    },
    
    getPanelContent: function(args) { 
        //console.log("GlobalNav.getPanelContent "); 
        // summary:
        //      Handles request for file (as specified in config) containing content
        //      for panel (PanelSubNav) associated with a given left-hand nav link

        var psubnav = args.psubnav;
        var self = this;
        if (psubnav.id === "psubnav_account") {
            $(psubnav.id).removeClassName("hidden");
            generic.events.fire({event:"globalnav:getcontent/my_mac"});  
        } else {
            //var url = "/js/brand/globalnav/data/" + psubnav.itemId + ".txt"; // for testing
            var url = psubnav.content.url + (psubnav.content.param ? ("?" + psubnav.content.param + "=" + psubnav.itemId) : "");
             
            new Ajax.Request(url, {
                method: 'get',
                onSuccess:  function(transport) { 
                    var data = transport.responseText;
                    this.initPanelContent(data, args); 
                }.bind(this) 
            }); 
        }
        
    },
    
    initPanelContent: function(data, panelArgs) {
        // summary:
        //      Handles response (as html or json) from getPanelContent
        //console.log("GlobalNav.initPanelContent ");   
       
        var psubnav = panelArgs.psubnav; 

        // place html content
        if (psubnav.content.handleAs === "html") {            
            psubnav.addSubItem(data);
            
            // set rollovers in html/cms nav content
            if (psubnav.content.cms) {  
                this.initCMSDisplay({scopeNode: psubnav.id});
            }
        
        // loop thru json data to render content variations
        } else {                               
            var sectionId = panelArgs.sectionId;
            var pnavItemId = (panelArgs.item ? panelArgs.item.id : panelArgs.itemId);
            //console.log("GlobalNav.initPanelContent: json "+ sectionId +  "/" + pnavItemId);               
                                     
            // response will either be an array of items or an object that contains the item array further down the tree
            if (typeof data === "string") data = data.evalJSON(true); 
            var items = data; // default
            
            // broadcaster
            generic.events.fire({event:"panel:prodcat", msg:{ data: data, request: psubnav}});

            if (typeof data === "object") {
                // ex: prodcat data
                if (data.sections) {
                    items = data.sections[0].items;
                
                // ex: faves (SectionDescSubNav)
                } else if (data.items) {
                    items = data.items;
                    // pass additional data props (above items)
                    if (typeof psubnav.setContent === "function") {
                        psubnav.setContent(data);
                    }
                }
            }
 
            // get content hierarchy type
            var useProductCategories = false; // flag true if prod content has 2-levels deep (categories & detail items) 
            if (pnavItemId !== "discontinued" && (sectionId === "products" || pnavItemId === "looks")) {        
                useProductCategories = items.any(function(item) { 
                    if (item.items) return true; 
                });   
                //console.log("GlobalNav.initPanelContent: useProductCategories "+useProductCategories); 
            }

            // prodcat special case: discontinued nav 
            if (pnavItemId === "discontinued") {
               //console.log("GlobalNav.initPanelContent: discontinued");  
               this.initDiscontinued(items, psubnav, panelArgs.item);
           
            //prodcat: if item hierarchy goes 2-levels deep
            } else if (useProductCategories) {            
                //console.log("GlobalNav.initPanelContent: prodcat: if item hierarchy goes 2-levels deep");   
                this.initProductCategories(items, psubnav, sectionId, pnavItemId);
               
            //prodcat & other sections w/ only 1 level deep
            } else { 
                 //console.log("GlobalNav.initPanelContent: prodcat & other sections w/ only 1 level");   
                // if no section defined, item is the section
                if (sectionId === "") {
                    sectionId = pnavItemId;
                } 
                
                // get default context
                var hasItemInDefaultCategory = false; // flag true if in default category & category contains default detail item 
                if (psubnav.isDefaultPanel) {
                    
                    // default detail item
                    var defaultDetail = this.getDefaultDetail(); 
                    hasItemInDefaultCategory = items.any(function(detailItem) { 
                        return (!!(detailItem.id === defaultDetail.id));
                    })
                    //console.log("GlobalNav.initPanelContent: hasItemInDefaultCategory "+hasItemInDefaultCategory);    
                    // PanelSubNav
                    if (hasItemInDefaultCategory) {
                        psubnav.setDefaultState();
                    }
                }                
 
                var self = this; 
                // check for specific method to override generic.widget class default DOM insertion (insertMixIn)
                // example: brand.globalnav.ArtistryInActionSubNav uses it's own addSubItem method
                var domInsertionMethod = null;
                if (psubnav.domInsertionMethodName && psubnav[psubnav.domInsertionMethodName]) {
                    domInsertionMethod = function(detail) {
                        psubnav[psubnav.domInsertionMethodName](detail.domNode);
                    }
                };
                
                items.each(function(detailItem, idx) {   
                    //console.log("GlobalNav.initPanelContent: items"+idx + ": "+detailItem.id+" / "+psubnav.id);    
                    // check for header links or detail modules
                    if (detailItem.type === "header_link") {
                        var isdefaultDetail = false;
                        if (defaultDetail && (detailItem.id === defaultDetail.id)) {
                            isdefaultDetail = true;
                        }
                        self.initHeader({type: "psubitem", item: detailItem, isdefault: isdefaultDetail, parentId: psubnav.id, domParentType: "PanelSubNav"});
                    
                    // detail module
                    } else {

                        var detail = self.initDetail({
                            item: detailItem,
                            sectionId: sectionId,
                            pnavItemId: pnavItemId,
                            defaultItem: defaultDetail,
                            isInDefaultCategory: hasItemInDefaultCategory,
                            parentId: psubnav.id,
                            domParentId: psubnav.containerNode,
                            domInsertionMethod: domInsertionMethod
                        }); 
                    } 
                });                
                
            }
            
        } //end loop through json
          
        // hide progress, show content 
        psubnav.onChildrenLoaded({ hasLoaded: true });
        // broadcaster for external hooks
        generic.events.fire({event:"panelnav:contentloaded", msg:{ psubnavId: psubnav.id, itemId: psubnav.itemId,  sectionId: panelArgs.sectionId }});
    },

    initProductCategories: function(supercatItems, psubnav, sectionId, pnavItemId) {
        // summary:
        //      Adds category and product level widgets that render category-detail modules
        //      (ProductCategoryDetail), category accordion headers (Accordion) and product-detail
        //      modules (Detail).
        //      These instances are attached to their parent instance of ProductSubNav
        //console.log("GlobalNav.initProductCategories: "+supercatItems.length); 
        
        var self = this;
        var defaultSupercat = this.defaultState.item;
        var defaultCat = (defaultSupercat ? defaultSupercat.item : null);
        var defaultCatId = (defaultCat ? defaultCat.id : null);
        var useAccordionMode = false; // flag true shows accordion headers as initial state (rather than category detail modules). used for default state of spp's & cat-level mpp's
        var hasMixedDetail = false; // flag true if category level includes non-category (childless) items.  Ex: brush-play detail link in Brushes category list

        // default detail
        if (psubnav.isDefaultPanel && defaultCat) { 
            var defaultDetail;
            try {
                defaultDetail = defaultCat.item;
            }
            catch (err) { }
        }
     
        supercatItems.each(function(catItem, catidx) {
            //console.log("GlobalNav.initProductCategories supercatItems "+catidx+": "+  catItem.id);
            var hasChildren = true; // flag true when item is a category w/ child detail items (i.e. will display as accordion group)
            
            if (!catItem.items) {
                //console.log("GlobalNav.initProductCategories id: "+catItem.id + " has no children");
                hasChildren = false;
                hasMixedDetail = true;
            }
            
            // get default context            
            var isDefaultCat = false; // default category w/ in default ProductSubNav
            var hasItemInDefaultCategory = false; // flag true if category contains default detail item
            // nav is rendering default state of psubnav and has levels beyond supercat
            if (psubnav.isDefaultPanel && defaultCat) {            
                useAccordionMode = true;             

                // verify that category contains a default detail
                // (usually true. false ex: mpp)
                if (catItem.id == defaultCatId) {
                    isDefaultCat = true;
                    if (defaultDetail) {
                        hasItemInDefaultCategory = true;
                    }
                }
            }
            
            // add detail module for item
            if (!useAccordionMode || !hasChildren) { 
                var dargs = {
                    id: "psubcat_"+catItem.id,   
                    displayName: catItem.name,
                    hdPath: catItem.header,
                    description: catItem.description,
                    thumbPath: catItem.thumbnail,
                    sectionId: sectionId,
                    pnavItemId: pnavItemId,
                    parentId: psubnav.id
                }
                //console.log("GlobalNav.initProductCategories "+dargs.id+" / " +psubnav.id);
                
                // add detail link
                if (!hasChildren) {
                    //console.log("GlobalNav.initProductCategories: !hasChildren"); 
                    dargs.item = catItem;
                    dargs.isInDefaultCategory = hasItemInDefaultCategory; 
                    dargs.domInsertionMethod = function(d) {
                        psubnav.addSubItem(d.domNode, psubnav.detailLinksContainerNode);
                    }
                    
                    var detail = self.initDetail(dargs);
                    return; 
                
                // add category detail
                } else if (!useAccordionMode) {
                    //console.log("GlobalNav.initProductCategories - !useAccordionMode "+dargs); 
                    var altDetailConfig = self.getAltTemplateConfig(catItem);
                    if (altDetailConfig) {
                        dargs.template = dargs.template || {} ;
                        dargs.template.detail = dargs.template.detail || {} ;
                        dargs.template.detail = Object.extend(dargs.template.detail, altDetailConfig);
                    } 
                    
                    psubnav.addCategoryDetail(dargs); 
                } 
            }
            
            //var img = catItem.header.replace(/200/g, "250");
            var subnavAccordion = psubnav.addCategoryAccordion({
                id:"psubcat_"+catItem.id,
                displayName: catItem.name,
                hdPath: catItem.header,
                description: catItem.description
            });                     
            
            catItem.items.each(function(detailItem, idx) {
                //console.log("GlobalNav.initP catItems "+idx+": "+ detailItem.id + "/" + subnavAccordion.id);
                 /*
                Detail:
                    - creates product-level detail html module
                
                Attachment notes:
                    Detail node gets passed to Accordion's addSubItem method 
                */
                
                var detail = self.initDetail({
                    item: detailItem,
                    sectionId: sectionId,
                    pnavItemId: pnavItemId,
                    defaultItem: defaultDetail,
                    isInDefaultCategory: hasItemInDefaultCategory,
                    parentId: psubnav.id,
                    domParent: subnavAccordion.containerNode
                });                
               
            }); // end: catItems foreach
                               
            // accordion has items, so toggle its display for default category state
            psubnav.setCategoryState({
                accordion: subnavAccordion,
                useAccordionMode: useAccordionMode,
                isDefaultCat: isDefaultCat, 
                hasItemInDefaultCategory: hasItemInDefaultCategory
            });
            
        });
      
        // psubnav content loaded, so toggle display for default panel state
        psubnav.setPanelState({ hasMixed: hasMixedDetail });
     
    },
   
    initDetail: function(args) { 
        // summary:
        //      Instantiates appropriate Detail widget class and properties according to
        //      content type. 
        
        // args
        var item = args.item;
        var config = brand.globalnav.config;
        var sectionId = args.sectionId;
        var pnavItemId = args.pnavItemId;  
        //console.log("GlobalNav.initDetail " +args.item.id + " / " + args.parentId + " / " +args.pnavItemId); 
        
        // get config if needed
        var sk = (sectionId ? this._configKeys[sectionId] : null);
        var sectionConfig = (sk ? this.config.items[sk.idx] : null);
        var sharedItemConfig = (sectionConfig ? sectionConfig.itemsConfig : null); 
        var pnk = (sectionConfig ? sk.items[pnavItemId] : null);
        var pnavItemConfig = (pnk ? sectionConfig.items[pnk.idx] : sectionConfig);

        // set if default
        var isInDefaultCategory = (args.isInDefaultCategory ? args.isInDefaultCategory : false);         
        var isdefault = false;
        if (isInDefaultCategory) {
            if (args.isdefault) {
                isdefault = args.isdefault;
            } else {
                var defaultId = (args.defaultItem ? args.defaultItem.id : null);
                if (item.id == defaultId) {
                    isdefault = true;
                }
            }
        }
        
        var itemName = (item.name ? item.name : "");
 
        // parameters that always get passed to detail-level widgets
        var wArgs = {
            id: "psubitem_" + item.id,
            displayName: itemName,
            hdPath: item.header,
            //hdHeight: item.header_image_height,
            thumbPath: item.thumbnail,
            thumbRolloverPath: item.thumbnail_rollover,
            url: item.uri,
            isdefault: isdefault,
            parentId: args.parentId,
            isInDefaultCategory: isInDefaultCategory
        }
        if (args.domParent) wArgs.domParent = args.domParent;
        if (args.domInsertionMethod) wArgs.domInsertionMethod = args.domInsertionMethod;
            
        // optional parameters
        // description:
        wArgs.description = (item.description ? item.description : null);
        // template:
        var t;
        if (args.template) {
            t = args.template;
        } else if (pnavItemConfig && pnavItemConfig.template) {
            t = pnavItemConfig.template;
        } else if (sharedItemConfig && sharedItemConfig.template) {
            t = sharedItemConfig.template;
        }

        if (t) {
            wArgs.template = t;
        }
        
        //console.log("GlobalNav.initDetail: add " +wArgs.id + " / " + wArgs.parentId); 
         
        // check for alternate "type" property: defines display styles for individual items that may differ from group
        var altDetailConfig = this.getAltTemplateConfig(item);
        if (altDetailConfig) {
            wArgs.template = ( wArgs.template ? wArgs.template : {} );
            wArgs.template.detail = ( wArgs.template.detail ? wArgs.template.detail : {} );
            Object.extend(wArgs.template.detail, altDetailConfig);
        }       
           
        var detail = new brand.globalnav.Detail(wArgs);
        return detail;
    },

    getAltTemplateConfig: function(item) {
        var dconfig = false;
        if (item.type) {
            var altTypes = this.config.altTypes;        
            var ait = altTypes[item.type];
            
            // if alt type is defined, pass it in template props
            if (ait && ait.detail) {  
                dconfig = ait.detail;
            }
        }        
        return dconfig;
    },
     
    initHeader: function(args) {    
        // summary:
        //      Creates instance of Header for items that don't open a panel or accordion
        //      (including default state of a PanelNav link)
        //      args: type, item, isdefault, parentId, domParent, domParentType
        
        var item = args.item;
        if (!args.item)  return;
        var domParentType = args.domParentType;
        // set contextual args
        var parentId, domParent;
        // get parent child context for top-level vs. second level panel navs
        if (domParentType === "GlobalSet") { // top-level item (e.g. giving_back)
            parentId = args.parentId;
            domParent = args.domParent;
        } else if (domParentType === "PanelNavSet") { // second-level item (e.g. whats new) parent is a PanelNavSet/Accordion
            parentId = args.domParent.id;
        } else if (domParentType === "PanelSubNav") { // e.g. gift cards
            parentId = args.parentId;
        } else {
            console.log("GlobalNav.initHeader : need context handling for id = "+item.id+" / parent type = "+domParentType);
        }
     
        var h = new brand.globalnav.Header({
            id: args.type+"_"+item.id,
            displayName: item.name,
            hdPath: (item.header ? item.header : ""),
            url: (item.uri ? item.uri : null),
            isdefault: args.isdefault,
            parentId: parentId,
            domParent: domParent
        });
    },

    searchError: function(sectionConfig, event) {
        
    },

    initSearch: function(sectionConfig) {
        // summary:
        //      
        
        $('utilitynav_form_search').observe("submit", function (sectionConfig, event) {
            var defaultState = page_data.panel_nav["default"];
            var query = $(sectionConfig.search.formFieldId).value;
            
            if ( !query || query === defaultState.searchDefault ) {
                var popid = sectionConfig.search.errorPopup;
                if (popid) {
                    brand.overlay.launch({
                        foregroundNode: $(popid),
                        displayInline: true,
                        removeOnHide: false,
                        displayDuration: 5000
                    }); 
                }
                
                event.stop();
            }
        }.curry(sectionConfig));
                
        //short circuiting for Endeca
        //return;
        
        
        
        var id = sectionConfig.id;
        var self = this; 
        
        // check for default
        var isDefaultPanel = false;
        if (this.defaultState.query && (this.defaultState.item.id === id)) {
            isDefaultPanel = true;
        }
        
        if (!isDefaultPanel) {
            var pnavManager = new brand.globalnav.panelManager({
                id: "pnav_" + id, 
                parentId: this.globalNavSetId,
                sectionId: id,
                item: sectionConfig
            });
            
            pnavManager.startup();
        }
     
        var psubnav = new brand.globalnav.PanelSubNav({
            templatePath: "jsTemplates.globalnav.SearchSubNav",
            id: "psubnav_" + id,
            parentId: "pnav_" + id,
            isDefaultPanel: isDefaultPanel,
            itemId: id,
            cache: false,
            callback: function() {
                var search = new brand.search({
                    config: sectionConfig,
                    panelManagerId: "pnav_" + id,
                    parentId: self.globalNavSetId,
                    isDefaultPanel: isDefaultPanel
                });
            }
        }); 
    },
    
    initDiscontinued: function(items, psubnav, sectionConfig) {  
        //console.log("GlobalNav.initDiscontinued ");  
        var self = this;
          
        // populate DiscontinuedSubNav.html-specific nodes
        if (psubnav.panelDescriptionNode) psubnav.panelDescriptionNode.innerHTML = items.panel_description;
        if (psubnav.searchDescriptionNode) psubnav.searchDescriptionNode.innerHTML = items.description;
      
        // featured goodbyes detail link
        var defaultDetail = this.getDefaultDetail();
        var isdefaultDetail = (defaultDetail.id === "featured_goodbyes" ? true : false);
        var featured = sectionConfig.content.featured;
        featured.description = items.featured_description;
     
        var detail = self.initDetail({
            item: featured,
            template: sectionConfig.template,
            isdefault: isdefaultDetail,
            isInDefaultCategory: isdefaultDetail,
            parentId: psubnav.id,
            domInsertionMethod: function(detail) {
                psubnav.addSubItem(detail.domNode, psubnav.featuredNode);
            }
        });
                        
        // show subnav content
        psubnav.onChildrenLoaded();
 
        if (sectionConfig.search) { 
            var dsearch = new brand.discontinuedSearch({
                config: sectionConfig,
                isDefaultPanel: psubnav.isDefaultPanel,
                progressNode: $("disc_search_progress")
            }); 
        }
        
    },
    
    getDefaultDetail: function(args) {
        // summary:
        //
        //      Find terminus/detail item in page_data.panel_nav.default
        //      args.includeParent: returns object starting with parent of terminus
        //console.log("GlobalNav.getDefaultDetail");
        var d = p = this.defaultState;
        var defaultSubsection = this.defaultState.item; // supercat-level for prodcat.  sub-section level for non-prodcat
        var includeParent = ((args && args.includeParent) ? args.includeParent : false);
        if (defaultSubsection) {
            // step down
            if (defaultSubsection.item) {
                d = defaultSubsection.item; // (ex: products - nails - spp)
                p = defaultSubsection;
                if (d.item) {
                    p = d;
                    d = d.item; // (ex: products - eyes - primer - spp)
                }
                
            // at detail level (ex: giving back - back to mac)
            } else {
                d = defaultSubsection;
            }
        }
        
        return (includeParent ? {detail: d, parent: p} : d);
    },
        
    initCMSDisplay: function(args) {
        // summary:
        //
        //      Sets rollovers for linked images
        //      If is default nav: finds image header in cms nav that matches id
        //      of current page.  Sets image to "on" state

        var scopeNode = (args.scopeNode ? args.scopeNode : "panel_open");
        var defaultImgId = null;
        var rollover;
        var defaultImg; 

        // find image associated with default page state
        if (args.isdefault) {        
            var d = this.getDefaultDetail({ includeParent: true });                        
            defaultImgId = "image_" + d.detail.id;
            // if default image isn't found, check for match in parent section id, or as "_index"
            if (!$(defaultImgId)) {
                var pId = "image_" + d.parent.id;
                if ($(pId)) {
                    defaultImgId = "image_" + d.parent.id;
                    
                // if detail item is terminus item try "_index"
                } else if (!d.detail.item) {
                    pId = "image_index";
                    if ($(pId)) defaultImgId = "image_index";
                }
            }
        } 
        
       // find images in specified nav, apply rollovers
       var imgs = $$("#" + scopeNode + " a img");
       imgs.each(
            function(imgn) {
                if (imgn.id === defaultImgId) {
                    defaultImg = new brand.img(imgn, ["off","on"]);
                    defaultImg.changeSrc("on");
                } else {
                   rollover = new brand.rollover(imgn, null);
                }
            }
        );
    
    }
    
});

 

brand.globalnav.GlobalSet = Class.create(Widget, 
{
    // summary:
    //      Container for set of nav items in left-nav. 
    //      Items/children are mixed widget types (Accordion, PanelNav and Header) 
    //      Children communicate about state changes with each other via the NavSet parent
    
    // activeItemId: String
    // currently active/open child widget id
    activeItemId: "",
     
    // _objChildren: Object
    // storage for children not gettable/settable via other methods
    _objChildren: {},
    
    initialize: function($super, properties) {
    $super(properties);     
    },
    
    addChild: function(child) {
        // summary:
        //      Stores child instances of objects not gettable/settable via other methods
        //      Ex: non-dijit brand.globalnav.panelManager
        
        this._objChildren[child.id] = child;  
    },
    
    getChild: function(childId) {
        // summary:
        //      Returns objects stored via addChild
        //console.log("brand.globalnav.GlobalSet.getChild: "+childId); 
        var child = false;
        var childCount = this.children.length;
        for (var i=0; i<childCount; i++) {
            if (this.children[i].id == childId) {
                child = this.children[i];
                break;
            }
        }  
        return child;
    },
 
    setActiveItem: function(/* String */itemId) {
        this.activeItemId = itemId;
    },

    onChildClick: function(/* String */sectionId,/* Boolean */fromDefault) {
        // summary:
        //      Called by child widgets
        //      Captures events occuring further down in navigation hierarchy
        //      If change is being triggered by default link, closes activeItem, but keeps
        //      default accordion 
        //console.log("GlobalSet.onChildClick "+this.activeItemId +" / "+ sectionId  +" / "+  fromDefault)
        if (this.activeItemId && (this.activeItemId !== sectionId || fromDefault)) {
        
            // get dijit
            var activeItem = $(this.activeItemId).widget;
            // otherwise, look for non-dijit child
            if (!activeItem) {
                try {
                    var gset = $(globalNavSetId).widget;
                    activeItem = this.getChild(this.activeItemId);
                }
                catch(err){};
            }
            
            var defaultId = (fromDefault ? sectionId : ""); 
            activeItem.close(defaultId);
            
        } 
    }
    
});
 
brand.globalnav.Accordion = Class.create( Widget, 
{
    // summary:
    //      Container for a set of nav links where only 1 nav category link's content is visible at a time
    //      Switching between nav categories slides the sub nav links in each nav category up/down 
    
    //templatePath: "/js/brand/globalnav/templates/Accordion.html",  
    templatePath: "jsTemplates.globalnav.Accordion",
    
    templateType: "",
    isContainer: true,

    // hasLoaded: Boolean
    // have template nodes loaded in DOM
    // declarative instance = true
    hasLoaded: false,

    // parentId: String (Optional: passed for declarative instances)
    // parent object id 
    parentId: "",
    
    // displayName: String
    displayName: "",
    
    // hdPath: String
    // header img src
    hdPath: "", 
    
    // isOpen: Boolean
    // is accordion sub nav open
    isOpen: false,

    // activeItemId: String
    // currently active/open child PanelNavs associated w/ this Accordion
    activeSubItemId: "",
   
    // open/close animation properties
    durationOpen: 0.4,     
    durationClose: 0.3,      
    durationFade: 0.3, 

    initialize: function($super, properties) { 
        this.setProperties(properties); 
        //console.log("Accordion.initialize "+this.id);
        
        /**MK: still used?
        if (this.templateType && this.templateType !== "") {  
            this.templatePath = "/js/brand/globalnav/templates/" + this.templateType + ".html";
          }**/
          
        $super();
    }, 
    postCreate: function() {  
        //console.log("Accordion.postCreate "+this.id);
        
        // set header img states
        var hdNode = $(this.id + "_hd"); 
        this.hdImg = new brand.img(hdNode, ["off","on","sel"]);
    },
 
    onClick: function() {    
        //console.log("Accordion.onClick: "+this.id);
        
        // tell parent of event
        if (this.parent) {
            this.parent.onChildClick(this.id);
        }   
 
        // toggle state
        if (this.isOpen) {
            this.close();
        } else {
            this.open();
        }   
       
    },

    open: function() {
         //console.log("Accordion.open "+this.id);  
         if (this.hdImg) this.hdImg.changeSrc("sel"); // switch img 
 
         this._showSubNav();
         this._setActive(true);  
         
         generic.events.fire({event:"accordion:open", msg:{ type: "accordion", id: this.id, parentId: this.parentId, displayName: this.displayName }});        
    },
    
    close: function(/* String */defaultId) {
        // summary:
        //      Closes the active accordion, unless it is associated with default item
        //      Close subitems (panelsubnavs) 
          
        //console.log("Accordion.close "+this.id + " " + defaultId); 
         
        // handle subitems
        if (this.activeSubItemId) {
            var activeSubItem = $(this.activeSubItemId).widget;  
            activeSubItem.close(); 
        } 
        // handle this accordion
        if (this.id !== defaultId) {
            if (this.hdImg) {
                this.hdImg.changeSrc("off"); // switch img
            }
            this._hideSubNav();
            this._setActive(false);
            generic.events.fire({event:"panelnav:hide", msg: { type: "accordion", id: this.id, parentId: this.parentId }}); 
        } 
    },
    
    _setActive: function(/* Boolean */state) {     
        this.isOpen = state;
         
        if (this.parent) {
            if (state == true) {
                this.parent.activeItemId = this.id;
            } else if (this.parent.activeItemId === this.id) {
               this.parent.activeItemId = "";
            }
        } 
    },
    
    _showSubNav: function() {
        var node = this.containerNode; 
        //console.log("Accordion._showSubNav "+ node.id); 
        node.setOpacity(0);
        node.style.overflow = "hidden";
        if (node.style.visibility == "hidden" || node.style.display == "none") { 
            node.style.height = "1px";
            node.style.display = "block";
            node.style.visibility = "";        
        } 
           
        var duration = this.durationOpen;
        var h = node.scrollHeight; 
        
        var expandAccordion  = function() { 
            new Effect.Morph( node, {
                duration    : duration, 
                style       : { height  : h + "px"},   
                afterFinish : fadeUp
            });
        };
        duration = this.durationFade;
        var fadeUp  = function() {  
            new Effect.Opacity ( node, {
                duration    : duration, 
                transition  : Effect.Transitions.linear, 
                from        : 0,
                to          : 1 
            });
        };   
        expandAccordion();   
    },
    
    _hideSubNav: function() {
        var node = this.containerNode;
        //console.log("Accordion._hideSubNav "+ node.id);  
        
       var duration = this.durationClose;
       var contractAccordion = function() {  
            new Effect.Morph(node, {
                duration    : duration, 
                style       : { height  : "1px" },   
                afterFinish : function() {
                    node.hide();
                    node.style.overflow = "hidden";
                }
            });
        };
        duration = this.durationFade;
        var fadeDown  = function() {  
            new Effect.Opacity ( node, {
                duration    : duration,  
                transition  : Effect.Transitions.linear, 
                from        : 1,
                to          : 0 
            });
         };   
         fadeDown();
         contractAccordion();      
    }, 
    
    _onMouseOver: function(e) {
        if (this.hdImg.changeSrc && !this.isOpen) {
            this.hdImg.changeSrc("on");
        }
    },
    
    _onMouseOut: function(e) { 
        if (this.hdImg.changeSrc && !this.isOpen) { 
            this.hdImg.changeSrc("off"); 
        }
    }
}); 
brand.globalnav.Detail = Class.create(Widget, 
{
    // summary:
    //      Most detailed level in heirarchy: includes description, thumbnail & url      
    //templatePath: "/js/brand/globalnav/templates/Detail.html",
    templatePath: "jsTemplates.globalnav.Detail",
    //templateType: "detail",    

    //simpleDetailPath: "/js/brand/globalnav/templates/SimpleDetail.html",
    simpleDetailPath: "jsTemplates.globalnav.SimpleDetail",
    
    // parentId: String
    // PanelSubnav/ProductCategorySubnav widget parent
    parentId: null,
    
    // displayName: String
    displayName: "",
    
    // hdPath: String
    // header img src
    hdPath: "",

    // hdStates: Array
    // possible suffixes for header img src states   
    hdStates: ["off","on","sel"],
    
    // thumbPath: String
    // thumbnail img src
    thumbPath: "",

    // thumbRolloverPath: String (optional)
    // thumbnail rollover img src
    thumbRolloverPath: null,
    
    // description: String
    description: "",
    
    // url: String
    // template href value
    url: "",
    
    // isdefault: Boolean
    // flagged true if item is default item (i.e. product item corresponding w/ current spp)
    isdefault: false,
    
    // isInDefaultCategory: Boolean
    // flagged true if item is within default category
    isInDefaultCategory: false,

    // baseClass: String (optional)
    // supplemental css class, ex: panelnav_link_category   
    baseClass: "", 

    offImg: "off",
    
    timer: null,    
    timerDuration: 3,
    
    initialize: function($super, properties) { 
        this.setProperties(properties);
        //console.log("Detail.init "+this.id); 
        
        if (this.template) {
            var t = this.template;
            if (t.detail) {
                var type = t.detail.type;
                if (type) { this.templatePath = type; }
                var baseClass = t.detail.baseClass;
                if (baseClass) { this.baseClass = baseClass; }
                var hstates = t.detail.headerStates;
                if (hstates) { this.hdStates = hstates; }
            }
        }
          
        $super();
    },
    
    postCreate: function() { 
        //console.log("Detail.postCreate "+ this.id + " / " + $(this.id)); 
           
        // header image 
        var hdNode = $(this.id + "_hd");
        if (hdNode) this.hdImg = new brand.img(hdNode, this.hdStates);

        if (this.isInDefaultCategory) {         
            if (this.isdefault) { 
                this.setDefaultState();
            } else {
                this.offImg = "sel"; // exception for "dimmed" style: "sel" state used for off state of hdImg
                this.setDefaultCategoryState();
            }
        }
  
        // thumbnail rollover
        var thumbImg = $(this.id + "_thumb");
        if (this.thumbRolloverPath && thumbImg) {
            this.thumbImg = thumbImg;
            var preload = new Image();
            preload.src = this.thumbRolloverPath; 
        }
    },
        
    _onMouseOver: function(e) {
        if (this.isdefault) return;
        var event = e || window.event;
        if (this.timer) clearTimeout(this.timer);
        if (this.hdImg) this.hdImg.changeSrc("on");
        if (this.thumbImg) this.thumbImg.src = this.thumbRolloverPath;         
        this.domNode.addClassName("panelnav_link_on");
        Event.stop(event);
    },
    
    _onMouseOut: function(e) {
        if (this.isdefault) return;
        var event = e || window.event;
        Event.stop(event);
        var self = this;
        var out = function() {
            if (self.hdImg) self.hdImg.changeSrc(self.offImg);
            if (self.thumbImg) self.thumbImg.src = self.thumbPath;        
            self.domNode.removeClassName("panelnav_link_on");
        }
        
        // IE workaround for item bg flickering. cancels bubbling unless mouse is totally out of item
        if (generic.env.isIE) {
            this.timer = setTimeout(out, this.timerDuration);
        } else {
            out();
        }
    },
    
    _onClick: function(e) {
        //console.log("Detail._onClick "+this.domParent );  
        // workaround for ie: use location prop as back-up for href
        if (this.url && generic.env.isIE) {
            location.href = this.url;
        }
    },
    
    setDefaultState: function() {
        //console.log("Detail.setDefaultState "+this.id+"/"+$(this.id));
        if (this.hdImg) {
            // most sites use on state for rollover & spp "default" state vs. others which 
            // use distinct xxx_active.gif img
            var activeImg = "on";
            var hdStates = this.hdStates;
            hdStates.any(function(state) { 
                if (state === "active") {
                    activeImg = state;
                    return true;
                } 
            });
            this.hdImg.changeSrc(activeImg);
        }
        
        this.domNode.addClassName("panelnav_default");
        
        // if url matches current page, remove link
        var loc = window.location.pathname;
        if (loc.indexOf(this.url) > -1) { 
            $(this.id).removeAttribute("href");
            this._onClick = function() { };
            $(this.id).addClassName("unclickable");
        } 
    },
    
    setDefaultCategoryState: function() {
        // displays "dimmed" state for detail items in same category as default item
        if (this.hdImg) this.hdImg.changeSrc(this.offImg);
    },
    
    reset: function() {
       this.destroy();
    }
});
 
brand.globalnav.ProductCategoryDetail = Class.create( brand.globalnav.Detail,
{
    // summary:
    //      Category detail-level of heirarchy:
    //      Includes details of category item, but is not the final level in nav heirarchy.
    //      onClick event triggers display of category list (Accordion).
    //
    //      Receives id args for widget parent ProductSubNav (parentId) & DOM parent (containerId)
    //      Refers to parent class method(s) for talking back up the heirarchy chain
    //      Uses container for placement in DOM only
    //
    //      Receives id arg for cousin Accordion to pass to parent.openAccorion
    
    //templatePath: "/js/brand/globalnav/templates/ProductCategoryDetail.html",
    templatePath: "jsTemplates.globalnav.ProductCategoryDetail",
    
    // containerNode: DOM node
    // container for CategoryDetail item
    containerNode: null,
    
    // accordionId: String
    // Accordion widget id
    accordionId: null,
    
    initialize: function($super, properties) { 
        //console.log("Detail.ProductCategoryDetail.init");
        if (this.containerNode) {
            this.domParent = this.containerNode; 
        }  
        $super(properties);  
    }, 
    
    _onClick: function(e) {
        //console.log("ProductCategoryDetail._onClick: "+this.domParent ); 
        // hide category detail
        this.containerNode.style.display = "none";
        // show accordion
        $(this.parentId).widget.getAccordion(this.accordionId);
    }
    
});

brand.globalnav.CollectionCategoryDetail = Class.create( brand.globalnav.ProductCategoryDetail,  
{
    
    //templatePath: "/js/brand/globalnav/templates/CollectionCategoryDetail.html", 
    templatePath: "jsTemplates.globalnav.CollectionCategoryDetail",
        
    isContainer: true,

    startup: function($super, arguments) {
        this.containerNode = $(this.parentId);
        this.accordionId = this.id+"_accordion";
        $super(arguments);
        this.parent = $(this.parentId).widget;
    },
    
    _onClick: function(e) {
        //console.log("CollectionCategoryDetail._onClick: "+this.categoryDetailNode.id); 
        // hide category detail
        this.categoryDetailNode.style.display = "none";
        // show accordion
        this.parent.getAccordion(this.accordionId);
    },
    
    onChildClick: function(/* String */sectionId) {
        //this.parent.onChildClick();
    }
    
});

brand.globalnav.SearchProductDetail = Class.create(brand.globalnav.Detail, 
{ 
    //templatePath: "/js/brand/globalnav/templates/SearchProductDetail.html", 
    templatePath: "jsTemplates.globalnav.SearchProductDetail",
        
    hex: "",
    actionImg: null,
    
    initialize: function($super, arguments) {
        //console.log("brand.globalnav.SearchProductDetail init"); 
        $super(arguments); 
    },
    startup: function($super, arguments) {
        //console.log("brand.globalnav.SearchProductDetail.startup"); 
        var actionImgNode = $(this.id + "_actionimg");         
        if (actionImgNode) this.actionImg = new brand.img(actionImgNode, ["off","on"]);
    },
    
    _onMouseOver: function($super, e) {
        $super(e);
        if (this.actionImg) this.actionImg.changeSrc("on");
    },
    
    _onMouseOut: function($super, e) {
        $super(e);
        if (this.actionImg) this.actionImg.changeSrc("off");
    }
 
});


brand.globalnav.SearchQuickBuyDetail = Class.create(brand.globalnav.Detail, 
{
    
    //templatePath: "/js/brand/globalnav/templates/SearchQuickBuyDetail.html", 
    templatePath: "jsTemplates.globalnav.SearchQuickBuyDetail",
        
    isContainer: true,
    product: null,
    hex: "",
    skupath: "",
    cartConfirmMsg: null,
    shadedResult: false,
    shaded: false,

    initialize: function($super, arguments) {
        //console.log("brand.globalnav.SearchQuickBuyDetail.init");
        $super(arguments);
    },
    postCreate: function($super, arguments) {
        $super(arguments);
        this.shadedResult = (this.product.shade_result ? true : false),
        this.skupath = this.product.sku.path;
        this.shaded = (this.product.shaded ? true : false);  
    },
    
    startup: function($super, arguments) {
        //console.log("brand.globalnav.SearchQuickBuyDetail.startup "+this.shadenameNode + "/" + this.descriptionNode);
 
        if ( this.shadedResult ) {
            this.shadenameNode.removeClassName("hidden");
        } else {
            this.descriptionNode.removeClassName("hidden");
        }
        
        this._initCartAction();
    },
    
    _initCartAction: function() {
        var shoppable = (this.product.sku.shoppable === "1" ? true : false);
        var self = this;
                
        if ( shoppable ) {
            //console.log("brand.globalnav.SearchQuickBuyDetail._initCartAction shoppable");
            this.cartConfirmMsg = new brand.product.cartConfirm({
                id: "search_cart_confirm-" + this.skupath,
                is_shaded: this.shaded,
                prodName: this.displayName,
                sku: this.product.sku,
                nodeToReplace: this.cartConfirmNode
            });
            
            //console.log("brand.globalnav.SearchQuickBuyDetail._initCartAction shoppable");
            // add to cart button 
            var btnNode = $(this.id + "_btn_add");
            btnNode.value = this.skupath;
            var button = brand.product.addButton({
                addButtonNode: btnNode,
                callback: function(response) {
                    self.cartConfirmMsg.show({ response: response });
                }
            });
        
        } else {
            //console.log("brand.globalnav.SearchQuickBuyDetail._initCartAction !shoppable");
            var btn = $(this.id + "_btn_add");
            if (btn) {
                btn.style.display = "none";
            }
            this.inventoryStatusNode.innerHTML = this.product.sku.inventory_status_message;
            this.inventoryStatusNode.style.display = "block";
        }
    },
    
    reset: function() {
        if (this.cartConfirmMsg) {
            this.cartConfirmMsg.destroy();
        }
        this.destroy();
    }
     
});

 
brand.globalnav.DiscontinuedProductDetail = Class.create( 
    brand.globalnav.Detail,
{
    
    //templatePath: "/js/brand/globalnav/templates/DiscontinuedProductDetail.html", 
    templatePath: "jsTemplates.globalnav.DiscontinuedProductDetail",
        
    sku: null,
    hex: "",
    shadename: "",
    shadedResult: false,

    initialize: function($super, properties) { 
        $super(properties); //calls Detail.postMixInProperties  
        
        if ( this.shadedResult && this.sku ) {
            this.hex = this.sku.color[0];
            this.shadename = this.sku.shade_name;
            this.url += "&SKU_ID=" + this.sku.sku_id;           
        }    
    },   
    
    startup: function($super, arguments) {
        $super(arguments);
        
        if ( this.shadedResult ) {
            this.shadenameNode.removeClassName("hidden");
        }       
    } 
});// summary:
//      PanelSubNav base class & extensions:
//      PanelSubNav, ProductSubNav

brand.globalnav.PanelSubNav = Class.create(Widget, 
{
    // summary:
    //      Contains subnav content for each PanelNav
    //      DOM child of Panel

    //templatePath: "/js/brand/globalnav/templates/PanelSubNav.html",   
    templatePath: "jsTemplates.globalnav.PanelSubNav",  
            
    isContainer: true,
    
    // parentId: String
    // id of PanelNav that displayed me
    parentId: "",

    // activeItemId: String
    // currently active/open child widget id
    activeItemId: "",

    // dataId: String
    // id as defined in json (ex: supercat id)
    dataId: "",

    // content: Object
    // contains parameters for requesting items from server, ex: content.url
    content: null,
    
    // isDefaultPanel: Boolean
    // true if panel is the open/page panel
    isDefaultPanel: false,

    // hasLoaded: Boolean
    // flag set to true when content has been rendered as html (true if declarative or after json request & render)
    hasLoaded: false,
    
    // cache: Boolean
    // cache after first request for content
    cache: true,
    
    initialize: function($super, properties) {
        this.setProperties(properties);
        //console.log("PanelSubNav.initialize: "+this.id + " / "+this.parentId + " " +this.isDefaultPanel+" properties = ",properties);
        
        // check for properties stored in this.content config settings
        var content = this.content;
        if (content) {
            if (content.reinsertNode) this.reinsertNode = content.reinsertNode;
            if (content.cache) this.cache = content.cache;
            if (content.hasLoaded) this.hasLoaded = content.hasLoaded;
        }
        
        if (this.isDefaultPanel) {
            this.domParent =  "panel_open"; 
        } else { 
            this.domParent = $($(this.parentId).widget.panelId).widget.containerNode;  
        }
        //console.log("PanelSubNav.initialize add subnav "+this.id + " / parent "+ this.parentId);

        $super();
    },  
    
    postCreate: function() {
        //console.log("PanelSubNav.postCreate "+this.id+ " / "+$(this.id)); 
        
        // method won't exist for the default panel on page, bc that content is in panel_open
        var parent = $(this.parentId);
        if (parent && parent.widget && parent.widget.addSubNav) {
             parent.widget.addSubNav(this);      
        }
       
        // show instances that are loaded, but hidden before init (i.e. my mac)
        if (this.hasLoaded) {
            //console.log("PanelSubNav.postCreate "+this.id+ " / "+$(this.id)+" reinsertNode = "+this.reinsertNode);
        
            this.showProgress(false);
            //psubnav.domNode.removeClassName("invisible"); 
            //$(this.id).removeClassName("invisible");
            this.containerNode.removeClassName("invisible");
        }
        
        if (this.callback) {
            //console.log("PanelSubNav.postCreate "+this.id+ " > callback");
            this.callback(); 
        }
    },

    onChildrenLoaded: function(args) {
        this.showProgress(false);
        this.containerNode.removeClassName("invisible"); // show loaded content
        
        if (args && args.hasLoaded) {
            this.hasLoaded = true;
        }
    },
    
    addSubItem: function(/* DOM node ? String */item,/* DOM node? */container) {
        // summary:
        //      Places child node(s)
        //      If passed a string, create a container node for placement 
        //console.log("PanelSubNav.addSubItem "+container+" / " + typeof container);
               
        if (!container) {
             container = this.containerNode || $(this.id); 
        }
        /**
        if (!container && this.containerNode) {
            container = this.containerNode; 
        } else {
            container = $(this.id); 
        }**/
        
        var node = item;
        if (typeof(item) === "string") {
            node = document.createElement("div");
            node.innerHTML = item;
            this.hasLoaded = true;
        }
        try {
            container.appendChild(node);
        }
        catch (e) { 
            console.log("PanelSubNav.addSubItem e: "+e.description);  
        }
    },
    
    onChildClick: function(/* String */sectionId) {
        // summary:
        //      Called by child widgets
        //      Captures events occuring further down in navigation heirarchy

        if (this.activeItemId && (this.activeItemId !== sectionId)) {
            var activeItem = $(this.activeItemId).widget;
            activeItem.close();
        }   

    },
    
    onParentClick: function() {
    },
    
    showProgress: function(/* Boolean */state) {
        this.progressNode.style.display = (state) ? "block" : "none"; 
    },
    
    setDefaultState: function() {
        $(this.id).addClassName("panelnav_category_default");
    }
    
});


brand.globalnav.ProductSubNav = Class.create( brand.globalnav.PanelSubNav,
{
    // summary:
    //      Contains product catalog subnav content for each PanelNav:
    //      category-level (ProductCategoryDetail) items and category list (Accordion) items.
    //      Detail-level (ProductDetail) items contained within category Accordion

    templateString: null,    
    //templatePath: "/js/brand/globalnav/templates/ProductSubNav.html",  
    templatePath: "jsTemplates.globalnav.ProductSubNav", 
        
    // inAccordionMode: Boolean
    // is subnav showing accordion items (rather than category detail items)
    inAccordionMode: false,

    // activeAccordionId: String
    // currently active/open child Accordion    
    activeAccordionId: "",
    
    initialize: function($super, properties) {
       //console.log("ProductSubNav.init"); 
       $super(properties);  
    },

    addCategoryDetail: function(/* Object */args) {
        // summary:
        //      Creates category detail element
        //      Places ProductCategoryDetail in detailContainerNode

        //console.log("ProductSubNav.addCategoryDetail "+args.id + "/ "+this.id);
        args.parentId = this.id; // this sub nav
        //JSTEST args.parentId =  $(this.id).widget.parent.id;
        args.containerNode = this.detailContainerNode; //JSTEST bc of Detail._onClick, change?
        args.domParent = this.detailContainerNode; // ProductCategoryDetail container 
        args.accordionId = args.id+"_accordion"; // id of corresponding Accordion 
             
        var detail = new brand.globalnav.ProductCategoryDetail(args);
        //detail.startup(); // SS: commented out since brand.globalnav.ProductCategoryDetail had an empty startup method
    },
    
    addCategoryAccordion: function(/* Object */args) {
        // summary:
        //      Creates category list/accordion element
        //      Places Accordion in accordionContainerNode
        //      Returns instance of Accordion
        //console.log("ProductSubNav.addCategoryAccordion "+args.id+"_accordion to "+this.accordionContainerNode.id);
        args.accordionId = args.id+"_accordion";
        
        var accordion = new brand.globalnav.Accordion({
            id: args.accordionId,
            displayName: args.displayName,
            domParent: this.accordionContainerNode,
            hdPath: args.hdPath,
            parentId: this.id
        });
        //JSTEST this.accordionContainerNode.appendChild(accordion.domNode);
        //accordion.startup(); // SS: accordion class no longer has startup method
                                    
        return accordion;
    },
    
    getAccordion: function(/* String */id) {
        var accordion = $(id).widget;
        this.openAccordion(accordion);
    },
    
    openAccordion: function(/* Widget */accordion) {
        //console.log("ProductSubNav.openAccordion "+accordion.id);  
        // show accordion container
        this.accordionContainerNode.style.display = "block"; 
        this.inAccordionMode = true;
        // show accordion nav
        accordion.open();
        this.activeAccordionId = accordion.id;
    },

    setCategoryState: function(args) {
        //console.log("ProductSubNav.setCategoryState "+args.accordion.id);  
        if (args.isDefaultCat && args.useAccordionMode) {
              //console.log("ProductSubNav.setCategoryState "+args.accordion.hdNode);  
       
            this.accordionContainerNode.removeClassName("hidden");
            var accordion = args.accordion;
            
            // show items in category under accordion
            function openAccordion() {
                accordion.open();
            }
            openAccordion.delay(1);
            
            this.detailContainerNode.style.display = "none";
            // apply subnav group style if there is default detail item to show
            if (args.hasItemInDefaultCategory) {
                $(accordion.id).addClassName("panelnav_category_default");
            }            
        }
    },

    setPanelState: function(args) {
        //if category level includes non-category (childless) items      
        if (args.hasMixed) {
            $(this.id).addClassName("panelnav_subnav_mixed");
            this.detailLinksContainerNode.style.display = "block";           
        }
    },
    
    reset: function() {
        // summary:
        //      Resets default state of subnav:
        //      shows ProductCategoryDetail container, hides Accordion container
        //console.log("ProductSubNav.reset"); 
        if (this.inAccordionMode) {
            this.detailContainerNode.style.display = "";
            this.accordionContainerNode.style.display = "";
            $(this.activeAccordionId).widget.close();
            this.inAccordionMode = false;
            this.activeAccordionId = "";
        }
    },
    
    onParentClick: function() {
        this.reset();
    }
    
});

brand.globalnav.SectionDescSubNav = Class.create( brand.globalnav.PanelSubNav,
{
    // summary:
    //      PanelSubNav with additional content attributes.  Contains:
    //      Large Section header & description text
    //      Detail-level (Detail) items

    templateString: null,    
    templatePath: "jsTemplates.globalnav.SectionDescSubNav", 
        
    // hdAlt: String
    hdAlt: "",

    // description: String
    description: "",

    setContent: function(args) {
        // populate additional content nodes
        this.panelDescriptionNode.innerHTML = args.description;
        this.hdNode.setAttribute("src", args.header);
        this.hdNode.setAttribute("alt", args.header_alt);
    },
    
    onChildrenLoaded: function(args) {
        this.showProgress(false);
        this.contentNode.removeClassName("invisible"); // show loaded content
        this.hasLoaded = true;
    }
    
});

brand.globalnav.ArtistryInActionSubNav = Class.create( brand.globalnav.PanelSubNav,
{
    // summary:
    //      PanelSubNav with additional content attributes.  Contains:
    //      Detail-level (Detail) items
    //      "Previous" button

    templateString: null,
    //templatePath: "/js/brand/globalnav/templates/ArtistryInActionSubNav.html",
    templatePath: "jsTemplates.globalnav.ArtistryInActionSubNav",
    itemCount: 0,
    featuredMax: null,
    domInsertionMethodName: "addSubItem", // tell GlobalNav class to use specific method for DOM Insertion to override generic.widget class default DOM insertion (insertMixIn)

    initialize: function($super, properties) {
        $super(properties);
        this.featuredMax = this.content.featuredMax;
    },

    postCreate: function($super) {
        $super();
        // set up event handler for "previous" button
        var previousBtn = $("psubnav_artistry_in_action_btnprevious");
        var btnContainer = this.containerNode.select("div.psubnav_artistry_in_action_btn_container")[0];
        var self = this;
        if (btnContainer && previousBtn) {
            previousBtn.observe("click", function (clickEvent) {
                clickEvent.preventDefault();
                btnContainer.hide();
                self.previousContainerNode.style.display = "block";
                // this isn't an accordion object, but the expansion functionality is the same, so we still want to broadcast the event for brand.view.heightHandler
                if (!self.isDefaultPanel) {
                    generic.events.fire({event:"accordion:open", msg:{ type: "accordion", id: self.id, parentId: self.parentId }}); 
                }
            });
        }
        
    },
    
    addSubItem: function(item) {
        // summary:
        //      Places child node(s)
        this.itemCount++;
        //console.log("this.itemCount = "+this.itemCount+" this.featuredMax = "+this.featuredMax);
        if (this.itemCount <= this.featuredMax) {
            container = this.featuredContainerNode; 
        } else {
            container = this.previousContainerNode;
        }
        
        var node = item;
        if (typeof(item) === "string") {
            node = document.createElement("div");
            node.innerHTML = item;
            this.hasLoaded = true;
        }
        try {
            container.appendChild(node);
        }
        catch (e) { 
            console.log("ArtistryInActionSubNav.addSubItem e: ",e);  
        }
    }
    
});brand.globalnav.Header = Class.create(Widget, 
{
    // summary:
    //      Renders header image for item that doesn't open a Panel or Accordion  
    templateLi: "jsTemplates.globalnav.headerLi",
    templateDiv: "jsTemplates.globalnav.headerDiv",

    // displayName: String
    displayName: "",
    
    // hdPath: String
    // header img src
    hdPath: "",

    // description: String
    description: "",
    
    // url: String (optional)
    // template href value
    url: "",

    // isdefault: Boolean
    // Item is (section, page, etc...) associated with default page state display
    isdefault: false,
    
    // hasLoaded: Boolean
    // have template nodes loaded in DOM
    // declarative instance = true
    hasLoaded: false,
    
    // parentId: String (optional)
    // parent object id
    // used to check status of other menus in a set (ex: if this is default item & clicking on it should close open menus to bring focus back to default)
    parentId: "",

    initialize: function($super, args) {
        this.setProperties(args);
        // set template group based on parent node element type
        this.templatePath = this.templateLi;
        var preNode = $(args.id);
        if (preNode) {
            var parentNode = preNode.parentNode;
        } else {
            var parentNode = $(args.parentId);
            if (!parentNode) {
                console.log("brand.globalnav.Header: Node UNDEFINED for args.parentId: "+args.parentId);
                return false;
            }
            var parent = parentNode.widget; 
            if (parent) {
                var parentNode = (parent.containerNode ? parent.containerNode : parent.domNode);
            }
        }  
        
        // eg Gift Card
        if (parentNode && parentNode.nodeName === "DIV") { 
            this.templatePath = this.templateDiv;
        }

        this.removeLink = (args.isdefault || !args.url);
        
        //console.log("brand.globalnav.Header "+args.id);
        $super();
        this.startup();
    },

    startup: function() {
        //console.log("STARTUP id = "+this.displayName+" isdefault = "+this.isdefault);
        if (this.removeLink) {
            var linkNode = $$("#"+this.id+" a")[0];
            if (linkNode) {
                linkNode.removeAttribute("href");
                linkNode.addClassName("unclickable"); 
                this.containerNode.removeClassName("clickable");
            }
        }    

        // if image is *not* already declared in page
        //console.log("Header: this.hasLoaded = "+this.hasLoaded);
        if (!this.hasLoaded || !this.isdefault) {
            var hdNode = this.hdNode;
            this.hdImg = new brand.img(hdNode, ["off","on","sel"]);
        }
        
        if (this.isdefault) {
           this.setDefaultState();
        } else {
            // set up mouseover events
            this.domNode.observe("mouseover", this._onMouseOver.bind(this));
            this.domNode.observe("mouseout", this._onMouseOut.bind(this));
        }
    },
     
    _showDefault: function(accordionId, parent) {
        //console.log("globalnav.Header._showDefault "+accordionId + " / " + parent); 
        // tell parent set to bring focus back to default
        if (parent.onChildClick && (parent.activeItemId !== "")) {
            parent.onChildClick(accordionId, true);
        }
    },
    
    _onMouseOver: function(e) {
        if (this.hdImg) {
            this.hdImg.changeSrc("on");
        }
    },
    
    _onMouseOut: function(e) {
        if (this.hdImg) {
            this.hdImg.changeSrc("off");
        }
    },
    
    setDefaultState: function() {
        // summary:
        //
        //      Handles state of header image in left nav associated with default/open panel

        //  set "default" state (on/sel)
        if (this.hdImg) this.hdImg.changeSrc("sel");
        
        var parentId = this.parentId;        
        if (parentId.indexOf("psubnav") != -1) { return; } // ignore header instances inside panelsubnav 
                
        var parent = $(parentId).widget;
        var accordionId = "";
        //console.log("globalnav.Header.setDefaultState: "+this.id + "/ parentId = "+parentId+" parent = "+parent); 
        // if parent is accordion in left nav, globalnav_container will be 1 level-up
        if (parentId !== "globalnav_container") {
            accordionId = parentId;
            parentId = parent.parentId;
            parent = $(parentId).widget;
        }
        if (parentId === "globalnav_container") { 
            //console.log("globalnav.Header.setDefaultState: "+this.id + "/" + accordionId +" / "+ parent.id); 
            var self = this;
            var onclick = function() {
                self._showDefault(accordionId, parent);
            }
             
            this.domNode.observe("click", onclick );   
        }
    }
    
});// summary:
//      Classes for elements needed to construct a panel:
//      Panel, PanelNavSet, PanelNav

  
brand.globalnav.PanelNavSet = Class.create(Widget,  
{
    // summary:
    //      Parent controller for multiple PanelNavs & PanelSubNavs associated with
    //      a single Panel (1 to many relationship)
    //      (Ex: Shop Products section only has 1 panel for all its subsections)
        
    isContainer: true,

    // parentId: String
    // parent object id 
    parentId: null,
    
    // isPanelSet: Boolean
    // referenced by PanelNav children to indicate that PanelNav's parent is a PanelNavSet (rather than globalnav container)
    isPanelSet: true,

    // panelId: String
    // Panel to slide in/out    
    panelId: "",
    
    // activeItemId: String
    // currently active/open NavItem
    activeItemId: "",
    
    initialize: function($super, properties) {
        //console.log("PanelNavSet.initialize "+properties.id);   
        $super(properties);
    },

    postCreate: function() {
        //console.log("brand.globalnav.PanelNavSet.postCreate " + this.id);  
        this.panelId = this._addPanel();
    },
    
    setActiveItem: function(/* String */itemId) {
        //console.log("PanelNavSet.setActiveItem: setting active to = "+itemId + " / notify parent "+this.parentId);
        this.activeItemId = itemId;
        this.parent.activeSubItemId = itemId; // tell Accordion
    },
    
    onChildClick: function(/* String */itemId) {
        //console.log("PanelNavSet.onChildClick triggered from item: "+itemId+" to parent: "+this.id+" activeItem id = "+this.activeItemId);
        if (this.activeItemId && (this.activeItemId !== itemId)) {
            this.hideItem(this.activeItemId); 
        }
    },
    
    _addPanel: function() {
        // summary
        //      Add 1 Panel element that contains multiple PanelNavs in this set
        //
        //console.log("PanelNavSet._addPanel "+this.id);
        var panel = new site.layout.Panel({
            id: this.id+"_panel",
            parentId: this.id 
        });
        return panel.id 
    },
    
    // fade in & toggle methods: only used w/ a PanelNavSet (iow when a PanelNav has siblings)
    fadeInSubNav: function(id) {
       var node = $(id); 
        
        // TODO: if panel is already open, we have to start w/ fade in, but if the panel isn't open, we can just do: s.display = ""; before the panel slides out 
        node.hide(); // hide it before opacity is set
        node.setOpacity(0); 
        node.show(); // ok to display now
        
        new Effect.Opacity (node, {
            duration    : 0.33, 
            transition  : Effect.Transitions.linear, 
            from        : 0,
            to          : 1 
        });

    },
    
    hideItem: function(/* String */itemId) {
        //console.log("PanelNavSet.hideItem "+itemId);
        var item = $(itemId).widget;
        if (item.hdImg) item.hdImg.changeSrc("off");
        this.toggleSubNav(item.subId, 0);
        item._setActive(false);
    },
    
    toggleSubNav: function(id, state) { 
        //console.log("PanelNavSet.toggleSubNav "+id+" / " + state);
        $(id).style.display = (state == 1) ? "" : "none"; 
    }
});


brand.globalnav.panelManager = Class.create (Widget, 
{
    // summary:
    //      Handles events from trigger to effect the panel associated with this item

    // hasPanelSiblings: Boolean
    // is this PanelNav part of a PanelNavSet   
    hasPanelSiblings: false,
    
    // panelId: String
    // Panel to slide in/out
    panelId: "",

    // parent: Object
    // parent widget 
    parent: null,
    
    // subId: String
    // id of associated PanelSubNav
    // fades in/out only if it is part of a PanelNavSet
    subId: "",
    
    // sectionId: String
    // brand.globalnav.config section id
    sectionId: "",

    // item: Object (optional)
    // brand.globalnav.config item-level
    item: null,
    
    initialize: function($super, args) {
        //console.log("panelManager.init "+args.id + "/"+args.parentId);
        this.id = args.id;
        this.item = args.item;
        try {
            this.parent = $(args.parentId).widget;
            this.hasPanelSiblings = (!!this.parent.isPanelSet);  //false? 
        } catch(e) {
            console.log("panelManager.init E "+args.id + "/"+args.parentId+" doesn't exist as a dom obj");
        }
        $super(args); 
    },

    startup: function() {
        // used only for panelManager (PanelNav as extension of panelManager is stored as dijit instead)
        this.panelId = this._addPanel(this.parent); 
        //this.parent.addChild(this); //search and other children of Global Set without panels
    },
    
    addSubNav: function(/* Widget */subNavItem) {
        // summary
        //      Captures PanelSubNav item as a sub item (subId) associated w/ this PanelNav
        //      Adds PanelSubNav item as DOM & widget child of the Panel for this PanelNav
        //      Hides PanelSubNav's with siblings (part of a PanelNavSet)
        //console.log("PanelMgr.addSubNav "+subNavItem.id + " / "+this.panelId + " / "+$(subNavItem.id));
        this.subId = subNavItem.id;
        //$(this.panelId).widget.addChild(subNavItem);  
        
        if (this.hasPanelSiblings && $(this.subId) ) {
            $(this.subId).style.display = "none"; 
        }   
    },
    
    _addPanel: function(/* Widget */parent) {
        // summary
        //      If PanelNav instance is part of a set, get its Panel instance already created by PanelNavSet
        //      Else PanelNav instance gets a Panel of its own
       
        //console.log("panelManager._addPanel "+this.id + "_panel / " + this.id + " / " + this.parentId);
        var id; 
        if (this.hasPanelSiblings) {
            id = parent.panelId;
        } else {
            panel = new site.layout.Panel({
                id: this.id + "_panel",
                parentId: this.id, 
                domParent: "panel_container"
            });
            id = panel.id;
        }
        return id;
    },
    
    _setActive: function(/* Boolean */state) {   
        //console.log("PanelManager._setActive");
        this.isActive = state;
        if (state == true) {
            this.parent.setActiveItem(this.id);
        } else if (this.parent.activeItemId === this.id) {
            this.parent.setActiveItem("");
        }
    },
    
    _onClick: function(e) {
        // summary:
        //      Click handler for PanelNav button (in template)

        //console.log("panelManager._onClick() this.id: "+this.id+ " / this.subId: "+this.subId);
   
        if (!this.isActive && this.subId) {
            var psubnav = $(this.subId).widget;
            if (!psubnav.hasLoaded || !psubnav.cache) {
                // wait for the panel to open before starting load (avoids animation stutters)
                // Panel.durationOpen = 400
                var si = this.sectionId; 
                var d = this.item;
                var di = this.itemId;
                var g = function() { 
                   params = { psubnav: psubnav, sectionId: si, item: d, itemId: di };
                   generic.events.fire({event:"panelnav:click", msg:params}); 
                }               
                setTimeout(g, 400);
            }
        }
        
        this.onTrigger(false);
    },

    onTrigger: function(stayOpen) {
        // summary:
        //      Handler for any "trigger" event (i.e. button click or search submission)
        //      stayOpen keeps panel from closing if it is already active (ex: search)
        
        //console.log("PanelMgr.onTrigger "+this.id + " / parent "+this.parentId + " / subnav " + this.subId);
       
        $(this.parentId).widget.onChildClick(this.id); // parent is either GlobalSet or PanelNavSet
         
        var psubnav = $(this.subId).widget; // psubnav (base class: PanelSubNav)
        // toggle state
        if (this.isActive) {
            if  (!stayOpen) {
                this.hideItem();
            }
        } else {
            psubnav.onParentClick();
            this.showPanel();
            if (!psubnav.hasLoaded || !psubnav.cache) {             
                if (!psubnav.hasLoaded && psubnav.progressNode) {
                    psubnav.progressNode.style.display = "block"; // show progress   
                }               
            }
        }
    },
    
    close: function() {
        // summary:
        //      Called from parent Accordion
        //      note: "close" used as generic name (matches Accordion method name)
        
        this.hideItem();
    },
    
    showPanel: function() {
        // summary:
        //      For single PanelNav: Panel slides out
        //      For PanelNavs w/ siblings: PanelSubNav fades in
        
        //console.log("brand.globalnav.PanelManager.showPanel"); 
        var panel = $(this.panelId).widget;
        
        if (this.hasPanelSiblings) {
            var parentSet = this.parent;            
            if (panel.isOpen) {
                //console.log("PanelManager.showPanel parentSet.fadeInSubNav")
                parentSet.fadeInSubNav(this.subId);
            } else {
                //console.log("PanelManager.showPanel parentSet.toggleSubNav") 
                parentSet.toggleSubNav(this.subId, 1);
                panel.open();
            }
        } else {
            panel.open();
        }
        
        this._setActive(true); 
        generic.events.fire({event:"panelnav:show", msg:{ type:"panel", id: this.panelId, itemId: this.itemId, subId: this.subId, sectionId: this.sectionId, displayName: this.displayName, parentId: this.parentId }}); 
    },
    
    hideItem: function() {
        // summary:
        //      For single PanelNav: Panel slides in
        //      For PanelNavs w/ siblings: PanelSubNav fades in
        //console.log("PanelManager.hideItem"); 
        var panel = $(this.panelId).widget;
        panel.close();
        if (this.hasPanelSiblings) {
            this.parent.toggleSubNav(this.subId, 0);
        }
        this._setActive(false);
        
        generic.events.fire({event:"panelnav:hide", msg: { type: "panel", id: this.panelId, itemId: this.itemId, subId: this.subId }} );
    }   
});


brand.globalnav.PanelNav = Class.create( brand.globalnav.panelManager, 
{
    // summary:
    //      Creates the trigger header in the left nav
    //      Handles events from trigger to effect the panel associated with this item
    //      Can be an child of a PanelNavSet (hasPanelSiblings = true),
    //      or a child of a single Panel if it is not part of a set (1 to 1 relationship)
    //      (Ex: My Mac section has 1 Panel with 1 PanelNav & 1 PanelSubNav)

    //templatePath: "/js/brand/globalnav/templates/PanelNav.html",
    templatePath: "jsTemplates.globalnav.PanelNav",
   
    // hasLoaded: Boolean
    // have template nodes loaded in DOM
    // declarative instance = true
    hasLoaded: false,
    
    // parentId: String
    // parent object id 
    parentId: "",
    
    // displayName: String
    displayName: "",
    
    // hdPath: String
    // header img src
    hdPath: "",
    
    // img: brand.img object
    hdImg: {},
    
    // isActive: Boolean
    // is this PanelNav active/visible    
    isActive: false,

    // itemId: String (optional)
    // brand.globalnav.config item-level id  
    itemId: "",
    
    initialize: function($super, properties) { 
        //console.log("PanelNav.init "+properties.id + "/"+properties.parentId + "/"+properties.domParent); 
        $super(properties);
    },
    postCreate: function() {
        //console.log("PanelNav.postCreate "+this.id+" to parent "+this.parentId);    
    
        if (!this.parent) { 
            //console.log("PARENT NOT FOUND w/ dijit.byId");
            //this.parent = this.getParent();
        }

        if (this.itemId === "") {
            try {
                this.itemId = this.item.id;
            }
            catch (err) { }
        }
        
        // declarative instance
        if (this.hasLoaded) {     
            //this.parent.addChild(this.id);  
            this.startup();
        } 
    },
    
    startup: function() {
        // summary
        //      Called from postCreate if declarative instance
        //      Called explictly if programmatic instance (Accordion has to wait for header to be placed in DOM)
        
        //console.log("PanelNav.startup "+this.parent.id);
        this.panelId = this._addPanel(this.parent); //moved from PanelMgr's initialize
        
        var hdNode = $(this.id + "_hd");
        this.hdImg = new brand.img(hdNode, ["off","on","sel"]);
    },

    showPanel: function($super, args) {
        // summary:
        //      Extends panelManager.showPanel
        
        //console.log("PanelNav.showPanel "+this.id);
        $super(args);
        
        if (this.hasPanelSiblings) {
            if (this.hdImg) this.hdImg.changeSrc("sel"); // gnav category img
        } else {
            if (this.hdImg) this.hdImg.changeSrc("on"); // gnav section img
        }
    },
    
    hideItem: function($super, args) {
        // summary:
        //      Extends panelManager.hideItem
        
        $super(args);
        
        if (this.hdImg) this.hdImg.changeSrc("off");
    },
    
    _onMouseOver: function(e) { 
        if (this.hdImg && !this.isActive) this.hdImg.changeSrc("on");
    },
    
    _onMouseOut: function(e) {
        if (this.hdImg && !this.isActive) this.hdImg.changeSrc("off");
    }
});

site.layout = {};//JSTEST temp
site.layout.Panel = Class.create(Widget, 
{
    // summary:
    //      Creates visual representation of sliding panel element with close button
    //      Can contain a single PanelSubNav or multiple navs as part of a PanelNavSet 
    //templatePath: "/js/brand/globalnav/templates/Panel.html",        
    templatePath: "jsTemplates.globalnav.Panel",  
    
    // isOpen: Boolean
    // Panel open/closed state
    isOpen: false,
    
    // parentId: String
    // pointer to PanelNav or PanelSet that spawned me
    parentId: "",
    domParent: "panel_container",
    
    // panel animation properties 
    closedpx: -96,
    openpx: 192,
    durationOpen: 0.4,
    durationClose: 0.3,
    
    initialize: function($super, properties) {
        this.setProperties(properties);  
        //console.log("site.layout.Panel.initialize "+this.id+ " / "+this.parentId + " / "+this.domParent); 
        $super();   
    },
    postCreate: function() {
        // place panel in panel container 
        //console.log("site.layout.Panel.postCreate"); 
        //$("panel_container").innerHTML += this.domNode;
    },
    
    _onClickClose: function() {
    // summary:
    //      Handler for close button on panel
    //      If this is a set, get active PanelNav via PanelNavSet
    //      Else, talk to PanelNav directly
        //console.log("site.layout.Panel._onClickClose()"); 
        var parent = $(this.parentId).widget;
        
        // otherwise, look for parent instance stored by GlobalSet
        if (!parent) {
            try {
                var gset = $(globalNavSetId);
                parent = gset.getChild(this.parentId);
            }
            catch(err){};
        }
            
        if (parent.isPanelSet) { 
            var activeItem = $(parent.activeItemId).widget;
            activeItem.hideItem();
        } else {
            parent.hideItem();
        }
    },
    
    open: function() {
        //console.log("site.layout.Panel.open "+this.id);
        var node = $(this.id);
        node.addClassName("panel_active");
        this._slide(1, node);
        this.isOpen = true;
    },
    
    close: function() { 
        //console.log("site.layout.Panel.close "+this.id);
        var node = $(this.id);
        node.removeClassName("panel_active");
        this._slide(0, node);
        this.isOpen = false;
    },
    
    _slide: function(state, node) { 
        //console.log("site.layout.Panel.open._slide: "+node.id + " to "+this.openpx);
        var duration, start, end, onEnd;
        if (state == 1) {
            node.style.display = "block";
            node.style.left = this.closedpx + "px";
            end = this.openpx;
            duration = this.durationOpen;
        } else {
            end = this.closedpx;
            duration = this.durationClose; 
        }
      
        new Effect.Move(node, { 
            duration: duration,
            x: end, 
            y: 0, 
            mode: 'absolute'
        });
    }
});site.customerService.init = function() {
    if (!page_data || site.customerService.abort) return;
    //console.log("site.customerService.init"); 
    
    // customer service pages
    var section = page_data.panel_nav["default"];
    if (section && section.id === "customer_service") {
    
        // sub-section/page
        var subsection = section.item;
    
        // top inquiries faq
        if (subsection && subsection.id === "top_inquiries") {
            site.customerService.faq.init();
        }

        // site map
        var maincontent = $("main_content");
        var sitemap = (maincontent ? maincontent.select('div.site-map')[0] : false);
        if ((subsection && subsection.id === "site_map") || (sitemap) ) {        
            // HACK: hide the customer service nav that automatically loads on the site map page
            // until the paths config in includes/global.tmpl is fixed
            var panel = $("panel_open");
            var nav = (panel ? panel.select('div.panel_cms_html')[0] : false);
            if (nav) {
                nav.style.display = "none";
            }           
        }
        
    }
}
site.product.init = function() { 
    if (!page_data || site.product.abort) return;
    //console.log("site.product.init ");
    
    // set rb keys
    site.product.getRBKeys();
    
    var catalog = page_data.catalog;
    
    if (catalog) {
    
        // init waitlist popover behavior
        site.product.waitlist.init();
        
        if (catalog.spp) {
            generic.events.fire({event:"PAGEDATA:RESULT",msg:"catalog.spp.product"});
            site.spp.init();
        }
        if (catalog.mpp) { // e.g. what's new, looks
            generic.events.fire({event:"PAGEDATA:RESULT",msg:"catalog.mpp.products"});
            site.mpp.initSections();
            
            // START: IGNORE SHOPPABLES ON FAVES PAGE FOR NOW
            var pageid = "";
            try {
                pageid = page_data.panel_nav["default"].item.id;
            }
            catch(err) {}
            if (pageid === "faves") {
                // for now: get rid of link styles
                var favesLinks = $$(".shoppable");
                favesLinks.each(function(node) {
                    node.style.textDecoration = "none";
                    node.style.cursor = "default";
                });
                return;
            }
            // END: IGNORE SHOPPABLES ON FAVES PAGE FOR NOW
            
            // content pages with product popovers
            if ($$(".shoppable")[0]) { 
                site.product.initShoppables({ products: catalog.mpp.products });
                return;
            }
        } 
    }
    if (page_data.video_products) { 
        site.product.videoPlayer.init();
    }
        
    // outlier pages     
    if (page_data.cms_generated && catalog) {
        if (catalog.cross_sell) {  // e.g. giving back viva glam 
            site.mpp.item.init({
                data: catalog.cross_sell,
                altNodeId: true
            }); 
        }
    }
};

site.product.getRBKeys = function () {
    site.product.rb = site.product.rb || {};
    
    // from global "language" bundle
    site.product.rb.added_to_shopping_bag = generic.rb.language.get("added_to_shopping_bag");
    //site.product.rb.items_in_bag = generic.rb.language.get("itemsinbag"); // needed?
    //site.product.rb.item_in_bag = generic.rb.language.get("item_in_bag"); // needed?
    site.product.rb.continue_shopping = generic.rb.language.get("continue_shopping");
    site.product.rb.checkout = generic.rb.language.get("checkout");
    site.product.rb.thank_you = generic.rb.language.get("thank_you");
    site.product.rb.add_to_bag = generic.rb.language.get("add_to_bag");
    site.product.rb.sorry = generic.rb.language.get("sorry");
    
    // from "brand" bundle
    var rbProduct = generic.rb("brand");
    site.product.rb["select"] = rbProduct.get("select");
    site.product.rb.to_shop = rbProduct.get("to_shop");
    site.product.rb.favorites = rbProduct.get("favorites");
    site.product.rb.added_to_favourites = rbProduct.get("was_added_to_your_favourites");
    //site.product.rb.items_in_favourites = rbProduct.get("items_in_favourites"); // needed?
    //site.product.rb.item_in_favourites = rbProduct.get("item_in_favourites"); // needed? 
    site.product.rb.limited = rbProduct.get("limited"); 
    site.product.rb.macpro = rbProduct.get("macpro");
    site.product.rb.step = rbProduct.get("step"); 
    site.product.rb.of = rbProduct.get("of");
    site.product.rb.search_results = rbProduct.get("search_results");
    site.product.rb.search_no_results = rbProduct.get("search_shades_no_results");
};


site.product.initShoppables = function(args) {  
    //console.log("site.product.initShoppables");
    brand.product.shoppableContent.init({ 
        products: args.products,
      
        // position popup near mouse
        positionPopup: function(evt, cartAddMsg, cartConfirmMsg) {
            var lmin = 6;
            var lmax = 530;
            var xOffset = 310; // left nav (190) & half of length of popup (120)   
            var yOffset = 100; // ~ height of popup 
            
            var t = (evt.pageY - yOffset);
            var l = (evt.pageX - xOffset); 
            // adjust for left & right edges
            if (l < lmin) {
                l = lmin;
            } else if (l > lmax) {
                l = lmax;
            }
            
            console.log("t = "+t+" l = "+l);
            cartAddMsg.position = {top: t, left: l};
            cartConfirmMsg.position = {top: t, left: l};            
        }
    });  
};

// extends brand.spp
if (!brand.spp) brand.spp = {};
site.spp = Object.extend(brand.spp, {

    // entity chars that should be replaced by unicode for display in select menus (e.g. "Matte&#178" finish)
    entitiesToUnicode: { 
        "&#178" : "\262",
        "&eacute;" : "é",
        "&aacute;" : "á",
        "&#233" : "é",
        "&#232" : "è",
        "&agrave" : "à",
        "&egrave" : "è"        
    },
    
    inventoryStatusNode: null,
    skuField: null,
    skuFavField: null,
        
    init: function() { 
        // console.log("site.spp.init");
        
        // SPP main product
        var is_shaded = false;
        this.skuField = skuField = $("prod_sku");
        this.skuFavField = skuFavField = $("btn_save_to_favorites");
        var prodBrowserSkuField = $("btn_color_play");
        var product = page_data.catalog.spp.product;
        this.inventoryStatusNode = $("inventory_btn_message");
        var isDiscontinued = false;
        var isSized = ((product.sized == 1 && product.skus.length > 1) ? true : false); // isSized: products with multiple skus based on size (non-shaded)
        /* TODO: distinguish between featured goodbyes products that are shoppable & unshoppabled disco prods.  Commenting out for now so that goodbyes are shoppable
        try {
            if (page_data.panel_nav["default"].item.id == "discontinued") isDiscontinued = true; 
        } catch(e) {
        }
        */

        var shadedType = null;
        var hasTabs = false;

        // solos, duos, trios+ notes:
        // solos & duos w/ 1 swatch per sku: page data "shaded" is 1
        // duos w/ 2 swatches per sku & multiple skus: data type is "other", not shaded
        // duos: sku[n]sku_multicolor_type is defined
        // "multi-colored" single skus (i.e. trio prod spp with only 1 sku containing 3 shades): page data "shaded" is 0, product.product_multicolor_type is defined
        if (product.shaded == 1 || product.product_multicolor_type) {
            // solos & multi-skued duos
            var multiShaded = false;
            if (product.shaded == 1) {
                is_shaded = true;
            }
            // check for multicolored
            var shadetype_data = brand.product.getShadeType({product: product, multicolor_min: 2}); // pass minimum # of shades to count as multi-colored
            shadedType = shadetype_data.type;
            
            // multi-shaded prods that display separate swatch images for each color value
            if ((shadetype_data.ismulti && !product.shaded)) {
                multiShaded = {};
                if (product.skus.length == 1) {
                    // skus that are 3+ multi-colored are displayed on SPP all by themselves (NOT the same as products with only 1 shoppable sku.  Former is a constant, latter
                    multiShaded.isSingleSkued = true;
                } else if (product.skus.length > 1) {
                    // multi-shaded products w/ multiple skus
                    multiShaded.isMultiSkued = true;
                }
             }
        }

        var cartConfirmMsg = new site.product.cartConfirm({
            id: "cart_confirm_spp",
            is_shaded: is_shaded,
            prodName: product.name,
            nodeToReplace: $("cart_confirm_placeholder")
        });

        // show swatch container for any singles, duos, trios, etc...
        if (is_shaded || shadedType) {
            //console.log("site.product.spp: is_shaded");

            if (!multiShaded) {           
                // tab container & toggling handler
                if ($("prod-tabs")) {
                    site.spp.tabContainer.init();
                    hasTabs = true;
                }
            }
            
            // selected or default or first sku as sorted by color
            var selectedSku = page_data.selected_sku;
            if (selectedSku && selectedSku.indexOf("SKU")) { // get sku base id from full path
                selectedSku = "SKU" + selectedSku.split("SKU")[1];
            }
            if (!selectedSku) {
                if (page_data.default_sku) {
                    selectedSku = page_data.default_sku;
                } else {
                    selectedSku = product.skus[0].sku_id;
                }
            }

            // init swatches
            var swatchArgs = {
                product: product,
                skuField: skuField,
                shadedType: shadedType,
                isDiscontinued: isDiscontinued,
                selectedSku: selectedSku,
                domParent: "spp-thumbs-wrapper",
                initDefault: true
            }
            var pageArgs = {
                multiShaded: multiShaded,
                prodBrowserSkuField: prodBrowserSkuField,
                favField: skuFavField,
                cartConfirm: cartConfirmMsg,
                hasTabs: hasTabs
            };

            var swatchSet = site.spp.initSwatches({
                node: $("spp-thumbs-container"),
                swatchArgs: swatchArgs,
                pageArgs: pageArgs
            });

            // init color play link (optional)
            site.spp.initColorPlayButton(prodBrowserSkuField);
            
            // init "more" description link
            brand.spp.initDescription({
                linkNode: $("descr-full-link"), 
                descriptionNode: $("descr-full"), 
                hasDescription: product.more_desc_flag
            });
        
        } else if (isSized) {
            // isSized: products with multiple skus based on size (non-shaded)
            //console.log("site.product.spp.init: sized prod");
            site.spp.initSized({
                skus: product.skus,
                skuField: skuField,
                favField: skuFavField,
                menuNode: $("menu-sizes"),
                cartConfirmMsg: cartConfirmMsg
            });

        } else {
            // non-shaded: 1st sku is default add-to-bag button value
            // (no swatches, no menus)
            //console.log("site.product.spp.init: non-shaded");
            this.setSkuSelection({ sku: product.skus[0], cartConfirmMsg: cartConfirmMsg });
        } 

        // main image rollover (optional)
        site.spp.photoRollover.init(product.image_medium, product.image_medium_rollover);
        
        // unless discontinued: set up cart/favorites buttons & cross-sell items
        if (isDiscontinued) return;
        
        // add to cart button
        var sppButton = brand.product.addButton({
            addButtonNode: skuField,
            progressNode: $("progress_add_to_bag"),
            callback: function(response) {
                //console.log("site.product.sppInit sppButton callback "+cartConfirmMsg);
                cartConfirmMsg.setDisplayProperties({ type: "cart", lockToNode: $("add_to_bag"), useLeftAlign: false });
                cartConfirmMsg.show({ response: response });
            }
        });
        
        // add to favorites button
        var favSppButton = brand.product.addButton({
            addButtonNode: skuFavField,
            skuField: skuField,
            progressNode: $("progress_add_to_fav"),
            itemType: "favorites",
            callback: function(response) {
                cartConfirmMsg.setDisplayProperties({ type: "favorites", lockToNode: $("add_to_fav"), useLeftAlign: false });
                cartConfirmMsg.show({ response: response });
            }
        });
 
        // MPP cross-sell products
        site.mpp.item.init({
            data: page_data.catalog.spp.product.cross_sell
        });
           
    }
});

if (!brand.mpp) brand.mpp = {};
site.mpp = Object.extend(brand.mpp, {
    initSections: function() { 
        //console.log("site.mpp.initSections");
        var pageContext = page_data.panel_nav["default"];
                
        if ( pageContext.item && pageContext.item.id === "looks") {
            site.mpp.initLooks();
            return;
        }

        if ( pageContext.item && page_data.catalog.subcollection_page ) {
            site.mpp.initSubcollection();
            return;                        
        }

        if ( pageContext.item && page_data.catalog.picks_page ) {
            site.mpp.initPicksCollection();
            return;                        
        }

        // favorites page
        if ( pageContext.item && pageContext.item.id === "favorites" ) {
            site.mpp.initFavorites();
            return; 
        }
    
        site.mpp.item.init({
            data: page_data.catalog.mpp.products,
            altNodeId: true,
            altType: true
        }); 
    },
    
    initLooks: function() { 
        // buttons
        var looksSkus = page_data["all_shoppable_looks_skus"]; 
        
        if (looksSkus) {
            var skus = [];
            looksSkus.each(function(sku){
                skus.push(sku+":1");
            });
            var addAllToCart = brand.product.addButton({
                addButtonNode: $("all_to_cart_img"),
                skus: skus,
                //itemType: "collection", // MERGE NOTE: results in server error
                progressNode: $("progress_all_to_cart"),
                callback: function(response) {
                    // cart confirm popover
                    brand.overlay.launch({
                        foregroundNode: $("popover-confirm-all-to-cart"),
                        displayInline: true,
                        removeOnHide: false
                    });
                }
            });
        }
        
        var products = page_data.catalog.mpp;
        for( catId in products ) {
            site.mpp.item.init({
                data: page_data.catalog.mpp[catId].products,
                initButtons: true,
                altNodeId: true,
                altType: true
            });
        }   
    }, // end initLooks     

    initSubcollection: function() {
        // confirmation popovers
        /* MERGE NOTE: old/needed?
        function initConfirmMsg(id) {
            var pop = new generic.popupMessage({
                popup: $("popup_confirm_"+id),
                buttonClose: $("popup_close_confirm_"+id)
            });
            return pop;
        }
        */
        var products = page_data.catalog.mpp;
        for( catId in products ) {
            site.mpp.item.init({
                data: page_data.catalog.mpp[catId].products,
                initButtons: true,
                altNodeId: true,
                altType: true
            });
        }
    }, // end initSubcollection
    
    initPicksCollection: function() {
        // confirmation popovers
        /* MERGE NOTE: old/needed?
        function initConfirmMsg(id) {
            var pop = new generic.popupMessage({
                popup: $("popup_confirm_"+id),
                buttonClose: $("popup_close_confirm_"+id)
            });
            return pop;
        }
        */
        var products = page_data.catalog.mpp;
        for( catId in products ) {
            site.mpp.item.init({
                data: page_data.catalog.mpp[catId].products,
                initButtons: true,
                altNodeId: true,
                altType: true
            });
        }
    } // end initSubcollection
});
site.checkout.init = function() {     
    //console.log("site.checkout.init "+page_data.panel_nav["default"].id);
     
    var cartHandler = generic.checkout.cart; 
    
    // on signout from panel nav
    generic.events.observe("cartCount:reset", function(totalItems) {  
        cartHandler.updateCartTotals( {"totalItems": totalItems} ); //sets the Cookie in case of signout 
    });
     
    // non-checkout pages
    if (page_data.panel_nav["default"].id !== "checkout") {  
        site.checkout.cartStatus.init();  
        cartHandler.getCartTotals();  
        return;
    }
    
    // checkout - all pages: "continue shopping" link
    site.checkout.makeExitBtn();  
    
    /* MERGE NOTE: needed?
    //Checkout - all pages except Signin
    site.checkout.makeAdditionalInfoBtns(); 
    site.checkout.hasErrors = Boolean(page_data.hasErrors); 
    var canContinueCheckout = !site.checkout.hasErrors;  
    */
    
    // confirmation page: reset gnav cart total display
    if (page_data.panel_nav["default"].item && page_data.panel_nav["default"].item.id=="confirm") { 
        cartHandler.updateCartTotals( {"totalItems": 0} ); // resets cart cookie
    
        /* MERGE NOTE: needed?
        if ($$(".btn-print-receipt")[0]) {
            var popup_returns = new generic.popup({ 
                activator: "btn-print-receipt",
                url: "/checkout/receipt"
            });      
        }
        */
    } 
}
site.account.init = function() {      
    //console.log("site.account.init");  
    
    // account panel nav links
    // defined in globalnav.config
    if (site.globalnav.config && site.globalnav.config.items) {
        var accountConfig = site.globalnav.config.items.find(function(item) {
            if (item.id === "account") return true;
        });
        if (accountConfig) {
            var accountSubNavConfig = accountConfig.accountnav;
            this.panel.init(accountSubNavConfig);
        }
    }
    
    var pageContext = page_data.panel_nav["default"];
    
    // account section
    if (pageContext.id !== "account") return;
    
    // messages page: display the video player if needed
    if(pageContext.item && pageContext.item.id == "messages" && $("flash_placeholder")) {
        site.product.videoPlayer.init();
    }
} 
site.view.init = function() { 
    //console.log("site.view.init");
    if (generic.env.isIE6) site.view.setFormSelectors();
    site.view.colorNav.embed();  
    site.view.initCustomViews();
    
    site.view.utilityNav.init({ 
        minTop: 440 // minimum num px from top of body to beginning of utility nav
    });
    
    site.view.footer.adjust();  
    site.view.flashPopover.embed(); // flash popover for shipping messages, etc...
    site.view.heightHandler.init();
    site.view.initRollovers();
}  
      
site.view.initCustomViews = function() { 
    if (typeof page_data == "undefined") return;
    var panelNavDefault = page_data.panel_nav["default"];

    if (panelNavDefault.item && panelNavDefault.item.id === "fromourlips") {
        site.view.fromourlips.init();
        return;
    }

    //what's new, looks, picks
    if (page_data.catalog && page_data.catalog.mpp) {
        site.view.collectionBrowser.init();
        return;
    }
    //brush play, color play, mascara finder
    if ($("productBrowser_resize")) { 
        site.view.productBrowser.init();
        return;
    }

    //artists  
    if ($("artists_block")) {
        site.view.artists.createRollOvers();
    }
    
    // home or shop page
    if (panelNavDefault.id === "index" || panelNavDefault.id === "home" || (panelNavDefault.item && panelNavDefault.item.id === "shop")) {
        site.view.home.init();
        return;
    }
         
    // giving_back/vivaglam:
    // add wrapper class so cross-sell modules can be controlled via external css
    if (page_data.cms_generated && page_data.catalog) {
        var maincontent = $("main_content_td");
        if (maincontent) {
            maincontent.addClassName("cms-product-content");
        }
    }
    
}
/*** NAV CONFIG ***/   

site.globalnav.config = {};

// default settings for items that are different in other contexts (e.g. ipad)
site.globalnav.contextualConfig = {
    artistryinaction: {
        name: "Maquilleurs En Action",
        id: "artistry_in_action",
        cmcat : "260",
        header : "/images/gnav/gnav_artistry_in_action_157x18_off.gif",
        content: {
            widget: "ArtistryInActionSubNav",
            url: "/cms/makeup_artistry/artistry_action/panel_nav.tmpl",
            featuredMax: 5 // number of videos to show in featured (initial display) list
        } 
    },
    sceneandspotted: {
        name: "Scene and Spotted",
        id: "sceneandspotted",
        header : "/images/gnav/gnav_scenespotted_157x18_off.gif",
        uri: "/sceneandspotted/index.tmpl#/all"
    }
};

// ipad-specific settings
site.globalnav.contextualConfigIPad = {
    backstage: null,
    artistryinaction: null
};



// Returns final set of config items based on site status
// Runs after page (page_data) has loaded so pro status is known
site.globalnav.getConfig = function() {
    var contextualConfig = site.globalnav.contextualConfig;
    if (global.isipad) {
        contextualConfig = site.globalnav.contextualConfigIPad;
    }

    var config = {
        items : [
            {
            name: "Nos Produits",
            id: "products",
            parentId: "globalnav_container",
            domParent: "globalnav",
            header: "/images/gnav/gnav_products_157x18_off.gif",

             
            // supercategories: shared attributes
            itemsConfig: {
                content: {
                    widget: "ProductSubNav",
                    url: "/includes/panel_nav/catalog.tmpl",
                    param: "CATEGORY_ID"
                }
                /* for intl sites w/ 4 img states
                // NOTE: hasn't been tested since port to perlgem
                template: {
                    detail: { headerStates: ["off", "on", "sel", "active"] }
                }
                */
            },
            // supercategories: data
            items : [
                contextualConfig.sceneandspotted,
                {
                    name : "Nouveautés",
                    id : "whatsnew",
                    cmcat : "CAT794",
                    header : "/images/gnav/gnav_whatsnew_157x18_off.gif",
                    content: {
                        widget: "ProductSubNav",
                        url: "/cms/whats_new/panel_nav.tmpl"
                    },
                    template: { 
                        detail: { baseClass: "panelnav_cell_category" }
                    }
                },
                {
                    name : "Yeux",
                    id : "CAT148",
                    cmcat : "CAT148",
                    header : "/images/gnav/gnav_eyes_157x18_off.gif"
                },
                {
                    name : "Lèvres",
                    id : "CAT163",
                    cmcat :  "CAT163",
                    header : "/images/gnav/gnav_lips_157x18_off.gif"
                },
                {
                    name : "Visage",
                    id : "CAT155",
                    cmcat : "CAT155",
                    header : "/images/gnav/gnav_face_157x18_off.gif"
                }, 
                {
                    name : "Prep+Primer",
                    id : "CAT172",
                    cmcat : "CAT172",
                    header : "/images/gnav/gnav_primer_157x18_off.gif"
                },
                {
                    name : "Studio",
                    id : "CAT1921",
                    cmcat :"CAT1921",
                    header : "/images/gnav/gnav_studio_157x18_off.gif"
                },
                 {
                    name : "Mineralize",
                    id : "CAT869",
                    cmcat :"CAT869",
                    header : "/images/gnav/gnav_mineralize_157x18_off.gif"
                },
                {
                    name : "Multi-Usage",
                    id : "CAT793",
                    cmcat : "CAT793",
                    header : "/images/gnav/gnav_multiuse_157x18_off.gif"
                },
                {
                    name : "Soins De La Peau",
                    id : "CAT176",
                    cmcat : "CAT176",
                    header : "/images/gnav/gnav_skincare_157x18_off.gif"
                },
                {
                    name : "Pinceaux",
                    id : "CAT144",
                    cmcat : "CAT144",
                    header : "/images/gnav/gnav_brushes_157x18_off.gif"
                },
                {
                    name : "Ongles",
                    id : "CAT170",
                    cmcat : "CAT170",
                    header : "/images/gnav/gnav_nails_157x18_off.gif"
                },
                {
                    name : "Fragrances",
                    id : "CAT161",
                    cmcat : "CAT161",
                    header : "/images/gnav/gnav_fragrance_157x18_off.gif"
                },
                {
                    name : "Les Essentiels",
                    id : "CAT133",
                    cmcat : "CAT133",
                    header : "/images/gnav/gnav_kit_essentials_157x18_off.gif"
                },
                {
                    name : "Accessoires",
                    id : "CAT139",
                    cmcat: "CAT139",
                    header : "/images/gnav/gnav_accessories_157x18_off.gif"
                } 
                /*
                {
                    name : "Custom Palette",
                    id : "CAT791",
                    cmcat : "CAT791",
                    header : "/images/gnav/gnav_custompalette_157x18_off.gif"
                },
                */
                /*
                {
                    name : "Goodbyes",
                    id : "discontinued",
                    cmcat : "CAT20833",
                    header : "/images/gnav/gnav_discontinuedproducts_157x18_off.gif",
                    content: { 
                        url: "/discontinued/panel_nav.tmpl",
                        param: "dquery",
                        // goodbyes data: should come from panel nav include
                        featured : {
                            "name" : "Featured Goodbyes", 
                            "id" : "featured_goodbyes",
                            "header" : "/images/goodbyes/pnav/pnav_featured_goodbyes_250x18_off.gif",
                            "uri" : "/products/featured_goodbyes.tmpl"
                        }
                    },
                    template: {
                        detail: {
                            type: "jsTemplates.globalnav.SimpleDetail",
                            baseClass: "panelnav_cell_category"
                        }
                    }
                    // No disc searc for now
                    //search: {
                        //formFieldId: "disc_search_input",
                        //formSubmitId: "disc_search_submit"
                        //errorPopup: "pop_search_invalid"
                    //}
                }
                */
                /*
                {
                    name : "Gift Card",
                    id : "CAT792",
                    cmcat : "CAT792",
                    header : "/images/gnav/gnav_giftcard_157x18_off.gif",
                    content: { url: "/giftcards/panel_nav.tmpl" }
                } 
                */
            ]
        }, // end products
      
        {
            name: "L'Art Du Maquillage",
            id: "makeup_artistry",
            parentId: "globalnav_container",
            domParent: "globalnav",
            header: "/images/gnav/gnav_artistry_157x18_off.gif",
            
            items : [
                // {
                //     name: "News",
                //     id: "newsworthy",
                //     header : "/images/gnav/gnav_newsworthy_157x18_off.gif",
                //     uri: "/makeup_artistry/newsworthy.tmpl"
                // },
                {
                    name: "Les Maquilleurs",
                    id: "artists",
                    cmcat : "CAT1048",
                    header : "/images/gnav/gnav_theartists_157x18_off.gif",
                    content: { 
                        url: "/cms/makeup_artistry/artists/panel_nav.tmpl",
                        handleAs: "html",
                        cms: true // global nav writes out cms panel navs for default state
                    }         
                },
                contextualConfig.artistryinaction,
                /* Bug 69839 - Removing Choix De Pros from global nav
                {
                    name: "Choix De Pros",
                    id: "faves",
                    header : "/images/gnav/gnav_faves_157x18_off.gif",
                    content: { 
                        url: "/cms/makeup_artistry/faves/panel_nav.tmpl",
                        widget: "SectionDescSubNav"
                    },
                    template: {
                        detail: { baseClass: "panelnav_cell_category" }
                    }
                },
                */
                /*
                {
                    name: "Email An Artist",
                    id: "email_an_artist",
                    cmcat: "1300",
                    header : "/images/gnav/gnav_email_an_artist_157x18_off.gif",
                    content: { url: "/cms/makeup_artistry/email_artist/panel_nav.tmpl" },
                    template: { 
                        detail: {
                            type: "jsTemplates.globalnav.SimpleDetail"
                        }
                    }
                },
                */
                {
                    name: "M·A·C Pro",
                    id: "macpro",
                    cmcat: "280",
                    header : "/images/gnav/gnav_macpro_157x18_off.gif",
                    content: { 
                        url: "/cms/makeup_artistry/mac_pro/panel_nav.tmpl",
                        handleAs: "html"
                    }
                }
            ]
        }, // end makeup_artistry

        {
            name: "Fonds Sida M·A·C",
            id: "giving_back",
            cmcat: "290",
            parentId: "globalnav_container",
            domParent: "globalnav",
            header: "/images/gnav/gnav_givingback_157x18_off.gif",          
            content: { url: "/cms/giving_back/panel_nav.tmpl" },
            template: { 
                detail: {
                    type: "jsTemplates.globalnav.SimpleDetail",
                    baseClass: "panelnav_cell_category"
                }
            }
        },

        {
            name: "Mon Espace M·A·C",
            id: "account",
            cmcat: "1000",
            parentId: "globalnav_container",
            domParent: "globalnav",
            header: "/images/gnav/gnav_mymac_157x18_off.gif",
            // subnav settings
            content: {
                hasLoaded: true, // subnav already placed into page
                handleAs: "html",
                cache: false,
                reinsertNode: true
            },
            "accountnav": {
                sections: ["account_index", "registration", "address_book", "order_history", "purchases", "favorites", "messages"]
            }
        },

        {
            name: "Service Clients",
            id: "customer_service",
            cmcat: "1400",
            parentId: "globalnav_container",
            domParent: "utilitynav_links",
            header: "/images/gnav/gnav_customer_service.gif",
            content: {
                url: "/cms/customer_service/panel_nav.tmpl",
                handleAs: "html",
                cms: true
            }
        }, 

        {
            id: "search",
            cmcat: "1700",
            content: { 
                url: "/search/includes/panel_nav.tmpl",
                param: "query"
            },
            // for product search in includes/global_nav.tmpl
            search: {
                formFieldId: "search",
                formSubmitId: "search_button",
                errorPopup: "pop_search_invalid"
            }
        }
    ],

    
    // key for handling non-default content types as passed in data as item.type (via loader config or via included tmpl JSON)
    // EX: collections Detail modules use default baseClass "panelnav_cell_category" as well as "panelnav_cell_header_only"
    altTypes : {
        "header_only" : { 
            detail: { baseClass: "panelnav_cell_header_only" }
        },
        "simple_detail" : {
            detail: { template: "SimpleDetail" }
        }
    }

    }; // end config

    // notify analtyics this is ready
    // document.fire('sitenav:loaded',config);

    return config;
    
};


// initialize globalnav 
site.globalnav.init = function() { 
    if (site.globalnav.abort || !$("globalnav")) return;
    console.log("site.globalnav.init "+page_data.panel_nav["default"].id);
    
    var config = site.globalnav.config = site.globalnav.getConfig();
    var section = page_data.panel_nav["default"].id;
    
    // get default page state 
    // structure: defaultState{ id: "", item: { id: "", item: {...} } }
    var defaultState = {};
    if (page_data && page_data.panel_nav) {
        defaultState = page_data.panel_nav["default"];
    }
    
    if (section === "products") {
        $("panel_open").addClassName("panel_open_products_panel");     
    }
    
    // in global_nav.tmpl
    globalNavSetId = "globalnav_container"; 
      
    // init entire nav
    var globalSet =  new site.globalnav.GlobalSet({id: globalNavSetId});
    globalSet.gnav = new site.globalnav.GlobalNav({
        config: config, // all nav items
        defaultState: defaultState, // default/open state data
        globalNavSetId: globalNavSetId
    });

    // after globalnav renders
    // sets reveal of content that's supposed to be hidden until gnav loads
    // hidden content specified by "hide-before-globalnav-load" css class
	if (!config.items) return;  
	var nodesToShow = $$(".hide-before-globalnav-load"); 
	nodesToShow.each(function(node) {
		node.removeClassName("hide-before-globalnav-load");
	});
	
	// init rollovers for whats new
    // MAC ME OVER only currently
    site.globalnav.collectionThumbnailRollovers.init({ ids: ["CAT6411", "CAT6412", "CAT6413"] });


};

// custom rollover states for collection panel nav thumbnails
site.globalnav.collectionThumbnailRollovers = {
    isInitialized: false,
    init: function(args) {
        var ids = args.ids;
        var self = this;
            
        var initRollover = function(imgNode, id) {
            //console.log("initRollover");
            var outImg = imgNode.src;
            var linkNode = $$("#psubitem_"+id+" A")[0]; // workaround for IE: get inner A element to attach event to since IE throws error on event handler for outer link element
            if (!outImg || !linkNode) return;
            var imgPath = outImg.split(".jpg")[0];
            if (imgPath) imgPath = imgPath.split("://")[1]; // filter out domain, so US ES file requests don't get funky.
            if (imgPath) imgPath = imgPath.split("/images/")[1]; // not assuming .com in domain so assume /images/ path instead
            var overImg = "/images/" + imgPath + "_alt." + "jpg"; // naming convention for images: xxx.jpg & xxx_alt.jpg. NOTE: "alt." & "jpg" not concatenated to avoid ES MP servers from replacing "_alt.jpg" with domain+"_alt.jpg"
            if (!overImg) return;
            linkNode.outImg = outImg;
            linkNode.imgNode = imgNode;
            linkNode.isOver = false;
            var overImgObj = new Image();
            overImgObj.src = overImg;
            linkNode.overImgObj = overImgObj;
           
            var over = function(e) {
                this.isOver = true;
                if (this.imgNode) this.imgNode.src = this.overImgObj.src; // this = event target node
            };
            
            var out = function(e) {
                var eventObj = this;
                var doOut = function() {
                    if (!generic.env.isIE || (generic.env.isIE && !eventObj.isOver)) {
                        if (eventObj.imgNode) eventObj.imgNode.src = eventObj.outImg;
                    }
                }
                // IE workaround for rollover flickering. cancel out animation unless mouse is totally out of item
                if (generic.env.isIE) {
                    eventObj.isOver = false;
                    doOut.delay(0.2);
                } else {
                    doOut();
                }
            };
            
            linkNode.observe("mouseover", over);
            linkNode.observe("mouseout", out);
        };
        
        // listen for collection panel nav loading event
        generic.events.observe("panelnav:contentloaded", function(event) {
            if (event && (event.itemId === "whatsnew")) {
                if (self.isInitialized) return;
                ids.each(function(id, idx) {
                    var imgNode = $("psubitem_"+id+"_thumb");
                    if (imgNode) initRollover(imgNode, id);
                });
                self.isInitialized = true;
            }
        });
    }
};

