(function (){

// save the ATGSvcs object in case something was written to it
window._ATGSvcs = window.ATGSvcs || {};

// instantiate the local and global ATGSvcs namespaces.
// Done this way so that the yui compressor will compress the local uses of ATGSvcs.
var ATGSvcs = window.CleverSet = window.ATGSvcs = {

  /** 
   * version numbering scheme: <major>.<minor>.<bugfix>
   * major: massive overhaul.
   * minor: api change.
   * bugfix: fixes bugs, should not 'negatively' change api or behavior.
   */
  REC_VERSION: "3.3.3",

  // default recommendation host
  REC_HOST: "recs.atgsvcs.com",

  // default recommendations base path
  REC_BASE_PATH: "pr/",

  /** configuration map extracted from #cs-cfg by #recs() */
  CFG: {},

  /** map[<slot-id>] -> configuration map. Extracted from .cs-slot by #recs() */
  SLOTS: {},

  // Recommendation response cache. Updated by render(). Accessed by #clickthru(), #cfg(), and also possibly by renderers.
  // #render() follows a minimalist update pattern where only the values in the response are over-written. This allows renderers to 
  // extend the tree with their own data.
  //
  // The primary key is the slot-id.
  REC_DATA: {},

  /** Map of page id attributes to rec id */
  REC_IDS: {},

  /** l10n object */
  LOCALE: {},

  // When writing a renderer there are certain requirements for maintaining functionality like click-thrus and img load errors:
  //  + any A element that exists inside a recommendation (class: cs-rec) has a click_thru binding automatically added via ATGSvcs#render().
  //
  // this keyword refers to map of response data.
  // this.recs -> [] of recs, each as a map of values.
  // @param s is a JQuery DOM handle to the slot element to be rendered.
  // @param $ is a shortcut to the jQlite package. (Allows useage of $ instead of jQlite inside renderer functions)
  //
  // e.g.:
  // RENDERERS['<name>']: function(s,$) {
  //   ...
  // },
  RENDERERS: {},

  // Name of default renderer
  DEFAULT_RENDERER: "tiles",

  // Hostname where renderers are found
  RENDERER_HOST: "static.atgsvcs.com",

  // Hash of recommendation builders
  RBLDR: {},

  // Name of default recommendation model
  DEFAULT_RBLDR: "tile",

  /** 
   * Map holding failover defaults and instance info.
   */
  FAILOVER: {
    host: "static.atgsvcs.com",
    timeout: 5000,
    T: null   // placeholder
  },

  // Name of the visitorId cookie
  VIS_C: "atgRecVisitorId",

  // Name of the sessionId cookie
  SES_C: "atgRecSessionId",

  /**
   * Programatic DOM builder.
   * Warning: there is a simplistic parser for properly applying the style attribute, it will not recognize when : and ; are in strings.
   */
  dom: {
    /**
     * Performs the heavy lifting of unstacking the function arguments into the attributes and children of the tag.
     *
     * When processing arguments as children the following decision tree is followed:
     *
     * 'false': skip it
     * String||Number: appendChild to tag as TextNode
     * Element: appendChild to tag
     * Array: process each entry as above
     *
     * @param tag String the tag name
     * @param args Array||Arguments index 0 may be: 'false' resulting in an empty tag, an Object that contains attributes for the element, 
     *    any other value is assumed to just be the first child.
     */
    create: function(tag, args) {
      var e = document.createElement(tag), a = args[0] || {}, i = 1, j, t, x, s, k, v;

      // decide which create strategy is in effect. Modify behavior appropriately.
      if (args[0] && (args[0].constructor != Object || args[0].tagName)) {
        a = {};
        i = 0;
      }

      // process attributes
      for (t in a) {
        switch(t) {
          case "Class":
          case "class":
            e.className = a[t];
            break;
          case "style":
            e.style.cssText = a[t];
            break;
          default:
            e[t] = a[t];
        }
      }

      // process all the remaining arguments
      for (; i < args.length; i++) {
        t = args[i];

        // skip 'false'
        if (!t) {
          continue;
        }

        if (t.constructor != Array) {
          t = [t];
        }

        for (j = 0; j < t.length; j++) {
          x = t[j];

          // skip 'false'
          if (!x) {
            continue;
          }

          // convert to TextNode
          if (typeof x == 'number' || typeof x == 'string')
            x = document.createTextNode('' + x);

          ;;; try {
            e.appendChild(x);
          ;;; } catch (error) {
          ;;;   alert (error + "\n" + x + "\n" + ATGSvcs.json.o(x));
          ;;; }
        }
      }

      return e;
    },

    /**
     * Add a new tag to the builder. This creates a closure that, when called, passes the tag name and Arguments object to #create().
     */
    tag: function(tag) {
      if (!tag || ATGSvcs.dom[tag]) {
        return;
      }

      ATGSvcs.dom[tag] = function() {
        return ATGSvcs.dom.create(tag,arguments);
      };
    },

    /** 
     * Creates a new "Easy Clearing" tag '<div style="clear: both"></div>'. 
     * 
     * This is a common technique for causing an element whose children are all floating to retain it's shape.
     *
     * Originally described here: http://web.archive.org/web/20050221002751/http://www.positioniseverything.net/articles/float-theory.html
     * 'Replaced' by some CSS nonsense here: http://www.positioniseverything.net/easyclearing.html
     *
     * I prefer the EZC method as it's pretty future proof and cheaper in per-byte cost.
     */
    ezc: function(){
      return ATGSvcs.dom.DIV({style: "clear: both"});
    },

    /**
     * Inject a multitude of tags from a single pipe-delimited ("|") list.
     * @param tags e.g. "A|DIV|IMG|LI|P|SCRIPT|SPAN|UL"
     */
    tags: function(tags) {
      tags = tags.split(/\|/);
      for (var i = 0; i < tags.length; i++) {
        ATGSvcs.dom.tag(tags[i]);
      }
    }
  },

  renderer: function(name, func) {
    ATGSvcs.RENDERERS[name] = func;

    ;;; ATGSvcs.debug(name, " renderer loaded.");

    // notify any listeners.
    ATGSvcs.erg.occur("renderer_" + name + "_loaded");
  },

  /**
   * Hook for phase-one activity. Called when DOM is ready to manipulate.
   */
  recs: function() {
    var t, cfg=ATGSvcs.cfg, checkout_activation, checkout_now;
    // Attempt to cancel out race conditions.
    ATGSvcs.recs = function(){};
    ATGSvcs.erg.occur("ready");

    // Dump some debugging info
    ;;; ATGSvcs.debug("ATGSvcs.recs()...");
    ;;; var dbg=[];
    ;;; dbg.push(ATGSvcs.dom.DT("visitorId")); dbg.push(ATGSvcs.dom.DD(ATGSvcs.visitorId()));
    ;;; dbg.push(ATGSvcs.dom.DT("sessionId")); dbg.push(ATGSvcs.dom.DD(ATGSvcs.sessionId()));
    ;;; ATGSvcs.debug("Cookies:",ATGSvcs.dom.DL.apply(null,dbg));

    // Recover any saved CFG data
    ;;; ATGSvcs.debug("Saved Config: ", ATGSvcs.dom.PRE(ATGSvcs.json.o(window._ATGSvcs.CFG, true)));
    jQlite.extend(true, ATGSvcs.CFG, window._ATGSvcs.CFG);
    // get cfg
    jQlite.extend(true, ATGSvcs.CFG, ATGSvcs.parse_cfg(Selector("#cs-cfg")[0]));
    ;;; ATGSvcs.debug("Complete Primary Configuration (CFG): ", ATGSvcs.dom.PRE(ATGSvcs.json.o(ATGSvcs.CFG, true)));

    // locale
    ATGSvcs.LOCALE = new ATGSvcs.l10n[cfg("locale") || "US"]();

    // fire event that cfg is parsed
    ATGSvcs.erg.occur("cfg_parsed");

    // CHECKOUT SETUP
    // One of: "now", or any valid CSS selector string. To be applied to the first element that we find. We only declare support for $("#ID") in the docs.
    // This does not (nor does it attempt to) work well with event listeners that try to interrupt flow (return false) from inside. Shouldn't be a problem, 
    // but now you know.
    checkout_activation = cfg("-checkout/activation");

    if (checkout_activation) {
      // notate that checkout is in play.
      ATGSvcs.erg.occur("checkout_ready");

      // if requested, perform checkout now, don't wait for activation.
      if (checkout_activation.toLowerCase() == 'now') {
        ;;; ATGSvcs.debug("Firing checkout due to activation trigger of '" + checkout_activation + "'");
        checkout_now = true;

      // if the checkout activation is not external javascript, then let's do that.
      } else if (checkout_activation.toLowerCase() != 'js') {
        // check if the activation is a valid CSS Selector, then we bind to the resulting element.
        var checkoutElements=Selector(checkout_activation);
        if (checkoutElements.length) {
          switch (checkoutElements[0].tagName.toLowerCase()) {
            case "html":
              ;;; ATGSvcs.debug("Cannot bind to ", checkout_activation);
              break;
            default:
              ;;; ATGSvcs.debug("Binding checkout to onMouseDown of ", ATGSvcs.dom.B(checkoutElements[0].tagName.toUpperCase()), " #", checkoutElements[0].id);
              jQlite.each(checkoutElements, function () {
                jQlite.addEvent(this, "mousedown", ATGSvcs.checkout);
              });
          }
        ;;; } else {  // no checkout binding
        ;;;   ATGSvcs.debug("Could not bind checkout to: ", checkout_activation);
        }

      ;;; } else {
      ;;; ATGSvcs.debug("Checkout is expected to be performed by external JS call.");
      }
    }

    // process each slot cfg
    jQlite.each(Selector("div.cs-slot"), function(){
      ATGSvcs.erg.occur("slot_found");
      ATGSvcs.SLOTS[this.id] = ATGSvcs.parse_cfg(this);

      // record renderer name (and optional url), unless it's already defined.
      r=cfg("renderer", this.id, ATGSvcs.DEFAULT_RENDERER);
      ATGSvcs.RENDERERS[r] = ATGSvcs.RENDERERS[r] || cfg("-renderer-url", this.id);
    });

    ;;; ATGSvcs.debug("Complete Slot Configuration (SLOTS): ", ATGSvcs.dom.PRE(ATGSvcs.json.o(ATGSvcs.SLOTS, true)));

    // Fetching of renderers
    ;;; ATGSvcs.debug("Considering fetching rendererers...");
    for (var z in ATGSvcs.RENDERERS) {
      var y = ATGSvcs.RENDERERS[z];
      // if there is a value, then it is assumed to be a url
      if (y) {
        if (y.constructor != Function) {
          ;;; ATGSvcs.debug("Fetching renderer (", z, ") @ " + y);
          Selector('head')[0].appendChild(ATGSvcs.dom.SCRIPT({src: y}));
        }
      } else {
        ATGSvcs.hook("/js/renderer/" + z + ".js", null, cfg("-renderer-host") || ATGSvcs.RENDERER_HOST);
      }
    };

    // start failover timer
    // must explicitly be false or we go.
    if (cfg('-failover', "", true)) {
      // check for slots on page
      if (ATGSvcs.erg.cond("slot_found")) {
        var u, f, F=ATGSvcs.FAILOVER, 

        // host
        h=cfg('-failover/host', "", F.host),

        // timeout
        t=cfg('-failover/timeout', "", F.timeout),

        // failover to a specific key
        k=cfg('-failover/key', "", F.key);

        // Either: /failover/<retailer_id>-callback.js or /failover/<retailer_id>-<key>-callback.js
        k = k ? "-" + k : "";

        // url
        u = "/failover/" + cfg("retailerId") + k + "-callback.js";

        // If debug mode, no reason to wait.
        ;;; if (cfg("-failover/skip-recommendations")) {
        ;;;   ATGSvcs.debug("Forcing failover timeout to 1ms, for debug mode.");
        ;;;   t=1;
        ;;; }

        // start timeout
        f = 'ATGSvcs.hook("'+ u +'","","' + h + '")';
        F.T = setTimeout(f, t);
        ATGSvcs.erg.occur("failover_started");

        ;;; ATGSvcs.debug("Failover timer started.");
      ;;; } else {
      ;;;   ATGSvcs.debug("Skipping failover because there are no slots.");
      }
    }

    // Make the request
    ATGSvcs.do_request(true, true, checkout_now);
  },

  /**
   * Reusable function to make the rec request
   * Calling this will overwrite any recommendations already present
   *
   * @param view Whether or not to include a view in the request (default false)
   * @param recs Whether or not to request recommendations in the request
   *             if there are slots on the page (default true)
   * @param checkout Whether or not to include a checkout in the request
   *             if there is checkout information on the page (default false)
   */
  do_request: function (view, recs, checkout) {
    // Build the request
    var slot, params = jQlite.extend({
      rcount: ATGSvcs.erg.E["render"]
    }, ATGSvcs.CFG), resource = "";

    // If the retailerId isn't set, don't do anything
    if (!params.retailerId) {
      ;;; ATGSvcs.debug("retailerId not set, aborting request.");
      return;
    }

    // remove the retailerId, it goes in the path
    delete params.retailerId;

    // If this is a VIEW, add the resource, url and referrer params
    if (view) {
      resource = "/view";
      // use extend in case the page doesn't namespace the view params
      jQlite.extend(true, params, {
        view: {
          // if there's no referrer, don't define the property
          referrer: document.referrer || undefined
        }
      });
      
      // If this is a VIEW and there's a cart, add CART to the resource path.
      if (ATGSvcs.cfg("cart")) {
        resource += "/cart";
      }
    }

    if (checkout) {
      ATGSvcs.erg.occur("checkout");
      resource += "/checkout";
      params.cart = ATGSvcs.cfg("-checkout/cart");
    }

    // if recs isn't set or is true, try to fetch recommendations
    if (recs == null || recs) {
      params.slots = ATGSvcs.SLOTS;

      // Loop through the slots and add the display attribute
      for (slot in params.slots) params.slots[slot].display = true;

      // if there's a slot, we need recommendations.  Add it to the resource path.
      // if there aren't any slots, remove the slots param
      if (slot) {
        resource += "/recommendations";
      } else {
        delete params.slots;
      }
    }

    // This is for testing failover, it will void out actual rec fetching
    ;;; if (ATGSvcs.cfg("-failover/skip-recommendations")) {
    ;;;   ATGSvcs.debug("Failsafe debug mode active, skipping reccommendation request");
    ;;; } else {
    ;;;   ATGSvcs.debug("Complete Request: ", ATGSvcs.dom.PRE(ATGSvcs.json.o(params, true)));

    // send request, but not if there's no cs-cfg block
    if (Selector("#cs-cfg").length) {
      ATGSvcs.hook(ATGSvcs.request_path(resource.substr(1)), params, null, ATGSvcs.render, "ATGSvcs.render");

      // Fire an event right after we send the request.
      // This is necessary for testing, and is helpful for debugging
      ATGSvcs.erg.occur("requested" + resource);
    ;;; } else { ATGSvcs.debug("Skipping rec request because there is no cs-cfg block");
    }

    ;;; }   // end failover cancellation debug/test mode
  },

  /**
   * Convenience function to send a checkout transaction.
   * @see #recs() where this function is bound when activation is a selector.
   */
  checkout: function () {
    ATGSvcs.do_request(false, false, true);
  },

  /** Called when a reccomendation is clicked on. Binds are injected by #render(). */
  click_thru: function () {
    var ancestors = jQlite.dir(this, "parentNode"),
    slotNode, recNode, i = ancestors.length,
    params = {
      click: {}
    };

    // Walk up the tree from the clicked element to locate the ancestors with class cs-slot and cs-rec
    while (i--) {
      slotNode = !~ancestors[i].className.indexOf('cs-slot') ? slotNode : ancestors[i];
      recNode = !~ancestors[i].className.indexOf('cs-rec') ? recNode : ancestors[i];
    }

    // finally get the rec set id from the recommendation cache.
    params.click.recSetId = ATGSvcs.REC_DATA[slotNode.id]['recSetId'];

    // and the product id from the id cache
    params.click.productId = ATGSvcs.REC_IDS[recNode.id];

    // cancel on failover
    if (ATGSvcs.cfg("-failover", "", true) && !ATGSvcs.erg.cond("failover_cancelled")) {
      return;
    }

    ATGSvcs.img_hook(ATGSvcs.request_path("clickThru"), params);

    return true;
  },

  /**
   * Hook for retrieving extra information about products from the server.  This builds and sends the request,
   * then calls a callback fucntion to process the returned data.  This is currently only called
   * by renderers but could potentially have other uses.  Sends and receives json, currently can only 
   * be called to get:
   * productUrl  type: String
   * imageUrl    type: String
   * desc        type: String
   * name        type: String
   * brand       type: String
   * category    type: String
   * inStock     type: Boolean
   * expire      type: String
   * price       type: Double
   * @param items Array of data items to retrieve
   * @param products Array of productIds about which to retrieve data
   * @param callback function name (string), takes returned json as parameter
   */
  getProductInfo: function (items, products, callback) {
    var params = {
      "productInfo" : {
        "productIds": products,
        "dataItems": items,
        "storeId" : ATGSvcs.cfg("storeId")
      }
    };

    ;;; ATGSvcs.debug("ProductInfo Request: ", ATGSvcs.dom.PRE(ATGSvcs.json.o(params, true)));
    ATGSvcs.hook(ATGSvcs.request_path("productInfo"), params, null,
                 callback ? eval(callback) : ATGSvcs.setProductInfo, 
                 callback || "ATGSvcs.setProductInfo");
  },

  /**
   * Reusable function to send a log message back to the servers
   */
  log: function (msg) {
    var params = {
      retailerId: ATGSvcs.cfg("retailerId"),
      visitorId: ATGSvcs.visitorId(),
      msg: msg,
      referrer: document.referrer || undefined
    };

    ;;; ATGSvcs.debug("Sending log message: ", ATGSvcs.dom.PRE(ATGSvcs.json.o(params, true)));

    ATGSvcs.img_hook("/log/", params);
  },

  /**
   * Hook for rendering recommendations. This is called when the recommender sends back recommendations.
   * It is reentrant and may be called multiple times, but care must be exercised not to corrupt the functionality of
   * functions that depend on REC_DATA, such as #click_thru().
   *
   * @param failover (Optional) 'hidden' flag, set only when rendering a failover.
   */
  render: function (resp, failover) {
    var d, f=ATGSvcs.FAILOVER, ck=ATGSvcs.cookie, tracking = resp.tracking;

    // register a render event. If render has already been called and this is a failover render, 
    // then ignore it.
    if (ATGSvcs.erg.occur("render") > 1 && failover) {
      return;
    }

    // kill failover timeout
    if (f.T && !failover) {
      clearTimeout(f.T);
      ATGSvcs.erg.occur("failover_cancelled");

      ;;; ATGSvcs.debug("#render() cancels failover timeout. ", (new Date().getTime()-ATGSvcs.erg.S["failover_started"][0]), "ms elapsed.");
    }

    // Set Tracking cookies
    tracking = resp.tracking;
    if (tracking) {
      // If sessionId or visitorId is returned, set them
      if (tracking.visitorId) ATGSvcs.visitorId(tracking.visitorId);

      if (tracking.sessionId) ATGSvcs.sessionId(tracking.sessionId); 
    }

    // Send slots to renderers
    for (var slotname in resp.slots) {
      var data=resp.slots[slotname],

      // find or create our REC_DATA position
      _d=(ATGSvcs.REC_DATA[slotname] || (ATGSvcs.REC_DATA[slotname]={})),

      // numRecs
      n=parseInt(ATGSvcs.cfg('numRecs', slotname, 3)),

      // shorthand for data.recs;
      e=data.recs;

      // randomize failover recs
      if (failover) {
        ;;; ATGSvcs.debug("Selecting ", n, " random recs from ", e.length, " for ", slotname);
        var s, attr, p=ATGSvcs.cfg('productId');
        for (s=new Array(n), i = n; --i >= 0;) {
          do {
          s[i]=e.splice(Math.random()*(e.length-1), 1)[0];
          ;;; if (p && p.toUpperCase()==s[i].productId) { ATGSvcs.debug('Skipping rec.id ' + p + ' matches current detail page.'); }
          } while(p && p.toUpperCase()==s[i].productId.toUpperCase())

          // make sure any urls match the current page protocol
          // slightly sloppy regex for the sake of saving a couple bytes
          for (attr in s[i]) {
            s[i][attr] = (s[i][attr] + "").replace(/^https?:\/\//, location.protocol + "//");
          }
        }

        // pull the headerText from the config
        data.headerText = ATGSvcs.cfg("headerText", slotname, null);
        ;;; ATGSvcs.debug("headerText set to " + data.headerText + " for slot " + slotname);

        // persist
        e=s;
      }

      // slice data to numRecs
      if (e.length > n) {
        ;;; ATGSvcs.debug("WARN: Slicing recs for ", ATGSvcs.dom.B("#", slotname), " from ", e.length, " to ", n, '.');
        e=e.slice(0,n);
      }

      // reset recs, just in case
      data.recs=e;

      // update REC_DATA for slot
      for (var y in data) {
        ;;; if (_d[y]) {
        ;;;   ATGSvcs.debug("Overwriting REC_DATA["+slotname+"]["+y+"]");
        ;;;}

        _d[y] = data[y];
      }

      ATGSvcs.do_render(slotname);
    }
  },

  // performs (or enqueues) rendering of a slot
  // @param renderer (for recursion, don't use)
  // @param count (for recursion, don't use)
  do_render: function(slotname) {
    // Find the renderer, div, and data to render.
    var t, r=ATGSvcs.cfg("renderer", slotname, ATGSvcs.DEFAULT_RENDERER),
        renderer=ATGSvcs.RENDERERS[r],
        div=document.getElementById(slotname),
        data=ATGSvcs.REC_DATA[slotname],
        gate={};

    // establish gate conditions
    gate["renderer_" + r + "_loaded"] = true;

    // TODO write a debug message if it's likely we're experiencing a missing renderer scenario.

    if (ATGSvcs.erg.gate(gate)) {
      ;;; ATGSvcs.debug("Rendering '", ATGSvcs.dom.B('#' + slotname), "' using '", ATGSvcs.dom.B(r), "' as the renderer.", ATGSvcs.dom.DIV({style: 'margin: auto 10px'}, ATGSvcs.dom.PRE(ATGSvcs.json.o(data, true))));

      // purge the slot contents
      div.innerHTML = "";

      // Run the renderer with the resp data and pass the JQuery DOM element as the parameter.
      // Don't call the renderer if there are no recs to display
      if (data.recs.length) renderer.call(data, div, jQlite, Selector);

      // Fire the rendered event for any listeners
      ATGSvcs.erg.occur("rendered");
    
      // Set event traps
      jQlite.each(Selector(".cs-rec a", div), function () {
        jQlite.addEvent(this, 'mousedown', ATGSvcs.click_thru);
      });

      jQlite.each(Selector(".cs-rec img", div), function () {
        jQlite.addEvent(this, 'error', ATGSvcs.img_error);
      });

      // Show slot;
      div.style.display = "block";

    // The gate condition is unmet, callback when it is met.
    } else {
      ;;; ATGSvcs.debug("Renderer ", ATGSvcs.dom.B(r), " not ready, ", ATGSvcs.dom.B(slotname), " will wait.");
      ATGSvcs.erg.wait(gate, ATGSvcs.do_render, slotname);
    }
  },

  /*
   * Registers a Recommendation Builder
   * @param name recommendation builder name
   * @param func recommendation builder function
   */
  rec_builder: function(name, func) {
    ATGSvcs.RBLDR[name] = func;

    // notify any listeners.  just in case something cares.
    ATGSvcs.erg.occur("rbldr_" + name + "_loaded");

    ;;; ATGSvcs.debug(name, " recommendation builder loaded.");
  },

  /*
   * Calls the appropriate Recommendation Builder, returns a dom object
   * @param sn rec slot name
   * @param rec hash of values returned by rec request
   * @param dr (optional) default recommendation builder for the renderer
   */
  build_rec: function(sn, rec, dr) {
    var rb=ATGSvcs.cfg('-rec-builder', sn, dr);

    // We are not dynamically loading rec builders, so it is unnecssary to use the event framework
    // If the desired model is not present we will use the global default
    ;;; ATGSvcs.debug("Using '" + ((ATGSvcs.RBLDR[rb])?rb:ATGSvcs.DEFAULT_RBLDR) + "' recommendation builder");

    return (ATGSvcs.RBLDR[rb] || ATGSvcs.RBLDR[ATGSvcs.DEFAULT_RBLDR])(sn, rec);
  },

  /**
   * Hook that is called when an image url returns an error code
   */
  img_error: function() {
    var ancestors = jQlite.dir(this, "parentNode"),
    slotNode, i = ancestors.length;

    // find the slot node in the list of ancestors
    while (i--) {
      slotNode = !~ancestors[i].className.indexOf('cs-slot') ? slotNode : ancestors[i];
    }
    this.src = location.protocol+"//"+ATGSvcs.cfg('-image-error', slotNode.id, 'static.atgsvcs.com/images/spacer.gif');
  },

  /**
   * Hook that is called when #render() fails to clear the failover timer.
   */
  failover: function (r) {
    ;;; ATGSvcs.debug("Failover activated with: ", ATGSvcs.dom.PRE(ATGSvcs.json.o(r, true)));

    // Alert the jslogging service that failover has been activated
    ATGSvcs.log("Failover activated!");

    // If there is not a slot named failover, we will assume a custom failover file is in play. One that knows valid slot-id(s).
    // If there IS a slot named failover, we fill all the recslots with them.  We can assume there are recslots because of an earlier check.
    var s=r.slots;
    if (s.failover) {
      r.slots = {};
      jQlite.each(Selector("div.cs-slot"), function () {
        r.slots[this.id]= ATGSvcs.util.clone(s.failover);
      });
    }

    ATGSvcs.render(r, true);
  },

  /**
   * Event Registry/Gating library. Registers event occurences and provides inspection mechanisms.
   */
  erg: {
    // Map of event_name -> number of times event occurred. (Read-only, Use #occur().)
    E: {},

    // Map of event_name -> millis. (Read-only, Use #occur().)
    S: {},

    // Array of entries like [{gate}, [callback info]]. (Read-only, @see #wait().)
    C: [],

    /**
     * Register an event occurance.
     */
    occur: function(name) {
      var g, c, r=ATGSvcs.erg, e=r.E, s=r.S, n=name;

      // Either increment the event count, or initialize the event count and millis tracker
      e[n] = e[n] ? e[n] + 1 : (s[n] = []) && 1;

      // push another timestamp onto the event tracker
      s[n][s[n].length] = new Date().getTime();

      // check gate callbacks [[gate,[callback]],...]
      for (var i=0; i < r.C.length;) {
        // [gate, [callback]]
        g = r.C[i][0];
        c = r.C[i][1];

        // if gate is met
        if (r.gate(g)) {
          // delete gate callback from registry
          r.C.splice(i,1);

          // if callback info is an array, then assume [function, args], otherwise assume function.
          // the function's this variable is set to the gate.
          c.constructor == Array ? c[0].apply(g, c[1]) : c.call(g);
        } else {
          // only increment i if the array has not been reduced in length
          i++;
        }
      }

      return e[n];
    },

    /**
     * Inspect a set of event criterias (a gate).
     * @return boolean depending on if all event criteria are met.
     * A gate is defined by a hash of event names whose values are either true, or a number.
     *
     * Criteria truth check definition: (If the criteria value)
     *   is true then the event must have occurred at least once.
     *   is an integer then the event must have occurred at least 'value' times.
     *
     * Example gate definitions:
     *   {
     *     renderer_tiles_loaded: true
     *   }
     *
     *   {
     *     render_called: 2
     *   }
     */
    gate: function(set) {
      var e = ATGSvcs.erg.E;

      // for every gate aspect in the set of aspects
      for (var name in set){
        if (set[name] === true ? !e[name] : !(e[name] - set[name] > 0)) {
          return false;
        }
      };

      return true;
    },

    /**
     * Like #gate(), but with only one condition.
     * @param n (optional) default is true, otherwise follows the numeric rules of #gate().
     */
    cond: function(name, n) {
      var e = ATGSvcs.erg.E;
      return n && n != true ? e[name] - n > 0 : e[name];
    },

    /**
     * Register a callback that is triggered when the gate's criteria is met. There is an additional
     * variant where extra arguments may be stored to be passed to the callback.
     *
     * The this value of the callback function is set to the gate.
     *
     * wait(gate, func)
     * wait(gate, func[, ...])
     */
    wait: function(gate, func) {
      ATGSvcs.erg.C.push([gate, (arguments.length > 2
          ? [func, [].slice.call(arguments, 2)]
          : func)]);
    }
  },

  /** Returns a parsed price if it should, null if not **/
  price: function(sn, p) {
    if(ATGSvcs.cfg('-inc-price-no-zero', sn))
      return p ? ATGSvcs.LOCALE.currency(p) : null;
    if(ATGSvcs.cfg('-inc-price', sn))
      return ATGSvcs.LOCALE.currency(p);
    return null;
  },

  /** Makes a click_thru compatible recommendation id. */
  rec_id: function(slot_id, rec_id) {
    var r = new RegExp('[^a-z0-9_]', 'g'),
        id = "cs-rec-" + slot_id.toLowerCase().replace(r, '_') + rec_id.toLowerCase().replace(r, '_');
    ATGSvcs.REC_IDS[id] = rec_id;
    return id;
  },

  /** Parses config map from DOM elements.
   * Basically we just break down a <DL>; setting <DT> to the key and <DD> to the value. 
   * If a <dd> is not present immediately after the <dt> then the value is defaulted to a boolean (not String) true.
   *
   * Finally, a <dt>..</dt><dl>...</dl> combination is recursively parsed and the resulting value is a hashmap.
   *
   * dd values are post-processed as follows:
   *   + leading and trailing spaces are stripped
   *   + truncated to 200 characters.
   *   + attempt conversion to int, float, or boolean (true/false only, not on/off, yes/no, etc.)
   */
  parse_cfg: function(e) {
    var k,v,s,t,q,c = {},is_array=true,a=[];

    if (!e) return c;
    q = (Selector("dl.cs-cfg", e)[0] || e).childNodes;

    for (var i = 0; i < q.length; i++) {
      switch (q[i].nodeName.toLowerCase()) {
      case 'dt':
        a[a.length] = k = jQlite.trim(q[i].innerHTML);
        c[k] = true;
        break;
      case 'dd':
        // not an array
        is_array = false;

        // nested dl(s)?
        //t = q[i].getElementsByTagName("dl");
        t = [];
        s = q[i].childNodes;
        for (var j = 0; j < s.length; j++) {
          if (s[j].nodeName.toLowerCase() == "dl") {
            t.push(s[j]);
          }
        }

        if (t.length) {
          v = [];
          for (var j = 0; j < t.length; j++) {
            v.push(ATGSvcs.parse_cfg(t[j]));
          }

          if (v.length == 1){
            v = v[0];
          }

        // nothing nested
        } else {
          v = jQlite.trim(q[i].innerHTML).substring(0,200);

          if (k.charAt(0) == '-') {
            // convert numbers
            if (isFinite(v)) {
              if (v.match(/^[\.0-9]+$/)) {
                v = parseFloat(v);
              } else if (v.match(/^[0-9]+$/)) {
                v = parseInt(v,10);
              }

            // convert booleans
            } else if (v.length < 6) {
              t = v.toLowerCase();
              if (t == "true") {
                v = true;
              } else if (t == "false") {
                v = false;
              }
            }
          }
        }

        c[k] = v;
        break;
      }
    }
    // if this is just a big array, return it
    if (is_array && a.length) c = a;

    return c;
  },

  /**
   * Performs fallthrough configuration lookup, and can do recursive key decent. Basically this means you should always use this method for finding
   * the value of any configuration option.
   *
   * First, the order of search is defined by slotname. If set then we look in REC_DATA[slotname] followed by SLOTS[slotname]. If not found, or 
   * slotname is 'false' then CFG is checked.
   *
   * For each of the configuration potentials (that list above), we will split the name on any forward slash '/' and assume a recursive lookup -- for each
   * name fragment, if found, we will try to go deeper. If we run out of fragments and still have a value, it is returned. If not then we move on to the
   * next configuration potential. If there is no forward slash, then it's just a name tree of one level and we return matches the same way.
   *
   * If all matching fails then we will return the value of _default.
   *
   * Basically, it does exactly what you'd expect if you asked for say: "retailerId", "renderer", or "-failover/key"
   *
   * @param name of configuration option, will also recursive property lookup on . delimited names
   * @param slotname (optional) look in REC_DATA[slotname] and SLOTS[slotname] before CFG
   * @param _default (optional) if no match is found, return default. If default is a fnction, run it and return that value
   */
  cfg: function(name, slotname, _default) {
    var r, a=(slotname?[ATGSvcs.REC_DATA[slotname], ATGSvcs.SLOTS[slotname]]:[]), k=name.split(/\//);
    a.push(ATGSvcs.CFG);

    for (var t,b,i=0; !r && i < a.length; i++) {
      // if there's even a map @ slot, keep it in b.
      if (b=a[i]) {
        // iterate (w/j) through the k set, use b for the descent temporary.
        for (var j=0; b && j < k.length; j++) {
          // special case: if b is an array, iterate through it looking for k[j]
          if (b instanceof Array)
            for (var arrayIteratorB=b.length; --arrayIteratorB >= 0;)
              if (b[arrayIteratorB] == k[j]) return true;

          // push b into r also, so it will bubble up if we run out of k.
          r=b=b[k[j]];

          // special case for r being the false literal.
          if (r === false) return r;
        }
      }
    };

    return r || ((_default && _default.constructor == Function) ? _default(name, slotname) : _default);
  },

  /**
   * @return the baseurl pattern for the curent page, including rec host override.
   */
  base_url: function (alt_host) {
    return document.location.protocol + "//" + (alt_host || ATGSvcs.cfg("-rec-host") || ATGSvcs.REC_HOST);
  },

  /*
   * @return the path for Product Recommender requests.
   */
  request_path: function (resource) {
    return "/" + ATGSvcs.cfg("-rec-base-path", null, ATGSvcs.REC_BASE_PATH) + resource + "/3.0/json/" + ATGSvcs.cfg("retailerId") + (ATGSvcs.visitorId() ? "/" + ATGSvcs.visitorId() : "");
  },

  /*
   * Set and/or Return visitorId
   * @return visitorId
   */
  visitorId: function (set_value) {
    if (set_value)
      ATGSvcs.cookie.set(ATGSvcs.VIS_C, set_value, true);

    // get the value, also check the old cookie name in case that's set
    return ATGSvcs.cookie.get(ATGSvcs.VIS_C) || ATGSvcs.cookie.get("cs-tag");
  },

  /*
   * Set and/or Return sessionId
   * @return visitorId
   */
  sessionId: function (set_value) {
    if (set_value)
      ATGSvcs.cookie.set(ATGSvcs.SES_C, set_value);

    // get the value from the cookie
    return ATGSvcs.cookie.get(ATGSvcs.SES_C) || undefined;
  },

  // utility library
  util: {
    /** 
     * Perform a deep clone of an object.
     * Can strip parameters that shouldn't be sent to the server.
     * These are:
     *  Params that start with a "-" character
     *
     * @param obj The Object that is to be deep cloned
     * @param strip A boolean that specifies whether or not properties should be stripped
     * @return A cloned copy of the Object, possibly stripped of some params
     */
    clone: function(obj, strip) {
      // if object evaluates to false or isn't iterable
      if (!obj || typeof(obj) != 'object') return obj;

      // create a new Array or Object
      var cloned = (obj.constructor === Array)? [] : {};

      // iterate over the property, cloning its children
      jQlite.each(obj, function(key, value) {
        // if we're supposed to strip, check for properties beginning with "-"
        if (!isNaN(key) || !strip || key.charAt(0) != "-") {
          cloned[key] = ATGSvcs.util.clone(value, strip);
        }
      });

      return cloned;
    },

    /** truncates a string to the length specified, doesn't split mid word */
    trunc: function(str, l) {
      var s = str.split(" "), t = "", i = 0;
      for (; i < s.length; i++) {
        if (t.length + s[i].length < l - 4)
          t += s[i] + " ";
        else return t + "...";
      }
      return t;
    },

    /** Checks to see if two values are within a range of eachother */
    within: function(v1, v2, range) {
      return (v1 - v2 < range && v1 - v2 > -range);
    }

  },

  // cookie library
  cookie: {
    // @return if a cookie of name exists
    has: function(name) {
      return document.cookie.match(new RegExp(name + "="));
    },

    // @return the value of a cookie of matched name, undefined if no cookie exists
    get: function(name) {
      var matched = document.cookie.match(new RegExp(name + "=([^;]+)"));

      if (matched)
        return matched[1];

      return undefined;
    },

    // @param lifespan (true == forever, integer == hours)
    set: function(name,value,lifespan) {
      var d, e, c = "; path=/";

      // Domain stuff
      d = document.location.hostname;
      if (/\.[^\.]+\.(COM|EDU|NET|ORG|GOV|MIL|INT)$/i.test(d)) {
        d = /\.[^\.]+\..{3}$/.exec(d);
        d = d ? d[0] : d;
      } else if (/\.[^\.]+\.[^\.]+\./.test(d)){
        d = /\.[^\.]+\.[^\.]+\.[^\.]+$/.exec(d);
        d = d ? d[0] : d;
      }
      c += (d ? ("; domain=" + d) : '');

      // Attach an expiration value?
      if (lifespan === true) {
        e = "Fri, 01 Jan 2038 00:00:00 GMT";
      } else if (lifespan !== undefined) {
        e = new Date();
        e.setTime(e.getTime() + (lifespan*60*60*1000));
        e = e.toGMTString();
      }

      if (e) {
        c += "; expires=" + e;
      }

      ;;; ATGSvcs.debug("Setting Cookie: " + name + "=" + encodeURIComponent(value) + c);
      document.cookie=name + "=" + encodeURIComponent(value) + c;
    }
  },

  /*
   * Recursive function to flatten JSON for transmission as URI arguments
   *
   * @param json JSON Object to flatten
   * @param prepend String to prepend to the aruments.  Used for recursion
   *
   * @returns flattened and encoded URI argument string
   */
  flatten: function (json, prepend) {
    var i, key, flat = [], prepend_key;
    
    // add a dot if we have ancestors
    prepend = prepend ? prepend + "." : "";

    // loop through the keys at this level
    for (key in json) {
      // ignore any property with an undefined value
      if (json[key] !== undefined) {
        prepend_key = prepend + key;
        // Values can be Objects, Arrays, Strings, Numerics or Booleans.

        if (json[key].constructor == Object) {
          ;;; ATGSvcs.debug ("flattening Hash : " + key);
          // It's an Object/Hash.  Recurse into it.  If it's empty, don't set the key.
          i = ATGSvcs.flatten(json[key], prepend_key);
          if (i) flat[flat.length] = i;

        } else if(json[key].constructor == Array) {
          ;;; ATGSvcs.debug ("flattening Array : " + key);
          // It's an Array.  Loop through it and add them as a comma delimited list.
          // Encode the value and escape backslashes and commas.
          for (i = json[key].length; i--;)
            json[key][i] = encodeURIComponent(json[key][i].replace('\\', '\\\\').replace(',', '\\,'));
          flat[flat.length] = prepend_key + "=" + json[key].join(",");

        } else {
          ;;; ATGSvcs.debug ("flattening String/Numeric/Boolean : " + key + " = " + json[key]);
          // It's a String, Numeric or Boolean.  Just add the property as a param.
          flat[flat.length] = prepend_key + "=" + encodeURIComponent(json[key].toString());

        }
      }
    }
    
    return flat.join("&");
  },

  /**
   * Sends a request to the server and executes the response
   * @param fragment appended to base_url
   * @param params (optional) a map, converted into query string name=value pairs
   * @param alt_host (optional) use a different host for building base_url.
   * @param callback (optional) a reference to a function to use as the callback.
   * @param callback_str (optional) the globally callable name of the callback function.
   */
  hook: function(fragment, params, alt_host, callback, callback_str) {
    var url = posturl = ATGSvcs.base_url(alt_host) + fragment,
    chr = ATGSvcs.cfg('charset', '', 'utf-8'),
    localWin = window, // get a local reference to window, as we'll be checking for the existance of attributes
    json = localWin.JSON,
    sessionId = ATGSvcs.sessionId(),
    request, postbody;

    // clone and strip unwanted params
    params = ATGSvcs.util.clone(params, 1);

    // if a callback function is set, attempt methods other than
    // script tag injection.  If no callback is set we assume the call
    // will return JavaScript not JSON and that it needs to execute.
    if (callback) {

      // if we have XMLHttpRequest and JSON and not the proprietary IE8 API, try using XHR
      if (localWin.XMLHttpRequest && json && json.stringify && json.parse && !localWin.XDomainRequest) {
        // get a new XHR object
        request = new XMLHttpRequest();

        // if CORS is supported, use it
        if ("withCredentials" in request) {

          // serialize the params
          postbody = json.stringify(params);

          // add the sessionId if we have it
          if (sessionId) {
            posturl += "?sessionId=" + sessionId;
          }

          ;;; ATGSvcs.debug("Sending Request via XHR:", ATGSvcs.dom.BR(), ATGSvcs.dom.P( url ), ATGSvcs.dom.P(postbody));

          // open a POST request
          request.open("POST", posturl, true);
          // when the server returns a response, call the passed callback function 
          // with the parsed results
          request.onreadystatechange = function () {
            if(this.readyState == 4 && this.status == 200) {
              callback(json.parse(this.responseText));
            }
          };
          // allow the server to set third party cookies (not supported by IE8)
          request.withCredentials = "true";
          // set our content-type header including the charset
          request.setRequestHeader("Content-Type", "text/plain;charset=" + chr);
          // send the request with the serialized JSON request
          request.send(postbody);
          // don't try to send it again
          return;
        }
      }
    }

    if (params) {
      if (callback_str) {
        params.callback = callback_str;
      }
      if (sessionId) {
        params.sessionId = sessionId;
      }

      url += "?" + ATGSvcs.flatten(params);
    }

    ;;; ATGSvcs.debug("Sending Request:", ATGSvcs.dom.BR(), ATGSvcs.dom.P("<script src=\"" + url + "\" />"));

    Selector('head')[0].appendChild(ATGSvcs.dom.SCRIPT({charset: chr, src: url}));
  },

  /**
   * Manufactures an IMG element and appends it to the BODY.
   * This is for use in server calls that require no response.
   * Better because SCRIPT tags can hang the JS engine indefinitely.
   *
   * @param fragment appended to base_url
   * @param params (optional) a map, converted into query string name=value pairs
   * @param alt_host (optional) use a different host for building base_url. Primary use is for failover.
   */
  img_hook: function(fragment, params, alt_host) {
    var url = ATGSvcs.base_url(alt_host) + fragment,
    sessionId = ATGSvcs.sessionId();

    // clone and strip unwanted characters from params
    params = ATGSvcs.util.clone(params, 1);

    // Tell the server not to return anything because we won't parse it
    params.response = false;

    if (params) {
      if (sessionId) {
        params.sessionId = sessionId;
      }
      url += "?" + ATGSvcs.flatten(params);
    }

    ;;; ATGSvcs.debug("Sending Request:", ATGSvcs.dom.BR(), ATGSvcs.dom.P("<img style=\"display:none\" src=\"" + url + "\" />"));

    Selector('body')[0].appendChild(ATGSvcs.dom.IMG({style: "display:none", src: url}));
  },

  // instantiate the fx namespace
  fx: {},

  /** 
   * version numbering scheme: <major>.<minor>.<bugfix>
   * major: massive overhaul.
   * minor: api change.
   * bugfix: fixes bugs, should not 'negatively' change api or behavior.
   */
  ESTARA_VERSION: "1.0",

  /**
   * default host
   */
  ESTARA_HOST: "as00.estara.com",
  
  /**
   * make  uoid 0 for unset
   */
  UOID: "0",

  /**
   * sets the UOID ... needs to be called by customer code after including the tag
   */
  setUOID: function(uoid) {
    ATGSvcs.UOID = uoid;
  },

  /**
   * Hook for phase-one activity. Called when DOM is ready to manipulate.
   */
  estara: function() {
    // Attempt to cancel out race conditions.
    ATGSvcs.estara = function(){};
    ATGSvcs.erg.occur("estara_ready");

    // Dump some debugging info
    ;;; ATGSvcs.debug("eStara :: ATGSvcs.estara()...");
    
    // get uoid
    if (ATGSvcs.UOID == "0") {
      // misconfiguration error
      ;;; ATGSvcs.debug("eStara :: UOID not set.  Aborting eStara load.");
      return;
    }

    // Make the request
    ATGSvcs.load_lr();
  },

  /**
   * Reusable function to make the request for lr.php
   */
  load_lr: function () {
    // Check to see if a <SCRIPT> tag for lr.php already exists.  If so they we don't need to 
    // add another one.  This can occur if this is the agent end of co-browse where the DOM
    // is passed in it's entirity from client to agent.
    var lr_loaded = false;

    jQlite.each(Selector("script"), function() {
      if (this.src.match("/fs/lr.php")) lr_loaded = true;
    });
    
    if (!lr_loaded) {
      // Build the request
      var params = {
          onload: 1,
          accountid: ATGSvcs.UOID,
          api_version: ATGSvcs.ESTARA_VERSION
        };
      ATGSvcs.hook("/fs/lr.php", params, ATGSvcs.ESTARA_HOST);
    ;;; } else {
    ;;; ATGSvcs.debug("eStara :: lr.php already loaded, don't load it again.");
    }
  },

  /**
   * Make the #estara() function wait until the onLoad event.  Must be called before domContentLoaded.
   */
  eStaraWaitForOnload: function () {
    ;;; ATGSvcs.debug("eStaraWaitForOnload() called.  Attempting to delay lr.php until the onLoad event is fired.");
    var jQlite = ATGSvcs.jQlite, i = jQlite.readyList.length;

    // only bother if domContentLoaded hasn't happened yet.  If it has, then we may fire twice.
    if (!jQlite.isReady) {
      while (i--) {
        if (jQlite.readyList[i] == ATGSvcs.estara)
          jQlite.readyList.splice(i, 1);
      }

      jQlite.addEvent(window, "load", ATGSvcs.estara);
    }
  }
};

/* JSON marshall/unmarshall.
 * Changes from reference implementation:
 *   + added debug output mode
 *   + ignores hash keys that start with '-'
 *
 * Now only included with test/debugging version.
;;; */
ATGSvcs.json = {
  map: {
    '\b': '\\b',
    '\t': '\\t',
    '\n': '\\n',
    '\f': '\\f',
    '\r': '\\r',
    '"' : '\\"',
    '\\': '\\\\'
  },

  func: {
    array: function (x,d,z) {
      var a = ['['], b, f, i, l = x.length, v;
      ;;; if (z) { a[a.length] = '\n' + d; }
      for (i = 0; i < l; i += 1) {
        v = x[i];
        f = ATGSvcs.json.func[typeof v];
        if (f) {
          v = f(v,d,z);
          if (typeof v == 'string') {
            if (b) {
              a[a.length] = ',';
              ;;; if (z) {
              ;;;   if (a[a.length-2].charAt(a[a.length-2].length-1) == '}') {
              ;;;     a[a.length] = '\n';
              ;;;   }
              ;;;   a[a.length] = '\n' + d;
              ;;; }
            }
            ;;; if (z) {a[a.length] = i + ': ';}
            a[a.length] = v;
            b = true;
          }
        }
      }
      ;;; if (z) { a[a.length] = '\n' + d.substring(2); }
      a[a.length] = ']';
      return a.join('');
    },

    'boolean': function (x) {
      return String(x);
    },

    'null': function (x) {
      return "null";
    },

    number: function (x) {
      return isFinite(x) ? String(x) : 'null';
    },

    object: function (x,d,z) {
      ;;; if (z) { d += "  "; }
      if (x) {
        if (x instanceof Array) {
          return ATGSvcs.json.func.array(x,d,z);
        }
        var a = ['{'], b, f, i, v;
        ;;; if (z) { a[a.length] = '\n' + d; }
        for (i in x) {
          // strip anything that starts with '-'
          if (!z && i.charAt(0) == '-') {
            continue;
          }

          v = x[i];
          f = ATGSvcs.json.func[typeof v];
          if (f) {
            v = f(v,d,z);
            if (typeof v == 'string') {
              if (b) {
                a[a.length] = ',';
                ;;; if (z) {  // debug
                ;;;   if (a[a.length-2].charAt(a[a.length-2].length-1) == '}') {
                ;;;     a[a.length] = '\n';
                ;;;   }
                ;;;   a[a.length] = '\n' + d;
                ;;; }
              }
              ;;; if (z) {  // debug
              ;;;   a.push(ATGSvcs.json.func.string(i), ': ', v);
              ;;; } else {
              a.push(ATGSvcs.json.func.string(i), ':', v);
              ;;; }
              b = true;
            }
          }
        }
        ;;; if (z) { a[a.length] = '\n' + d.substring(2); }
        a[a.length] = '}';
        return a.join('');
      }
      return 'null';
    },

    string: function (x) {
      if (/["\\\x00-\x1f]/.test(x)) {
        x = x.replace(/([\x00-\x1f\\"])/g, function(a, b) {
          var c = ATGSvcs.json.map[b];
          if (c) {
            return c;
          }
          c = b.charCodeAt();
          return '\\u00' +
            Math.floor(c / 16).toString(16) +
            (c % 16).toString(16);
        });
      }
      return '"' + x + '"';
    }
  },

  // Convert an object graph to JSON. Ideally this would be named to, but it's a keyword.
  // WARNING: DEBUG mode spews slightly invalid JSON, now you know.
  o: function(o,debug) {
    switch (typeof o) {
      case "object":
        return ATGSvcs.json.func.object(o,'',debug);
      case "array":
        return ATGSvcs.json.func.array(o,'',debug);
    }
  },

  from: function (s) {
    try {
      return !(/[^,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]/.test(s.replace(/"(\\.|[^"\\])*"/g, ''))) && eval('(' + s + ')');
    } catch (e) {
      return false;
    }
  }
};

// Injects the debug function only in test mode.
ATGSvcs.debug = function() {
   // make sure dom.DIV is initialized
   ATGSvcs.dom.tag("DIV");

   var a = [];
   for (var i = 0; i < arguments.length; i++) {
     if (!arguments[i]) { continue; }
     a[a.length] = arguments[i].cloneNode ? arguments[i].cloneNode(true) : arguments[i].toString();
   }

   var div = ATGSvcs.dom.DIV({Class: 'debug-entry', style: 'border-top: 1px solid #999; margin-top: 5px; padding-top: 5px'}, a);
   if (Selector('#cs-debug').length)
     Selector('#cs-debug')[0].appendChild(div);
};
;;; /*
;;; This is here to remove the whole script from production code.
*/
/*
 * ATGSvcs localization support
 * API stores an instance of the correct object in ATGSvcs.LOCALE
 * "Correct" means either the default of US or whatever is set in CFG.locale
 *
 * The "__" class is the parent class that all other locales extend.
 * Register is just a helper function to add the subclasses.
*/
ATGSvcs.l10n = {
  __: function () {
    this.CUR_SYM = "$";

    /*
     * currency is the public function
     * It splits the passed double fnum and passes the results to currency_string
    */
    this.currency = function(fnum) {
      var a=[], t=Math.floor(fnum).toString(),
          m=fnum.toString(), i=m.indexOf('.')+1,
          ms=i === 0 ? "00" : m.substring(i, i+2);

      // decimal
      ms=ms.length < 2 ? ms+"0" : ms;

      // triplets
      while (t.length > 3) {
        a.unshift(t.substring(t.length-3));
        t=t.substring(0, t.length-3);
      }

      if (t.length > 0) {
        a.unshift(t);
      }

      return this.currency_string(a, ms);
    };

    /*
     * currency calls this function and returns its results
     * a is an array of triplets of whole numbers
     * ms is the 2 digit decimal
    */
    this.currency_string = function(a, ms) {
      return this.CUR_SYM + a.join(",") + "." + ms;
    };
  },
  register: function (code, ext) {
    var l=ATGSvcs.l10n;
    l[code] = ext;
    l[code].prototype = new l.__;
  }
};

/*
 * This section is for registering new locales
 * New locales should be added as they are required by integrating retailers
*/
ATGSvcs.l10n.register("US", function () {});
ATGSvcs.l10n.register("UK", function () {
  this.CUR_SYM = "\u00a3";
});
ATGSvcs.l10n.register("EU", function () {
  this.CUR_SYM = "\u20ac";
  this.currency_string = function(a, ms) {
    return this.CUR_SYM + a.join(".") + "," + ms;
  };
});
ATGSvcs.l10n.register("JP", function () {
  this.CUR_SYM = "￥";
  this.currency_string = function(a, ms) {
    return this.CUR_SYM + a.join(",");
  };
});

/**
 * jQlite - selected jQuery and other helpful functions
 */

var toString = Object.prototype.toString,
userAgent = navigator.userAgent.toLowerCase(),

jQlite = {
  /**
   * Trims leading and trailing whitespace from passed text.
   * Taken from jQuery core.js @6268 lines 1073-1075
   *
   * No Modifications
   */
  trim: function( text ) {
    return (text || "").replace( /^\s+|\s+$/g, "" );
  },

  /**
   * Checks if obj is a function
   * Taken from jQuery core.js @6268 lines 649-651
   *
   * No Modifications
   */
  isFunction: function( obj ) {
    return toString.call(obj) === "[object Function]";
  },

  /**
   * Checks if obj is an Array
   * Taken from jQuery core.js @6268 lines 653-655
   *
   * No Modifications
   */
  isArray: function( obj ) {
    return toString.call(obj) === "[object Array]";
  },

  /**
   * Regex to match properties to not add px to values of
   * Taken from jQuery core.js @6268 lines 631
   *
   * No Modifications
   */
  exclude: /z-?index|font-?weight|opacity|zoom|line-?height/i,

  /**
   * cache defaultView
   * Taken from jQuery core.js @6268 lines 633
   *
   * No Modifications
   */
  defaultView: document.defaultView || {},

  /**
   * Number of ms since the epoch
   * Take from jQuery core.js @6268 lines 576-578
   *
   * No Modifications
   */
  now: function() {
    return +new Date;
  },

  /**
   * Iterates over some type of iteratable object and applies the passed
   * function to each element.
   * Taken from jQuery core.js @6268 lines 689-714
   *
   * No Modifications
   */
	each: function( object, callback, args ) {
		var name, i = 0, length = object.length;

		if ( args ) {
			if ( length === undefined ) {
				for ( name in object )
					if ( callback.apply( object[ name ], args ) === false )
						break;
			} else
				for ( ; i < length; )
					if ( callback.apply( object[ i++ ], args ) === false )
						break;

		// A special, fast, case for the most common use of each
		} else {
			if ( length === undefined ) {
				for ( name in object )
					if ( callback.call( object[ name ], name, object[ name ] ) === false )
						break;
			} else
				for ( var value = object[0];
					i < length && callback.call( value, i, value ) !== false; value = object[++i] ){}
		}

		return object;
	},

  /**
   * Extends a passed object with the values from another object.
   * Taken from jQuery core.js @6268 lines 580-628
   *
   * Modifications:
   *  Changed this to jQlite in "extend itself" section
   */
	extend: function() {
		// copy reference to target object
		var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;

		// Handle a deep copy situation
		if ( typeof target === "boolean" ) {
			deep = target;
			target = arguments[1] || {};
			// skip the boolean and the target
			i = 2;
		}

		// Handle case when target is a string or something (possible in deep copy)
		if ( typeof target !== "object" && !jQlite.isFunction(target) )
			target = {};

		// extend jQlite itself if only one argument is passed
		if ( length == i ) {
			target = jQlite;
			--i;
		}

		for ( ; i < length; i++ )
			// Only deal with non-null/undefined values
			if ( (options = arguments[ i ]) != null )
				// Extend the base object
				for ( var name in options ) {
					var src = target[ name ], copy = options[ name ];

					// Prevent never-ending loop
					if ( target === copy )
						continue;

					// Recurse if we're merging object values
					if ( deep && copy && typeof copy === "object" && !copy.nodeType )
						target[ name ] = jQlite.extend( deep, 
							// Never move original objects, clone them
							src || ( copy.length != null ? [ ] : { } )
						, copy );

					// Don't bring in undefined values
					else if ( copy !== undefined )
						target[ name ] = copy;

				}

		// Return the modified object
		return target;
	},

  /**
   * Gets all of the nodes in a direction and returns them in an array
   * Taken from jQuery selector.js @6268 lines 996-1004
   *
   * No Modifications
   */
	dir: function( elem, dir ){
		var matched = [], cur = elem[dir];
		while ( cur && cur != document ) {
			if ( cur.nodeType == 1 )
				matched.push( cur );
			cur = cur[dir];
		}
		return matched;
	},

  /**
   * Simple function to add an event handler to an object
   *
   * @param obj The object to add the event handler to, may be a list of elements
   * @param evt The name of the event to add the event handler on
   * @param fn The function to use as the event handler
   */
  addEvent: function(obj,evt,fn) {
    if (obj) {
      var handle = function () {
        fn.apply(obj, arguments);
      }

      if (obj.addEventListener)
        obj.addEventListener(evt, handle, false);
      else if (obj.attachEvent)
        obj.attachEvent('on'+evt, handle);
    }
  },

  /***********************************
   * Begin Ready Functionality Section
   ***********************************/

  /**
   * Adds a function to the list to be executed when the browser has
   * finished parsing the DOM.
   * Taken from jQuery event.js @6268 lines 530-545
   *
   * Modifications:
   *  Changed jQuery to jQlite
   *  removed return at end
   */
  ready: function(fn) {
    // Attach the listeners
    jQlite.bindReady();

    // If the DOM is already ready
    if ( jQlite.isReady )
      // Execute the function immediately
      fn.call( document );

    // Otherwise, remember the function for later
    else
      // Add the function to the wait list
      jQlite.readyList.push( fn );
  },

  /**
   * Whether the browser is already 'ready' or not.
   * Taken from jQuery event.js @6268 lines 593
   *
   * No Modifications
   */
  isReady: false,

  /**
   * The list of functions to execute when the browser is ready
   * Taken from jQuery event.js @6268 lines 594
   *
   * No Modifications
   */
  readyList: [],

  /**
   * Handle when the DOM is ready.  Actually calls the functions
   * added by ready.
   * Taken from jQuery event.js @6268 lines 596-616
   *
   * Modifications:
   *  Changed name to doReady
   *  Changed jQuery references to jQlite
   *  Removed triggerHandler call at end
   */
  doReady: function() {
    // Make sure that the DOM is not already loaded
    if ( !jQlite.isReady ) {
      // Remember that the DOM is ready
      jQlite.isReady = true;

      // If there are functions bound, to execute
      if ( jQlite.readyList ) {
        // Execute all of them 
        jQlite.each( jQlite.readyList, function(){
          this.call( document );
        });

        // Reset the list of functions
        jQlite.readyList = null;
      }
    }
  },

  /**
   * Whether the ready events have been bound yet or not
   * Taken from jQuery event.js @6268 lines 619
   *
   * No Modifications
   */
  readyBound: false,

  /**
   * Binds the ready events to the various browser functions that
   * trigger them.  Responsible for calling doReady when the browser
   * is ready.
   * Taken from jQuery event.js @6268 lines 621-665
   * 
   * Modifications:
   *  Changed readyBound references to jQlite.readyBound
   *  Changed jQuery.ready references to jQlite.doReady
   *  Changed jQuery.isReady references to jQlite.isReady
   *  Changed jQuery.event.add to jQlite.addEvent
   */
  bindReady: function() {
    if ( jQlite.readyBound ) return;
    jQlite.readyBound = true;

    // Mozilla, Opera and webkit nightlies currently support this event
    if ( document.addEventListener ) {
      // Use the handy event callback
      document.addEventListener( "DOMContentLoaded", function(){
        document.removeEventListener( "DOMContentLoaded", arguments.callee, false );
        jQlite.doReady();
      }, false );

    // If IE event model is used
    } else if ( document.attachEvent ) {
      // ensure firing before onload,
      // maybe late but safe also for iframes
      document.attachEvent("onreadystatechange", function(){
        if ( document.readyState === "complete" ) {
          document.detachEvent( "onreadystatechange", arguments.callee );
          jQlite.doReady();
        }
      });

      // If IE and not an iframe
      // continually check to see if the document is ready
      if ( document.documentElement.doScroll && window == window.top ) (function(){
        if ( jQlite.isReady ) return;

        try {
          // If IE is used, use the trick by Diego Perini
          // http://javascript.nwbox.com/IEContentLoaded/
          document.documentElement.doScroll("left");
        } catch( error ) {
          setTimeout( arguments.callee, 0 );
          return;
        }

        // and execute any waiting functions
        jQlite.doReady();
      })();
    }

    // A fallback to window.onload, that will always work
    jQlite.addEvent( window, "load", jQlite.doReady );
  },

  /***********************************
   * End Ready Functionality Section
   ***********************************/

  /***********************************
   * Begin CSS Functionality Section
   ***********************************/

	/**
   * A method for quickly swapping in/out CSS properties to get correct calculations
   * Taken from jQuery coe.js @6268 lines 742-755
   *
   * No Modifications
   */
	swap: function( elem, options, callback ) {
		var old = {};
		// Remember the old values, and insert the new ones
		for ( var name in options ) {
			old[ name ] = elem.style[ name ];
			elem.style[ name ] = options[ name ];
		}

		callback.call( elem );

		// Revert the old values
		for ( var name in options )
			elem.style[ name ] = old[ name ];
	},

  /**
   * CSS functionality wrapper, meant to mimic the behavior of jQuery's css method.
   * Can both get and set CSS properties.
   */
  css: function () {
    if (typeof arguments[1] !== "object" && (arguments[2] === undefined || typeof arguments[2] === "boolean"))
      return jQlite.getCSS.apply(this, arguments);

    return jQlite.setCSS.apply(this, arguments);
  },

  /**
   * Get the value of a CSS property for some element.
   * Taken from jQuery core.js @6268 lines 757-786
   *
   * Modifications:
   *  Changed name to getCSS
   *  Changed references from jQuery to jQlite
   */
	getCSS: function( elem, name, force, extra ) {
		if ( name == "width" || name == "height" ) {
			var val, props = { position: "absolute", visibility: "hidden", display:"block" }, which = name == "width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ];

			function getWH() {
				val = name == "width" ? elem.offsetWidth : elem.offsetHeight;

				if ( extra === "border" )
					return;

				jQlite.each( which, function() {
					if ( !extra )
						val -= parseFloat(jQlite.curCSS( elem, "padding" + this, true)) || 0;
					if ( extra === "margin" )
						val += parseFloat(jQlite.curCSS( elem, "margin" + this, true)) || 0;
					else
						val -= parseFloat(jQlite.curCSS( elem, "border" + this + "Width", true)) || 0;
				});
			}

			if ( elem.offsetWidth !== 0 )
				getWH();
			else
				jQlite.swap( elem, props, getWH );

			return Math.max(0, Math.round(val));
		}

		return jQlite.curCSS( elem, name, force );
	},

  /**
   * Calculates the current value of a CSS property for an element
   * Taken from jQuery core.js @6268 lines 788-852
   * 
   * Modifications:
   *  Changed jQuery references to jQlite
   *  Changed defaultView references to jQlite.defaultView
   *  Added check for support hash init
   */
	curCSS: function( elem, name, force ) {
    if (!jQlite.supportReady) initSupport();

		var ret, style = elem.style;

		// We need to handle opacity special in IE
		if ( name == "opacity" && !jQlite.support.opacity ) {
			ret = jQlite.attr( elem, style, "opacity" );

			return ret == "" ?
				"1" :
				ret;
		}

		// Make sure we're using the right name for getting the float value
		if ( name.match( /float/i ) )
			name = styleFloat;

		if ( !force && style && style[ name ] )
			ret = style[ name ];

		else if ( jQlite.defaultView.getComputedStyle ) {

			// Only "float" is needed here
			if ( name.match( /float/i ) )
				name = "float";

			name = name.replace( /([A-Z])/g, "-$1" ).toLowerCase();

			var computedStyle = jQlite.defaultView.getComputedStyle( elem, null );

			if ( computedStyle )
				ret = computedStyle.getPropertyValue( name );

			// We should always get a number back from opacity
			if ( name == "opacity" && ret == "" )
				ret = "1";

		} else if ( elem.currentStyle ) {
			var camelCase = name.replace(/\-(\w)/g, function(all, letter){
				return letter.toUpperCase();
			});

			ret = elem.currentStyle[ name ] || elem.currentStyle[ camelCase ];

			// From the awesome hack by Dean Edwards
			// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291

			// If we're not dealing with a regular pixel number
			// but a number that has a weird ending, we need to convert it to pixels
			if ( !/^\d+(px)?$/i.test( ret ) && /^\d/.test( ret ) ) {
				// Remember the original values
				var left = style.left, rsLeft = elem.runtimeStyle.left;

				// Put in the new values to get a computed value out
				elem.runtimeStyle.left = elem.currentStyle.left;
				style.left = ret || 0;
				ret = style.pixelLeft + "px";

				// Revert the changed values
				style.left = left;
				elem.runtimeStyle.left = rsLeft;
			}
		}

		return ret;
	},

  /**
   * Sets CSS values for an element
   * Taken from jQuery core.js @6268 lines 207-212
   *
   * Modifications:
   *  Changed name to setCSS
   *  Changed this.attr to jQlite.fattr.call
   */
	setCSS: function( elem, key, value ) {
		// ignore negative width and height values
		if ( (key == 'width' || key == 'height') && parseFloat(value) < 0 )
			value = undefined;
		return jQlite.fattr.call( elem, key, value, "curCSS" );
	},

  /**
   * Sets an attribute on an element
   * Taken from jQuery core.js @6268 lines 168-205
   *
   * Modifications:
   *  Changed name to fattr
   *  Changed jQuery references to jQlite
   *  Changed to assume this is one element instead of an Array
   */
	fattr: function( name, value, type ) {
		var options = name, isFunction = jQlite.isFunction( value );

		// Look for the case where we're accessing a style value
		if ( typeof name === "string" ) {
			if ( value === undefined ) {
				return this && jQlite[ type || "attr" ]( this, name );

			} else {
				options = {};
				options[ name ] = value;
			}
		}

    // Set all the styles
    for ( var prop in options ) {
      value = options[prop];

      if ( isFunction ) {
        value = value.call( this, i );
      }

      if ( typeof value === "number" && type === "curCSS" && !jQlite.exclude.test(prop) ) {
        value = value + "px";
      }

      jQlite.attr( type ? this.style : this, prop, value );
    }

		return this;
	},

  /**
   * Returns true if the nodeName case insensitive matches the passed name
   * Taken from jQuery core.js @6268 lines 684-686
   *
   * No Modifications
   */
	nodeName: function( elem, name ) {
		return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
	},

  /**
   * Sets an attribute on an element
   * Taken from jQuery core.js @6268 lines 974-1071
   *
   * Modifications:
   *  Changed jQuery references to jQlite
   *  Removed all notxml checks
   *  Added check for support hash init
   */
	attr: function( elem, name, value ) {
    if (!jQlite.supportReady) jQlite.initSupport();

		// don't set attributes on text and comment nodes
		if (!elem || elem.nodeType == 3 || elem.nodeType == 8)
			return undefined;

    // Whether we are setting (or getting)
		var set = value !== undefined;

		// Try to normalize/fix the name
		name = jQlite.props[ name ] || name;

		// Only do all the following if this is a node (faster for style)
		// IE elem.getAttribute passes even for style
		if ( elem.tagName ) {

			// These attributes require special treatment
			var special = /href|src|style/.test( name );

			// Safari mis-reports the default selected property of a hidden option
			// Accessing the parent's selectedIndex property fixes it
			if ( name == "selected" && elem.parentNode )
				elem.parentNode.selectedIndex;

			// If applicable, access the attribute via the DOM 0 way
			if ( name in elem && !special ) {
				if ( set ){
					// We can't allow the type property to be changed (since it causes problems in IE)
					if ( name == "type" && jQlite.nodeName( elem, "input" ) && elem.parentNode )
						throw "type property can't be changed";

					elem[ name ] = value;
				}

				// browsers index elements by id/name on forms, give priority to attributes.
				if( jQlite.nodeName( elem, "form" ) && elem.getAttributeNode(name) )
					return elem.getAttributeNode( name ).nodeValue;

				// elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set
				// http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
				if ( name == "tabIndex" ) {
					var attributeNode = elem.getAttributeNode( "tabIndex" );
					return attributeNode && attributeNode.specified
						? attributeNode.value
						: elem.nodeName.match(/(button|input|object|select|textarea)/i)
							? 0
							: elem.nodeName.match(/^(a|area)$/i) && elem.href
								? 0
								: undefined;
				}

				return elem[ name ];
			}

			if ( !jQlite.support.style &&  name == "style" )
				return jQlite.attr( elem.style, "cssText", value );

			if ( set )
				// convert the value to a string (all browsers do this but IE) see #1070
				elem.setAttribute( name, "" + value );

			var attr = !jQlite.support.hrefNormalized && special
					// Some attributes require a special call on IE
					? elem.getAttribute( name, 2 )
					: elem.getAttribute( name );

			// Non-existent attributes return null, we normalize to undefined

			return attr === null ? undefined : attr;
		}

		// elem is actually elem.style ... set the style

		// IE uses filters for opacity
		if ( !jQlite.support.opacity && name == "opacity" ) {
			if ( set ) {
				// IE has trouble with opacity if it does not have layout
				// Force it by setting the zoom level
				elem.zoom = 1;

				// Set the alpha filter to set the opacity
				elem.filter = (elem.filter || "").replace( /alpha\([^)]*\)/, "" ) +
					(parseInt( value ) + '' == "NaN" ? "" : "alpha(opacity=" + value * 100 + ")");
			}

			return elem.filter && elem.filter.indexOf("opacity=") >= 0 ?
				(parseFloat( elem.filter.match(/opacity=([^)]*)/)[1] ) / 100) + '':
				"";
		}

		name = name.replace(/-([a-z])/ig, function(all, letter){
			return letter.toUpperCase();
		});

		if ( set )
			elem[ name ] = value;

		return elem[ name ];
	},

  /**
   * A flag used to determine if the support hash has been
   * populated yet or not.
   */
  supportReady: false,

  /**
   * Used for determining browser support
   * Taken from jQuery support.js @6268 lines 5-53,94
   *
   * Modifications:
   *  Completely refactored into a function with much less checked
   *  This was done to allow it to be run asynchronously
   */
  initSupport: function () {
    if (jQlite.supportReady) return;

    var support_div = document.createElement("div");
    support_div.style.display = "none";
    support_div.innerHTML = '<a href="/a" style="color:red;float:left;opacity:.5;">a</a>';

    var support_a = support_div.getElementsByTagName("a")[0],
    styleFloat = !!support_a.style.cssFloat ? "cssFloat" : "styleFloat";

    jQlite.support = {
      // Get the style information from getAttribute
      // (IE uses .cssText insted)
      style: /red/.test( support_a.getAttribute("style") ),
      
      // Make sure that URLs aren't manipulated
      // (IE normalizes it by default)
      hrefNormalized: support_a.getAttribute("href") === "/a",
      
      // Make sure that element opacity exists
      // (IE uses filter instead)
      opacity: support_a.style.opacity === "0.5",
      
      // Verify style float existence
      // (IE uses styleFloat instead of cssFloat)
      cssFloat: !!support_a.style.cssFloat
    };

    jQlite.props["float"] = styleFloat;
    jQlite.props.cssFloat = styleFloat;
    jQlite.props.styleFloat = styleFloat;

    jQlite.supportReady = true;
  },

  /**
   * Used for browser specific property matching
   * Taken from jQuery support.js @6268 lines 96-107
   * 
   * Modifications:
   *  Float definitions moved to initSupport function
   */
	props: {
		"for": "htmlFor",
		"class": "className",
		readonly: "readOnly",
		maxlength: "maxLength",
		cellspacing: "cellSpacing",
		rowspan: "rowSpan",
		tabindex: "tabIndex"
	},

  /**
   * Used to determine browser support.  Similar to jQuery but
   * not actually using any of the jQuery userAgent checking.
   */
  browser: {
    gomez: /gomezagent/.test(userAgent)
  }
};

window.setTimeout(jQlite.initSupport, 0);

/**
 * Animate package - extends jQlite to support the animate function
 */

jQlite.extend(jQlite, {
  /**
   * Id of setInterval functions
   * Taken from jQuery fx.js @6268 lines 2
   *
   * No Modifications
   */
  timerId: null,

  /**
   * Returns whether an element is hidden or not
   * Taken from jQuery selector.js @6268 lines 974-976
   * 
   * Modifications:
   *  Changed from a sizzle filter to a function
   */
  hidden: function (elem) {
    return elem.offsetWidth === 0 || elem.offsetHeight === 0;
  },

  /**
   * Used for determining the speed of an animation
   * Taken from jQuery fx.js @6268 lines 204-225
   *
   * Modifications:
   *  Changed jQuery references to jQlite
   *  Removed all references to queue support
   *  Removed reference to jQuery.fx.speeds hash
   */
	speed: function(speed, easing, fn) {
		var opt = typeof speed === "object" ? speed : {
			complete: fn || !fn && easing ||
				jQlite.isFunction( speed ) && speed,
			duration: speed,
			easing: fn && easing || easing && !jQlite.isFunction(easing) && easing
		};

		// Queueing
		opt.old = opt.complete;
		opt.complete = function(){
			if ( jQlite.isFunction( opt.old ) )
				opt.old.call( this );
		};

		return opt;
	},

  /**
   * Used fro determining the easing of an animation
   * Taken from jQuery fx.js @6268 lines 227-234
   *
   * No Modifications
   */
	easing: {
		linear: function( p, n, firstNum, diff ) {
			return firstNum + diff * p;
		},
		swing: function( p, n, firstNum, diff ) {
			return ((-Math.cos(p*Math.PI)/2) + 0.5) * diff + firstNum;
		}
	},

  /**
   * Timers Array
   * Taken from jQuery fx.js @6268 lines 236
   *
   * No Modifications
   */
	timers: [],

  /**
   * FX class
   * Taken from jQuery fx.js @6268 lines 238-245
   *
   * No Modifications
   */
  fx: function( elem, options, prop ){
    this.options = options;
    this.elem = elem;
    this.prop = prop;

    if ( !options.orig )
      options.orig = {};
  },


  /**
   * Step options hash
   * Taken from jQuery fx.js @6268 lines 394-406
   *
   * Modifications:
   *  Changed namespace from jQuery.fx to jQlite
   */
  step: {
		opacity: function(fx){
			jQlite.attr(fx.elem.style, "opacity", fx.now);
		},

		_default: function(fx){
			if ( fx.elem.style && fx.elem.style[ fx.prop ] != null )
				fx.elem.style[ fx.prop ] = fx.now + fx.unit;
			else
				fx.elem[ fx.prop ] = fx.now;
		}
  },

  /**
   * Animate a property change of a set of elements
   * Taken from jQuery fx.js @6268 lines 101-161
   *
   * Modifications:
   *  Changed jQuery references to jQlite
   *  Removed all references to queue support
   *  Added a parameter elems and changed the one this reference to elems
   *  Added a check to wrap elems in an Array if it is not already
   */
	animate: function( elems, prop, speed, easing, callback ) {
		var optall = jQlite.speed(speed, easing, callback);

    // Added code!  Check to see if elems is an Array.  If not, wrap it so each won't fail.
    if (!elems.length) elems = [elems];

		return jQlite.each(elems, function(){
		
			var opt = jQlite.extend({}, optall), p,
				hidden = this.nodeType == 1 && jQlite.hidden(this),
				self = this;
	
			for ( p in prop ) {
				if ( ( p == "height" || p == "width" ) && this.style ) {
					// Store display property
					opt.display = jQlite.css(this, "display");

					// Make sure that nothing sneaks out
					opt.overflow = this.style.overflow;
				}
			}

			if ( opt.overflow != null )
				this.style.overflow = "hidden";

			opt.curAnim = jQlite.extend({}, prop);

			jQlite.each( prop, function(name, val){
				var e = new jQlite.fx( self, opt, name );

				var parts = val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),
					start = e.cur(true) || 0;

				if ( parts ) {
					var end = parseFloat(parts[2]),
						unit = parts[3] || "px";

					// We need to compute starting value
					if ( unit != "px" ) {
						self.style[ name ] = (end || 1) + unit;
						start = ((end || 1) / e.cur(true)) * start;
						self.style[ name ] = start + unit;
					}

					// If a +=/-= token was provided, we're doing a relative animation
					if ( parts[1] )
						end = ((parts[1] == "-=" ? -1 : 1) * end) + start;

					e.custom( start, end, unit );
				} else
					e.custom( start, val, "" );

			});

			// For JS strict compliance
			return true;
		});
	}

});

/**
 * Extend the jQuery.fx class
 */
jQlite.fx.prototype = {

	/**
   * Simple function for setting a style value
   * Taken from jQuery fx.js @6268 lines 252-261
   *
   * Modifications:
   *  Changed jQuery references to jQlite
   *  Changed jQuery.fx.step references to jQlite.step
   */
	update: function(){
		if ( this.options.step )
			this.options.step.call( this.elem, this.now, this );

		(jQlite.step[this.prop] || jQlite.step._default)( this );

		// Set display property to block for height/width animations
		if ( ( this.prop == "height" || this.prop == "width" ) && this.elem.style )
			this.elem.style.display = "block";
	},

	/**
   * Get the current size
   * Taken from jQuery fx.js @6268 lines 264-270
   *
   * No Modifications
   */
	cur: function(force){
		if ( this.elem[this.prop] != null && (!this.elem.style || this.elem.style[this.prop] == null) )
			return this.elem[ this.prop ];

		var r = parseFloat(jQlite.css(this.elem, this.prop, force));
		return r && r > -10000 ? r : parseFloat(jQlite.curCSS(this.elem, this.prop)) || 0;
	},

	/**
   * Start an animation from one number to another
   * Taken from jQuery fx.js @6268 lines 273-302
   *
   * Modifications:
   *  Changed jQuery references to jQlite
   *  Changed timerId to jQlite.timerId
   */
	custom: function(from, to, unit){
		this.startTime = jQlite.now();
		this.start = from;
		this.end = to;
		this.unit = unit || this.unit || "px";
		this.now = this.start;
		this.pos = this.state = 0;

		var self = this;
		function t(gotoEnd){
			return self.step(gotoEnd);
		}

		t.elem = this.elem;

		if ( t() && jQlite.timers.push(t) && !jQlite.timerId ) {
			jQlite.timerId = setInterval(function(){
				var timers = jQlite.timers;

				for ( var i = 0; i < timers.length; i++ )
					if ( !timers[i]() )
						timers.splice(i--, 1);

				if ( !timers.length ) {
					clearInterval( jQlite.timerId );
					jQlite.timerId = undefined;
				}
			}, 13);
		}
	},

	/**
   * Each step of an animation
   * Taken from jQuery fx.js @6268 lines 330-383
   *
   * Modifications:
   *  Changed jQuery references to jQlite
   */ 
	step: function(gotoEnd){
		var t = jQlite.now();

		if ( gotoEnd || t >= this.options.duration + this.startTime ) {
			this.now = this.end;
			this.pos = this.state = 1;
			this.update();

			this.options.curAnim[ this.prop ] = true;

			var done = true;
			for ( var i in this.options.curAnim )
				if ( this.options.curAnim[i] !== true )
					done = false;

			if ( done ) {
				if ( this.options.display != null ) {
					// Reset the overflow
					this.elem.style.overflow = this.options.overflow;

					// Reset the display
					this.elem.style.display = this.options.display;
					if ( jQlite.css(this.elem, "display") == "none" )
						this.elem.style.display = "block";
				}

				// Reset the properties, if the item has been hidden or shown
				if ( this.options.hide || this.options.show )
					for ( var p in this.options.curAnim )
						jQlite.attr(this.elem.style, p, this.options.orig[p]);
					
				// Execute the complete function
				this.options.complete.call( this.elem );
			}

			return false;
		} else {
			var n = t - this.startTime;
			this.state = n / this.options.duration;

			// Perform the easing function, defaults to swing
			this.pos = jQlite.easing[this.options.easing || (jQlite.easing.swing ? "swing" : "linear")](this.state, n, 0, 1, this.options.duration);
			this.now = this.start + ((this.end - this.start) * this.pos);

			// Perform the next step of the animation
			this.update();
		}

		return true;
	}

};

var Selector = (function(){
/** Based on Essential Selector
 * @author          Andrea Giammarchi
 * @blog            WebReflection
 * 
 * @param   String      CSS selector
 * @param   HTMLElement optional context to use with selector (makes search faster)
 * @return  Array       a JavaScript Array with 0, 1, or more, found elements
 */
var slice   = Array.prototype.slice,
    div     = document.createElement("div");
try{slice.call(div.childNodes)}catch(e){
    slice = function(i){
        var result = [],
            length = this.length;
        while(i < length)
            result[i] = this[i++];
        return result;
    };
};
if(div.querySelectorAll){
    div = null;
    return function(selector, HTMLElement){
        return slice.call((HTMLElement || document).querySelectorAll(selector), 0);
    };
};
var id          = /^#[a-zA-Z0-9_\-]+$/,                        // match id selectors (#blah)
    nodeName    = /^(\*|[a-zA-Z0-9]+)$/,                       // match nodename selectors (foo)
    className   = /^\.[a-zA-Z0-9_\-]+$/,                       // match classname selectors (.bar)
    TagClass    = /^(\*|[a-zA-Z0-9]+)\.[a-zA-Z0-9_\-]+$/,      // match tag.class selectors (foo.bar)
    descendSearch  = /[^\>|\+|\-|\*|~]\s+(?!\>|\+|\-|\*|~)/,   // match descedant selectors (whitespace between other selectors)
    counter     = 975864012,    // must be a number of no more than 9 digits to work with orphans
    clean = function(result){
        var length = result.length;
        while(length)
            delete result[--length][counter];
        return result;
    },
    getDescendantElementById = div.all ?
        function(id, context){
            var match = context.all[id];
            return match ? match[0] || match : null; // IE6 and IE7 may return a collection
        }:
        function(id, context){
            if (typeof context.getElementById !== "undefined")
                return context.getElementById(id);

            var match = document.getElementById(id);

            // if an element with the id is found, make sure it's a descendant of the context element
            return match ? match.compareDocumentPosition(context) & 8 ? match : null : null;
        }
    ,
    getElementsByClassName = div.getElementsByClassName ?
        function(className, HTMLElement){
            return slice.call(HTMLElement.getElementsByClassName(className), 0);
        }:
        function(className, HTMLElement){
            return getElementsByTagClass("*." + className, HTMLElement);
        }
    ,
    getElementsByTagClass = function(selector, HTMLElement){
        for(var
            split   = selector.split("."),
            childNodes = HTMLElement.getElementsByTagName(split.shift()),
            re      = new RegExp("(?:\\s|^)" + split.shift() + "(?:\\s|$)"),
            length  = childNodes.length,
            i       = 0,
            j       = 0;
            i < length; ++i
        ){
            HTMLElement = childNodes[i];
            if(re.test(HTMLElement.className))
                split[j++] = HTMLElement;
        };
        return split;
    },
    path = function path(HTMLElement){
        var parentNode  = HTMLElement.parentNode,
            result      = [HTMLElement.nodeName],
            i           = 0;
        while(parentNode && parentNode.nodeType != 9){
            result[++i] = parentNode.nodeName;
            parentNode = parentNode.parentNode;
        };
        return result.reverse().join(" ");    
    },
    querySelectorAll = document.defaultView && document.defaultView.getComputedStyle ?
        (function(style){
            return function(selector, HTMLElement){
                for(var
                    localCounter = "_" + counter, // Opera 9 requires a non-numeric value apparently
                    selectPath   = HTMLElement === document ? "" : path(HTMLElement),
                    selector     = selectPath.concat(" ", selector),
                    text = style.appendChild(document.createTextNode(
                        selector.concat("{counter-increment:", localCounter, ";}")
                    )),
                    defaultView  = document.defaultView,
                    childNodes   = HTMLElement.getElementsByTagName(resolve(selector[0])),
                    result       = [],
                    length       = childNodes.length,
                    i            = 0,
                    j            = 0,
                    tmp;
                    i < length;
                    ++i
                ){
                    tmp = childNodes[i];
                    if(-1 < defaultView.getComputedStyle(tmp, null).getPropertyValue("counter-increment").indexOf(localCounter))
                        result[j++] = tmp;
                };
                style.removeChild(text);
                return result;
            };
        })((document.getElementsByTagName("head")[0] || document.documentElement).appendChild(document.createElement("style"))):
        (function(style){
            return function(selector, HTMLElement){
                var increment   = "orphans:" + counter,
                    text        = HTMLElement === document ? "" : path(HTMLElement),
                    childNodes  = HTMLElement.getElementsByTagName(resolve(selector)),
                    result      = [],
                    length      = childNodes.length,
                    i           = 0,
                    j           = 0,
                    tmp;

                style.addRule(selector = text.concat(" ", selector), increment, -1);

                for(; i < length; ++i) {
                    tmp = childNodes[i];
                    if(tmp.currentStyle["orphans"] == counter)
                        result[j++] = tmp;
                };
                style.removeRule(selector);
                return result;
            };
        })(document.createStyleSheet()),

    resolve = function(selector){                           // dean suggested a speed up
        selector = selector.split(" ").pop();
        return nodeName.test(selector) ? selector : TagClass.test(selector) ? selector.split(".").shift() : "*";
    },
    unique = function(childNodes){
        for(var
            result  = [],
            length  = childNodes.length,
            i       = 0,
            j       = 0,
            current;
            i < length;
            ++i
        ){
            current = childNodes[i];
            if(!current[counter]){
                current[counter] = true;
                result[j++] = current;
            };
        };
        return  clean(result);
    }
;
div[counter] = true;
try{
    delete div[counter];
}catch(e){
    clean = function(result){
        var length = result.length;
        while(length)
            result[--length].removeAttribute(counter);
        return result;
    };
};
div = null;
return function(selector, HTMLElement){
    for(var
        context = HTMLElement || document,
        split   = selector.split(/\s*,\s*/),
        length  = split.length,
        i       = 0,
        result  = [],
        current, loopCurrent, undefined, 
        path, position, j;
        i < length;
        ++i
    ){
        loopCurrent = split[i].replace(/^\s+|\s+$/g, "");       // get current in loop and trim whitespace
        position = loopCurrent.search(descendSearch) + 1; // look for descendant selectors (whitespace)

        if (position) {   // if descendant selectors are found, get the first one
          current = loopCurrent.substr(0, position);
        } else {          // otherwise just use the whole selector
          current = loopCurrent;
        }
        path = [];

        switch(true){
            case    id.test(current):
                if(current = getDescendantElementById(current.substring(1), context))
                    path.push(current);
                break;
            case    nodeName.test(current):
                path.push.apply(path, slice.call(context.getElementsByTagName(current), 0));
                break;
            case    className.test(current):
                path.push.apply(path, getElementsByClassName(current.substring(1), context));
                break;
            case    TagClass.test(current):
                path.push.apply(path, getElementsByTagClass(current, context));
                break;
            default:
                path.push.apply(path, querySelectorAll(current, context));
                break;
        };

        if (position) { // if there are more selectors to descend, recurse and add the results
          j = path.length;
          while (j) {
            result = result.concat(arguments.callee(loopCurrent.substr(position), path[--j]));
          }
          result = unique(result);
        } else {        // otherwise add the results of the switch statement
          result = result.concat(path);
        }
    };
    return result;
};
})();

/**
 ** Included in this file:
 **
 **   + Tile Recommendation Builder
 **   + Tiles Renderer
 **
 ** Together these functions form the default presentation mechanism for recommendations.These 
 ** functions are always available once the JavaScript include has been loaded. They are a good
 ** reference point because they are very simple and because they utitilize many of the available
 ** renderer facilities. The facilities in use here are from two namespaces:
 **
 **   + ATGSvcs.dom -- a programatic DOM construction toolkit.
 **   + ATGSvcs.util -- a collection of common utility functions.
 **
 ** Complete documentation of all renderer related facilities is available in the RDK or online at
 ** http://recmanager.atg.com/api/renderer/
 **/


/** Tile Recommendation Builder
 **
 ** @param slot_name is the ID value for the element on the page that we render recommendations into.
 ** @param rec_data is an array of objects representing the recommendation data to display.
 **
 ** Three functions from the ATGSvcs namespace are used:
 **
 **   + price() returns a formatted price string or null.
 **   + util.trunc() returns a truncated string that doesn't split mid word.
 **   + rec_id() returns an page id for a recommendation.
 **
 ** IMPORTANT: A container element having the class "cs-rec" MUST wrap each recommendation. This
 ** container element also MUST have an id generated by the rec_id function. This function takes
 ** two parameters:
 **
 **   + slot_name (the same value that is passed in to the rec_builder)
 **   + recommendation.id (the server side id value that is part of the passed in rec_data array)
 **/
ATGSvcs.rec_builder("tile", function (slot_name, rec_data) {
  // short handle for the DOM Builder
  var dom = ATGSvcs.dom, 
  // short handle for the configuration function
  cfg = ATGSvcs.cfg,

  // returns a formatted price or null depending on the page configuration
  price_string = ATGSvcs.price(slot_name, rec_data.price),

  // create a <span> tag or leave price null
  price = price_string ? dom.SPAN({Class: "cs-price"}, price_string) : null,

  // check to see if there is a limit set on the product name
  name_length = cfg('-name-length', slot_name, 0),

  // if so, truncate the name using the trunc() function
  name = name_length ? ATGSvcs.util.trunc(rec_data.name, name_length) : rec_data.name,

  // if retailer specified text to append, do so
  append_string = cfg("-append-title", slot_name, null),

  append_title = append_string ? dom.SPAN({Class: "cs-append-title"}, append_string) : null,

  anchor_element = dom.A({href: rec_data.url},
    dom.IMG({Class: "cs-image", src: rec_data.image}),

    dom.SPAN({Class: "cs-title cs-name"}, name, append_title),

    price
  ),

  data_items = cfg("dataItems", slot_name, null), i, attribute_value;

  // iterate over any other requested data items and add them as span elements
  if (data_items) {
    for (i = 0; i < data_items.length;) {
      attribute_value = rec_data[data_items[i]];

      // The attribute can be an Array, in which case we join the values as a comma separated list
      attribute_value = ATGSvcs.jQlite.isArray(attribute_value) ? attribute_value.join(", ") : attribute_value;

      // add the span element with a class of cs-ATTRIBUTENAME
      anchor_element.appendChild(dom.SPAN({Class: "cs-" + data_items[i++]}, attribute_value));
    }
  }

  // return the recommendation DOM element
  return dom.DIV({Class: "cs-rec", id: ATGSvcs.rec_id(slot_name, rec_data.productId)},
    anchor_element
  );
});


/** Tiles Renderer: uses a rec_builder to render the recommendation data into a DOM representation.
 **
 ** @param slot_element a dom node whose element which is the slot where recommendations are to be 
 ** rendered.
 **
 ** When the function is called the this keyword is set to the recommendation data object returned
 ** by the server. Its properties contain the data used for rendering the recommendation slot.
 **
 ** The call to ATGSvcs.build_rec() will use the recommendation builder function (above) to render
 ** the DOM elements for each recommendation. It takes two parameters:
 **
 **   + slot name -- here we derive this value from the slot_element
 **   + rec data (this.recs[i]) -- an object whose properties are the data values for the recommendation.
 **/
ATGSvcs.renderer("tiles", function (slot_element){
  // the slot name is the id attribute of the slot element
  var slot_name = slot_element.id;

  // if headerText is set, create an element for it
  if (this.headerText) {
    // Add a <div> if the headerText is set
    slot_element.appendChild(ATGSvcs.dom.DIV({Class: "cs-header-text"}, this.headerText));
  }

  // loop through each of the recommendations and add the recommendation element for each one
  for (var i=0; i < this.recs.length; i++) {
    slot_element.appendChild(
      ATGSvcs.build_rec(slot_name, this.recs[i])
    );
  }

  // ATGSvcs.dom.ezc() or "E-Z clearing div" is equivalent to <div style="clear:both"></div>
  slot_element.appendChild(ATGSvcs.dom.ezc());
});
// These were publicly documented in their old locations so we'll put these here for legacy sake
jQlite.extend(ATGSvcs.util, {
// DEPRECATED: use ATGSvcs.cfg
  cfg: ATGSvcs.cfg,

// DEPRECATED: use ATGSvcs.rec_id
  rec_id: ATGSvcs.rec_id,

// DEPRECATED: use ATGSvcs.price
  price: ATGSvcs.price
});

// Initialize ATGSvcs dom builder...
ATGSvcs.dom.tags("A|BR|DIV|IMG|LI|P|SCRIPT|SPAN|UL|B");

// Some extra tags that are fun for debugging
;;; ATGSvcs.dom.tags("DL|DT|DD|PRE");

// Expose utility package to the world via ATGSvcs.jQlite
ATGSvcs.jQlite = jQlite;

// Expose the selector engine
ATGSvcs.Selector = Selector;

// Check to see if this is Gomez.  If so simply return because they can't do
// DOMContentLoaded or anything similar
if (!jQlite.browser.gomez) {
  // Trigger ATGSvcs when the DOM is loaded
  jQlite.ready(ATGSvcs.recs);
  // Trigger eStara as well
  jQlite.ready(ATGSvcs.estara);
;;; } else {
;;; ATGSvcs.debug("Gomez Browser Detected.  Skipping DOMContentLoaded Handlers.");
}
})();
