(function () {

/*** Variables ***/

  var win = window,
    doc = document,
    attrProto = {
      setAttribute: Element.prototype.setAttribute,
      removeAttribute: Element.prototype.removeAttribute
    },
    hasShadow = Element.prototype.createShadowRoot,
    container = doc.createElement('div'),
    noop = function(){},
    trueop = function(){ return true; },
    regexCamelToDash = /([a-z])([A-Z])/g,
    regexPseudoParens = /\(|\)/g,
    regexPseudoCapture = /:(\w+)\u276A(.+?(?=\u276B))|:(\w+)/g,
    regexDigits = /(\d+)/g,
    keypseudo = {
      action: function (pseudo, event) {
        return pseudo.value.match(regexDigits).indexOf(String(event.keyCode)) > -1 == (pseudo.name == 'keypass') || null;
      }
    },
    /*
      - The prefix object generated here is added to the xtag object as xtag.prefix later in the code
      - Prefix provides a variety of prefix variations for the browser in which your code is running
      - The 4 variations of prefix are as follows:
        * prefix.dom: the correct prefix case and form when used on DOM elements/style properties
        * prefix.lowercase: a lowercase version of the prefix for use in various user-code situations
        * prefix.css: the lowercase, dashed version of the prefix
        * prefix.js: addresses prefixed APIs present in global and non-Element contexts
    */
    prefix = (function () {
      var styles = win.getComputedStyle(doc.documentElement, ''),
          pre = (Array.prototype.slice
            .call(styles)
            .join('')
            .match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o'])
          )[1];
      return {
        dom: pre == 'ms' ? 'MS' : pre,
        lowercase: pre,
        css: '-' + pre + '-',
        js: pre == 'ms' ? pre : pre[0].toUpperCase() + pre.substr(1)
      };
    })(),
    matchSelector = Element.prototype.matches || Element.prototype.matchesSelector || Element.prototype[prefix.lowercase + 'MatchesSelector'];

/*** Functions ***/

// Utilities

  /*
    This is an enhanced typeof check for all types of objects. Where typeof would normaly return
    'object' for many common DOM objects (like NodeLists and HTMLCollections).
    - For example: typeOf(document.children) will correctly return 'htmlcollection'
  */
  var typeCache = {},
      typeString = typeCache.toString,
      typeRegexp = /\s([a-zA-Z]+)/;
  function typeOf(obj) {
    var type = typeString.call(obj);
    return typeCache[type] || (typeCache[type] = type.match(typeRegexp)[1].toLowerCase());
  }

  function clone(item, type){
    var fn = clone[type || typeOf(item)];
    return fn ? fn(item) : item;
  }
    clone.object = function(src){
      var obj = {};
      for (var key in src) obj[key] = clone(src[key]);
      return obj;
    };
    clone.array = function(src){
      var i = src.length, array = new Array(i);
      while (i--) array[i] = clone(src[i]);
      return array;
    };

  /*
    The toArray() method allows for conversion of any object to a true array. For types that
    cannot be converted to an array, the method returns a 1 item array containing the passed-in object.
  */
  var unsliceable = { 'undefined': 1, 'null': 1, 'number': 1, 'boolean': 1, 'string': 1, 'function': 1 };
  function toArray(obj){
    return unsliceable[typeOf(obj)] ? [obj] : Array.prototype.slice.call(obj, 0);
  }

// DOM

  var str = '';
  function query(element, selector){
    return (selector || str).length ? toArray(element.querySelectorAll(selector)) : [];
  }

// Pseudos

  function parsePseudo(fn){fn();}

// Mixins

  function mergeOne(source, key, current){
    var type = typeOf(current);
    if (type == 'object' && typeOf(source[key]) == 'object') xtag.merge(source[key], current);
    else source[key] = clone(current, type);
    return source;
  }

  function mergeMixin(tag, original, mixin, name) {
    var key, keys = {};
    for (var z in original) keys[z.split(':')[0]] = z;
    for (z in mixin) {
      key = keys[z.split(':')[0]];
      if (typeof original[key] == 'function') {
        if (!key.match(':mixins')) {
          original[key + ':mixins'] = original[key];
          delete original[key];
          key = key + ':mixins';
        }
        original[key].__mixin__ = xtag.applyPseudos(z + (z.match(':mixins') ? '' : ':mixins'), mixin[z], tag.pseudos, original[key].__mixin__);
      }
      else {
        original[z] = mixin[z];
        delete original[key];
      }
    }
  }

  var uniqueMixinCount = 0;
  function addMixin(tag, original, mixin){
    for (var z in mixin){
      original[z + ':__mixin__(' + (uniqueMixinCount++) + ')'] = xtag.applyPseudos(z, mixin[z], tag.pseudos);
    }
  }

  function resolveMixins(mixins, output){
    var index = mixins.length;
    while (index--){
      output.unshift(mixins[index]);
      if (xtag.mixins[mixins[index]].mixins) resolveMixins(xtag.mixins[mixins[index]].mixins, output);
    }
    return output;
  }

  function applyMixins(tag) {
    resolveMixins(tag.mixins, []).forEach(function(name){
      var mixin = xtag.mixins[name];
      for (var type in mixin) {
        var item = mixin[type],
            original = tag[type];
        if (!original) tag[type] = item;
        else {
          switch (type){
            case 'mixins': break;
            case 'events': addMixin(tag, original, item); break;
            case 'accessors':
            case 'prototype':
              for (var z in item) {
                if (!original[z]) original[z] = item[z];
                else mergeMixin(tag, original[z], item[z], name);
              }
              break;
            default: mergeMixin(tag, original, item, name);
          }
        }
      }
    });
    return tag;
  }

// Events

  function delegateAction(pseudo, event) {
    var match,
        target = event.target,
        root = event.currentTarget;
    while (!match && target && target != root) {
      if (target.tagName && matchSelector.call(target, pseudo.value)) match = target;
      target = target.parentNode;
    }
    if (!match && root.tagName && matchSelector.call(root, pseudo.value)) match = root;
    return match ? pseudo.listener = pseudo.listener.bind(match) : null;
  }

  function touchFilter(event){
    return event.button === 0;
  }

  function writeProperty(key, event, base, desc){
    if (desc) event[key] = base[key];
    else Object.defineProperty(event, key, {
      writable: true,
      enumerable: true,
      value: base[key]
    });
  }

  var skipProps = {};
  for (var z in doc.createEvent('CustomEvent')) skipProps[z] = 1;
  function inheritEvent(event, base){
    var desc = Object.getOwnPropertyDescriptor(event, 'target');
    for (var z in base) {
      if (!skipProps[z]) writeProperty(z, event, base, desc);
    }
    event.baseEvent = base;
  }

// Accessors

  function modAttr(element, attr, name, value, method){
    attrProto[method].call(element, name, attr && attr.boolean ? '' : value);
  }

  function syncAttr(element, attr, name, value, method){
    if (attr && (attr.property || attr.selector)) {
      var nodes = attr.property ? [element.xtag[attr.property]] : attr.selector ? xtag.query(element, attr.selector) : [],
          index = nodes.length;
      while (index--) nodes[index][method](name, value);
    }
  }

  function attachProperties(tag, prop, z, accessor, attr, name){
    var key = z.split(':'), type = key[0];
    if (type == 'get') {
      key[0] = prop;
      tag.prototype[prop].get = xtag.applyPseudos(key.join(':'), accessor[z], tag.pseudos, accessor[z]);
    }
    else if (type == 'set') {
      key[0] = prop;
      var setter = tag.prototype[prop].set = xtag.applyPseudos(key.join(':'), attr ? function(value){
        var old, method = 'setAttribute';
        if (attr.boolean){
          value = !!value;
          old = this.hasAttribute(name);
          if (!value) method = 'removeAttribute';
        }
        else {
          value = attr.validate ? attr.validate.call(this, value) : value;
          old = this.getAttribute(name);
        }
        modAttr(this, attr, name, value, method);
        accessor[z].call(this, value, old);
        syncAttr(this, attr, name, value, method);
      } : accessor[z] ? function(value){
        accessor[z].call(this, value);
      } : null, tag.pseudos, accessor[z]);

      if (attr) attr.setter = accessor[z];
    }
    else tag.prototype[prop][z] = accessor[z];
  }

  function parseAccessor(tag, prop){
    tag.prototype[prop] = {};
    var accessor = tag.accessors[prop],
        attr = accessor.attribute,
        name;

    if (attr) {
      name = attr.name = (attr ? (attr.name || prop.replace(regexCamelToDash, '$1-$2')) : prop).toLowerCase();
      attr.key = prop;
      tag.attributes[name] = attr;
    }

    for (var z in accessor) attachProperties(tag, prop, z, accessor, attr, name);

    if (attr) {
      if (!tag.prototype[prop].get) {
        var method = (attr.boolean ? 'has' : 'get') + 'Attribute';
        tag.prototype[prop].get = function(){
          return this[method](name);
        };
      }
      if (!tag.prototype[prop].set) tag.prototype[prop].set = function(value){
        value = attr.boolean ? !!value : attr.validate ? attr.validate.call(this, value) : value;
        var method = attr.boolean ? (value ? 'setAttribute' : 'removeAttribute') : 'setAttribute';
        modAttr(this, attr, name, value, method);
        syncAttr(this, attr, name, value, method);
      };
    }
  }

  var unwrapComment = /\/\*!?(?:\@preserve)?[ \t]*(?:\r\n|\n)([\s\S]*?)(?:\r\n|\n)\s*\*\//;
  function parseMultiline(fn){
    return typeof fn == 'function' ? unwrapComment.exec(fn.toString())[1] : fn;
  }

/*** X-Tag Object Definition ***/

  var xtag = {
    tags: {},
    defaultOptions: {
      pseudos: [],
      mixins: [],
      events: {},
      methods: {},
      accessors: {},
      lifecycle: {},
      attributes: {},
      'prototype': {
        xtag: {
          get: function(){
            return this.__xtag__ ? this.__xtag__ : (this.__xtag__ = { data: {} });
          }
        }
      }
    },
    register: function (name, options) {
      var _name;
      if (typeof name == 'string') _name = name.toLowerCase();
      else throw 'First argument must be a Custom Element string name';
      xtag.tags[_name] = options || {};

      var basePrototype = options.prototype;
      delete options.prototype;
      var tag = xtag.tags[_name].compiled = applyMixins(xtag.merge({}, xtag.defaultOptions, options));

      for (var z in tag.events) tag.events[z] = xtag.parseEvent(z, tag.events[z]);
      for (z in tag.lifecycle) tag.lifecycle[z.split(':')[0]] = xtag.applyPseudos(z, tag.lifecycle[z], tag.pseudos, tag.lifecycle[z]);
      for (z in tag.methods) tag.prototype[z.split(':')[0]] = { value: xtag.applyPseudos(z, tag.methods[z], tag.pseudos, tag.methods[z]), enumerable: true };
      for (z in tag.accessors) parseAccessor(tag, z);

      if (tag.shadow) tag.shadow = tag.shadow.nodeName ? tag.shadow : xtag.createFragment(tag.shadow);
      if (tag.content) tag.content = tag.content.nodeName ? tag.content.innerHTML : parseMultiline(tag.content);
      var created = tag.lifecycle.created;
      tag.prototype.createdCallback = {
        enumerable: true,
        value: function(){
          var element = this;
          if (tag.shadow && hasShadow) this.createShadowRoot().appendChild(tag.shadow.cloneNode(true));
          if (tag.content) this.appendChild(document.createElement('div')).outerHTML = tag.content;
          xtag.addEvents(this, tag.events);
          var output = created ? created.apply(this, arguments) : null;
          for (var name in tag.attributes) {
            var attr = tag.attributes[name],
                hasAttr = this.hasAttribute(name),
                hasDefault = attr.def !== undefined;
            if (hasAttr || attr.boolean || hasDefault) {
              this[attr.key] = attr.boolean ? hasAttr : !hasAttr && hasDefault ? attr.def : this.getAttribute(name);
            }
          }
          tag.pseudos.forEach(function(obj){
            obj.onAdd.call(element, obj);
          });
          this.xtagComponentReady = true;
          return output;
        }
      };

      var inserted = tag.lifecycle.inserted,
          removed = tag.lifecycle.removed;
      if (inserted || removed) {
        tag.prototype.attachedCallback = { value: function(){
          if (removed) this.xtag.__parentNode__ = this.parentNode;
          if (inserted) return inserted.apply(this, arguments);
        }, enumerable: true };
      }
      if (removed) {
        tag.prototype.detachedCallback = { value: function(){
          var args = toArray(arguments);
          args.unshift(this.xtag.__parentNode__);
          var output = removed.apply(this, args);
          delete this.xtag.__parentNode__;
          return output;
        }, enumerable: true };
      }
      if (tag.lifecycle.attributeChanged) tag.prototype.attributeChangedCallback = { value: tag.lifecycle.attributeChanged, enumerable: true };

      tag.prototype.setAttribute = {
        writable: true,
        enumerable: true,
        value: function (name, value){
          var old;
          var _name = name.toLowerCase();
          var attr = tag.attributes[_name];
          if (attr) {
            old = this.getAttribute(_name);
            value = attr.boolean ? '' : attr.validate ? attr.validate.call(this, value) : value;
          }
          modAttr(this, attr, _name, value, 'setAttribute');
          if (attr) {
            if (attr.setter) attr.setter.call(this, attr.boolean ? true : value, old);
            syncAttr(this, attr, _name, value, 'setAttribute');
          }
        }
      };

      tag.prototype.removeAttribute = {
        writable: true,
        enumerable: true,
        value: function (name){
          var _name = name.toLowerCase();
          var attr = tag.attributes[_name];
          var old = this.hasAttribute(_name);
          modAttr(this, attr, _name, '', 'removeAttribute');
          if (attr) {
            if (attr.setter) attr.setter.call(this, attr.boolean ? false : undefined, old);
            syncAttr(this, attr, _name, '', 'removeAttribute');
          }
        }
      };

      var elementProto = basePrototype ?
            basePrototype :
            tag['extends'] ?
            Object.create(doc.createElement(tag['extends']).constructor).prototype :
            win.HTMLElement.prototype;

      var definition = {
        'prototype': Object.create(elementProto, tag.prototype)
      };
      if (tag['extends']) {
        definition['extends'] = tag['extends'];
      }
      var reg = doc.registerElement(_name, definition);
      return reg;
    },

    /* Exposed Variables */

    mixins: {},
    prefix: prefix,
    captureEvents: { focus: 1, blur: 1, scroll: 1, DOMMouseScroll: 1 },
    customEvents: {
      animationstart: {
        attach: [prefix.dom + 'AnimationStart']
      },
      animationend: {
        attach: [prefix.dom + 'AnimationEnd']
      },
      transitionend: {
        attach: [prefix.dom + 'TransitionEnd']
      },
      move: {
        attach: ['pointermove']
      },
      enter: {
        attach: ['pointerenter']
      },
      leave: {
        attach: ['pointerleave']
      },
      scrollwheel: {
        attach: ['DOMMouseScroll', 'mousewheel'],
        condition: function(event){
          event.delta = event.wheelDelta ? event.wheelDelta / 40 : Math.round(event.detail / 3.5 * -1);
          return true;
        }
      },
      tap: {
        attach: ['pointerdown', 'pointerup'],
        condition: function(event, custom){
          if (event.type == 'pointerdown') {
            custom.startX = event.clientX;
            custom.startY = event.clientY;
          }
          else if (event.button === 0 &&
                   Math.abs(custom.startX - event.clientX) < 10 &&
                   Math.abs(custom.startY - event.clientY) < 10) return true;
        }
      },
      tapstart: {
        attach: ['pointerdown'],
        condition: touchFilter
      },
      tapend: {
        attach: ['pointerup'],
        condition: touchFilter
      },
      tapmove: {
        attach: ['pointerdown', 'pointerup'],
        condition: function(event, custom){
          if (event.type == 'pointerdown') {
            if (!custom.moveListener) custom.moveListener = xtag.addEvent(this, 'pointermove', custom.listener);
          }
          else if (event.type == 'pointerup') {
            xtag.removeEvent(this, custom.moveListener);
            custom.moveListener = null;
          }
          else return true;
        }
      },
      taphold: {
        attach: ['pointerdown', 'pointerup'],
        condition: function(event, custom){
          if (event.type == 'pointerdown') {
            (custom.pointers = custom.pointers || {})[event.pointerId] = setTimeout(
              xtag.fireEvent.bind(null, this, 'taphold'),
              custom.duration || 1000
            );
          }
          else if (event.type == 'pointerup') {
            if (custom.pointers) {
              clearTimeout(custom.pointers[event.pointerId]);
              delete custom.pointers[event.pointerId];
            }
          }
          else return true;
        }
      }
    },
    pseudos: {
      __mixin__: {},
      mixins: {
        onCompiled: function(fn, pseudo){
          var mixin = pseudo.source && pseudo.source.__mixin__ || pseudo.source;
          if (mixin) switch (pseudo.value) {
            case null: case '': case 'before': return function(){
              mixin.apply(this, arguments);
              return fn.apply(this, arguments);
            };
            case 'after': return function(){
              var returns = fn.apply(this, arguments);
              mixin.apply(this, arguments);
              return returns;
            };
            case 'none': return fn;
          }
          else return fn;
        }
      },
      keypass: keypseudo,
      keyfail: keypseudo,
      delegate: {
        action: delegateAction
      },
      preventable: {
        action: function (pseudo, event) {
          return !event.defaultPrevented;
        }
      },
      duration: {
        onAdd: function(pseudo){
          pseudo.source.duration = Number(pseudo.value);
        }
      },
      capture: {
        onCompiled: function(fn, pseudo){
          if (pseudo.source) pseudo.source.capture = true;
        }
      }
    },

    /* UTILITIES */

    clone: clone,
    typeOf: typeOf,
    toArray: toArray,

    wrap: function (original, fn) {
      return function(){
        var output = original.apply(this, arguments);
        fn.apply(this, arguments);
        return output;
      };
    },
    /*
      Recursively merges one object with another. The first argument is the destination object,
      all other objects passed in as arguments are merged from right to left, conflicts are overwritten
    */
    merge: function(source, k, v){
      if (typeOf(k) == 'string') return mergeOne(source, k, v);
      for (var i = 1, l = arguments.length; i < l; i++){
        var object = arguments[i];
        for (var key in object) mergeOne(source, key, object[key]);
      }
      return source;
    },

    /*
      ----- This should be simplified! -----
      Generates a random ID string
    */
    uid: function(){
      return Math.random().toString(36).substr(2,10);
    },

    /* DOM */

    query: query,

    skipTransition: function(element, fn, bind){
      var prop = prefix.js + 'TransitionProperty';
      element.style[prop] = element.style.transitionProperty = 'none';
      var callback = fn ? fn.call(bind || element) : null;
      return xtag.skipFrame(function(){
        element.style[prop] = element.style.transitionProperty = '';
        if (callback) callback.call(bind || element);
      });
    },

    requestFrame: (function(){
      var raf = win.requestAnimationFrame ||
                win[prefix.lowercase + 'RequestAnimationFrame'] ||
                function(fn){ return win.setTimeout(fn, 20); };
      return function(fn){ return raf(fn); };
    })(),

    cancelFrame: (function(){
      var cancel = win.cancelAnimationFrame ||
                   win[prefix.lowercase + 'CancelAnimationFrame'] ||
                   win.clearTimeout;
      return function(id){ return cancel(id); };
    })(),

    skipFrame: function(fn){
      var id = xtag.requestFrame(function(){ id = xtag.requestFrame(fn); });
      return id;
    },

    matchSelector: function (element, selector) {
      return matchSelector.call(element, selector);
    },

    set: function (element, method, value) {
      element[method] = value;
      if (window.CustomElements) CustomElements.upgradeAll(element);
    },

    innerHTML: function(el, html){
      xtag.set(el, 'innerHTML', html);
    },

    hasClass: function (element, klass) {
      return element.className.split(' ').indexOf(klass.trim())>-1;
    },

    addClass: function (element, klass) {
      var list = element.className.trim().split(' ');
      klass.trim().split(' ').forEach(function (name) {
        if (!~list.indexOf(name)) list.push(name);
      });
      element.className = list.join(' ').trim();
      return element;
    },

    removeClass: function (element, klass) {
      var classes = klass.trim().split(' ');
      element.className = element.className.trim().split(' ').filter(function (name) {
        return name && !~classes.indexOf(name);
      }).join(' ');
      return element;
    },

    toggleClass: function (element, klass) {
      return xtag[xtag.hasClass(element, klass) ? 'removeClass' : 'addClass'].call(null, element, klass);
    },

    /*
      Runs a query on only the children of an element
    */
    queryChildren: function (element, selector) {
      var id = element.id,
          attr = '#' + (element.id = id || 'x_' + xtag.uid()) + ' > ',
          parent = element.parentNode || !container.appendChild(element);
      selector = attr + (selector + '').replace(',', ',' + attr, 'g');
      var result = element.parentNode.querySelectorAll(selector);
      if (!id) element.removeAttribute('id');
      if (!parent) container.removeChild(element);
      return toArray(result);
    },

    /*
      Creates a document fragment with the content passed in - content can be
      a string of HTML, an element, or an array/collection of elements
    */
    createFragment: function(content) {
      var template = document.createElement('template');
      if (content) {
        if (content.nodeName) toArray(arguments).forEach(function(e){
          template.content.appendChild(e);
        });
        else template.innerHTML = parseMultiline(content);
      }
      return template.content;
    },

    /*
      Removes an element from the DOM for more performant node manipulation. The element
      is placed back into the DOM at the place it was taken from.
    */
    manipulate: function(element, fn){
      var next = element.nextSibling,
          parent = element.parentNode,
          returned = fn.call(element) || element;
      if (next) parent.insertBefore(returned, next);
      else parent.appendChild(returned);
    },

    /* PSEUDOS */

    applyPseudos: function(key, fn, target, source) {
      var listener = fn,
          pseudos = {};
      if (key.match(':')) {
        var matches = [],
            valueFlag = 0;
        key.replace(regexPseudoParens, function(match){
          if (match == '(') return ++valueFlag == 1 ? '\u276A' : '(';
          return !--valueFlag ? '\u276B' : ')';
        }).replace(regexPseudoCapture, function(z, name, value, solo){
          matches.push([name || solo, value]);
        });
        var i = matches.length;
        while (i--) parsePseudo(function(){
          var name = matches[i][0],
              value = matches[i][1];
          if (!xtag.pseudos[name]) throw "pseudo not found: " + name + " " + value;
          value = (value === '' || typeof value == 'undefined') ? null : value;
          var pseudo = pseudos[i] = Object.create(xtag.pseudos[name]);
          pseudo.key = key;
          pseudo.name = name;
          pseudo.value = value;
          pseudo['arguments'] = (value || '').split(',');
          pseudo.action = pseudo.action || trueop;
          pseudo.source = source;
          pseudo.onAdd = pseudo.onAdd || noop;
          pseudo.onRemove = pseudo.onRemove || noop;
          var original = pseudo.listener = listener;
          listener = function(){
            var output = pseudo.action.apply(this, [pseudo].concat(toArray(arguments)));
            if (output === null || output === false) return output;
            output = pseudo.listener.apply(this, arguments);
            pseudo.listener = original;
            return output;
          };
          if (!target) pseudo.onAdd.call(fn, pseudo);
          else target.push(pseudo);
        });
      }
      for (var z in pseudos) {
        if (pseudos[z].onCompiled) listener = pseudos[z].onCompiled(listener, pseudos[z]) || listener;
      }
      return listener;
    },

    removePseudos: function(target, pseudos){
      pseudos.forEach(function(obj){
        obj.onRemove.call(target, obj);
      });
    },

  /*** Events ***/

    parseEvent: function(type, fn) {
      var pseudos = type.split(':'),
          key = pseudos.shift(),
          custom = xtag.customEvents[key],
          event = xtag.merge({
            type: key,
            stack: noop,
            condition: trueop,
            capture: xtag.captureEvents[key],
            attach: [],
            _attach: [],
            pseudos: '',
            _pseudos: [],
            onAdd: noop,
            onRemove: noop
          }, custom || {});
      event.attach = toArray(event.base || event.attach);
      event.chain = key + (event.pseudos.length ? ':' + event.pseudos : '') + (pseudos.length ? ':' + pseudos.join(':') : '');
      var stack = xtag.applyPseudos(event.chain, fn, event._pseudos, event);
      event.stack = function(e){
        e.currentTarget = e.currentTarget || this;
        var detail = e.detail || {};
        if (!detail.__stack__) return stack.apply(this, arguments);
        else if (detail.__stack__ == stack) {
          e.stopPropagation();
          e.cancelBubble = true;
          return stack.apply(this, arguments);
        }
      };
      event.listener = function(e){
        var args = toArray(arguments),
            output = event.condition.apply(this, args.concat([event]));
        if (!output) return output;
        // The second condition in this IF is to address the following Blink regression: https://code.google.com/p/chromium/issues/detail?id=367537
        // Remove this when affected browser builds with this regression fall below 5% marketshare
        if (e.type != key && (e.baseEvent && e.type != e.baseEvent.type)) {
          xtag.fireEvent(e.target, key, {
            baseEvent: e,
            detail: output !== true && (output.__stack__ = stack) ? output : { __stack__: stack }
          });
        }
        else return event.stack.apply(this, args);
      };
      event.attach.forEach(function(name) {
        event._attach.push(xtag.parseEvent(name, event.listener));
      });
      return event;
    },

    addEvent: function (element, type, fn, capture) {
      var event = typeof fn == 'function' ? xtag.parseEvent(type, fn) : fn;
      event._pseudos.forEach(function(obj){
        obj.onAdd.call(element, obj);
      });
      event._attach.forEach(function(obj) {
        xtag.addEvent(element, obj.type, obj);
      });
      event.onAdd.call(element, event, event.listener);
      element.addEventListener(event.type, event.stack, capture || event.capture);
      return event;
    },

    addEvents: function (element, obj) {
      var events = {};
      for (var z in obj) {
        events[z] = xtag.addEvent(element, z, obj[z]);
      }
      return events;
    },

    removeEvent: function (element, type, event) {
      event = event || type;
      event.onRemove.call(element, event, event.listener);
      xtag.removePseudos(element, event._pseudos);
      event._attach.forEach(function(obj) {
        xtag.removeEvent(element, obj);
      });
      element.removeEventListener(event.type, event.stack);
    },

    removeEvents: function(element, obj){
      for (var z in obj) xtag.removeEvent(element, obj[z]);
    },

    fireEvent: function(element, type, options){
      var event = doc.createEvent('CustomEvent');
      options = options || {};
      event.initCustomEvent(type,
        options.bubbles !== false,
        options.cancelable !== false,
        options.detail
      );
      if (options.baseEvent) inheritEvent(event, options.baseEvent);
      element.dispatchEvent(event);
    }

  };

  win.xtag = xtag;
  if (typeof define == 'function' && define.amd) define(xtag);

  doc.addEventListener('WebComponentsReady', function(){
    xtag.fireEvent(doc.body, 'DOMComponentsLoaded');
  });

})();
