const config = {
  recaptchaDevSiteKey: '6Ld7KBIeAAAAAK_Xv_6o-cJ4cR95XkVZEtdBOjpi',
  recaptchaLiveSiteKey: '6Ld8CUseAAAAANeA6ahMHnoKGCd_M29VQHP95j2f',
  eventNames: {
    APP_READY: 'APP_READY',
    APP_RESIZE_START: 'APP_RESIZE_START', // *** on begin resizing, an opportunity to conceal an element before repainting layout etc
    APP_RESIZE_END: 'APP_RESIZE_END', // *** on resizing complete
    APP_BREAKPOINT_READY: 'APP_BREAKPOINT_READY', // *** breakpoint value has been set and is ready to use
    BEHAVIOUR_BINDING_SIGNAL: 'BEHAVIOUR_BINDING_SIGNAL', // *** allows behaviours to communicate, keyed by uids
    BEHAVIOUR_ADDED: 'BEHAVIOUR_ADDED', // *** helpful internally to know when a behaviour has been added to avoid race conditions
    APP_NETWORK_ACTIVITY: 'APP_NETWORK_ACTIVITY', // *** whenever something fetchy happens, contains status in payload
    HERO_DROPDOWN_SELECTION: 'HERO_DROPDOWN_SELECTION', // *** subscribe to this to receive data from hero selection
    PAUSE_OTHER_VIDEOS: 'PAUSE_OTHER_VIDEOS', // *** Player.js: stop any current playing videos when new video, thumb or tab is clicked
    DESTROY_THUMB_VIDEO: 'DESTROY_THUMB_VIDEO', // *** Player/VideoPlayer.js: remove instance reference for thumb video removed from DOM
    TRIGGER_OVERLAY: 'TRIGGER_OVERLAY',
    TRIGGER_NOTIFICATION: 'TRIGGER_NOTIFICATION', // *** event triggers notifcation
    PAGINATOR_INTENTION: 'PAGINATOR_INTENTION',
    TRIGGER_PRODUCT_QUICKVIEW: 'TRIGGER_PRODUCT_QUICKVIEW', // added also alchemy-templates/ProductCard as plain string, so update there if this name changes

    LAUNCH_POPOVER: 'LAUNCH_POPOVER', // *** popover paradigm from https://thebioagency.atlassian.net/browse/SMIT-2304
    CLOSE_POPOVER: 'CLOSE_POPOVER', // *** popover paradigm from https://thebioagency.atlassian.net/browse/SMIT-2304
    ACCORDION_TOGGLE: 'ACCORDION_TOGGLE', // for accordion, when we use inside tabstack we need to know when height increases
    APILIST_DOM_UPDATED: 'APILIST_DOM_UPDATED', // for apiLIst, when we use inside tabstack we need to know when dom changes so we can call setHeight()
    FORM_SUBMIT: 'FORM_SUBMIT', // for FormController/FormElement , controller tells Element when submit button is clicked
    FORM_REGISTER_ELEMENT: 'FORM_REGISTER_ELEMENT', // for FormController/FormElement , Element tells Controller it exists
    FORM_ELEMENT_CHANGE: 'FORM_ELEMENT_CHANGE', // for FormController/FormElement shared script, lets selectboxes talk to controller
    FORM_SELECT_FILTER: 'FORM_SELECT_FILTER', // for FormElement shared script, lets selectboxes talk & filter each other
    FORM_CHECKBOX_ENABLE: 'FORM_CHECKBOX_ENABLE', // for FormElement shared script, lets checkboxes talk & enable/disable each other
    FORM_CONDITIONAL_REQUIRE: 'FORM_CONDITIONAL_REQUIRE', // for FormElement shared script, lets 1 element control anothers 'required' state
    FORM_STEPS_COMPLETED: 'FORM_STEPS_COMPLETED', // for FormController to broadcast to other scripts when a stepped/form-wizard form is finally valid
    FORM_PARTIAL_SUCCESS: 'FORM_PARTIAL_SUCCESS', // for FormController to broadcast to other scripts when a partial form step is complete
    FORM_SPLIT_VALIDATE: 'FORM_SPLIT_VALIDATE', // for FormElement split element fields (eg phone) to communicate while validating
    SCROLL_ANCHOR: 'SCROLL_ANCHOR', // announce to stickynav to scroll to a given anchor from another element/script
    CAROUSEL_READY: 'CAROUSEL_READY', // for carousel to tell hero carosuel when to fire
    STICKYNAV_REMOVE_ITEM: 'STICKYNAV_REMOVE_ITEM', // or stickynav should we need to remove an item/anchor
    PATIENT_SPECIALTY_DROPDOWN_SELECTION: 'PATIENT_SPECIALTY_DROPDOWN_SELECTION', // for patient specialty filter by implementing level 3 tab selection filter
  },

  features: {
    debugging: {
      debugWindow: { // *** useful for mobile debugging
        isEnabled: false,  // *** when true, window can be launched by quadruple tapping on footer
        launchOnPageLoad: false, // *** only honoured when isEnabled = true, obvs
      },
      warnings: {
        show: true,
      },
    },
    scrollFader: {
      // *** page scroll fade effect, detailed config available here: src/core/js/animation/scrollFader/fadeConfig.js
      // enabled: true, // *** deprecated - feature is enabled by presence of 'feature--scroll-fader' class on body
      initDelay: 666,
    },
    // useScrollFadeEffect: true, // REMOVE?
    
    backButtonIgnoreHashes: {
      isEnabled: false,
    }
  },

  timings: {
    resizeDebounceDelayTime: 666, // *** delay before retriggering APP_RESIZE_START, APP_RESIZE_DONE

    animation: {
      base: 1.3,
      short: () => {
        return config.timings.animation.base / 4;
      },
      medium: () => {
        return config.timings.animation.base / 2;
      },
      long: () => {
        return config.timings.animation.base / 1.5;
      },
      longest: () => {
        return config.timings.animation.base;
      },
    },
  },

  classes: {
    heroAnimated: 'hero--animated', // *** to ensure parity when used as a flag
  },

  matchMedia: {
    // *** ensure in sync with CSS!
    mobile: 767,
    tablet_portrait: 768,
    tablet_landscape: 1024,
    desktop: 1366,
  },

  attributeParser: {
    // *** for data-attribute parsing to js
    // *** TODO - add new symbols for array parsing
    // arraySymbol: '#' // *** e.g. `data-behaviour-binding="to: carousel-1 & carousel-2"` -> `{to: ["carousel-1", "carousel-2"]`
  },
};

const optimizedCallers = new Map([
	[2, (fn, args) => fn(args[0], args[1])],
	[3, (fn, args) => fn(args[0], args[1], args[2])],
	[4, (fn, args) => fn(args[0], args[1], args[2], args[3])],
	[5, (fn, args) => fn(args[0], args[1], args[2], args[3], args[4])]
]);

const multiCaller = (fn, args) => fn(...args);

const multiple = (fns, args) => {
	const caller = optimizedCallers.has(args.length) ? 
		optimizedCallers.get(args.length) : 
		multiCaller;
	for (const fn of fns) {
		caller(fn, args);
	}
};

const single = (fns, arg) => {
	for (const fn of fns) {
		fn(arg);
	}
};

var key = Symbol('events');

const formatMessage = (method, message) => `signal-js: method .${method} ${message}`;

const isFunction$2 = value => typeof value === 'function';

const isString$1 = value => typeof value === 'string';

const isSymbol$1 = value => typeof value === 'symbol';

const isValidKey = value => isString$1(value) || Number.isFinite(value) || isSymbol$1(value);

// https://gist.github.com/Integralist/749153aa53fea7168e7e
const flatten = list => list.reduce(
	(memo, value) => memo.concat(Array.isArray(value) ? flatten(value) : value), []
);

const proto = {
	// disable | enable *************************************
	disable() {
		this.disabled = true;
		return this;
	},

	enable() {
		this.disabled = false;
		return this;
	},

	// on | off ************************************************
	on(name, fn) {
		if (!isValidKey(name)) throw new Error(formatMessage('on', 'requires an event name'));
		if (!isFunction$2(fn)) throw new Error(formatMessage('on', 'requires a function'));

		const location = this[key];
		const fns = location.has(name) ? location.get(name) : location.set(name, new Set()).get(name);
		fns.add(fn);
		
		return this;
	},

	off(name, fn) {
		if (!isValidKey(name)) throw new Error(formatMessage('off', 'requires an event name'));

		const location = this[key];  

		if (!location.has(name)) return this;

		// remove single
		if (fn) {
			const fns = location.get(name);
			
			// remove this function
			fns.has(fn) && fns.delete(fn);

			// check size and delete location if empty
			fns.size === 0 && location.delete(name);
			return this;
		}

		// remove all
		location.delete(name);
		return this;
	},

	once(name, fn) {
		if (!isValidKey(name)) throw new Error(formatMessage('once', 'requires an event name'));
		if (!isFunction$2(fn)) throw new Error(formatMessage('once', 'requires a function'));

		// slow path the params...this is for flexibility
		// and since these are single calls, the depotimization
		// shouldn't be a concern
		const callback = (...parms) => {
			this.off(name, callback);
			fn(...parms);
		};
		return this.on(name, callback);
	},

	// emit ************************************************
	emit(name, arg) {
		if (!isValidKey(name)) throw new Error(formatMessage('emit', 'requires an event name'));

		if (this.disabled) return this;

		const location = this[key];

		// nothing at the location
		if (!location.has(name)) return this;

		const fns = location.get(name);

		// no events at the location
		if (!fns.size) return this;

		// we have an array of functions to call
		const args = arguments;
		const numOfArgs = args.length;
		
		// fast path
		if (numOfArgs <= 2) {
			single(fns, arg);
			return this;
		}

		// prevent this function from being de-optimized
		// because of using the arguments:
		// http://reefpoints.dockyard.com/2014/09/22/javascript-performance-for-the-win.html
		// We only need the arguments after the event name
		let idx = 1;               
		const argsArray = new Array(numOfArgs - 1);
		for (; idx < numOfArgs; idx += 1) {
			argsArray[idx - 1] = args[idx];
		}

		multiple(fns, argsArray);
		return this;
	},

	// listeners / names ************************************************
	listeners(name) {
		const location = this[key];

		// make sure to always send an array and clean any 
		// references so that we cant mutate to undefined behavior
		
		if (name !== undefined) {
			return location.has(name) ? 
				Array.from(location.get(name)) : 
				[];
		}

		return flatten(
			Array.from(location.values())
				.map(set => Array.from(set))
		);
	},
	
	names() {
		const location = this[key];
		return Array.from(location.keys());
	},

	size(name) {
		const location = this[key];

		// make sure to always send an array and clean any 
		// references so that we cant mutate to undefined behavior
		
		if (name !== undefined) {
			return location.has(name) ? 
				location.get(name).size : 
				0;
		}

		return Array.from(location.values())
			.reduce((memo, set) => memo + set.size, 0);
	},

	// clear ************************************************
	clear() {
		this[key].clear();
		return this;
	},
};

// proxy methods
proto.addListener = proto.subscribe = proto.bind = proto.on;
proto.removeListender = proto.unsubscribe = proto.unbind = proto.off;
proto.trigger = proto.dispatch = proto.emit;

const create = function() {
	const signal = function signal() {
		return create();
	};
	signal[key] = new Map();
	signal.__proto__ = proto;
	return signal;
};

// create a pub/sub to expose a signal singleton
const signal = create();

// version
signal.VERSION = '2.0.0';

var bind = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};

/*global toString:true*/

// utils is a library of generic helper functions non-specific to axios

var toString = Object.prototype.toString;

/**
 * Determine if a value is an Array
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an Array, otherwise false
 */
function isArray(val) {
  return toString.call(val) === '[object Array]';
}

/**
 * Determine if a value is undefined
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if the value is undefined, otherwise false
 */
function isUndefined(val) {
  return typeof val === 'undefined';
}

/**
 * Determine if a value is a Buffer
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Buffer, otherwise false
 */
function isBuffer(val) {
  return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
    && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val);
}

/**
 * Determine if a value is an ArrayBuffer
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an ArrayBuffer, otherwise false
 */
function isArrayBuffer(val) {
  return toString.call(val) === '[object ArrayBuffer]';
}

/**
 * Determine if a value is a FormData
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an FormData, otherwise false
 */
function isFormData(val) {
  return (typeof FormData !== 'undefined') && (val instanceof FormData);
}

/**
 * Determine if a value is a view on an ArrayBuffer
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false
 */
function isArrayBufferView(val) {
  var result;
  if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) {
    result = ArrayBuffer.isView(val);
  } else {
    result = (val) && (val.buffer) && (val.buffer instanceof ArrayBuffer);
  }
  return result;
}

/**
 * Determine if a value is a String
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a String, otherwise false
 */
function isString(val) {
  return typeof val === 'string';
}

/**
 * Determine if a value is a Number
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Number, otherwise false
 */
function isNumber(val) {
  return typeof val === 'number';
}

/**
 * Determine if a value is an Object
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an Object, otherwise false
 */
function isObject$1(val) {
  return val !== null && typeof val === 'object';
}

/**
 * Determine if a value is a plain Object
 *
 * @param {Object} val The value to test
 * @return {boolean} True if value is a plain Object, otherwise false
 */
function isPlainObject(val) {
  if (toString.call(val) !== '[object Object]') {
    return false;
  }

  var prototype = Object.getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

/**
 * Determine if a value is a Date
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Date, otherwise false
 */
function isDate(val) {
  return toString.call(val) === '[object Date]';
}

/**
 * Determine if a value is a File
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a File, otherwise false
 */
function isFile(val) {
  return toString.call(val) === '[object File]';
}

/**
 * Determine if a value is a Blob
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Blob, otherwise false
 */
function isBlob(val) {
  return toString.call(val) === '[object Blob]';
}

/**
 * Determine if a value is a Function
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Function, otherwise false
 */
function isFunction$1(val) {
  return toString.call(val) === '[object Function]';
}

/**
 * Determine if a value is a Stream
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Stream, otherwise false
 */
function isStream(val) {
  return isObject$1(val) && isFunction$1(val.pipe);
}

/**
 * Determine if a value is a URLSearchParams object
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a URLSearchParams object, otherwise false
 */
function isURLSearchParams(val) {
  return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams;
}

/**
 * Trim excess whitespace off the beginning and end of a string
 *
 * @param {String} str The String to trim
 * @returns {String} The String freed of excess whitespace
 */
function trim(str) {
  return str.replace(/^\s*/, '').replace(/\s*$/, '');
}

/**
 * Determine if we're running in a standard browser environment
 *
 * This allows axios to run in a web worker, and react-native.
 * Both environments support XMLHttpRequest, but not fully standard globals.
 *
 * web workers:
 *  typeof window -> undefined
 *  typeof document -> undefined
 *
 * react-native:
 *  navigator.product -> 'ReactNative'
 * nativescript
 *  navigator.product -> 'NativeScript' or 'NS'
 */
function isStandardBrowserEnv() {
  if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' ||
                                           navigator.product === 'NativeScript' ||
                                           navigator.product === 'NS')) {
    return false;
  }
  return (
    typeof window !== 'undefined' &&
    typeof document !== 'undefined'
  );
}

/**
 * Iterate over an Array or an Object invoking a function for each item.
 *
 * If `obj` is an Array callback will be called passing
 * the value, index, and complete array for each item.
 *
 * If 'obj' is an Object callback will be called passing
 * the value, key, and complete object for each property.
 *
 * @param {Object|Array} obj The object to iterate
 * @param {Function} fn The callback to invoke for each item
 */
function forEach(obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // Force an array if not already something iterable
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // Iterate over object keys
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

/**
 * Accepts varargs expecting each argument to be an object, then
 * immutably merges the properties of each object and returns result.
 *
 * When multiple objects contain the same key the later object in
 * the arguments list will take precedence.
 *
 * Example:
 *
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // outputs 456
 * ```
 *
 * @param {Object} obj1 Object to merge
 * @returns {Object} Result of all merge properties
 */
function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) {
      result[key] = merge({}, val);
    } else if (isArray(val)) {
      result[key] = val.slice();
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

/**
 * Extends object a by mutably adding to it the properties of object b.
 *
 * @param {Object} a The object to be extended
 * @param {Object} b The object to copy properties from
 * @param {Object} thisArg The object to bind function to
 * @return {Object} The resulting value of object a
 */
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

/**
 * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
 *
 * @param {string} content with BOM
 * @return {string} content value without BOM
 */
function stripBOM(content) {
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}

var utils$1 = {
  isArray: isArray,
  isArrayBuffer: isArrayBuffer,
  isBuffer: isBuffer,
  isFormData: isFormData,
  isArrayBufferView: isArrayBufferView,
  isString: isString,
  isNumber: isNumber,
  isObject: isObject$1,
  isPlainObject: isPlainObject,
  isUndefined: isUndefined,
  isDate: isDate,
  isFile: isFile,
  isBlob: isBlob,
  isFunction: isFunction$1,
  isStream: isStream,
  isURLSearchParams: isURLSearchParams,
  isStandardBrowserEnv: isStandardBrowserEnv,
  forEach: forEach,
  merge: merge,
  extend: extend,
  trim: trim,
  stripBOM: stripBOM
};

function encode(val) {
  return encodeURIComponent(val).
    replace(/%3A/gi, ':').
    replace(/%24/g, '$').
    replace(/%2C/gi, ',').
    replace(/%20/g, '+').
    replace(/%5B/gi, '[').
    replace(/%5D/gi, ']');
}

/**
 * Build a URL by appending params to the end
 *
 * @param {string} url The base of the url (e.g., http://www.google.com)
 * @param {object} [params] The params to be appended
 * @returns {string} The formatted url
 */
var buildURL = function buildURL(url, params, paramsSerializer) {
  /*eslint no-param-reassign:0*/
  if (!params) {
    return url;
  }

  var serializedParams;
  if (paramsSerializer) {
    serializedParams = paramsSerializer(params);
  } else if (utils$1.isURLSearchParams(params)) {
    serializedParams = params.toString();
  } else {
    var parts = [];

    utils$1.forEach(params, function serialize(val, key) {
      if (val === null || typeof val === 'undefined') {
        return;
      }

      if (utils$1.isArray(val)) {
        key = key + '[]';
      } else {
        val = [val];
      }

      utils$1.forEach(val, function parseValue(v) {
        if (utils$1.isDate(v)) {
          v = v.toISOString();
        } else if (utils$1.isObject(v)) {
          v = JSON.stringify(v);
        }
        parts.push(encode(key) + '=' + encode(v));
      });
    });

    serializedParams = parts.join('&');
  }

  if (serializedParams) {
    var hashmarkIndex = url.indexOf('#');
    if (hashmarkIndex !== -1) {
      url = url.slice(0, hashmarkIndex);
    }

    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
  }

  return url;
};

function InterceptorManager() {
  this.handlers = [];
}

/**
 * Add a new interceptor to the stack
 *
 * @param {Function} fulfilled The function to handle `then` for a `Promise`
 * @param {Function} rejected The function to handle `reject` for a `Promise`
 *
 * @return {Number} An ID used to remove interceptor later
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

/**
 * Remove an interceptor from the stack
 *
 * @param {Number} id The ID that was returned by `use`
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

/**
 * Iterate over all the registered interceptors
 *
 * This method is particularly useful for skipping over any
 * interceptors that may have become `null` calling `eject`.
 *
 * @param {Function} fn The function to call for each interceptor
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils$1.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

var InterceptorManager_1 = InterceptorManager;

/**
 * Transform the data for a request or a response
 *
 * @param {Object|String} data The data to be transformed
 * @param {Array} headers The headers for the request or response
 * @param {Array|Function} fns A single function or Array of functions
 * @returns {*} The resulting transformed data
 */
var transformData = function transformData(data, headers, fns) {
  /*eslint no-param-reassign:0*/
  utils$1.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });

  return data;
};

var isCancel = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};

var normalizeHeaderName = function normalizeHeaderName(headers, normalizedName) {
  utils$1.forEach(headers, function processHeader(value, name) {
    if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
      headers[normalizedName] = value;
      delete headers[name];
    }
  });
};

/**
 * Update an Error with the specified config, error code, and response.
 *
 * @param {Error} error The error to update.
 * @param {Object} config The config.
 * @param {string} [code] The error code (for example, 'ECONNABORTED').
 * @param {Object} [request] The request.
 * @param {Object} [response] The response.
 * @returns {Error} The error.
 */
var enhanceError = function enhanceError(error, config, code, request, response) {
  error.config = config;
  if (code) {
    error.code = code;
  }

  error.request = request;
  error.response = response;
  error.isAxiosError = true;

  error.toJSON = function toJSON() {
    return {
      // Standard
      message: this.message,
      name: this.name,
      // Microsoft
      description: this.description,
      number: this.number,
      // Mozilla
      fileName: this.fileName,
      lineNumber: this.lineNumber,
      columnNumber: this.columnNumber,
      stack: this.stack,
      // Axios
      config: this.config,
      code: this.code
    };
  };
  return error;
};

/**
 * Create an Error with the specified message, config, error code, request and response.
 *
 * @param {string} message The error message.
 * @param {Object} config The config.
 * @param {string} [code] The error code (for example, 'ECONNABORTED').
 * @param {Object} [request] The request.
 * @param {Object} [response] The response.
 * @returns {Error} The created error.
 */
var createError = function createError(message, config, code, request, response) {
  var error = new Error(message);
  return enhanceError(error, config, code, request, response);
};

/**
 * Resolve or reject a Promise based on response status.
 *
 * @param {Function} resolve A function that resolves the promise.
 * @param {Function} reject A function that rejects the promise.
 * @param {object} response The response.
 */
var settle = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};

var cookies = (
  utils$1.isStandardBrowserEnv() ?

  // Standard browser envs support document.cookie
    (function standardBrowserEnv() {
      return {
        write: function write(name, value, expires, path, domain, secure) {
          var cookie = [];
          cookie.push(name + '=' + encodeURIComponent(value));

          if (utils$1.isNumber(expires)) {
            cookie.push('expires=' + new Date(expires).toGMTString());
          }

          if (utils$1.isString(path)) {
            cookie.push('path=' + path);
          }

          if (utils$1.isString(domain)) {
            cookie.push('domain=' + domain);
          }

          if (secure === true) {
            cookie.push('secure');
          }

          document.cookie = cookie.join('; ');
        },

        read: function read(name) {
          var match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'));
          return (match ? decodeURIComponent(match[3]) : null);
        },

        remove: function remove(name) {
          this.write(name, '', Date.now() - 86400000);
        }
      };
    })() :

  // Non standard browser env (web workers, react-native) lack needed support.
    (function nonStandardBrowserEnv() {
      return {
        write: function write() {},
        read: function read() { return null; },
        remove: function remove() {}
      };
    })()
);

/**
 * Determines whether the specified URL is absolute
 *
 * @param {string} url The URL to test
 * @returns {boolean} True if the specified URL is absolute, otherwise false
 */
var isAbsoluteURL = function isAbsoluteURL(url) {
  // A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
  // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
  // by any combination of letters, digits, plus, period, or hyphen.
  return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
};

/**
 * Creates a new URL by combining the specified URLs
 *
 * @param {string} baseURL The base URL
 * @param {string} relativeURL The relative URL
 * @returns {string} The combined URL
 */
var combineURLs = function combineURLs(baseURL, relativeURL) {
  return relativeURL
    ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '')
    : baseURL;
};

/**
 * Creates a new URL by combining the baseURL with the requestedURL,
 * only when the requestedURL is not already an absolute URL.
 * If the requestURL is absolute, this function returns the requestedURL untouched.
 *
 * @param {string} baseURL The base URL
 * @param {string} requestedURL Absolute or relative URL to combine
 * @returns {string} The combined full path
 */
var buildFullPath = function buildFullPath(baseURL, requestedURL) {
  if (baseURL && !isAbsoluteURL(requestedURL)) {
    return combineURLs(baseURL, requestedURL);
  }
  return requestedURL;
};

// Headers whose duplicates are ignored by node
// c.f. https://nodejs.org/api/http.html#http_message_headers
var ignoreDuplicateOf = [
  'age', 'authorization', 'content-length', 'content-type', 'etag',
  'expires', 'from', 'host', 'if-modified-since', 'if-unmodified-since',
  'last-modified', 'location', 'max-forwards', 'proxy-authorization',
  'referer', 'retry-after', 'user-agent'
];

/**
 * Parse headers into an object
 *
 * ```
 * Date: Wed, 27 Aug 2014 08:58:49 GMT
 * Content-Type: application/json
 * Connection: keep-alive
 * Transfer-Encoding: chunked
 * ```
 *
 * @param {String} headers Headers needing to be parsed
 * @returns {Object} Headers parsed into an object
 */
var parseHeaders = function parseHeaders(headers) {
  var parsed = {};
  var key;
  var val;
  var i;

  if (!headers) { return parsed; }

  utils$1.forEach(headers.split('\n'), function parser(line) {
    i = line.indexOf(':');
    key = utils$1.trim(line.substr(0, i)).toLowerCase();
    val = utils$1.trim(line.substr(i + 1));

    if (key) {
      if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {
        return;
      }
      if (key === 'set-cookie') {
        parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);
      } else {
        parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
      }
    }
  });

  return parsed;
};

var isURLSameOrigin = (
  utils$1.isStandardBrowserEnv() ?

  // Standard browser envs have full support of the APIs needed to test
  // whether the request URL is of the same origin as current location.
    (function standardBrowserEnv() {
      var msie = /(msie|trident)/i.test(navigator.userAgent);
      var urlParsingNode = document.createElement('a');
      var originURL;

      /**
    * Parse a URL to discover it's components
    *
    * @param {String} url The URL to be parsed
    * @returns {Object}
    */
      function resolveURL(url) {
        var href = url;

        if (msie) {
        // IE needs attribute set twice to normalize properties
          urlParsingNode.setAttribute('href', href);
          href = urlParsingNode.href;
        }

        urlParsingNode.setAttribute('href', href);

        // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
        return {
          href: urlParsingNode.href,
          protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
          host: urlParsingNode.host,
          search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
          hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
          hostname: urlParsingNode.hostname,
          port: urlParsingNode.port,
          pathname: (urlParsingNode.pathname.charAt(0) === '/') ?
            urlParsingNode.pathname :
            '/' + urlParsingNode.pathname
        };
      }

      originURL = resolveURL(window.location.href);

      /**
    * Determine if a URL shares the same origin as the current location
    *
    * @param {String} requestURL The URL to test
    * @returns {boolean} True if URL shares the same origin, otherwise false
    */
      return function isURLSameOrigin(requestURL) {
        var parsed = (utils$1.isString(requestURL)) ? resolveURL(requestURL) : requestURL;
        return (parsed.protocol === originURL.protocol &&
            parsed.host === originURL.host);
      };
    })() :

  // Non standard browser envs (web workers, react-native) lack needed support.
    (function nonStandardBrowserEnv() {
      return function isURLSameOrigin() {
        return true;
      };
    })()
);

var xhr = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;

    if (utils$1.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }

    var request = new XMLHttpRequest();

    // HTTP basic authentication
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }

    var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    request.timeout = config.timeout;

    // Listen for ready state
    request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }

      // The request errored out and we didn't get a response, this will be
      // handled by onerror instead
      // With one exception: request that using file: protocol, most browsers
      // will return status as 0 even though it's a successful request
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      settle(resolve, reject, response);

      // Clean up request
      request = null;
    };

    // Handle browser request cancellation (as opposed to a manual cancellation)
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // Clean up request
      request = null;
    };

    // Handle low level network errors
    request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
    };

    // Add xsrf header
    // This is only done if running in a standard browser environment.
    // Specifically not if we're in a web worker, or react-native.
    if (utils$1.isStandardBrowserEnv()) {
      // Add xsrf header
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;

      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    // Add headers to the request
    if ('setRequestHeader' in request) {
      utils$1.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // Remove Content-Type if data is undefined
          delete requestHeaders[key];
        } else {
          // Otherwise add header to the request
          request.setRequestHeader(key, val);
        }
      });
    }

    // Add withCredentials to request if needed
    if (!utils$1.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // Add responseType to request if needed
    if (config.responseType) {
      try {
        request.responseType = config.responseType;
      } catch (e) {
        // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2.
        // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function.
        if (config.responseType !== 'json') {
          throw e;
        }
      }
    }

    // Handle progress if needed
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }

    if (!requestData) {
      requestData = null;
    }

    // Send the request
    request.send(requestData);
  });
};

var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};

function setContentTypeIfUnset(headers, value) {
  if (!utils$1.isUndefined(headers) && utils$1.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = xhr;
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = xhr;
  }
  return adapter;
}

var defaults = {
  adapter: getDefaultAdapter(),

  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils$1.isFormData(data) ||
      utils$1.isArrayBuffer(data) ||
      utils$1.isBuffer(data) ||
      utils$1.isStream(data) ||
      utils$1.isFile(data) ||
      utils$1.isBlob(data)
    ) {
      return data;
    }
    if (utils$1.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils$1.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils$1.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],

  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,
  maxBodyLength: -1,

  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};

utils$1.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils$1.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils$1.merge(DEFAULT_CONTENT_TYPE);
});

var defaults_1 = defaults;

/**
 * Throws a `Cancel` if cancellation has been requested.
 */
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 * @returns {Promise} The Promise to be fulfilled
 */
var dispatchRequest = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Ensure headers exist
  config.headers = config.headers || {};

  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // Flatten headers
  config.headers = utils$1.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );

  utils$1.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  var adapter = config.adapter || defaults_1.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};

/**
 * Config-specific merge-function which creates a new config-object
 * by merging two configuration objects together.
 *
 * @param {Object} config1
 * @param {Object} config2
 * @returns {Object} New object resulting from merging config2 to config1
 */
var mergeConfig = function mergeConfig(config1, config2) {
  // eslint-disable-next-line no-param-reassign
  config2 = config2 || {};
  var config = {};

  var valueFromConfig2Keys = ['url', 'method', 'data'];
  var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
  var defaultToConfig2Keys = [
    'baseURL', 'transformRequest', 'transformResponse', 'paramsSerializer',
    'timeout', 'timeoutMessage', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
    'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'decompress',
    'maxContentLength', 'maxBodyLength', 'maxRedirects', 'transport', 'httpAgent',
    'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
  ];
  var directMergeKeys = ['validateStatus'];

  function getMergedValue(target, source) {
    if (utils$1.isPlainObject(target) && utils$1.isPlainObject(source)) {
      return utils$1.merge(target, source);
    } else if (utils$1.isPlainObject(source)) {
      return utils$1.merge({}, source);
    } else if (utils$1.isArray(source)) {
      return source.slice();
    }
    return source;
  }

  function mergeDeepProperties(prop) {
    if (!utils$1.isUndefined(config2[prop])) {
      config[prop] = getMergedValue(config1[prop], config2[prop]);
    } else if (!utils$1.isUndefined(config1[prop])) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  }

  utils$1.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) {
    if (!utils$1.isUndefined(config2[prop])) {
      config[prop] = getMergedValue(undefined, config2[prop]);
    }
  });

  utils$1.forEach(mergeDeepPropertiesKeys, mergeDeepProperties);

  utils$1.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) {
    if (!utils$1.isUndefined(config2[prop])) {
      config[prop] = getMergedValue(undefined, config2[prop]);
    } else if (!utils$1.isUndefined(config1[prop])) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  });

  utils$1.forEach(directMergeKeys, function merge(prop) {
    if (prop in config2) {
      config[prop] = getMergedValue(config1[prop], config2[prop]);
    } else if (prop in config1) {
      config[prop] = getMergedValue(undefined, config1[prop]);
    }
  });

  var axiosKeys = valueFromConfig2Keys
    .concat(mergeDeepPropertiesKeys)
    .concat(defaultToConfig2Keys)
    .concat(directMergeKeys);

  var otherKeys = Object
    .keys(config1)
    .concat(Object.keys(config2))
    .filter(function filterAxiosKeys(key) {
      return axiosKeys.indexOf(key) === -1;
    });

  utils$1.forEach(otherKeys, mergeDeepProperties);

  return config;
};

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager_1(),
    response: new InterceptorManager_1()
  };
}

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};

// Provide aliases for supported request methods
utils$1.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

utils$1.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

var Axios_1 = Axios;

/**
 * A `Cancel` is an object that is thrown when an operation is canceled.
 *
 * @class
 * @param {string=} message The message.
 */
function Cancel(message) {
  this.message = message;
}

Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

var Cancel_1 = Cancel;

/**
 * A `CancelToken` is an object that can be used to request cancellation of an operation.
 *
 * @class
 * @param {Function} executor The executor function.
 */
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel_1(message);
    resolvePromise(token.reason);
  });
}

/**
 * Throws a `Cancel` if cancellation has been requested.
 */
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

var CancelToken_1 = CancelToken;

/**
 * Syntactic sugar for invoking a function and expanding an array for arguments.
 *
 * Common use case would be to use `Function.prototype.apply`.
 *
 *  ```js
 *  function f(x, y, z) {}
 *  var args = [1, 2, 3];
 *  f.apply(null, args);
 *  ```
 *
 * With `spread` this example can be re-written.
 *
 *  ```js
 *  spread(function(x, y, z) {})([1, 2, 3]);
 *  ```
 *
 * @param {Function} callback
 * @returns {Function}
 */
var spread = function spread(callback) {
  return function wrap(arr) {
    return callback.apply(null, arr);
  };
};

/**
 * Determines whether the payload is an error thrown by Axios
 *
 * @param {*} payload The value to test
 * @returns {boolean} True if the payload is an error thrown by Axios, otherwise false
 */
var isAxiosError = function isAxiosError(payload) {
  return (typeof payload === 'object') && (payload.isAxiosError === true);
};

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  var context = new Axios_1(defaultConfig);
  var instance = bind(Axios_1.prototype.request, context);

  // Copy axios.prototype to instance
  utils$1.extend(instance, Axios_1.prototype, context);

  // Copy context to instance
  utils$1.extend(instance, context);

  return instance;
}

// Create the default instance to be exported
var axios$1 = createInstance(defaults_1);

// Expose Axios class to allow class inheritance
axios$1.Axios = Axios_1;

// Factory for creating new instances
axios$1.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios$1.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios$1.Cancel = Cancel_1;
axios$1.CancelToken = CancelToken_1;
axios$1.isCancel = isCancel;

// Expose all/spread
axios$1.all = function all(promises) {
  return Promise.all(promises);
};
axios$1.spread = spread;

// Expose isAxiosError
axios$1.isAxiosError = isAxiosError;

var axios_1 = axios$1;

// Allow use of default import syntax in TypeScript
var _default = axios$1;
axios_1.default = _default;

var axios = axios_1;

const apiService = {
  props: {
    signal: null,
    blocking: 'none',
  },

  call: async (options, queryParams = {}) => {
    //console.log('/apiService/ -call', options, queryParams);

    if (!options.datasource) {
      console.warn('/apiService/ -call --datasource not set, quiting');
      return;
    }

    // const params = {params: queryParams}; // *** use with direct .get method only

    apiService.props.signal = options.signal;
    apiService.props.blocking = options.blocking || 'none';

    apiService.dispatch({
      state: 'start',
      status: null,
      caller: options.caller || null,
    });

    // *** config for axios request, defaults to GET. setting 'method' on the fetchOptions when invoking apiService will override it here
    // @ja *** based on method, need to switch between config.params for GET and config.data for POST method
    const config = {
      method: options.method ? options.method : 'get',
      url: options.datasource,
    };

    if (config.method === 'get') {
      config.params = queryParams;
    }

    if (config.method === 'post') {
      config.data = queryParams;
    }

    // *** conditionally extend/add/delete props where required based on {options}
    return axios(config)
      .then((response) => {
        //console.log('/apiService/ -SUCCESS', response.data);
        apiService.dispatch({
          state: 'end',
          status: 'ok',
          caller: options.caller || null,
        });

        /*cache.add({
          // TEST
          label: 'last_good_response',
          payload: {
            data: response.data,
            request: config,
          },
        });*/

        return {
          status: 'ok',
          // type: options.type,
          payload: response.data,
        };
      })
      .catch((error) => {
        console.warn('/apiService/ -ERROR', error);

        apiService.dispatch({
          state: 'end',
          status: 'fail',
          caller: options.caller || null,
        });

        return { status: 'fail', payload: error };
      })
      .finally(() => {
        //console.log('/fetchService/ -FINISHED');
      });
  },

  // *** dispatch activity notifications
  dispatch: (options) => {
    apiService.props.signal.emit(config.eventNames.APP_NETWORK_ACTIVITY, {
      state: options.state,
      status: options.status,
      caller: options.caller,
      blocking: apiService.props.blocking,
    });
  },
};

const general = {
  // *** debouncer, e.g. resizing
  debounce: (fn, delay) => {
    let timerId;
    return (...args) => {
      clearTimeout(timerId);
      timerId = setTimeout(() => {
        fn(...args);
      }, delay);
    };
  },

  // *** check url and return one of 'development' or 'production'
  environment: () => {
    const domain = window.location.hostname;
    const livedomain = 'www.smith-nephew.com';
	const jpLiveDomain = 'prod-jp-cd.smith-nephew.com';
	const weLiveDomain = 'prod-we-cd.smith-nephew.com';
	const usLiveDomain = 'prod-us-cd.smith-nephew.com';
	let environment = 'development';
	
	if (domain === livedomain || domain === jpLiveDomain || domain === weLiveDomain || domain === usLiveDomain)
	{
		environment = 'production';
	}
    // console.log(`domain: ${domain}`);
    // console.log(`environment: ${environment}`);
    return environment;
  },

  // *** parse html attribute to js object
  attributeParser: (attr, type = null, parseMode = 'normal') => {
    if (!attr) return null; // *** reject all
    
    let splitChar = ',';
    let keyChar = ':';
    
    // *** @as added cookie type, just overwrite control chars
    if (parseMode === 'cookie') {
      splitChar = ';';
      keyChar = '=';
    }

    const obj = {};

    // *** JSON method, works with @data-behaviour-content
    if (parseMode === 'json') {
      const re = /"(.*?)"/g;
      const result = [];
      // lets first remove any line breaks that could interfere with parsing
      const cleanedAttribute = attr.replace(/(\r\n|\n|\r)/gm, '');
      let current;
      while ((current = re.exec(cleanedAttribute))) {
        result.push(current.pop());
      }

      result.filter((item, index) => {
        let key;
        let value;
        if (index % 2 === 0) {
          key = result[index];
          value = result[index + 1];
          return (obj[key] = value);
        }
      });
      return obj;
    }

    // *** NORMAL method, NOTE can also accept json as per @data-json-mapping, but for passing along text based content (with special chars) call with parseMode = 'json' to use above method instead
    attr.split(splitChar).map((item) => {
      item = item.replace(keyChar, '~'); // *** replaces first colon to mark as key, maintains subsequent occurrences to allow value to contain colons, e.g. 'https:'
      item = item.split('~');
      if (item.length !== 2) {
        /*
        console.warn(
          '/general/ -attributeParser --ignored:',
          item,
          'in type:',
          `${type ? type : 'unknown'}`
        );
*/
        return null; // *** reject malformed prop
      }

      // *** removes json formatting: quotes, curly braces, returns, line feeds
      const stripChars = (string) => {
        return string.replace(/[{}"\r\n]+/g, '').trim();
      };

      const key = [stripChars(item[0])];
      let value = stripChars(item[1]);

      const asNumber = parseFloat(value, 10);      
      value = Number.isNaN(asNumber) ? value : ['ytVideoId', 'NewsStoryTypeTags'].indexOf(key[0]) !== -1 ? value : asNumber; // *** output as number or string

      const asArray = (value) => {
        const clean = value.substr(1, value.length);
        const pairs = clean.split('&');
        return pairs.map((item) => {
          return item.split('+');
        });
      };

      try {
        if (!asNumber && value.includes('#')) {
          value = asArray(value);
        }
      } catch (error) {
        /*console.warn(
          '*** /general/ -attributeParser --FAILED in:',
          type,
          'with value:',
          value
        );*/
      }

      obj[key] = value;
    });

    return obj;
  },

  // *** utility function to flatten API response objs
  // Changes nested objs to single key (concatenated with '.') / Value pairs
  // Eg data.Result.Success: true
  toFlatPropertyMap: (obj, keySeparator = '.') => {
    const flattenRecursive = (obj, parentProperty, propertyMap = {}) => {
      for (const [key, value] of Object.entries(obj)) {
        const property = parentProperty
          ? `${parentProperty}${keySeparator}${key}`
          : key;
        if (value && typeof value === 'object') {
          flattenRecursive(value, property, propertyMap);
        } else {
          propertyMap[property] = value;
        }
      }
      return propertyMap;
    };
    return flattenRecursive(obj);
  },

  // *** generate a short uid/uuid. thanks stack overflow..!
  uid: () => {
    // ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => // long uid
    return ([1e7] + 1e3).replace(/[018]/g, (c) =>
      (
        c ^
        (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
      ).toString(16)
    );
  },

  // *** return a random number between min and max
  randomIntegerRange: (min, max) => {
    return Math.floor(Math.random() * (max - min + 1) + min);
  },
  lazyLoadImages: () => {
    // lazy load images
    let lazyImageObserver = new IntersectionObserver(function (entries, observer) {
      entries.forEach(function (entry) {
          if (entry.isIntersecting) {
              let lazyImage = entry.target;
              if (lazyImage.dataset.src) {
                  lazyImage.src = lazyImage.dataset.src;
              }
              lazyImageObserver.unobserve(lazyImage);
          }
      });
    });
    var lazyImages = [...document.body.querySelectorAll("img")];
    lazyImages.forEach(function (lazyImage) {
        lazyImageObserver.observe(lazyImage);
    });
  }
};

// *** expose to console for easy uid generation
// *** also can be used from outside app context, i.e. alchemy-templates
window.uid = (log = true) => {
  const uid = general.uid();
  return uid;
};

const requestModel = {
  format: (options) => {
    // console.log('/requestModel/ -format', options);
    /**
     * <writeKeys>
     * write dynamic key/value to the output object
     * has 3 tries at this:
     * - 'Key' has been provided, outputs <namedKey>:value (one single pair)
     * - receives json-esque payload or flat data delimited with '&' symbol, in this case writes multiple pairs
     *
     * NOTE
     * for cases where a guid is passed in, e.g. autoFetchDefaultValue: '6DAAC2F4-8945-43E2-ABF8-B6C68FD3EA2A',
     * it is necessary to wrap within a single quote to avoid general.attributeParser from breaking it
     */
    const writeKeys = (request) => {
      // *** standard operation with provided Key e.g. @data-component-props="key: SpecialityIds ... - this takes precedence
      // *** @ja added the ability to pass in 1 or more Key values(isMultiKey) e.g. @data-component-props="key: Tags='tag1,tag2,tag3'"
      if (options.Key) {
        const isMultiKey = options.Key.indexOf('=') !== -1 ? true : false;
        if (isMultiKey) {
          const label = options.Key.split('=')[0];
          const value = options.Key.split('=')[1].replace(/\|/g, ','); // *** clean string;
          request[label] = value.replace(/'/g, ''); // *** clean string;;
        } else {
          if (!options.Values) {
            console.warn('/requestModel/ -writeKeys --no values passed, quitting... --payload:', options);
            return;
          }
          request[options.Key] = options.Values.replace(/'/g, ''); // *** clean string;
          return;
        }
      }
      // no point in staying if we dont have Values, will just error..
      if (!options.Values) return;
      // *** multiple dynamic keys
      const split = options.Values.split(',');

      // *** write when keys provided in json string, e.g. <option value='{"StartDate": "2021-1-1", "EndDate": "2021-3-31"}'>
      const asJson = split.map((item) => {
        const obj = general.attributeParser(item, null, 'json');
        if (!obj) return; // *** defense

        return Object.keys(obj).map((key) => {
          return (request[key] = obj[key]); // *** write to request object
        });
      });

      // *** finally try alternative method when asJson fails test, e.g. autoFetchDefaultValue: StartDate: '2021-1-1' & EndDate: '2021-3-31'"
      const isValid = !!asJson.flat().length;
      if (!isValid) {
        const asFlat = general.attributeParser(
          options.Values.split('&').join(',')
        );
        return Object.keys(asFlat).map((key) => {
          return (request[key] = asFlat[key].replace(/'/g, '')); // *** clean value and write to request object
        });
      }
    };
	
	// *** gives option to add one other key to request
	const writeOtherKey = (request) => {
      // *** standard operation with provided Key e.g. @data-component-props="key: SpecialityIds ... - this takes precedence
      // *** @ja added the ability to pass in 1 or more Key values(isMultiKey) e.g. @data-component-props="key: Tags='tag1,tag2,tag3'"
      if (options.OtherKey) {
        const isValidOtherKey = options.OtherKey.indexOf('=') !== -1 ? true : false;
        if (isValidOtherKey) {
          const label = options.OtherKey.split('=')[0];
          const value = options.OtherKey.split('=')[1].replace(/\|/g, ','); // *** clean string;
          return request[label] = value.replace(/'/g, ''); // *** clean string;;
        } else {
			console.warn('/requestModel/ -writeKeys --no values passed, quitting... --payload:', options);
			return;
        }
      }
    };

    // *** add 'Pillar' to request when available
    const writePillar = (request) => {
      const pillar = document.body.getAttribute('data-pillar');
      if (pillar) request.Pillar = pillar;
    };

    // *** base shape
    let request = {
      Paginate: options.Paginate || true,
      PageNumber: options.PageNumber || 1,
      PageSize: options.PageSize || 10,
      BypassCache: options.BypassCache || false,
    };

    // *** add keys
    writeKeys(request);
    writePillar(request);
	writeOtherKey(request);
    // console.log('/requestModel/ -format', request);
    return request;
  },
};

/* @as 24.11.21 - this needs to be removed, and parsing should be executed within api/parsers */
const staffCards = {
  
  // TODO refactor parsing into separate utils for reuse
  init: (data, options) => {
     // console.log('/StaffCards/ -init', options);
    
    let criteria = [];
    
    data.map((item) => {
      criteria.push(item[options.key]); // *** get all values
    });
    criteria = [...new Set(criteria)]; // *** unique values only
    
    // *** chunk data by criteria
    const chunks = criteria.map((value) => {
      return data.filter((item) => {
        return item[options.key] === value;
      })
    });
    
    // *** build nodes from data
    const nodes = chunks.map((chunk) => {
      return chunk.map((item) => {
        return staffCards.build(item);
      })
    });
    
    const getLabel = (criteria) => {
      let values = options.values.filter((item) => {
        return (item[0] === criteria);
      });
      
      if (values.length > 0) {
        return values[0][1];
      }
      
      return 'Unset';
    };
    
    // *** wrap output object and return
    return nodes.map((item, index) => {
      return {
        label: getLabel(criteria[index]),
        nodes: nodes[index]
      }
    });
  },
  
  build: (item) => {
    
    // *** NOTE - placeholder image to come from BE?
    const image = () => {
      if (item.ImageUrl) {
        return `<img src="${item.ImageUrl}" alt="${item.Name}, ${item.JobTitle}">`;
      }
      return `<img src="https://via.placeholder.com/200x200.jpg" alt="${item.Name}, ${item.JobTitle}">`;
    };
    
    const template = `
        <div class="card__content">
        
          <div class="image-wrapper">
            ${image()}
          </div>
          
          <div class="text-block">
            <h5 class="card__title">${item.Name}</h5>
            <div class="paragraphs paragraph--small">
              <p>${item.JobTitle}</p>
            </div>
          </div>
        
        </div>
        
        <div class="button-group">
          <a href="${item.Url}">Find out more</a>
        </div>
            `;
    
    const el = document.createElement('aside');
    el.classList.add('card');
    el.classList.add('card--staff');
    el.innerHTML = template;
    return el;
  }
};

const caseReviewCards = {
  // TODO refactor parsing into separate utils for reuse
  init: (data, options) => {
    // console.log('/caseReviewCards/ -init', options);

    let criteria = [];

    data.map((item) => {
      criteria.push(item[options.key]); // *** get all values
    });
    criteria = [...new Set(criteria)]; // *** unique values only

    // *** chunk data by criteria
    const chunks = criteria.map((value) => {
      return data.filter((item) => {
        return item[options.key] === value;
      });
    });

    // *** build nodes from data
    const nodes = chunks.map((chunk) => {
      return chunk.map((item) => {
        return caseReviewCards.build(item);
      });
    });

    const getLabel = (criteria) => {
      let values = options.values.filter((item) => {
        return item[0] === criteria;
      });

      if (values.length > 0) {
        return values[0][1];
      }

      return 'Unset';
    };

    // *** wrap output object and return
    return nodes.map((item, index) => {
      return {
        label: getLabel(criteria[index]),
        nodes: nodes[index],
      };
    });
  },

  build: (item) => {

    const template = `
          <div class="card__content">
          
            <div
                class="image-wrapper image-wrapper--video"
                data-behaviour-uid="video-carousel-thumb-1"
                data-behaviour-config="${item.videoConfig}"
                style="background-image: url('${item.ImageUrl}')"
            >
                <div class="videoplayer__cta videoplayer__cta--play">
                    <span class="sn-icon-play"></span>
                </div>
            </div>

            <div class="text-block">
                <h5 class="card__title">${item.Title}</h5>
                <div class="paragraphs paragraph--small">
                <p>${item.Description}</p>
                </div>
            </div>

          </div>
          
          <div class="button-group">
            <a
            class="cta-button cta-button--primary cta-button--medium"
            href="${item.Url}"
            target="_self"
            >
            <span class="cta-button__label">Download</span>
            <span class="cta-button__icon sn-icon-download"></span>
            </a>              
          </div>        
              `;

    const el = document.createElement('aside');
    el.classList.add('card');
    el.classList.add('card--staff');
    el.innerHTML = template;
    return el;
  },
};

const selectOptions = {
  
  // *** required values for <option> elements
  keys: {
    value: null,
    label: null,
  },
  
  init: (data, options) => {
    
    // *** extract keys to use for data retrieval
    options.map.map((item) => {
      if(item[0] === 'value') selectOptions.keys.value = item[1];
      if(item[0] === 'label') selectOptions.keys.label = item[1];
    });
    
    if (!selectOptions.keys.value) {
      console.warn('/selectOptions/ -init required {value} key not declared');
      return;
    }
    if (!selectOptions.keys.label) {
      console.warn('/selectOptions/ -init required {label} key not declared');
      return;
    }
    
    // *** if all ok, pass generated elements out to invoker
    return data.map((item) => {
      return selectOptions.build(item);
    });
  },
  
  // *** for each item in the json, create an <option> element
  build: (item) => {
    const el = document.createElement('option');
    el.setAttribute('value', item[selectOptions.keys.value]);
    el.innerText = item[selectOptions.keys.label];
    return el;
  }
};

const uiFactory = {
  types: {
    ['staffcards']: staffCards,
    ['caseReviewCards']: caseReviewCards,
    ['selectoptions']: selectOptions,
  },

  init: (signal) => {},

  examine: (data, options) => {
    //console.log('/index/ -examine', data, options);

    if (!options.template) {
      console.warn('/uiFactory/ -examine --undeclared template', options);
      return;
    }

    const type = options.template.toLowerCase();
    const handler = uiFactory.types[type];

    if (!handler) {
      console.warn('/uiFactory/ -examine', type, 'does not exist');
      return;
    }

    return handler.init(data.payload.Result, options); // *** TODO refactor away 'Result', correct scope should be passed in by invoker
  },
};

/**
 * lodash (Custom Build) <https://lodash.com/>
 * Build: `lodash modularize exports="npm" -o ./`
 * Copyright jQuery Foundation and other contributors <https://jquery.org/>
 * Released under MIT license <https://lodash.com/license>
 * Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
 * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
 */
/** Used as references for various `Number` constants. */
var INFINITY = 1 / 0,
    MAX_SAFE_INTEGER = 9007199254740991,
    MAX_INTEGER = 1.7976931348623157e+308,
    NAN = 0 / 0;

/** `Object#toString` result references. */
var funcTag = '[object Function]',
    genTag = '[object GeneratorFunction]',
    symbolTag = '[object Symbol]';

/** Used to match leading and trailing whitespace. */
var reTrim = /^\s+|\s+$/g;

/** Used to detect bad signed hexadecimal string values. */
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;

/** Used to detect binary string values. */
var reIsBinary = /^0b[01]+$/i;

/** Used to detect octal string values. */
var reIsOctal = /^0o[0-7]+$/i;

/** Used to detect unsigned integer values. */
var reIsUint = /^(?:0|[1-9]\d*)$/;

/** Built-in method references without a dependency on `root`. */
var freeParseInt = parseInt;

/** Used for built-in method references. */
var objectProto = Object.prototype;

/**
 * Used to resolve the
 * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
 * of values.
 */
var objectToString = objectProto.toString;

/* Built-in method references for those with the same name as other `lodash` methods. */
var nativeCeil = Math.ceil,
    nativeMax = Math.max;

/**
 * The base implementation of `_.slice` without an iteratee call guard.
 *
 * @private
 * @param {Array} array The array to slice.
 * @param {number} [start=0] The start position.
 * @param {number} [end=array.length] The end position.
 * @returns {Array} Returns the slice of `array`.
 */
function baseSlice(array, start, end) {
  var index = -1,
      length = array.length;

  if (start < 0) {
    start = -start > length ? 0 : (length + start);
  }
  end = end > length ? length : end;
  if (end < 0) {
    end += length;
  }
  length = start > end ? 0 : ((end - start) >>> 0);
  start >>>= 0;

  var result = Array(length);
  while (++index < length) {
    result[index] = array[index + start];
  }
  return result;
}

/**
 * Checks if `value` is a valid array-like index.
 *
 * @private
 * @param {*} value The value to check.
 * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
 * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
 */
function isIndex(value, length) {
  length = length == null ? MAX_SAFE_INTEGER : length;
  return !!length &&
    (typeof value == 'number' || reIsUint.test(value)) &&
    (value > -1 && value % 1 == 0 && value < length);
}

/**
 * Checks if the given arguments are from an iteratee call.
 *
 * @private
 * @param {*} value The potential iteratee value argument.
 * @param {*} index The potential iteratee index or key argument.
 * @param {*} object The potential iteratee object argument.
 * @returns {boolean} Returns `true` if the arguments are from an iteratee call,
 *  else `false`.
 */
function isIterateeCall(value, index, object) {
  if (!isObject(object)) {
    return false;
  }
  var type = typeof index;
  if (type == 'number'
        ? (isArrayLike(object) && isIndex(index, object.length))
        : (type == 'string' && index in object)
      ) {
    return eq(object[index], value);
  }
  return false;
}

/**
 * Creates an array of elements split into groups the length of `size`.
 * If `array` can't be split evenly, the final chunk will be the remaining
 * elements.
 *
 * @static
 * @memberOf _
 * @since 3.0.0
 * @category Array
 * @param {Array} array The array to process.
 * @param {number} [size=1] The length of each chunk
 * @param- {Object} [guard] Enables use as an iteratee for methods like `_.map`.
 * @returns {Array} Returns the new array of chunks.
 * @example
 *
 * _.chunk(['a', 'b', 'c', 'd'], 2);
 * // => [['a', 'b'], ['c', 'd']]
 *
 * _.chunk(['a', 'b', 'c', 'd'], 3);
 * // => [['a', 'b', 'c'], ['d']]
 */
function chunk(array, size, guard) {
  if ((guard ? isIterateeCall(array, size, guard) : size === undefined)) {
    size = 1;
  } else {
    size = nativeMax(toInteger(size), 0);
  }
  var length = array ? array.length : 0;
  if (!length || size < 1) {
    return [];
  }
  var index = 0,
      resIndex = 0,
      result = Array(nativeCeil(length / size));

  while (index < length) {
    result[resIndex++] = baseSlice(array, index, (index += size));
  }
  return result;
}

/**
 * Performs a
 * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
 * comparison between two values to determine if they are equivalent.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to compare.
 * @param {*} other The other value to compare.
 * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
 * @example
 *
 * var object = { 'a': 1 };
 * var other = { 'a': 1 };
 *
 * _.eq(object, object);
 * // => true
 *
 * _.eq(object, other);
 * // => false
 *
 * _.eq('a', 'a');
 * // => true
 *
 * _.eq('a', Object('a'));
 * // => false
 *
 * _.eq(NaN, NaN);
 * // => true
 */
function eq(value, other) {
  return value === other || (value !== value && other !== other);
}

/**
 * Checks if `value` is array-like. A value is considered array-like if it's
 * not a function and has a `value.length` that's an integer greater than or
 * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
 * @example
 *
 * _.isArrayLike([1, 2, 3]);
 * // => true
 *
 * _.isArrayLike(document.body.children);
 * // => true
 *
 * _.isArrayLike('abc');
 * // => true
 *
 * _.isArrayLike(_.noop);
 * // => false
 */
function isArrayLike(value) {
  return value != null && isLength(value.length) && !isFunction(value);
}

/**
 * Checks if `value` is classified as a `Function` object.
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a function, else `false`.
 * @example
 *
 * _.isFunction(_);
 * // => true
 *
 * _.isFunction(/abc/);
 * // => false
 */
function isFunction(value) {
  // The use of `Object#toString` avoids issues with the `typeof` operator
  // in Safari 8-9 which returns 'object' for typed array and other constructors.
  var tag = isObject(value) ? objectToString.call(value) : '';
  return tag == funcTag || tag == genTag;
}

/**
 * Checks if `value` is a valid array-like length.
 *
 * **Note:** This method is loosely based on
 * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
 * @example
 *
 * _.isLength(3);
 * // => true
 *
 * _.isLength(Number.MIN_VALUE);
 * // => false
 *
 * _.isLength(Infinity);
 * // => false
 *
 * _.isLength('3');
 * // => false
 */
function isLength(value) {
  return typeof value == 'number' &&
    value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
}

/**
 * Checks if `value` is the
 * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
 * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
 *
 * @static
 * @memberOf _
 * @since 0.1.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is an object, else `false`.
 * @example
 *
 * _.isObject({});
 * // => true
 *
 * _.isObject([1, 2, 3]);
 * // => true
 *
 * _.isObject(_.noop);
 * // => true
 *
 * _.isObject(null);
 * // => false
 */
function isObject(value) {
  var type = typeof value;
  return !!value && (type == 'object' || type == 'function');
}

/**
 * Checks if `value` is object-like. A value is object-like if it's not `null`
 * and has a `typeof` result of "object".
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
 * @example
 *
 * _.isObjectLike({});
 * // => true
 *
 * _.isObjectLike([1, 2, 3]);
 * // => true
 *
 * _.isObjectLike(_.noop);
 * // => false
 *
 * _.isObjectLike(null);
 * // => false
 */
function isObjectLike(value) {
  return !!value && typeof value == 'object';
}

/**
 * Checks if `value` is classified as a `Symbol` primitive or object.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to check.
 * @returns {boolean} Returns `true` if `value` is a symbol, else `false`.
 * @example
 *
 * _.isSymbol(Symbol.iterator);
 * // => true
 *
 * _.isSymbol('abc');
 * // => false
 */
function isSymbol(value) {
  return typeof value == 'symbol' ||
    (isObjectLike(value) && objectToString.call(value) == symbolTag);
}

/**
 * Converts `value` to a finite number.
 *
 * @static
 * @memberOf _
 * @since 4.12.0
 * @category Lang
 * @param {*} value The value to convert.
 * @returns {number} Returns the converted number.
 * @example
 *
 * _.toFinite(3.2);
 * // => 3.2
 *
 * _.toFinite(Number.MIN_VALUE);
 * // => 5e-324
 *
 * _.toFinite(Infinity);
 * // => 1.7976931348623157e+308
 *
 * _.toFinite('3.2');
 * // => 3.2
 */
function toFinite(value) {
  if (!value) {
    return value === 0 ? value : 0;
  }
  value = toNumber(value);
  if (value === INFINITY || value === -INFINITY) {
    var sign = (value < 0 ? -1 : 1);
    return sign * MAX_INTEGER;
  }
  return value === value ? value : 0;
}

/**
 * Converts `value` to an integer.
 *
 * **Note:** This method is loosely based on
 * [`ToInteger`](http://www.ecma-international.org/ecma-262/7.0/#sec-tointeger).
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to convert.
 * @returns {number} Returns the converted integer.
 * @example
 *
 * _.toInteger(3.2);
 * // => 3
 *
 * _.toInteger(Number.MIN_VALUE);
 * // => 0
 *
 * _.toInteger(Infinity);
 * // => 1.7976931348623157e+308
 *
 * _.toInteger('3.2');
 * // => 3
 */
function toInteger(value) {
  var result = toFinite(value),
      remainder = result % 1;

  return result === result ? (remainder ? result - remainder : result) : 0;
}

/**
 * Converts `value` to a number.
 *
 * @static
 * @memberOf _
 * @since 4.0.0
 * @category Lang
 * @param {*} value The value to process.
 * @returns {number} Returns the number.
 * @example
 *
 * _.toNumber(3.2);
 * // => 3.2
 *
 * _.toNumber(Number.MIN_VALUE);
 * // => 5e-324
 *
 * _.toNumber(Infinity);
 * // => Infinity
 *
 * _.toNumber('3.2');
 * // => 3.2
 */
function toNumber(value) {
  if (typeof value == 'number') {
    return value;
  }
  if (isSymbol(value)) {
    return NAN;
  }
  if (isObject(value)) {
    var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
    value = isObject(other) ? (other + '') : other;
  }
  if (typeof value != 'string') {
    return value === 0 ? value : +value;
  }
  value = value.replace(reTrim, '');
  var isBinary = reIsBinary.test(value);
  return (isBinary || reIsOctal.test(value))
    ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
    : (reIsBadHex.test(value) ? NAN : +value);
}

var lodash_chunk = chunk;

function _assertThisInitialized$1(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _inheritsLoose$1(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }

/*!
 * GSAP 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var _config$1 = {
  autoSleep: 120,
  force3D: "auto",
  nullTargetWarn: 1,
  units: {
    lineHeight: ""
  }
},
    _defaults$1 = {
  duration: .5,
  overwrite: false,
  delay: 0
},
    _suppressOverwrites$1,
    _bigNum$2 = 1e8,
    _tinyNum = 1 / _bigNum$2,
    _2PI = Math.PI * 2,
    _HALF_PI = _2PI / 4,
    _gsID = 0,
    _sqrt$1 = Math.sqrt,
    _cos$1 = Math.cos,
    _sin$1 = Math.sin,
    _isString$4 = function _isString(value) {
  return typeof value === "string";
},
    _isFunction$4 = function _isFunction(value) {
  return typeof value === "function";
},
    _isNumber$2 = function _isNumber(value) {
  return typeof value === "number";
},
    _isUndefined$2 = function _isUndefined(value) {
  return typeof value === "undefined";
},
    _isObject$2 = function _isObject(value) {
  return typeof value === "object";
},
    _isNotFalse = function _isNotFalse(value) {
  return value !== false;
},
    _windowExists$7 = function _windowExists() {
  return typeof window !== "undefined";
},
    _isFuncOrString = function _isFuncOrString(value) {
  return _isFunction$4(value) || _isString$4(value);
},
    _isTypedArray = typeof ArrayBuffer === "function" && ArrayBuffer.isView || function () {},
    // note: IE10 has ArrayBuffer, but NOT ArrayBuffer.isView().
_isArray$1 = Array.isArray,
    _strictNumExp = /(?:-?\.?\d|\.)+/gi,
    //only numbers (including negatives and decimals) but NOT relative values.
_numExp$1 = /[-+=.]*\d+[.e\-+]*\d*[e\-+]*\d*/g,
    //finds any numbers, including ones that start with += or -=, negative numbers, and ones in scientific notation like 1e-8.
_numWithUnitExp = /[-+=.]*\d+[.e-]*\d*[a-z%]*/g,
    _complexStringNumExp = /[-+=.]*\d+\.?\d*(?:e-|e\+)?\d*/gi,
    //duplicate so that while we're looping through matches from exec(), it doesn't contaminate the lastIndex of _numExp which we use to search for colors too.
_relExp = /[+-]=-?[.\d]+/,
    _delimitedValueExp = /[#\-+.]*\b[a-z\d-=+%.]+/gi,
    _unitExp = /[\d.+\-=]+(?:e[-+]\d*)*/i,
    _globalTimeline,
    _win$6,
    _coreInitted$5,
    _doc$5,
    _globals = {},
    _installScope = {},
    _coreReady,
    _install = function _install(scope) {
  return (_installScope = _merge(scope, _globals)) && gsap$9;
},
    _missingPlugin = function _missingPlugin(property, value) {
  return console.warn("Invalid property", property, "set to", value, "Missing plugin? gsap.registerPlugin()");
},
    _warn$2 = function _warn(message, suppress) {
  return !suppress && console.warn(message);
},
    _addGlobal = function _addGlobal(name, obj) {
  return name && (_globals[name] = obj) && _installScope && (_installScope[name] = obj) || _globals;
},
    _emptyFunc$2 = function _emptyFunc() {
  return 0;
},
    _reservedProps = {},
    _lazyTweens = [],
    _lazyLookup = {},
    _lastRenderedFrame,
    _plugins = {},
    _effects = {},
    _nextGCFrame = 30,
    _harnessPlugins = [],
    _callbackNames = "",
    _harness = function _harness(targets) {
  var target = targets[0],
      harnessPlugin,
      i;
  _isObject$2(target) || _isFunction$4(target) || (targets = [targets]);

  if (!(harnessPlugin = (target._gsap || {}).harness)) {
    // find the first target with a harness. We assume targets passed into an animation will be of similar type, meaning the same kind of harness can be used for them all (performance optimization)
    i = _harnessPlugins.length;

    while (i-- && !_harnessPlugins[i].targetTest(target)) {}

    harnessPlugin = _harnessPlugins[i];
  }

  i = targets.length;

  while (i--) {
    targets[i] && (targets[i]._gsap || (targets[i]._gsap = new GSCache(targets[i], harnessPlugin))) || targets.splice(i, 1);
  }

  return targets;
},
    _getCache = function _getCache(target) {
  return target._gsap || _harness(toArray(target))[0]._gsap;
},
    _getProperty = function _getProperty(target, property, v) {
  return (v = target[property]) && _isFunction$4(v) ? target[property]() : _isUndefined$2(v) && target.getAttribute && target.getAttribute(property) || v;
},
    _forEachName = function _forEachName(names, func) {
  return (names = names.split(",")).forEach(func) || names;
},
    //split a comma-delimited list of names into an array, then run a forEach() function and return the split array (this is just a way to consolidate/shorten some code).
_round$3 = function _round(value) {
  return Math.round(value * 100000) / 100000 || 0;
},
    _arrayContainsAny = function _arrayContainsAny(toSearch, toFind) {
  //searches one array to find matches for any of the items in the toFind array. As soon as one is found, it returns true. It does NOT return all the matches; it's simply a boolean search.
  var l = toFind.length,
      i = 0;

  for (; toSearch.indexOf(toFind[i]) < 0 && ++i < l;) {}

  return i < l;
},
    _parseVars = function _parseVars(params, type, parent) {
  //reads the arguments passed to one of the key methods and figures out if the user is defining things with the OLD/legacy syntax where the duration is the 2nd parameter, and then it adjusts things accordingly and spits back the corrected vars object (with the duration added if necessary, as well as runBackwards or startAt or immediateRender). type 0 = to()/staggerTo(), 1 = from()/staggerFrom(), 2 = fromTo()/staggerFromTo()
  var isLegacy = _isNumber$2(params[1]),
      varsIndex = (isLegacy ? 2 : 1) + (type < 2 ? 0 : 1),
      vars = params[varsIndex],
      irVars;

  isLegacy && (vars.duration = params[1]);
  vars.parent = parent;

  if (type) {
    irVars = vars;

    while (parent && !("immediateRender" in irVars)) {
      // inheritance hasn't happened yet, but someone may have set a default in an ancestor timeline. We could do vars.immediateRender = _isNotFalse(_inheritDefaults(vars).immediateRender) but that'd exact a slight performance penalty because _inheritDefaults() also runs in the Tween constructor. We're paying a small kb price here to gain speed.
      irVars = parent.vars.defaults || {};
      parent = _isNotFalse(parent.vars.inherit) && parent.parent;
    }

    vars.immediateRender = _isNotFalse(irVars.immediateRender);
    type < 2 ? vars.runBackwards = 1 : vars.startAt = params[varsIndex - 1]; // "from" vars
  }

  return vars;
},
    _lazyRender = function _lazyRender() {
  var l = _lazyTweens.length,
      a = _lazyTweens.slice(0),
      i,
      tween;

  _lazyLookup = {};
  _lazyTweens.length = 0;

  for (i = 0; i < l; i++) {
    tween = a[i];
    tween && tween._lazy && (tween.render(tween._lazy[0], tween._lazy[1], true)._lazy = 0);
  }
},
    _lazySafeRender = function _lazySafeRender(animation, time, suppressEvents, force) {
  _lazyTweens.length && _lazyRender();
  animation.render(time, suppressEvents, force);
  _lazyTweens.length && _lazyRender(); //in case rendering caused any tweens to lazy-init, we should render them because typically when someone calls seek() or time() or progress(), they expect an immediate render.
},
    _numericIfPossible = function _numericIfPossible(value) {
  var n = parseFloat(value);
  return (n || n === 0) && (value + "").match(_delimitedValueExp).length < 2 ? n : _isString$4(value) ? value.trim() : value;
},
    _passThrough$1 = function _passThrough(p) {
  return p;
},
    _setDefaults$2 = function _setDefaults(obj, defaults) {
  for (var p in defaults) {
    p in obj || (obj[p] = defaults[p]);
  }

  return obj;
},
    _setKeyframeDefaults = function _setKeyframeDefaults(obj, defaults) {
  for (var p in defaults) {
    p in obj || p === "duration" || p === "ease" || (obj[p] = defaults[p]);
  }
},
    _merge = function _merge(base, toMerge) {
  for (var p in toMerge) {
    base[p] = toMerge[p];
  }

  return base;
},
    _mergeDeep = function _mergeDeep(base, toMerge) {
  for (var p in toMerge) {
    p !== "__proto__" && p !== "constructor" && p !== "prototype" && (base[p] = _isObject$2(toMerge[p]) ? _mergeDeep(base[p] || (base[p] = {}), toMerge[p]) : toMerge[p]);
  }

  return base;
},
    _copyExcluding = function _copyExcluding(obj, excluding) {
  var copy = {},
      p;

  for (p in obj) {
    p in excluding || (copy[p] = obj[p]);
  }

  return copy;
},
    _inheritDefaults = function _inheritDefaults(vars) {
  var parent = vars.parent || _globalTimeline,
      func = vars.keyframes ? _setKeyframeDefaults : _setDefaults$2;

  if (_isNotFalse(vars.inherit)) {
    while (parent) {
      func(vars, parent.vars.defaults);
      parent = parent.parent || parent._dp;
    }
  }

  return vars;
},
    _arraysMatch = function _arraysMatch(a1, a2) {
  var i = a1.length,
      match = i === a2.length;

  while (match && i-- && a1[i] === a2[i]) {}

  return i < 0;
},
    _addLinkedListItem = function _addLinkedListItem(parent, child, firstProp, lastProp, sortBy) {
  if (firstProp === void 0) {
    firstProp = "_first";
  }

  if (lastProp === void 0) {
    lastProp = "_last";
  }

  var prev = parent[lastProp],
      t;

  if (sortBy) {
    t = child[sortBy];

    while (prev && prev[sortBy] > t) {
      prev = prev._prev;
    }
  }

  if (prev) {
    child._next = prev._next;
    prev._next = child;
  } else {
    child._next = parent[firstProp];
    parent[firstProp] = child;
  }

  if (child._next) {
    child._next._prev = child;
  } else {
    parent[lastProp] = child;
  }

  child._prev = prev;
  child.parent = child._dp = parent;
  return child;
},
    _removeLinkedListItem = function _removeLinkedListItem(parent, child, firstProp, lastProp) {
  if (firstProp === void 0) {
    firstProp = "_first";
  }

  if (lastProp === void 0) {
    lastProp = "_last";
  }

  var prev = child._prev,
      next = child._next;

  if (prev) {
    prev._next = next;
  } else if (parent[firstProp] === child) {
    parent[firstProp] = next;
  }

  if (next) {
    next._prev = prev;
  } else if (parent[lastProp] === child) {
    parent[lastProp] = prev;
  }

  child._next = child._prev = child.parent = null; // don't delete the _dp just so we can revert if necessary. But parent should be null to indicate the item isn't in a linked list.
},
    _removeFromParent = function _removeFromParent(child, onlyIfParentHasAutoRemove) {
  child.parent && (!onlyIfParentHasAutoRemove || child.parent.autoRemoveChildren) && child.parent.remove(child);
  child._act = 0;
},
    _uncache = function _uncache(animation, child) {
  if (animation && (!child || child._end > animation._dur || child._start < 0)) {
    // performance optimization: if a child animation is passed in we should only uncache if that child EXTENDS the animation (its end time is beyond the end)
    var a = animation;

    while (a) {
      a._dirty = 1;
      a = a.parent;
    }
  }

  return animation;
},
    _recacheAncestors = function _recacheAncestors(animation) {
  var parent = animation.parent;

  while (parent && parent.parent) {
    //sometimes we must force a re-sort of all children and update the duration/totalDuration of all ancestor timelines immediately in case, for example, in the middle of a render loop, one tween alters another tween's timeScale which shoves its startTime before 0, forcing the parent timeline to shift around and shiftChildren() which could affect that next tween's render (startTime). Doesn't matter for the root timeline though.
    parent._dirty = 1;
    parent.totalDuration();
    parent = parent.parent;
  }

  return animation;
},
    _hasNoPausedAncestors = function _hasNoPausedAncestors(animation) {
  return !animation || animation._ts && _hasNoPausedAncestors(animation.parent);
},
    _elapsedCycleDuration = function _elapsedCycleDuration(animation) {
  return animation._repeat ? _animationCycle(animation._tTime, animation = animation.duration() + animation._rDelay) * animation : 0;
},
    // feed in the totalTime and cycleDuration and it'll return the cycle (iteration minus 1) and if the playhead is exactly at the very END, it will NOT bump up to the next cycle.
_animationCycle = function _animationCycle(tTime, cycleDuration) {
  var whole = Math.floor(tTime /= cycleDuration);
  return tTime && whole === tTime ? whole - 1 : whole;
},
    _parentToChildTotalTime = function _parentToChildTotalTime(parentTime, child) {
  return (parentTime - child._start) * child._ts + (child._ts >= 0 ? 0 : child._dirty ? child.totalDuration() : child._tDur);
},
    _setEnd = function _setEnd(animation) {
  return animation._end = _round$3(animation._start + (animation._tDur / Math.abs(animation._ts || animation._rts || _tinyNum) || 0));
},
    _alignPlayhead = function _alignPlayhead(animation, totalTime) {
  // adjusts the animation's _start and _end according to the provided totalTime (only if the parent's smoothChildTiming is true and the animation isn't paused). It doesn't do any rendering or forcing things back into parent timelines, etc. - that's what totalTime() is for.
  var parent = animation._dp;

  if (parent && parent.smoothChildTiming && animation._ts) {
    animation._start = _round$3(parent._time - (animation._ts > 0 ? totalTime / animation._ts : ((animation._dirty ? animation.totalDuration() : animation._tDur) - totalTime) / -animation._ts));

    _setEnd(animation);

    parent._dirty || _uncache(parent, animation); //for performance improvement. If the parent's cache is already dirty, it already took care of marking the ancestors as dirty too, so skip the function call here.
  }

  return animation;
},

/*
_totalTimeToTime = (clampedTotalTime, duration, repeat, repeatDelay, yoyo) => {
	let cycleDuration = duration + repeatDelay,
		time = _round(clampedTotalTime % cycleDuration);
	if (time > duration) {
		time = duration;
	}
	return (yoyo && (~~(clampedTotalTime / cycleDuration) & 1)) ? duration - time : time;
},
*/
_postAddChecks = function _postAddChecks(timeline, child) {
  var t;

  if (child._time || child._initted && !child._dur) {
    //in case, for example, the _start is moved on a tween that has already rendered. Imagine it's at its end state, then the startTime is moved WAY later (after the end of this timeline), it should render at its beginning.
    t = _parentToChildTotalTime(timeline.rawTime(), child);

    if (!child._dur || _clamp$1(0, child.totalDuration(), t) - child._tTime > _tinyNum) {
      child.render(t, true);
    }
  } //if the timeline has already ended but the inserted tween/timeline extends the duration, we should enable this timeline again so that it renders properly. We should also align the playhead with the parent timeline's when appropriate.


  if (_uncache(timeline, child)._dp && timeline._initted && timeline._time >= timeline._dur && timeline._ts) {
    //in case any of the ancestors had completed but should now be enabled...
    if (timeline._dur < timeline.duration()) {
      t = timeline;

      while (t._dp) {
        t.rawTime() >= 0 && t.totalTime(t._tTime); //moves the timeline (shifts its startTime) if necessary, and also enables it. If it's currently zero, though, it may not be scheduled to render until later so there's no need to force it to align with the current playhead position. Only move to catch up with the playhead.

        t = t._dp;
      }
    }

    timeline._zTime = -_tinyNum; // helps ensure that the next render() will be forced (crossingStart = true in render()), even if the duration hasn't changed (we're adding a child which would need to get rendered). Definitely an edge case. Note: we MUST do this AFTER the loop above where the totalTime() might trigger a render() because this _addToTimeline() method gets called from the Animation constructor, BEFORE tweens even record their targets, etc. so we wouldn't want things to get triggered in the wrong order.
  }
},
    _addToTimeline = function _addToTimeline(timeline, child, position, skipChecks) {
  child.parent && _removeFromParent(child);
  child._start = _round$3(position + child._delay);
  child._end = _round$3(child._start + (child.totalDuration() / Math.abs(child.timeScale()) || 0));

  _addLinkedListItem(timeline, child, "_first", "_last", timeline._sort ? "_start" : 0);

  timeline._recent = child;
  skipChecks || _postAddChecks(timeline, child);
  return timeline;
},
    _scrollTrigger = function _scrollTrigger(animation, trigger) {
  return (_globals.ScrollTrigger || _missingPlugin("scrollTrigger", trigger)) && _globals.ScrollTrigger.create(trigger, animation);
},
    _attemptInitTween = function _attemptInitTween(tween, totalTime, force, suppressEvents) {
  _initTween(tween, totalTime);

  if (!tween._initted) {
    return 1;
  }

  if (!force && tween._pt && (tween._dur && tween.vars.lazy !== false || !tween._dur && tween.vars.lazy) && _lastRenderedFrame !== _ticker.frame) {
    _lazyTweens.push(tween);

    tween._lazy = [totalTime, suppressEvents];
    return 1;
  }
},
    _parentPlayheadIsBeforeStart = function _parentPlayheadIsBeforeStart(_ref) {
  var parent = _ref.parent;
  return parent && parent._ts && parent._initted && !parent._lock && (parent.rawTime() < 0 || _parentPlayheadIsBeforeStart(parent));
},
    // check parent's _lock because when a timeline repeats/yoyos and does its artificial wrapping, we shouldn't force the ratio back to 0
_renderZeroDurationTween = function _renderZeroDurationTween(tween, totalTime, suppressEvents, force) {
  var prevRatio = tween.ratio,
      ratio = totalTime < 0 || !totalTime && (!tween._start && _parentPlayheadIsBeforeStart(tween) || (tween._ts < 0 || tween._dp._ts < 0) && tween.data !== "isFromStart" && tween.data !== "isStart") ? 0 : 1,
      // if the tween or its parent is reversed and the totalTime is 0, we should go to a ratio of 0.
  repeatDelay = tween._rDelay,
      tTime = 0,
      pt,
      iteration,
      prevIteration;

  if (repeatDelay && tween._repeat) {
    // in case there's a zero-duration tween that has a repeat with a repeatDelay
    tTime = _clamp$1(0, tween._tDur, totalTime);
    iteration = _animationCycle(tTime, repeatDelay);
    prevIteration = _animationCycle(tween._tTime, repeatDelay);
    tween._yoyo && iteration & 1 && (ratio = 1 - ratio);

    if (iteration !== prevIteration) {
      prevRatio = 1 - ratio;
      tween.vars.repeatRefresh && tween._initted && tween.invalidate();
    }
  }

  if (ratio !== prevRatio || force || tween._zTime === _tinyNum || !totalTime && tween._zTime) {
    if (!tween._initted && _attemptInitTween(tween, totalTime, force, suppressEvents)) {
      // if we render the very beginning (time == 0) of a fromTo(), we must force the render (normal tweens wouldn't need to render at a time of 0 when the prevTime was also 0). This is also mandatory to make sure overwriting kicks in immediately.
      return;
    }

    prevIteration = tween._zTime;
    tween._zTime = totalTime || (suppressEvents ? _tinyNum : 0); // when the playhead arrives at EXACTLY time 0 (right on top) of a zero-duration tween, we need to discern if events are suppressed so that when the playhead moves again (next time), it'll trigger the callback. If events are NOT suppressed, obviously the callback would be triggered in this render. Basically, the callback should fire either when the playhead ARRIVES or LEAVES this exact spot, not both. Imagine doing a timeline.seek(0) and there's a callback that sits at 0. Since events are suppressed on that seek() by default, nothing will fire, but when the playhead moves off of that position, the callback should fire. This behavior is what people intuitively expect.

    suppressEvents || (suppressEvents = totalTime && !prevIteration); // if it was rendered previously at exactly 0 (_zTime) and now the playhead is moving away, DON'T fire callbacks otherwise they'll seem like duplicates.

    tween.ratio = ratio;
    tween._from && (ratio = 1 - ratio);
    tween._time = 0;
    tween._tTime = tTime;
    pt = tween._pt;

    while (pt) {
      pt.r(ratio, pt.d);
      pt = pt._next;
    }

    tween._startAt && totalTime < 0 && tween._startAt.render(totalTime, true, true);
    tween._onUpdate && !suppressEvents && _callback(tween, "onUpdate");
    tTime && tween._repeat && !suppressEvents && tween.parent && _callback(tween, "onRepeat");

    if ((totalTime >= tween._tDur || totalTime < 0) && tween.ratio === ratio) {
      ratio && _removeFromParent(tween, 1);

      if (!suppressEvents) {
        _callback(tween, ratio ? "onComplete" : "onReverseComplete", true);

        tween._prom && tween._prom();
      }
    }
  } else if (!tween._zTime) {
    tween._zTime = totalTime;
  }
},
    _findNextPauseTween = function _findNextPauseTween(animation, prevTime, time) {
  var child;

  if (time > prevTime) {
    child = animation._first;

    while (child && child._start <= time) {
      if (!child._dur && child.data === "isPause" && child._start > prevTime) {
        return child;
      }

      child = child._next;
    }
  } else {
    child = animation._last;

    while (child && child._start >= time) {
      if (!child._dur && child.data === "isPause" && child._start < prevTime) {
        return child;
      }

      child = child._prev;
    }
  }
},
    _setDuration = function _setDuration(animation, duration, skipUncache, leavePlayhead) {
  var repeat = animation._repeat,
      dur = _round$3(duration) || 0,
      totalProgress = animation._tTime / animation._tDur;
  totalProgress && !leavePlayhead && (animation._time *= dur / animation._dur);
  animation._dur = dur;
  animation._tDur = !repeat ? dur : repeat < 0 ? 1e10 : _round$3(dur * (repeat + 1) + animation._rDelay * repeat);
  totalProgress && !leavePlayhead ? _alignPlayhead(animation, animation._tTime = animation._tDur * totalProgress) : animation.parent && _setEnd(animation);
  skipUncache || _uncache(animation.parent, animation);
  return animation;
},
    _onUpdateTotalDuration = function _onUpdateTotalDuration(animation) {
  return animation instanceof Timeline ? _uncache(animation) : _setDuration(animation, animation._dur);
},
    _zeroPosition = {
  _start: 0,
  endTime: _emptyFunc$2
},
    _parsePosition$1 = function _parsePosition(animation, position) {
  var labels = animation.labels,
      recent = animation._recent || _zeroPosition,
      clippedDuration = animation.duration() >= _bigNum$2 ? recent.endTime(false) : animation._dur,
      //in case there's a child that infinitely repeats, users almost never intend for the insertion point of a new child to be based on a SUPER long value like that so we clip it and assume the most recently-added child's endTime should be used instead.
  i,
      offset;

  if (_isString$4(position) && (isNaN(position) || position in labels)) {
    //if the string is a number like "1", check to see if there's a label with that name, otherwise interpret it as a number (absolute value).
    i = position.charAt(0);

    if (i === "<" || i === ">") {
      return (i === "<" ? recent._start : recent.endTime(recent._repeat >= 0)) + (parseFloat(position.substr(1)) || 0);
    }

    i = position.indexOf("=");

    if (i < 0) {
      position in labels || (labels[position] = clippedDuration);
      return labels[position];
    }

    offset = +(position.charAt(i - 1) + position.substr(i + 1));
    return i > 1 ? _parsePosition(animation, position.substr(0, i - 1)) + offset : clippedDuration + offset;
  }

  return position == null ? clippedDuration : +position;
},
    _conditionalReturn = function _conditionalReturn(value, func) {
  return value || value === 0 ? func(value) : func;
},
    _clamp$1 = function _clamp(min, max, value) {
  return value < min ? min : value > max ? max : value;
},
    getUnit = function getUnit(value) {
  if (typeof value !== "string") {
    return "";
  }

  var v = _unitExp.exec(value);

  return v ? value.substr(v.index + v[0].length) : "";
},
    // note: protect against padded numbers as strings, like "100.100". That shouldn't return "00" as the unit. If it's numeric, return no unit.
clamp = function clamp(min, max, value) {
  return _conditionalReturn(value, function (v) {
    return _clamp$1(min, max, v);
  });
},
    _slice = [].slice,
    _isArrayLike = function _isArrayLike(value, nonEmpty) {
  return value && _isObject$2(value) && "length" in value && (!nonEmpty && !value.length || value.length - 1 in value && _isObject$2(value[0])) && !value.nodeType && value !== _win$6;
},
    _flatten = function _flatten(ar, leaveStrings, accumulator) {
  if (accumulator === void 0) {
    accumulator = [];
  }

  return ar.forEach(function (value) {
    var _accumulator;

    return _isString$4(value) && !leaveStrings || _isArrayLike(value, 1) ? (_accumulator = accumulator).push.apply(_accumulator, toArray(value)) : accumulator.push(value);
  }) || accumulator;
},
    //takes any value and returns an array. If it's a string (and leaveStrings isn't true), it'll use document.querySelectorAll() and convert that to an array. It'll also accept iterables like jQuery objects.
toArray = function toArray(value, leaveStrings) {
  return _isString$4(value) && !leaveStrings && (_coreInitted$5 || !_wake()) ? _slice.call(_doc$5.querySelectorAll(value), 0) : _isArray$1(value) ? _flatten(value, leaveStrings) : _isArrayLike(value) ? _slice.call(value, 0) : value ? [value] : [];
},
    shuffle = function shuffle(a) {
  return a.sort(function () {
    return .5 - Math.random();
  });
},
    // alternative that's a bit faster and more reliably diverse but bigger:   for (let j, v, i = a.length; i; j = Math.floor(Math.random() * i), v = a[--i], a[i] = a[j], a[j] = v); return a;
//for distributing values across an array. Can accept a number, a function or (most commonly) a function which can contain the following properties: {base, amount, from, ease, grid, axis, length, each}. Returns a function that expects the following parameters: index, target, array. Recognizes the following
distribute = function distribute(v) {
  if (_isFunction$4(v)) {
    return v;
  }

  var vars = _isObject$2(v) ? v : {
    each: v
  },
      //n:1 is just to indicate v was a number; we leverage that later to set v according to the length we get. If a number is passed in, we treat it like the old stagger value where 0.1, for example, would mean that things would be distributed with 0.1 between each element in the array rather than a total "amount" that's chunked out among them all.
  ease = _parseEase(vars.ease),
      from = vars.from || 0,
      base = parseFloat(vars.base) || 0,
      cache = {},
      isDecimal = from > 0 && from < 1,
      ratios = isNaN(from) || isDecimal,
      axis = vars.axis,
      ratioX = from,
      ratioY = from;

  if (_isString$4(from)) {
    ratioX = ratioY = {
      center: .5,
      edges: .5,
      end: 1
    }[from] || 0;
  } else if (!isDecimal && ratios) {
    ratioX = from[0];
    ratioY = from[1];
  }

  return function (i, target, a) {
    var l = (a || vars).length,
        distances = cache[l],
        originX,
        originY,
        x,
        y,
        d,
        j,
        max,
        min,
        wrapAt;

    if (!distances) {
      wrapAt = vars.grid === "auto" ? 0 : (vars.grid || [1, _bigNum$2])[1];

      if (!wrapAt) {
        max = -_bigNum$2;

        while (max < (max = a[wrapAt++].getBoundingClientRect().left) && wrapAt < l) {}

        wrapAt--;
      }

      distances = cache[l] = [];
      originX = ratios ? Math.min(wrapAt, l) * ratioX - .5 : from % wrapAt;
      originY = ratios ? l * ratioY / wrapAt - .5 : from / wrapAt | 0;
      max = 0;
      min = _bigNum$2;

      for (j = 0; j < l; j++) {
        x = j % wrapAt - originX;
        y = originY - (j / wrapAt | 0);
        distances[j] = d = !axis ? _sqrt$1(x * x + y * y) : Math.abs(axis === "y" ? y : x);
        d > max && (max = d);
        d < min && (min = d);
      }

      from === "random" && shuffle(distances);
      distances.max = max - min;
      distances.min = min;
      distances.v = l = (parseFloat(vars.amount) || parseFloat(vars.each) * (wrapAt > l ? l - 1 : !axis ? Math.max(wrapAt, l / wrapAt) : axis === "y" ? l / wrapAt : wrapAt) || 0) * (from === "edges" ? -1 : 1);
      distances.b = l < 0 ? base - l : base;
      distances.u = getUnit(vars.amount || vars.each) || 0; //unit

      ease = ease && l < 0 ? _invertEase(ease) : ease;
    }

    l = (distances[i] - distances.min) / distances.max || 0;
    return _round$3(distances.b + (ease ? ease(l) : l) * distances.v) + distances.u; //round in order to work around floating point errors
  };
},
    _roundModifier = function _roundModifier(v) {
  //pass in 0.1 get a function that'll round to the nearest tenth, or 5 to round to the closest 5, or 0.001 to the closest 1000th, etc.
  var p = v < 1 ? Math.pow(10, (v + "").length - 2) : 1; //to avoid floating point math errors (like 24 * 0.1 == 2.4000000000000004), we chop off at a specific number of decimal places (much faster than toFixed()

  return function (raw) {
    var n = Math.round(parseFloat(raw) / v) * v * p;
    return (n - n % 1) / p + (_isNumber$2(raw) ? 0 : getUnit(raw)); // n - n % 1 replaces Math.floor() in order to handle negative values properly. For example, Math.floor(-150.00000000000003) is 151!
  };
},
    snap = function snap(snapTo, value) {
  var isArray = _isArray$1(snapTo),
      radius,
      is2D;

  if (!isArray && _isObject$2(snapTo)) {
    radius = isArray = snapTo.radius || _bigNum$2;

    if (snapTo.values) {
      snapTo = toArray(snapTo.values);

      if (is2D = !_isNumber$2(snapTo[0])) {
        radius *= radius; //performance optimization so we don't have to Math.sqrt() in the loop.
      }
    } else {
      snapTo = _roundModifier(snapTo.increment);
    }
  }

  return _conditionalReturn(value, !isArray ? _roundModifier(snapTo) : _isFunction$4(snapTo) ? function (raw) {
    is2D = snapTo(raw);
    return Math.abs(is2D - raw) <= radius ? is2D : raw;
  } : function (raw) {
    var x = parseFloat(is2D ? raw.x : raw),
        y = parseFloat(is2D ? raw.y : 0),
        min = _bigNum$2,
        closest = 0,
        i = snapTo.length,
        dx,
        dy;

    while (i--) {
      if (is2D) {
        dx = snapTo[i].x - x;
        dy = snapTo[i].y - y;
        dx = dx * dx + dy * dy;
      } else {
        dx = Math.abs(snapTo[i] - x);
      }

      if (dx < min) {
        min = dx;
        closest = i;
      }
    }

    closest = !radius || min <= radius ? snapTo[closest] : raw;
    return is2D || closest === raw || _isNumber$2(raw) ? closest : closest + getUnit(raw);
  });
},
    random = function random(min, max, roundingIncrement, returnFunction) {
  return _conditionalReturn(_isArray$1(min) ? !max : roundingIncrement === true ? !!(roundingIncrement = 0) : !returnFunction, function () {
    return _isArray$1(min) ? min[~~(Math.random() * min.length)] : (roundingIncrement = roundingIncrement || 1e-5) && (returnFunction = roundingIncrement < 1 ? Math.pow(10, (roundingIncrement + "").length - 2) : 1) && Math.floor(Math.round((min - roundingIncrement / 2 + Math.random() * (max - min + roundingIncrement * .99)) / roundingIncrement) * roundingIncrement * returnFunction) / returnFunction;
  });
},
    pipe = function pipe() {
  for (var _len = arguments.length, functions = new Array(_len), _key = 0; _key < _len; _key++) {
    functions[_key] = arguments[_key];
  }

  return function (value) {
    return functions.reduce(function (v, f) {
      return f(v);
    }, value);
  };
},
    unitize = function unitize(func, unit) {
  return function (value) {
    return func(parseFloat(value)) + (unit || getUnit(value));
  };
},
    normalize = function normalize(min, max, value) {
  return mapRange(min, max, 0, 1, value);
},
    _wrapArray = function _wrapArray(a, wrapper, value) {
  return _conditionalReturn(value, function (index) {
    return a[~~wrapper(index)];
  });
},
    wrap = function wrap(min, max, value) {
  // NOTE: wrap() CANNOT be an arrow function! A very odd compiling bug causes problems (unrelated to GSAP).
  var range = max - min;
  return _isArray$1(min) ? _wrapArray(min, wrap(0, min.length), max) : _conditionalReturn(value, function (value) {
    return (range + (value - min) % range) % range + min;
  });
},
    wrapYoyo = function wrapYoyo(min, max, value) {
  var range = max - min,
      total = range * 2;
  return _isArray$1(min) ? _wrapArray(min, wrapYoyo(0, min.length - 1), max) : _conditionalReturn(value, function (value) {
    value = (total + (value - min) % total) % total || 0;
    return min + (value > range ? total - value : value);
  });
},
    _replaceRandom = function _replaceRandom(value) {
  //replaces all occurrences of random(...) in a string with the calculated random value. can be a range like random(-100, 100, 5) or an array like random([0, 100, 500])
  var prev = 0,
      s = "",
      i,
      nums,
      end,
      isArray;

  while (~(i = value.indexOf("random(", prev))) {
    end = value.indexOf(")", i);
    isArray = value.charAt(i + 7) === "[";
    nums = value.substr(i + 7, end - i - 7).match(isArray ? _delimitedValueExp : _strictNumExp);
    s += value.substr(prev, i - prev) + random(isArray ? nums : +nums[0], isArray ? 0 : +nums[1], +nums[2] || 1e-5);
    prev = end + 1;
  }

  return s + value.substr(prev, value.length - prev);
},
    mapRange = function mapRange(inMin, inMax, outMin, outMax, value) {
  var inRange = inMax - inMin,
      outRange = outMax - outMin;
  return _conditionalReturn(value, function (value) {
    return outMin + ((value - inMin) / inRange * outRange || 0);
  });
},
    interpolate = function interpolate(start, end, progress, mutate) {
  var func = isNaN(start + end) ? 0 : function (p) {
    return (1 - p) * start + p * end;
  };

  if (!func) {
    var isString = _isString$4(start),
        master = {},
        p,
        i,
        interpolators,
        l,
        il;

    progress === true && (mutate = 1) && (progress = null);

    if (isString) {
      start = {
        p: start
      };
      end = {
        p: end
      };
    } else if (_isArray$1(start) && !_isArray$1(end)) {
      interpolators = [];
      l = start.length;
      il = l - 2;

      for (i = 1; i < l; i++) {
        interpolators.push(interpolate(start[i - 1], start[i])); //build the interpolators up front as a performance optimization so that when the function is called many times, it can just reuse them.
      }

      l--;

      func = function func(p) {
        p *= l;
        var i = Math.min(il, ~~p);
        return interpolators[i](p - i);
      };

      progress = end;
    } else if (!mutate) {
      start = _merge(_isArray$1(start) ? [] : {}, start);
    }

    if (!interpolators) {
      for (p in end) {
        _addPropTween.call(master, start, p, "get", end[p]);
      }

      func = function func(p) {
        return _renderPropTweens(p, master) || (isString ? start.p : start);
      };
    }
  }

  return _conditionalReturn(progress, func);
},
    _getLabelInDirection = function _getLabelInDirection(timeline, fromTime, backward) {
  //used for nextLabel() and previousLabel()
  var labels = timeline.labels,
      min = _bigNum$2,
      p,
      distance,
      label;

  for (p in labels) {
    distance = labels[p] - fromTime;

    if (distance < 0 === !!backward && distance && min > (distance = Math.abs(distance))) {
      label = p;
      min = distance;
    }
  }

  return label;
},
    _callback = function _callback(animation, type, executeLazyFirst) {
  var v = animation.vars,
      callback = v[type],
      params,
      scope;

  if (!callback) {
    return;
  }

  params = v[type + "Params"];
  scope = v.callbackScope || animation;
  executeLazyFirst && _lazyTweens.length && _lazyRender(); //in case rendering caused any tweens to lazy-init, we should render them because typically when a timeline finishes, users expect things to have rendered fully. Imagine an onUpdate on a timeline that reports/checks tweened values.

  return params ? callback.apply(scope, params) : callback.call(scope);
},
    _interrupt = function _interrupt(animation) {
  _removeFromParent(animation);

  animation.scrollTrigger && animation.scrollTrigger.kill(false);
  animation.progress() < 1 && _callback(animation, "onInterrupt");
  return animation;
},
    _quickTween,
    _createPlugin = function _createPlugin(config) {
  config = !config.name && config["default"] || config; //UMD packaging wraps things oddly, so for example MotionPathHelper becomes {MotionPathHelper:MotionPathHelper, default:MotionPathHelper}.

  var name = config.name,
      isFunc = _isFunction$4(config),
      Plugin = name && !isFunc && config.init ? function () {
    this._props = [];
  } : config,
      //in case someone passes in an object that's not a plugin, like CustomEase
  instanceDefaults = {
    init: _emptyFunc$2,
    render: _renderPropTweens,
    add: _addPropTween,
    kill: _killPropTweensOf,
    modifier: _addPluginModifier,
    rawVars: 0
  },
      statics = {
    targetTest: 0,
    get: 0,
    getSetter: _getSetter$1,
    aliases: {},
    register: 0
  };

  _wake();

  if (config !== Plugin) {
    if (_plugins[name]) {
      return;
    }

    _setDefaults$2(Plugin, _setDefaults$2(_copyExcluding(config, instanceDefaults), statics)); //static methods


    _merge(Plugin.prototype, _merge(instanceDefaults, _copyExcluding(config, statics))); //instance methods


    _plugins[Plugin.prop = name] = Plugin;

    if (config.targetTest) {
      _harnessPlugins.push(Plugin);

      _reservedProps[name] = 1;
    }

    name = (name === "css" ? "CSS" : name.charAt(0).toUpperCase() + name.substr(1)) + "Plugin"; //for the global name. "motionPath" should become MotionPathPlugin
  }

  _addGlobal(name, Plugin);

  config.register && config.register(gsap$9, Plugin, PropTween$2);
},

/*
 * --------------------------------------------------------------------------------------
 * COLORS
 * --------------------------------------------------------------------------------------
 */
_255 = 255,
    _colorLookup = {
  aqua: [0, _255, _255],
  lime: [0, _255, 0],
  silver: [192, 192, 192],
  black: [0, 0, 0],
  maroon: [128, 0, 0],
  teal: [0, 128, 128],
  blue: [0, 0, _255],
  navy: [0, 0, 128],
  white: [_255, _255, _255],
  olive: [128, 128, 0],
  yellow: [_255, _255, 0],
  orange: [_255, 165, 0],
  gray: [128, 128, 128],
  purple: [128, 0, 128],
  green: [0, 128, 0],
  red: [_255, 0, 0],
  pink: [_255, 192, 203],
  cyan: [0, _255, _255],
  transparent: [_255, _255, _255, 0]
},
    _hue = function _hue(h, m1, m2) {
  h = h < 0 ? h + 1 : h > 1 ? h - 1 : h;
  return (h * 6 < 1 ? m1 + (m2 - m1) * h * 6 : h < .5 ? m2 : h * 3 < 2 ? m1 + (m2 - m1) * (2 / 3 - h) * 6 : m1) * _255 + .5 | 0;
},
    splitColor = function splitColor(v, toHSL, forceAlpha) {
  var a = !v ? _colorLookup.black : _isNumber$2(v) ? [v >> 16, v >> 8 & _255, v & _255] : 0,
      r,
      g,
      b,
      h,
      s,
      l,
      max,
      min,
      d,
      wasHSL;

  if (!a) {
    if (v.substr(-1) === ",") {
      //sometimes a trailing comma is included and we should chop it off (typically from a comma-delimited list of values like a textShadow:"2px 2px 2px blue, 5px 5px 5px rgb(255,0,0)" - in this example "blue," has a trailing comma. We could strip it out inside parseComplex() but we'd need to do it to the beginning and ending values plus it wouldn't provide protection from other potential scenarios like if the user passes in a similar value.
      v = v.substr(0, v.length - 1);
    }

    if (_colorLookup[v]) {
      a = _colorLookup[v];
    } else if (v.charAt(0) === "#") {
      if (v.length < 6) {
        //for shorthand like #9F0 or #9F0F (could have alpha)
        r = v.charAt(1);
        g = v.charAt(2);
        b = v.charAt(3);
        v = "#" + r + r + g + g + b + b + (v.length === 5 ? v.charAt(4) + v.charAt(4) : "");
      }

      if (v.length === 9) {
        // hex with alpha, like #fd5e53ff
        a = parseInt(v.substr(1, 6), 16);
        return [a >> 16, a >> 8 & _255, a & _255, parseInt(v.substr(7), 16) / 255];
      }

      v = parseInt(v.substr(1), 16);
      a = [v >> 16, v >> 8 & _255, v & _255];
    } else if (v.substr(0, 3) === "hsl") {
      a = wasHSL = v.match(_strictNumExp);

      if (!toHSL) {
        h = +a[0] % 360 / 360;
        s = +a[1] / 100;
        l = +a[2] / 100;
        g = l <= .5 ? l * (s + 1) : l + s - l * s;
        r = l * 2 - g;
        a.length > 3 && (a[3] *= 1); //cast as number

        a[0] = _hue(h + 1 / 3, r, g);
        a[1] = _hue(h, r, g);
        a[2] = _hue(h - 1 / 3, r, g);
      } else if (~v.indexOf("=")) {
        //if relative values are found, just return the raw strings with the relative prefixes in place.
        a = v.match(_numExp$1);
        forceAlpha && a.length < 4 && (a[3] = 1);
        return a;
      }
    } else {
      a = v.match(_strictNumExp) || _colorLookup.transparent;
    }

    a = a.map(Number);
  }

  if (toHSL && !wasHSL) {
    r = a[0] / _255;
    g = a[1] / _255;
    b = a[2] / _255;
    max = Math.max(r, g, b);
    min = Math.min(r, g, b);
    l = (max + min) / 2;

    if (max === min) {
      h = s = 0;
    } else {
      d = max - min;
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
      h = max === r ? (g - b) / d + (g < b ? 6 : 0) : max === g ? (b - r) / d + 2 : (r - g) / d + 4;
      h *= 60;
    }

    a[0] = ~~(h + .5);
    a[1] = ~~(s * 100 + .5);
    a[2] = ~~(l * 100 + .5);
  }

  forceAlpha && a.length < 4 && (a[3] = 1);
  return a;
},
    _colorOrderData = function _colorOrderData(v) {
  // strips out the colors from the string, finds all the numeric slots (with units) and returns an array of those. The Array also has a "c" property which is an Array of the index values where the colors belong. This is to help work around issues where there's a mis-matched order of color/numeric data like drop-shadow(#f00 0px 1px 2px) and drop-shadow(0x 1px 2px #f00). This is basically a helper function used in _formatColors()
  var values = [],
      c = [],
      i = -1;
  v.split(_colorExp).forEach(function (v) {
    var a = v.match(_numWithUnitExp) || [];
    values.push.apply(values, a);
    c.push(i += a.length + 1);
  });
  values.c = c;
  return values;
},
    _formatColors = function _formatColors(s, toHSL, orderMatchData) {
  var result = "",
      colors = (s + result).match(_colorExp),
      type = toHSL ? "hsla(" : "rgba(",
      i = 0,
      c,
      shell,
      d,
      l;

  if (!colors) {
    return s;
  }

  colors = colors.map(function (color) {
    return (color = splitColor(color, toHSL, 1)) && type + (toHSL ? color[0] + "," + color[1] + "%," + color[2] + "%," + color[3] : color.join(",")) + ")";
  });

  if (orderMatchData) {
    d = _colorOrderData(s);
    c = orderMatchData.c;

    if (c.join(result) !== d.c.join(result)) {
      shell = s.replace(_colorExp, "1").split(_numWithUnitExp);
      l = shell.length - 1;

      for (; i < l; i++) {
        result += shell[i] + (~c.indexOf(i) ? colors.shift() || type + "0,0,0,0)" : (d.length ? d : colors.length ? colors : orderMatchData).shift());
      }
    }
  }

  if (!shell) {
    shell = s.split(_colorExp);
    l = shell.length - 1;

    for (; i < l; i++) {
      result += shell[i] + colors[i];
    }
  }

  return result + shell[l];
},
    _colorExp = function () {
  var s = "(?:\\b(?:(?:rgb|rgba|hsl|hsla)\\(.+?\\))|\\B#(?:[0-9a-f]{3,4}){1,2}\\b",
      //we'll dynamically build this Regular Expression to conserve file size. After building it, it will be able to find rgb(), rgba(), # (hexadecimal), and named color values like red, blue, purple, etc.,
  p;

  for (p in _colorLookup) {
    s += "|" + p + "\\b";
  }

  return new RegExp(s + ")", "gi");
}(),
    _hslExp = /hsl[a]?\(/,
    _colorStringFilter = function _colorStringFilter(a) {
  var combined = a.join(" "),
      toHSL;
  _colorExp.lastIndex = 0;

  if (_colorExp.test(combined)) {
    toHSL = _hslExp.test(combined);
    a[1] = _formatColors(a[1], toHSL);
    a[0] = _formatColors(a[0], toHSL, _colorOrderData(a[1])); // make sure the order of numbers/colors match with the END value.

    return true;
  }
},

/*
 * --------------------------------------------------------------------------------------
 * TICKER
 * --------------------------------------------------------------------------------------
 */
_tickerActive,
    _ticker = function () {
  var _getTime = Date.now,
      _lagThreshold = 500,
      _adjustedLag = 33,
      _startTime = _getTime(),
      _lastUpdate = _startTime,
      _gap = 1000 / 240,
      _nextTime = _gap,
      _listeners = [],
      _id,
      _req,
      _raf,
      _self,
      _delta,
      _i,
      _tick = function _tick(v) {
    var elapsed = _getTime() - _lastUpdate,
        manual = v === true,
        overlap,
        dispatch,
        time,
        frame;

    elapsed > _lagThreshold && (_startTime += elapsed - _adjustedLag);
    _lastUpdate += elapsed;
    time = _lastUpdate - _startTime;
    overlap = time - _nextTime;

    if (overlap > 0 || manual) {
      frame = ++_self.frame;
      _delta = time - _self.time * 1000;
      _self.time = time = time / 1000;
      _nextTime += overlap + (overlap >= _gap ? 4 : _gap - overlap);
      dispatch = 1;
    }

    manual || (_id = _req(_tick)); //make sure the request is made before we dispatch the "tick" event so that timing is maintained. Otherwise, if processing the "tick" requires a bunch of time (like 15ms) and we're using a setTimeout() that's based on 16.7ms, it'd technically take 31.7ms between frames otherwise.

    if (dispatch) {
      for (_i = 0; _i < _listeners.length; _i++) {
        // use _i and check _listeners.length instead of a variable because a listener could get removed during the loop, and if that happens to an element less than the current index, it'd throw things off in the loop.
        _listeners[_i](time, _delta, frame, v);
      }
    }
  };

  _self = {
    time: 0,
    frame: 0,
    tick: function tick() {
      _tick(true);
    },
    deltaRatio: function deltaRatio(fps) {
      return _delta / (1000 / (fps || 60));
    },
    wake: function wake() {
      if (_coreReady) {
        if (!_coreInitted$5 && _windowExists$7()) {
          _win$6 = _coreInitted$5 = window;
          _doc$5 = _win$6.document || {};
          _globals.gsap = gsap$9;
          (_win$6.gsapVersions || (_win$6.gsapVersions = [])).push(gsap$9.version);

          _install(_installScope || _win$6.GreenSockGlobals || !_win$6.gsap && _win$6 || {});

          _raf = _win$6.requestAnimationFrame;
        }

        _id && _self.sleep();

        _req = _raf || function (f) {
          return setTimeout(f, _nextTime - _self.time * 1000 + 1 | 0);
        };

        _tickerActive = 1;

        _tick(2);
      }
    },
    sleep: function sleep() {
      (_raf ? _win$6.cancelAnimationFrame : clearTimeout)(_id);
      _tickerActive = 0;
      _req = _emptyFunc$2;
    },
    lagSmoothing: function lagSmoothing(threshold, adjustedLag) {
      _lagThreshold = threshold || 1 / _tinyNum; //zero should be interpreted as basically unlimited

      _adjustedLag = Math.min(adjustedLag, _lagThreshold, 0);
    },
    fps: function fps(_fps) {
      _gap = 1000 / (_fps || 240);
      _nextTime = _self.time * 1000 + _gap;
    },
    add: function add(callback) {
      _listeners.indexOf(callback) < 0 && _listeners.push(callback);

      _wake();
    },
    remove: function remove(callback) {
      var i;
      ~(i = _listeners.indexOf(callback)) && _listeners.splice(i, 1) && _i >= i && _i--;
    },
    _listeners: _listeners
  };
  return _self;
}(),
    _wake = function _wake() {
  return !_tickerActive && _ticker.wake();
},
    //also ensures the core classes are initialized.

/*
* -------------------------------------------------
* EASING
* -------------------------------------------------
*/
_easeMap = {},
    _customEaseExp = /^[\d.\-M][\d.\-,\s]/,
    _quotesExp = /["']/g,
    _parseObjectInString = function _parseObjectInString(value) {
  //takes a string like "{wiggles:10, type:anticipate})" and turns it into a real object. Notice it ends in ")" and includes the {} wrappers. This is because we only use this function for parsing ease configs and prioritized optimization rather than reusability.
  var obj = {},
      split = value.substr(1, value.length - 3).split(":"),
      key = split[0],
      i = 1,
      l = split.length,
      index,
      val,
      parsedVal;

  for (; i < l; i++) {
    val = split[i];
    index = i !== l - 1 ? val.lastIndexOf(",") : val.length;
    parsedVal = val.substr(0, index);
    obj[key] = isNaN(parsedVal) ? parsedVal.replace(_quotesExp, "").trim() : +parsedVal;
    key = val.substr(index + 1).trim();
  }

  return obj;
},
    _valueInParentheses = function _valueInParentheses(value) {
  var open = value.indexOf("(") + 1,
      close = value.indexOf(")"),
      nested = value.indexOf("(", open);
  return value.substring(open, ~nested && nested < close ? value.indexOf(")", close + 1) : close);
},
    _configEaseFromString = function _configEaseFromString(name) {
  //name can be a string like "elastic.out(1,0.5)", and pass in _easeMap as obj and it'll parse it out and call the actual function like _easeMap.Elastic.easeOut.config(1,0.5). It will also parse custom ease strings as long as CustomEase is loaded and registered (internally as _easeMap._CE).
  var split = (name + "").split("("),
      ease = _easeMap[split[0]];
  return ease && split.length > 1 && ease.config ? ease.config.apply(null, ~name.indexOf("{") ? [_parseObjectInString(split[1])] : _valueInParentheses(name).split(",").map(_numericIfPossible)) : _easeMap._CE && _customEaseExp.test(name) ? _easeMap._CE("", name) : ease;
},
    _invertEase = function _invertEase(ease) {
  return function (p) {
    return 1 - ease(1 - p);
  };
},
    // allow yoyoEase to be set in children and have those affected when the parent/ancestor timeline yoyos.
_propagateYoyoEase = function _propagateYoyoEase(timeline, isYoyo) {
  var child = timeline._first,
      ease;

  while (child) {
    if (child instanceof Timeline) {
      _propagateYoyoEase(child, isYoyo);
    } else if (child.vars.yoyoEase && (!child._yoyo || !child._repeat) && child._yoyo !== isYoyo) {
      if (child.timeline) {
        _propagateYoyoEase(child.timeline, isYoyo);
      } else {
        ease = child._ease;
        child._ease = child._yEase;
        child._yEase = ease;
        child._yoyo = isYoyo;
      }
    }

    child = child._next;
  }
},
    _parseEase = function _parseEase(ease, defaultEase) {
  return !ease ? defaultEase : (_isFunction$4(ease) ? ease : _easeMap[ease] || _configEaseFromString(ease)) || defaultEase;
},
    _insertEase = function _insertEase(names, easeIn, easeOut, easeInOut) {
  if (easeOut === void 0) {
    easeOut = function easeOut(p) {
      return 1 - easeIn(1 - p);
    };
  }

  if (easeInOut === void 0) {
    easeInOut = function easeInOut(p) {
      return p < .5 ? easeIn(p * 2) / 2 : 1 - easeIn((1 - p) * 2) / 2;
    };
  }

  var ease = {
    easeIn: easeIn,
    easeOut: easeOut,
    easeInOut: easeInOut
  },
      lowercaseName;

  _forEachName(names, function (name) {
    _easeMap[name] = _globals[name] = ease;
    _easeMap[lowercaseName = name.toLowerCase()] = easeOut;

    for (var p in ease) {
      _easeMap[lowercaseName + (p === "easeIn" ? ".in" : p === "easeOut" ? ".out" : ".inOut")] = _easeMap[name + "." + p] = ease[p];
    }
  });

  return ease;
},
    _easeInOutFromOut = function _easeInOutFromOut(easeOut) {
  return function (p) {
    return p < .5 ? (1 - easeOut(1 - p * 2)) / 2 : .5 + easeOut((p - .5) * 2) / 2;
  };
},
    _configElastic = function _configElastic(type, amplitude, period) {
  var p1 = amplitude >= 1 ? amplitude : 1,
      //note: if amplitude is < 1, we simply adjust the period for a more natural feel. Otherwise the math doesn't work right and the curve starts at 1.
  p2 = (period || (type ? .3 : .45)) / (amplitude < 1 ? amplitude : 1),
      p3 = p2 / _2PI * (Math.asin(1 / p1) || 0),
      easeOut = function easeOut(p) {
    return p === 1 ? 1 : p1 * Math.pow(2, -10 * p) * _sin$1((p - p3) * p2) + 1;
  },
      ease = type === "out" ? easeOut : type === "in" ? function (p) {
    return 1 - easeOut(1 - p);
  } : _easeInOutFromOut(easeOut);

  p2 = _2PI / p2; //precalculate to optimize

  ease.config = function (amplitude, period) {
    return _configElastic(type, amplitude, period);
  };

  return ease;
},
    _configBack = function _configBack(type, overshoot) {
  if (overshoot === void 0) {
    overshoot = 1.70158;
  }

  var easeOut = function easeOut(p) {
    return p ? --p * p * ((overshoot + 1) * p + overshoot) + 1 : 0;
  },
      ease = type === "out" ? easeOut : type === "in" ? function (p) {
    return 1 - easeOut(1 - p);
  } : _easeInOutFromOut(easeOut);

  ease.config = function (overshoot) {
    return _configBack(type, overshoot);
  };

  return ease;
}; // a cheaper (kb and cpu) but more mild way to get a parameterized weighted ease by feeding in a value between -1 (easeIn) and 1 (easeOut) where 0 is linear.
// _weightedEase = ratio => {
// 	let y = 0.5 + ratio / 2;
// 	return p => (2 * (1 - p) * p * y + p * p);
// },
// a stronger (but more expensive kb/cpu) parameterized weighted ease that lets you feed in a value between -1 (easeIn) and 1 (easeOut) where 0 is linear.
// _weightedEaseStrong = ratio => {
// 	ratio = .5 + ratio / 2;
// 	let o = 1 / 3 * (ratio < .5 ? ratio : 1 - ratio),
// 		b = ratio - o,
// 		c = ratio + o;
// 	return p => p === 1 ? p : 3 * b * (1 - p) * (1 - p) * p + 3 * c * (1 - p) * p * p + p * p * p;
// };


_forEachName("Linear,Quad,Cubic,Quart,Quint,Strong", function (name, i) {
  var power = i < 5 ? i + 1 : i;

  _insertEase(name + ",Power" + (power - 1), i ? function (p) {
    return Math.pow(p, power);
  } : function (p) {
    return p;
  }, function (p) {
    return 1 - Math.pow(1 - p, power);
  }, function (p) {
    return p < .5 ? Math.pow(p * 2, power) / 2 : 1 - Math.pow((1 - p) * 2, power) / 2;
  });
});

_easeMap.Linear.easeNone = _easeMap.none = _easeMap.Linear.easeIn;

_insertEase("Elastic", _configElastic("in"), _configElastic("out"), _configElastic());

(function (n, c) {
  var n1 = 1 / c,
      n2 = 2 * n1,
      n3 = 2.5 * n1,
      easeOut = function easeOut(p) {
    return p < n1 ? n * p * p : p < n2 ? n * Math.pow(p - 1.5 / c, 2) + .75 : p < n3 ? n * (p -= 2.25 / c) * p + .9375 : n * Math.pow(p - 2.625 / c, 2) + .984375;
  };

  _insertEase("Bounce", function (p) {
    return 1 - easeOut(1 - p);
  }, easeOut);
})(7.5625, 2.75);

_insertEase("Expo", function (p) {
  return p ? Math.pow(2, 10 * (p - 1)) : 0;
});

_insertEase("Circ", function (p) {
  return -(_sqrt$1(1 - p * p) - 1);
});

_insertEase("Sine", function (p) {
  return p === 1 ? 1 : -_cos$1(p * _HALF_PI) + 1;
});

_insertEase("Back", _configBack("in"), _configBack("out"), _configBack());

_easeMap.SteppedEase = _easeMap.steps = _globals.SteppedEase = {
  config: function config(steps, immediateStart) {
    if (steps === void 0) {
      steps = 1;
    }

    var p1 = 1 / steps,
        p2 = steps + (immediateStart ? 0 : 1),
        p3 = immediateStart ? 1 : 0,
        max = 1 - _tinyNum;
    return function (p) {
      return ((p2 * _clamp$1(0, max, p) | 0) + p3) * p1;
    };
  }
};
_defaults$1.ease = _easeMap["quad.out"];

_forEachName("onComplete,onUpdate,onStart,onRepeat,onReverseComplete,onInterrupt", function (name) {
  return _callbackNames += name + "," + name + "Params,";
});
/*
 * --------------------------------------------------------------------------------------
 * CACHE
 * --------------------------------------------------------------------------------------
 */


var GSCache = function GSCache(target, harness) {
  this.id = _gsID++;
  target._gsap = this;
  this.target = target;
  this.harness = harness;
  this.get = harness ? harness.get : _getProperty;
  this.set = harness ? harness.getSetter : _getSetter$1;
};
/*
 * --------------------------------------------------------------------------------------
 * ANIMATION
 * --------------------------------------------------------------------------------------
 */

var Animation = /*#__PURE__*/function () {
  function Animation(vars, time) {
    var parent = vars.parent || _globalTimeline;
    this.vars = vars;
    this._delay = +vars.delay || 0;

    if (this._repeat = vars.repeat === Infinity ? -2 : vars.repeat || 0) {
      // TODO: repeat: Infinity on a timeline's children must flag that timeline internally and affect its totalDuration, otherwise it'll stop in the negative direction when reaching the start.
      this._rDelay = vars.repeatDelay || 0;
      this._yoyo = !!vars.yoyo || !!vars.yoyoEase;
    }

    this._ts = 1;

    _setDuration(this, +vars.duration, 1, 1);

    this.data = vars.data;
    _tickerActive || _ticker.wake();
    parent && _addToTimeline(parent, this, time || time === 0 ? time : parent._time, 1);
    vars.reversed && this.reverse();
    vars.paused && this.paused(true);
  }

  var _proto = Animation.prototype;

  _proto.delay = function delay(value) {
    if (value || value === 0) {
      this.parent && this.parent.smoothChildTiming && this.startTime(this._start + value - this._delay);
      this._delay = value;
      return this;
    }

    return this._delay;
  };

  _proto.duration = function duration(value) {
    return arguments.length ? this.totalDuration(this._repeat > 0 ? value + (value + this._rDelay) * this._repeat : value) : this.totalDuration() && this._dur;
  };

  _proto.totalDuration = function totalDuration(value) {
    if (!arguments.length) {
      return this._tDur;
    }

    this._dirty = 0;
    return _setDuration(this, this._repeat < 0 ? value : (value - this._repeat * this._rDelay) / (this._repeat + 1));
  };

  _proto.totalTime = function totalTime(_totalTime, suppressEvents) {
    _wake();

    if (!arguments.length) {
      return this._tTime;
    }

    var parent = this._dp;

    if (parent && parent.smoothChildTiming && this._ts) {
      _alignPlayhead(this, _totalTime);

      !parent._dp || parent.parent || _postAddChecks(parent, this); // edge case: if this is a child of a timeline that already completed, for example, we must re-activate the parent.
      //in case any of the ancestor timelines had completed but should now be enabled, we should reset their totalTime() which will also ensure that they're lined up properly and enabled. Skip for animations that are on the root (wasteful). Example: a TimelineLite.exportRoot() is performed when there's a paused tween on the root, the export will not complete until that tween is unpaused, but imagine a child gets restarted later, after all [unpaused] tweens have completed. The start of that child would get pushed out, but one of the ancestors may have completed.

      while (parent.parent) {
        if (parent.parent._time !== parent._start + (parent._ts >= 0 ? parent._tTime / parent._ts : (parent.totalDuration() - parent._tTime) / -parent._ts)) {
          parent.totalTime(parent._tTime, true);
        }

        parent = parent.parent;
      }

      if (!this.parent && this._dp.autoRemoveChildren && (this._ts > 0 && _totalTime < this._tDur || this._ts < 0 && _totalTime > 0 || !this._tDur && !_totalTime)) {
        //if the animation doesn't have a parent, put it back into its last parent (recorded as _dp for exactly cases like this). Limit to parents with autoRemoveChildren (like globalTimeline) so that if the user manually removes an animation from a timeline and then alters its playhead, it doesn't get added back in.
        _addToTimeline(this._dp, this, this._start - this._delay);
      }
    }

    if (this._tTime !== _totalTime || !this._dur && !suppressEvents || this._initted && Math.abs(this._zTime) === _tinyNum || !_totalTime && !this._initted && (this.add || this._ptLookup)) {
      // check for _ptLookup on a Tween instance to ensure it has actually finished being instantiated, otherwise if this.reverse() gets called in the Animation constructor, it could trigger a render() here even though the _targets weren't populated, thus when _init() is called there won't be any PropTweens (it'll act like the tween is non-functional)
      this._ts || (this._pTime = _totalTime); // otherwise, if an animation is paused, then the playhead is moved back to zero, then resumed, it'd revert back to the original time at the pause
      //if (!this._lock) { // avoid endless recursion (not sure we need this yet or if it's worth the performance hit)
      //   this._lock = 1;

      _lazySafeRender(this, _totalTime, suppressEvents); //   this._lock = 0;
      //}

    }

    return this;
  };

  _proto.time = function time(value, suppressEvents) {
    return arguments.length ? this.totalTime(Math.min(this.totalDuration(), value + _elapsedCycleDuration(this)) % this._dur || (value ? this._dur : 0), suppressEvents) : this._time; // note: if the modulus results in 0, the playhead could be exactly at the end or the beginning, and we always defer to the END with a non-zero value, otherwise if you set the time() to the very end (duration()), it would render at the START!
  };

  _proto.totalProgress = function totalProgress(value, suppressEvents) {
    return arguments.length ? this.totalTime(this.totalDuration() * value, suppressEvents) : this.totalDuration() ? Math.min(1, this._tTime / this._tDur) : this.ratio;
  };

  _proto.progress = function progress(value, suppressEvents) {
    return arguments.length ? this.totalTime(this.duration() * (this._yoyo && !(this.iteration() & 1) ? 1 - value : value) + _elapsedCycleDuration(this), suppressEvents) : this.duration() ? Math.min(1, this._time / this._dur) : this.ratio;
  };

  _proto.iteration = function iteration(value, suppressEvents) {
    var cycleDuration = this.duration() + this._rDelay;

    return arguments.length ? this.totalTime(this._time + (value - 1) * cycleDuration, suppressEvents) : this._repeat ? _animationCycle(this._tTime, cycleDuration) + 1 : 1;
  } // potential future addition:
  // isPlayingBackwards() {
  // 	let animation = this,
  // 		orientation = 1; // 1 = forward, -1 = backward
  // 	while (animation) {
  // 		orientation *= animation.reversed() || (animation.repeat() && !(animation.iteration() & 1)) ? -1 : 1;
  // 		animation = animation.parent;
  // 	}
  // 	return orientation < 0;
  // }
  ;

  _proto.timeScale = function timeScale(value) {
    if (!arguments.length) {
      return this._rts === -_tinyNum ? 0 : this._rts; // recorded timeScale. Special case: if someone calls reverse() on an animation with timeScale of 0, we assign it -_tinyNum to remember it's reversed.
    }

    if (this._rts === value) {
      return this;
    }

    var tTime = this.parent && this._ts ? _parentToChildTotalTime(this.parent._time, this) : this._tTime; // make sure to do the parentToChildTotalTime() BEFORE setting the new _ts because the old one must be used in that calculation.
    // prioritize rendering where the parent's playhead lines up instead of this._tTime because there could be a tween that's animating another tween's timeScale in the same rendering loop (same parent), thus if the timeScale tween renders first, it would alter _start BEFORE _tTime was set on that tick (in the rendering loop), effectively freezing it until the timeScale tween finishes.

    this._rts = +value || 0;
    this._ts = this._ps || value === -_tinyNum ? 0 : this._rts; // _ts is the functional timeScale which would be 0 if the animation is paused.

    return _recacheAncestors(this.totalTime(_clamp$1(-this._delay, this._tDur, tTime), true));
  };

  _proto.paused = function paused(value) {
    if (!arguments.length) {
      return this._ps;
    }

    if (this._ps !== value) {
      this._ps = value;

      if (value) {
        this._pTime = this._tTime || Math.max(-this._delay, this.rawTime()); // if the pause occurs during the delay phase, make sure that's factored in when resuming.

        this._ts = this._act = 0; // _ts is the functional timeScale, so a paused tween would effectively have a timeScale of 0. We record the "real" timeScale as _rts (recorded time scale)
      } else {
        _wake();

        this._ts = this._rts; //only defer to _pTime (pauseTime) if tTime is zero. Remember, someone could pause() an animation, then scrub the playhead and resume(). If the parent doesn't have smoothChildTiming, we render at the rawTime() because the startTime won't get updated.

        this.totalTime(this.parent && !this.parent.smoothChildTiming ? this.rawTime() : this._tTime || this._pTime, this.progress() === 1 && (this._tTime -= _tinyNum) && Math.abs(this._zTime) !== _tinyNum); // edge case: animation.progress(1).pause().play() wouldn't render again because the playhead is already at the end, but the call to totalTime() below will add it back to its parent...and not remove it again (since removing only happens upon rendering at a new time). Offsetting the _tTime slightly is done simply to cause the final render in totalTime() that'll pop it off its timeline (if autoRemoveChildren is true, of course). Check to make sure _zTime isn't -_tinyNum to avoid an edge case where the playhead is pushed to the end but INSIDE a tween/callback, the timeline itself is paused thus halting rendering and leaving a few unrendered. When resuming, it wouldn't render those otherwise.
      }
    }

    return this;
  };

  _proto.startTime = function startTime(value) {
    if (arguments.length) {
      this._start = value;
      var parent = this.parent || this._dp;
      parent && (parent._sort || !this.parent) && _addToTimeline(parent, this, value - this._delay);
      return this;
    }

    return this._start;
  };

  _proto.endTime = function endTime(includeRepeats) {
    return this._start + (_isNotFalse(includeRepeats) ? this.totalDuration() : this.duration()) / Math.abs(this._ts);
  };

  _proto.rawTime = function rawTime(wrapRepeats) {
    var parent = this.parent || this._dp; // _dp = detatched parent

    return !parent ? this._tTime : wrapRepeats && (!this._ts || this._repeat && this._time && this.totalProgress() < 1) ? this._tTime % (this._dur + this._rDelay) : !this._ts ? this._tTime : _parentToChildTotalTime(parent.rawTime(wrapRepeats), this);
  };

  _proto.globalTime = function globalTime(rawTime) {
    var animation = this,
        time = arguments.length ? rawTime : animation.rawTime();

    while (animation) {
      time = animation._start + time / (animation._ts || 1);
      animation = animation._dp;
    }

    return time;
  };

  _proto.repeat = function repeat(value) {
    if (arguments.length) {
      this._repeat = value === Infinity ? -2 : value;
      return _onUpdateTotalDuration(this);
    }

    return this._repeat === -2 ? Infinity : this._repeat;
  };

  _proto.repeatDelay = function repeatDelay(value) {
    if (arguments.length) {
      this._rDelay = value;
      return _onUpdateTotalDuration(this);
    }

    return this._rDelay;
  };

  _proto.yoyo = function yoyo(value) {
    if (arguments.length) {
      this._yoyo = value;
      return this;
    }

    return this._yoyo;
  };

  _proto.seek = function seek(position, suppressEvents) {
    return this.totalTime(_parsePosition$1(this, position), _isNotFalse(suppressEvents));
  };

  _proto.restart = function restart(includeDelay, suppressEvents) {
    return this.play().totalTime(includeDelay ? -this._delay : 0, _isNotFalse(suppressEvents));
  };

  _proto.play = function play(from, suppressEvents) {
    from != null && this.seek(from, suppressEvents);
    return this.reversed(false).paused(false);
  };

  _proto.reverse = function reverse(from, suppressEvents) {
    from != null && this.seek(from || this.totalDuration(), suppressEvents);
    return this.reversed(true).paused(false);
  };

  _proto.pause = function pause(atTime, suppressEvents) {
    atTime != null && this.seek(atTime, suppressEvents);
    return this.paused(true);
  };

  _proto.resume = function resume() {
    return this.paused(false);
  };

  _proto.reversed = function reversed(value) {
    if (arguments.length) {
      !!value !== this.reversed() && this.timeScale(-this._rts || (value ? -_tinyNum : 0)); // in case timeScale is zero, reversing would have no effect so we use _tinyNum.

      return this;
    }

    return this._rts < 0;
  };

  _proto.invalidate = function invalidate() {
    this._initted = this._act = 0;
    this._zTime = -_tinyNum;
    return this;
  };

  _proto.isActive = function isActive() {
    var parent = this.parent || this._dp,
        start = this._start,
        rawTime;
    return !!(!parent || this._ts && this._initted && parent.isActive() && (rawTime = parent.rawTime(true)) >= start && rawTime < this.endTime(true) - _tinyNum);
  };

  _proto.eventCallback = function eventCallback(type, callback, params) {
    var vars = this.vars;

    if (arguments.length > 1) {
      if (!callback) {
        delete vars[type];
      } else {
        vars[type] = callback;
        params && (vars[type + "Params"] = params);
        type === "onUpdate" && (this._onUpdate = callback);
      }

      return this;
    }

    return vars[type];
  };

  _proto.then = function then(onFulfilled) {
    var self = this;
    return new Promise(function (resolve) {
      var f = _isFunction$4(onFulfilled) ? onFulfilled : _passThrough$1,
          _resolve = function _resolve() {
        var _then = self.then;
        self.then = null; // temporarily null the then() method to avoid an infinite loop (see https://github.com/greensock/GSAP/issues/322)

        _isFunction$4(f) && (f = f(self)) && (f.then || f === self) && (self.then = _then);
        resolve(f);
        self.then = _then;
      };

      if (self._initted && self.totalProgress() === 1 && self._ts >= 0 || !self._tTime && self._ts < 0) {
        _resolve();
      } else {
        self._prom = _resolve;
      }
    });
  };

  _proto.kill = function kill() {
    _interrupt(this);
  };

  return Animation;
}();

_setDefaults$2(Animation.prototype, {
  _time: 0,
  _start: 0,
  _end: 0,
  _tTime: 0,
  _tDur: 0,
  _dirty: 0,
  _repeat: 0,
  _yoyo: false,
  parent: null,
  _initted: false,
  _rDelay: 0,
  _ts: 1,
  _dp: 0,
  ratio: 0,
  _zTime: -_tinyNum,
  _prom: 0,
  _ps: false,
  _rts: 1
});
/*
 * -------------------------------------------------
 * TIMELINE
 * -------------------------------------------------
 */


var Timeline = /*#__PURE__*/function (_Animation) {
  _inheritsLoose$1(Timeline, _Animation);

  function Timeline(vars, time) {
    var _this;

    if (vars === void 0) {
      vars = {};
    }

    _this = _Animation.call(this, vars, time) || this;
    _this.labels = {};
    _this.smoothChildTiming = !!vars.smoothChildTiming;
    _this.autoRemoveChildren = !!vars.autoRemoveChildren;
    _this._sort = _isNotFalse(vars.sortChildren);
    _this.parent && _postAddChecks(_this.parent, _assertThisInitialized$1(_this));
    vars.scrollTrigger && _scrollTrigger(_assertThisInitialized$1(_this), vars.scrollTrigger);
    return _this;
  }

  var _proto2 = Timeline.prototype;

  _proto2.to = function to(targets, vars, position) {
    new Tween(targets, _parseVars(arguments, 0, this), _parsePosition$1(this, _isNumber$2(vars) ? arguments[3] : position));
    return this;
  };

  _proto2.from = function from(targets, vars, position) {
    new Tween(targets, _parseVars(arguments, 1, this), _parsePosition$1(this, _isNumber$2(vars) ? arguments[3] : position));
    return this;
  };

  _proto2.fromTo = function fromTo(targets, fromVars, toVars, position) {
    new Tween(targets, _parseVars(arguments, 2, this), _parsePosition$1(this, _isNumber$2(fromVars) ? arguments[4] : position));
    return this;
  };

  _proto2.set = function set(targets, vars, position) {
    vars.duration = 0;
    vars.parent = this;
    _inheritDefaults(vars).repeatDelay || (vars.repeat = 0);
    vars.immediateRender = !!vars.immediateRender;
    new Tween(targets, vars, _parsePosition$1(this, position), 1);
    return this;
  };

  _proto2.call = function call(callback, params, position) {
    return _addToTimeline(this, Tween.delayedCall(0, callback, params), _parsePosition$1(this, position));
  } //ONLY for backward compatibility! Maybe delete?
  ;

  _proto2.staggerTo = function staggerTo(targets, duration, vars, stagger, position, onCompleteAll, onCompleteAllParams) {
    vars.duration = duration;
    vars.stagger = vars.stagger || stagger;
    vars.onComplete = onCompleteAll;
    vars.onCompleteParams = onCompleteAllParams;
    vars.parent = this;
    new Tween(targets, vars, _parsePosition$1(this, position));
    return this;
  };

  _proto2.staggerFrom = function staggerFrom(targets, duration, vars, stagger, position, onCompleteAll, onCompleteAllParams) {
    vars.runBackwards = 1;
    _inheritDefaults(vars).immediateRender = _isNotFalse(vars.immediateRender);
    return this.staggerTo(targets, duration, vars, stagger, position, onCompleteAll, onCompleteAllParams);
  };

  _proto2.staggerFromTo = function staggerFromTo(targets, duration, fromVars, toVars, stagger, position, onCompleteAll, onCompleteAllParams) {
    toVars.startAt = fromVars;
    _inheritDefaults(toVars).immediateRender = _isNotFalse(toVars.immediateRender);
    return this.staggerTo(targets, duration, toVars, stagger, position, onCompleteAll, onCompleteAllParams);
  };

  _proto2.render = function render(totalTime, suppressEvents, force) {
    var prevTime = this._time,
        tDur = this._dirty ? this.totalDuration() : this._tDur,
        dur = this._dur,
        tTime = this !== _globalTimeline && totalTime > tDur - _tinyNum && totalTime >= 0 ? tDur : totalTime < _tinyNum ? 0 : totalTime,
        crossingStart = this._zTime < 0 !== totalTime < 0 && (this._initted || !dur),
        time,
        child,
        next,
        iteration,
        cycleDuration,
        prevPaused,
        pauseTween,
        timeScale,
        prevStart,
        prevIteration,
        yoyo,
        isYoyo;

    if (tTime !== this._tTime || force || crossingStart) {
      if (prevTime !== this._time && dur) {
        //if totalDuration() finds a child with a negative startTime and smoothChildTiming is true, things get shifted around internally so we need to adjust the time accordingly. For example, if a tween starts at -30 we must shift EVERYTHING forward 30 seconds and move this timeline's startTime backward by 30 seconds so that things align with the playhead (no jump).
        tTime += this._time - prevTime;
        totalTime += this._time - prevTime;
      }

      time = tTime;
      prevStart = this._start;
      timeScale = this._ts;
      prevPaused = !timeScale;

      if (crossingStart) {
        dur || (prevTime = this._zTime); //when the playhead arrives at EXACTLY time 0 (right on top) of a zero-duration timeline, we need to discern if events are suppressed so that when the playhead moves again (next time), it'll trigger the callback. If events are NOT suppressed, obviously the callback would be triggered in this render. Basically, the callback should fire either when the playhead ARRIVES or LEAVES this exact spot, not both. Imagine doing a timeline.seek(0) and there's a callback that sits at 0. Since events are suppressed on that seek() by default, nothing will fire, but when the playhead moves off of that position, the callback should fire. This behavior is what people intuitively expect.

        (totalTime || !suppressEvents) && (this._zTime = totalTime);
      }

      if (this._repeat) {
        //adjust the time for repeats and yoyos
        yoyo = this._yoyo;
        cycleDuration = dur + this._rDelay;

        if (this._repeat < -1 && totalTime < 0) {
          return this.totalTime(cycleDuration * 100 + totalTime, suppressEvents, force);
        }

        time = _round$3(tTime % cycleDuration); //round to avoid floating point errors. (4 % 0.8 should be 0 but some browsers report it as 0.79999999!)

        if (tTime === tDur) {
          // the tDur === tTime is for edge cases where there's a lengthy decimal on the duration and it may reach the very end but the time is rendered as not-quite-there (remember, tDur is rounded to 4 decimals whereas dur isn't)
          iteration = this._repeat;
          time = dur;
        } else {
          iteration = ~~(tTime / cycleDuration);

          if (iteration && iteration === tTime / cycleDuration) {
            time = dur;
            iteration--;
          }

          time > dur && (time = dur);
        }

        prevIteration = _animationCycle(this._tTime, cycleDuration);
        !prevTime && this._tTime && prevIteration !== iteration && (prevIteration = iteration); // edge case - if someone does addPause() at the very beginning of a repeating timeline, that pause is technically at the same spot as the end which causes this._time to get set to 0 when the totalTime would normally place the playhead at the end. See https://greensock.com/forums/topic/23823-closing-nav-animation-not-working-on-ie-and-iphone-6-maybe-other-older-browser/?tab=comments#comment-113005

        if (yoyo && iteration & 1) {
          time = dur - time;
          isYoyo = 1;
        }
        /*
        make sure children at the end/beginning of the timeline are rendered properly. If, for example,
        a 3-second long timeline rendered at 2.9 seconds previously, and now renders at 3.2 seconds (which
        would get translated to 2.8 seconds if the timeline yoyos or 0.2 seconds if it just repeats), there
        could be a callback or a short tween that's at 2.95 or 3 seconds in which wouldn't render. So
        we need to push the timeline to the end (and/or beginning depending on its yoyo value). Also we must
        ensure that zero-duration tweens at the very beginning or end of the Timeline work.
        */


        if (iteration !== prevIteration && !this._lock) {
          var rewinding = yoyo && prevIteration & 1,
              doesWrap = rewinding === (yoyo && iteration & 1);
          iteration < prevIteration && (rewinding = !rewinding);
          prevTime = rewinding ? 0 : dur;
          this._lock = 1;
          this.render(prevTime || (isYoyo ? 0 : _round$3(iteration * cycleDuration)), suppressEvents, !dur)._lock = 0;
          !suppressEvents && this.parent && _callback(this, "onRepeat");
          this.vars.repeatRefresh && !isYoyo && (this.invalidate()._lock = 1);

          if (prevTime && prevTime !== this._time || prevPaused !== !this._ts || this.vars.onRepeat && !this.parent && !this._act) {
            // if prevTime is 0 and we render at the very end, _time will be the end, thus won't match. So in this edge case, prevTime won't match _time but that's okay. If it gets killed in the onRepeat, eject as well.
            return this;
          }

          dur = this._dur; // in case the duration changed in the onRepeat

          tDur = this._tDur;

          if (doesWrap) {
            this._lock = 2;
            prevTime = rewinding ? dur : -0.0001;
            this.render(prevTime, true);
          }

          this._lock = 0;

          if (!this._ts && !prevPaused) {
            return this;
          } //in order for yoyoEase to work properly when there's a stagger, we must swap out the ease in each sub-tween.


          _propagateYoyoEase(this, isYoyo);
        }
      }

      if (this._hasPause && !this._forcing && this._lock < 2) {
        pauseTween = _findNextPauseTween(this, _round$3(prevTime), _round$3(time));

        if (pauseTween) {
          tTime -= time - (time = pauseTween._start);
        }
      }

      this._tTime = tTime;
      this._time = time;
      this._act = !timeScale; //as long as it's not paused, force it to be active so that if the user renders independent of the parent timeline, it'll be forced to re-render on the next tick.

      if (!this._initted) {
        this._onUpdate = this.vars.onUpdate;
        this._initted = 1;
        this._zTime = totalTime;
        prevTime = 0; // upon init, the playhead should always go forward; someone could invalidate() a completed timeline and then if they restart(), that would make child tweens render in reverse order which could lock in the wrong starting values if they build on each other, like tl.to(obj, {x: 100}).to(obj, {x: 0}).
      }

      !prevTime && time && !suppressEvents && _callback(this, "onStart");

      if (time >= prevTime && totalTime >= 0) {
        child = this._first;

        while (child) {
          next = child._next;

          if ((child._act || time >= child._start) && child._ts && pauseTween !== child) {
            if (child.parent !== this) {
              // an extreme edge case - the child's render could do something like kill() the "next" one in the linked list, or reparent it. In that case we must re-initiate the whole render to be safe.
              return this.render(totalTime, suppressEvents, force);
            }

            child.render(child._ts > 0 ? (time - child._start) * child._ts : (child._dirty ? child.totalDuration() : child._tDur) + (time - child._start) * child._ts, suppressEvents, force);

            if (time !== this._time || !this._ts && !prevPaused) {
              //in case a tween pauses or seeks the timeline when rendering, like inside of an onUpdate/onComplete
              pauseTween = 0;
              next && (tTime += this._zTime = -_tinyNum); // it didn't finish rendering, so flag zTime as negative so that so that the next time render() is called it'll be forced (to render any remaining children)

              break;
            }
          }

          child = next;
        }
      } else {
        child = this._last;
        var adjustedTime = totalTime < 0 ? totalTime : time; //when the playhead goes backward beyond the start of this timeline, we must pass that information down to the child animations so that zero-duration tweens know whether to render their starting or ending values.

        while (child) {
          next = child._prev;

          if ((child._act || adjustedTime <= child._end) && child._ts && pauseTween !== child) {
            if (child.parent !== this) {
              // an extreme edge case - the child's render could do something like kill() the "next" one in the linked list, or reparent it. In that case we must re-initiate the whole render to be safe.
              return this.render(totalTime, suppressEvents, force);
            }

            child.render(child._ts > 0 ? (adjustedTime - child._start) * child._ts : (child._dirty ? child.totalDuration() : child._tDur) + (adjustedTime - child._start) * child._ts, suppressEvents, force);

            if (time !== this._time || !this._ts && !prevPaused) {
              //in case a tween pauses or seeks the timeline when rendering, like inside of an onUpdate/onComplete
              pauseTween = 0;
              next && (tTime += this._zTime = adjustedTime ? -_tinyNum : _tinyNum); // it didn't finish rendering, so adjust zTime so that so that the next time render() is called it'll be forced (to render any remaining children)

              break;
            }
          }

          child = next;
        }
      }

      if (pauseTween && !suppressEvents) {
        this.pause();
        pauseTween.render(time >= prevTime ? 0 : -_tinyNum)._zTime = time >= prevTime ? 1 : -1;

        if (this._ts) {
          //the callback resumed playback! So since we may have held back the playhead due to where the pause is positioned, go ahead and jump to where it's SUPPOSED to be (if no pause happened).
          this._start = prevStart; //if the pause was at an earlier time and the user resumed in the callback, it could reposition the timeline (changing its startTime), throwing things off slightly, so we make sure the _start doesn't shift.

          _setEnd(this);

          return this.render(totalTime, suppressEvents, force);
        }
      }

      this._onUpdate && !suppressEvents && _callback(this, "onUpdate", true);
      if (tTime === tDur && tDur >= this.totalDuration() || !tTime && prevTime) if (prevStart === this._start || Math.abs(timeScale) !== Math.abs(this._ts)) if (!this._lock) {
        (totalTime || !dur) && (tTime === tDur && this._ts > 0 || !tTime && this._ts < 0) && _removeFromParent(this, 1); // don't remove if the timeline is reversed and the playhead isn't at 0, otherwise tl.progress(1).reverse() won't work. Only remove if the playhead is at the end and timeScale is positive, or if the playhead is at 0 and the timeScale is negative.

        if (!suppressEvents && !(totalTime < 0 && !prevTime) && (tTime || prevTime)) {
          _callback(this, tTime === tDur ? "onComplete" : "onReverseComplete", true);

          this._prom && !(tTime < tDur && this.timeScale() > 0) && this._prom();
        }
      }
    }

    return this;
  };

  _proto2.add = function add(child, position) {
    var _this2 = this;

    _isNumber$2(position) || (position = _parsePosition$1(this, position));

    if (!(child instanceof Animation)) {
      if (_isArray$1(child)) {
        child.forEach(function (obj) {
          return _this2.add(obj, position);
        });
        return this;
      }

      if (_isString$4(child)) {
        return this.addLabel(child, position);
      }

      if (_isFunction$4(child)) {
        child = Tween.delayedCall(0, child);
      } else {
        return this;
      }
    }

    return this !== child ? _addToTimeline(this, child, position) : this; //don't allow a timeline to be added to itself as a child!
  };

  _proto2.getChildren = function getChildren(nested, tweens, timelines, ignoreBeforeTime) {
    if (nested === void 0) {
      nested = true;
    }

    if (tweens === void 0) {
      tweens = true;
    }

    if (timelines === void 0) {
      timelines = true;
    }

    if (ignoreBeforeTime === void 0) {
      ignoreBeforeTime = -_bigNum$2;
    }

    var a = [],
        child = this._first;

    while (child) {
      if (child._start >= ignoreBeforeTime) {
        if (child instanceof Tween) {
          tweens && a.push(child);
        } else {
          timelines && a.push(child);
          nested && a.push.apply(a, child.getChildren(true, tweens, timelines));
        }
      }

      child = child._next;
    }

    return a;
  };

  _proto2.getById = function getById(id) {
    var animations = this.getChildren(1, 1, 1),
        i = animations.length;

    while (i--) {
      if (animations[i].vars.id === id) {
        return animations[i];
      }
    }
  };

  _proto2.remove = function remove(child) {
    if (_isString$4(child)) {
      return this.removeLabel(child);
    }

    if (_isFunction$4(child)) {
      return this.killTweensOf(child);
    }

    _removeLinkedListItem(this, child);

    if (child === this._recent) {
      this._recent = this._last;
    }

    return _uncache(this);
  };

  _proto2.totalTime = function totalTime(_totalTime2, suppressEvents) {
    if (!arguments.length) {
      return this._tTime;
    }

    this._forcing = 1;

    if (!this._dp && this._ts) {
      //special case for the global timeline (or any other that has no parent or detached parent).
      this._start = _round$3(_ticker.time - (this._ts > 0 ? _totalTime2 / this._ts : (this.totalDuration() - _totalTime2) / -this._ts));
    }

    _Animation.prototype.totalTime.call(this, _totalTime2, suppressEvents);

    this._forcing = 0;
    return this;
  };

  _proto2.addLabel = function addLabel(label, position) {
    this.labels[label] = _parsePosition$1(this, position);
    return this;
  };

  _proto2.removeLabel = function removeLabel(label) {
    delete this.labels[label];
    return this;
  };

  _proto2.addPause = function addPause(position, callback, params) {
    var t = Tween.delayedCall(0, callback || _emptyFunc$2, params);
    t.data = "isPause";
    this._hasPause = 1;
    return _addToTimeline(this, t, _parsePosition$1(this, position));
  };

  _proto2.removePause = function removePause(position) {
    var child = this._first;
    position = _parsePosition$1(this, position);

    while (child) {
      if (child._start === position && child.data === "isPause") {
        _removeFromParent(child);
      }

      child = child._next;
    }
  };

  _proto2.killTweensOf = function killTweensOf(targets, props, onlyActive) {
    var tweens = this.getTweensOf(targets, onlyActive),
        i = tweens.length;

    while (i--) {
      _overwritingTween !== tweens[i] && tweens[i].kill(targets, props);
    }

    return this;
  };

  _proto2.getTweensOf = function getTweensOf(targets, onlyActive) {
    var a = [],
        parsedTargets = toArray(targets),
        child = this._first,
        isGlobalTime = _isNumber$2(onlyActive),
        // a number is interpreted as a global time. If the animation spans
    children;

    while (child) {
      if (child instanceof Tween) {
        if (_arrayContainsAny(child._targets, parsedTargets) && (isGlobalTime ? (!_overwritingTween || child._initted && child._ts) && child.globalTime(0) <= onlyActive && child.globalTime(child.totalDuration()) > onlyActive : !onlyActive || child.isActive())) {
          // note: if this is for overwriting, it should only be for tweens that aren't paused and are initted.
          a.push(child);
        }
      } else if ((children = child.getTweensOf(parsedTargets, onlyActive)).length) {
        a.push.apply(a, children);
      }

      child = child._next;
    }

    return a;
  } // potential future feature - targets() on timelines
  // targets() {
  // 	let result = [];
  // 	this.getChildren(true, true, false).forEach(t => result.push(...t.targets()));
  // 	return result;
  // }
  ;

  _proto2.tweenTo = function tweenTo(position, vars) {
    vars = vars || {};

    var tl = this,
        endTime = _parsePosition$1(tl, position),
        _vars = vars,
        startAt = _vars.startAt,
        _onStart = _vars.onStart,
        onStartParams = _vars.onStartParams,
        immediateRender = _vars.immediateRender,
        tween = Tween.to(tl, _setDefaults$2({
      ease: vars.ease || "none",
      lazy: false,
      immediateRender: false,
      time: endTime,
      overwrite: "auto",
      duration: vars.duration || Math.abs((endTime - (startAt && "time" in startAt ? startAt.time : tl._time)) / tl.timeScale()) || _tinyNum,
      onStart: function onStart() {
        tl.pause();
        var duration = vars.duration || Math.abs((endTime - tl._time) / tl.timeScale());
        tween._dur !== duration && _setDuration(tween, duration, 0, 1).render(tween._time, true, true);
        _onStart && _onStart.apply(tween, onStartParams || []); //in case the user had an onStart in the vars - we don't want to overwrite it.
      }
    }, vars));

    return immediateRender ? tween.render(0) : tween;
  };

  _proto2.tweenFromTo = function tweenFromTo(fromPosition, toPosition, vars) {
    return this.tweenTo(toPosition, _setDefaults$2({
      startAt: {
        time: _parsePosition$1(this, fromPosition)
      }
    }, vars));
  };

  _proto2.recent = function recent() {
    return this._recent;
  };

  _proto2.nextLabel = function nextLabel(afterTime) {
    if (afterTime === void 0) {
      afterTime = this._time;
    }

    return _getLabelInDirection(this, _parsePosition$1(this, afterTime));
  };

  _proto2.previousLabel = function previousLabel(beforeTime) {
    if (beforeTime === void 0) {
      beforeTime = this._time;
    }

    return _getLabelInDirection(this, _parsePosition$1(this, beforeTime), 1);
  };

  _proto2.currentLabel = function currentLabel(value) {
    return arguments.length ? this.seek(value, true) : this.previousLabel(this._time + _tinyNum);
  };

  _proto2.shiftChildren = function shiftChildren(amount, adjustLabels, ignoreBeforeTime) {
    if (ignoreBeforeTime === void 0) {
      ignoreBeforeTime = 0;
    }

    var child = this._first,
        labels = this.labels,
        p;

    while (child) {
      if (child._start >= ignoreBeforeTime) {
        child._start += amount;
        child._end += amount;
      }

      child = child._next;
    }

    if (adjustLabels) {
      for (p in labels) {
        if (labels[p] >= ignoreBeforeTime) {
          labels[p] += amount;
        }
      }
    }

    return _uncache(this);
  };

  _proto2.invalidate = function invalidate() {
    var child = this._first;
    this._lock = 0;

    while (child) {
      child.invalidate();
      child = child._next;
    }

    return _Animation.prototype.invalidate.call(this);
  };

  _proto2.clear = function clear(includeLabels) {
    if (includeLabels === void 0) {
      includeLabels = true;
    }

    var child = this._first,
        next;

    while (child) {
      next = child._next;
      this.remove(child);
      child = next;
    }

    this._dp && (this._time = this._tTime = this._pTime = 0);
    includeLabels && (this.labels = {});
    return _uncache(this);
  };

  _proto2.totalDuration = function totalDuration(value) {
    var max = 0,
        self = this,
        child = self._last,
        prevStart = _bigNum$2,
        prev,
        start,
        parent;

    if (arguments.length) {
      return self.timeScale((self._repeat < 0 ? self.duration() : self.totalDuration()) / (self.reversed() ? -value : value));
    }

    if (self._dirty) {
      parent = self.parent;

      while (child) {
        prev = child._prev; //record it here in case the tween changes position in the sequence...

        child._dirty && child.totalDuration(); //could change the tween._startTime, so make sure the animation's cache is clean before analyzing it.

        start = child._start;

        if (start > prevStart && self._sort && child._ts && !self._lock) {
          //in case one of the tweens shifted out of order, it needs to be re-inserted into the correct position in the sequence
          self._lock = 1; //prevent endless recursive calls - there are methods that get triggered that check duration/totalDuration when we add().

          _addToTimeline(self, child, start - child._delay, 1)._lock = 0;
        } else {
          prevStart = start;
        }

        if (start < 0 && child._ts) {
          //children aren't allowed to have negative startTimes unless smoothChildTiming is true, so adjust here if one is found.
          max -= start;

          if (!parent && !self._dp || parent && parent.smoothChildTiming) {
            self._start += start / self._ts;
            self._time -= start;
            self._tTime -= start;
          }

          self.shiftChildren(-start, false, -1e999);
          prevStart = 0;
        }

        child._end > max && child._ts && (max = child._end);
        child = prev;
      }

      _setDuration(self, self === _globalTimeline && self._time > max ? self._time : max, 1, 1);

      self._dirty = 0;
    }

    return self._tDur;
  };

  Timeline.updateRoot = function updateRoot(time) {
    if (_globalTimeline._ts) {
      _lazySafeRender(_globalTimeline, _parentToChildTotalTime(time, _globalTimeline));

      _lastRenderedFrame = _ticker.frame;
    }

    if (_ticker.frame >= _nextGCFrame) {
      _nextGCFrame += _config$1.autoSleep || 120;
      var child = _globalTimeline._first;
      if (!child || !child._ts) if (_config$1.autoSleep && _ticker._listeners.length < 2) {
        while (child && !child._ts) {
          child = child._next;
        }

        child || _ticker.sleep();
      }
    }
  };

  return Timeline;
}(Animation);

_setDefaults$2(Timeline.prototype, {
  _lock: 0,
  _hasPause: 0,
  _forcing: 0
});

var _addComplexStringPropTween = function _addComplexStringPropTween(target, prop, start, end, setter, stringFilter, funcParam) {
  //note: we call _addComplexStringPropTween.call(tweenInstance...) to ensure that it's scoped properly. We may call it from within a plugin too, thus "this" would refer to the plugin.
  var pt = new PropTween$2(this._pt, target, prop, 0, 1, _renderComplexString, null, setter),
      index = 0,
      matchIndex = 0,
      result,
      startNums,
      color,
      endNum,
      chunk,
      startNum,
      hasRandom,
      a;
  pt.b = start;
  pt.e = end;
  start += ""; //ensure values are strings

  end += "";

  if (hasRandom = ~end.indexOf("random(")) {
    end = _replaceRandom(end);
  }

  if (stringFilter) {
    a = [start, end];
    stringFilter(a, target, prop); //pass an array with the starting and ending values and let the filter do whatever it needs to the values.

    start = a[0];
    end = a[1];
  }

  startNums = start.match(_complexStringNumExp) || [];

  while (result = _complexStringNumExp.exec(end)) {
    endNum = result[0];
    chunk = end.substring(index, result.index);

    if (color) {
      color = (color + 1) % 5;
    } else if (chunk.substr(-5) === "rgba(") {
      color = 1;
    }

    if (endNum !== startNums[matchIndex++]) {
      startNum = parseFloat(startNums[matchIndex - 1]) || 0; //these nested PropTweens are handled in a special way - we'll never actually call a render or setter method on them. We'll just loop through them in the parent complex string PropTween's render method.

      pt._pt = {
        _next: pt._pt,
        p: chunk || matchIndex === 1 ? chunk : ",",
        //note: SVG spec allows omission of comma/space when a negative sign is wedged between two numbers, like 2.5-5.3 instead of 2.5,-5.3 but when tweening, the negative value may switch to positive, so we insert the comma just in case.
        s: startNum,
        c: endNum.charAt(1) === "=" ? parseFloat(endNum.substr(2)) * (endNum.charAt(0) === "-" ? -1 : 1) : parseFloat(endNum) - startNum,
        m: color && color < 4 ? Math.round : 0
      };
      index = _complexStringNumExp.lastIndex;
    }
  }

  pt.c = index < end.length ? end.substring(index, end.length) : ""; //we use the "c" of the PropTween to store the final part of the string (after the last number)

  pt.fp = funcParam;

  if (_relExp.test(end) || hasRandom) {
    pt.e = 0; //if the end string contains relative values or dynamic random(...) values, delete the end it so that on the final render we don't actually set it to the string with += or -= characters (forces it to use the calculated value).
  }

  this._pt = pt; //start the linked list with this new PropTween. Remember, we call _addComplexStringPropTween.call(tweenInstance...) to ensure that it's scoped properly. We may call it from within a plugin too, thus "this" would refer to the plugin.

  return pt;
},
    _addPropTween = function _addPropTween(target, prop, start, end, index, targets, modifier, stringFilter, funcParam) {
  _isFunction$4(end) && (end = end(index || 0, target, targets));
  var currentValue = target[prop],
      parsedStart = start !== "get" ? start : !_isFunction$4(currentValue) ? currentValue : funcParam ? target[prop.indexOf("set") || !_isFunction$4(target["get" + prop.substr(3)]) ? prop : "get" + prop.substr(3)](funcParam) : target[prop](),
      setter = !_isFunction$4(currentValue) ? _setterPlain : funcParam ? _setterFuncWithParam : _setterFunc,
      pt;

  if (_isString$4(end)) {
    if (~end.indexOf("random(")) {
      end = _replaceRandom(end);
    }

    if (end.charAt(1) === "=") {
      end = parseFloat(parsedStart) + parseFloat(end.substr(2)) * (end.charAt(0) === "-" ? -1 : 1) + (getUnit(parsedStart) || 0);
    }
  }

  if (parsedStart !== end) {
    if (!isNaN(parsedStart * end)) {
      pt = new PropTween$2(this._pt, target, prop, +parsedStart || 0, end - (parsedStart || 0), typeof currentValue === "boolean" ? _renderBoolean : _renderPlain, 0, setter);
      funcParam && (pt.fp = funcParam);
      modifier && pt.modifier(modifier, this, target);
      return this._pt = pt;
    }

    !currentValue && !(prop in target) && _missingPlugin(prop, end);
    return _addComplexStringPropTween.call(this, target, prop, parsedStart, end, setter, stringFilter || _config$1.stringFilter, funcParam);
  }
},
    //creates a copy of the vars object and processes any function-based values (putting the resulting values directly into the copy) as well as strings with "random()" in them. It does NOT process relative values.
_processVars = function _processVars(vars, index, target, targets, tween) {
  _isFunction$4(vars) && (vars = _parseFuncOrString(vars, tween, index, target, targets));

  if (!_isObject$2(vars) || vars.style && vars.nodeType || _isArray$1(vars) || _isTypedArray(vars)) {
    return _isString$4(vars) ? _parseFuncOrString(vars, tween, index, target, targets) : vars;
  }

  var copy = {},
      p;

  for (p in vars) {
    copy[p] = _parseFuncOrString(vars[p], tween, index, target, targets);
  }

  return copy;
},
    _checkPlugin = function _checkPlugin(property, vars, tween, index, target, targets) {
  var plugin, pt, ptLookup, i;

  if (_plugins[property] && (plugin = new _plugins[property]()).init(target, plugin.rawVars ? vars[property] : _processVars(vars[property], index, target, targets, tween), tween, index, targets) !== false) {
    tween._pt = pt = new PropTween$2(tween._pt, target, property, 0, 1, plugin.render, plugin, 0, plugin.priority);

    if (tween !== _quickTween) {
      ptLookup = tween._ptLookup[tween._targets.indexOf(target)]; //note: we can't use tween._ptLookup[index] because for staggered tweens, the index from the fullTargets array won't match what it is in each individual tween that spawns from the stagger.

      i = plugin._props.length;

      while (i--) {
        ptLookup[plugin._props[i]] = pt;
      }
    }
  }

  return plugin;
},
    _overwritingTween,
    //store a reference temporarily so we can avoid overwriting itself.
_initTween = function _initTween(tween, time) {
  var vars = tween.vars,
      ease = vars.ease,
      startAt = vars.startAt,
      immediateRender = vars.immediateRender,
      lazy = vars.lazy,
      onUpdate = vars.onUpdate,
      onUpdateParams = vars.onUpdateParams,
      callbackScope = vars.callbackScope,
      runBackwards = vars.runBackwards,
      yoyoEase = vars.yoyoEase,
      keyframes = vars.keyframes,
      autoRevert = vars.autoRevert,
      dur = tween._dur,
      prevStartAt = tween._startAt,
      targets = tween._targets,
      parent = tween.parent,
      fullTargets = parent && parent.data === "nested" ? parent.parent._targets : targets,
      autoOverwrite = tween._overwrite === "auto" && !_suppressOverwrites$1,
      tl = tween.timeline,
      cleanVars,
      i,
      p,
      pt,
      target,
      hasPriority,
      gsData,
      harness,
      plugin,
      ptLookup,
      index,
      harnessVars,
      overwritten;
  tl && (!keyframes || !ease) && (ease = "none");
  tween._ease = _parseEase(ease, _defaults$1.ease);
  tween._yEase = yoyoEase ? _invertEase(_parseEase(yoyoEase === true ? ease : yoyoEase, _defaults$1.ease)) : 0;

  if (yoyoEase && tween._yoyo && !tween._repeat) {
    //there must have been a parent timeline with yoyo:true that is currently in its yoyo phase, so flip the eases.
    yoyoEase = tween._yEase;
    tween._yEase = tween._ease;
    tween._ease = yoyoEase;
  }

  if (!tl) {
    //if there's an internal timeline, skip all the parsing because we passed that task down the chain.
    harness = targets[0] ? _getCache(targets[0]).harness : 0;
    harnessVars = harness && vars[harness.prop]; //someone may need to specify CSS-specific values AND non-CSS values, like if the element has an "x" property plus it's a standard DOM element. We allow people to distinguish by wrapping plugin-specific stuff in a css:{} object for example.

    cleanVars = _copyExcluding(vars, _reservedProps);
    prevStartAt && prevStartAt.render(-1, true).kill();

    if (startAt) {
      _removeFromParent(tween._startAt = Tween.set(targets, _setDefaults$2({
        data: "isStart",
        overwrite: false,
        parent: parent,
        immediateRender: true,
        lazy: _isNotFalse(lazy),
        startAt: null,
        delay: 0,
        onUpdate: onUpdate,
        onUpdateParams: onUpdateParams,
        callbackScope: callbackScope,
        stagger: 0
      }, startAt))); //copy the properties/values into a new object to avoid collisions, like var to = {x:0}, from = {x:500}; timeline.fromTo(e, from, to).fromTo(e, to, from);


      if (immediateRender) {
        if (time > 0) {
          autoRevert || (tween._startAt = 0); //tweens that render immediately (like most from() and fromTo() tweens) shouldn't revert when their parent timeline's playhead goes backward past the startTime because the initial render could have happened anytime and it shouldn't be directly correlated to this tween's startTime. Imagine setting up a complex animation where the beginning states of various objects are rendered immediately but the tween doesn't happen for quite some time - if we revert to the starting values as soon as the playhead goes backward past the tween's startTime, it will throw things off visually. Reversion should only happen in Timeline instances where immediateRender was false or when autoRevert is explicitly set to true.
        } else if (dur && !(time < 0 && prevStartAt)) {
          time && (tween._zTime = time);
          return; //we skip initialization here so that overwriting doesn't occur until the tween actually begins. Otherwise, if you create several immediateRender:true tweens of the same target/properties to drop into a Timeline, the last one created would overwrite the first ones because they didn't get placed into the timeline yet before the first render occurs and kicks in overwriting.
        }
      } else if (autoRevert === false) {
        tween._startAt = 0;
      }
    } else if (runBackwards && dur) {
      //from() tweens must be handled uniquely: their beginning values must be rendered but we don't want overwriting to occur yet (when time is still 0). Wait until the tween actually begins before doing all the routines like overwriting. At that time, we should render at the END of the tween to ensure that things initialize correctly (remember, from() tweens go backwards)
      if (prevStartAt) {
        !autoRevert && (tween._startAt = 0);
      } else {
        time && (immediateRender = false); //in rare cases (like if a from() tween runs and then is invalidate()-ed), immediateRender could be true but the initial forced-render gets skipped, so there's no need to force the render in this context when the _time is greater than 0

        p = _setDefaults$2({
          overwrite: false,
          data: "isFromStart",
          //we tag the tween with as "isFromStart" so that if [inside a plugin] we need to only do something at the very END of a tween, we have a way of identifying this tween as merely the one that's setting the beginning values for a "from()" tween. For example, clearProps in CSSPlugin should only get applied at the very END of a tween and without this tag, from(...{height:100, clearProps:"height", delay:1}) would wipe the height at the beginning of the tween and after 1 second, it'd kick back in.
          lazy: immediateRender && _isNotFalse(lazy),
          immediateRender: immediateRender,
          //zero-duration tweens render immediately by default, but if we're not specifically instructed to render this tween immediately, we should skip this and merely _init() to record the starting values (rendering them immediately would push them to completion which is wasteful in that case - we'd have to render(-1) immediately after)
          stagger: 0,
          parent: parent //ensures that nested tweens that had a stagger are handled properly, like gsap.from(".class", {y:gsap.utils.wrap([-100,100])})

        }, cleanVars);
        harnessVars && (p[harness.prop] = harnessVars); // in case someone does something like .from(..., {css:{}})

        _removeFromParent(tween._startAt = Tween.set(targets, p));

        if (!immediateRender) {
          _initTween(tween._startAt, _tinyNum); //ensures that the initial values are recorded

        } else if (!time) {
          return;
        }
      }
    }

    tween._pt = 0;
    lazy = dur && _isNotFalse(lazy) || lazy && !dur;

    for (i = 0; i < targets.length; i++) {
      target = targets[i];
      gsData = target._gsap || _harness(targets)[i]._gsap;
      tween._ptLookup[i] = ptLookup = {};
      _lazyLookup[gsData.id] && _lazyTweens.length && _lazyRender(); //if other tweens of the same target have recently initted but haven't rendered yet, we've got to force the render so that the starting values are correct (imagine populating a timeline with a bunch of sequential tweens and then jumping to the end)

      index = fullTargets === targets ? i : fullTargets.indexOf(target);

      if (harness && (plugin = new harness()).init(target, harnessVars || cleanVars, tween, index, fullTargets) !== false) {
        tween._pt = pt = new PropTween$2(tween._pt, target, plugin.name, 0, 1, plugin.render, plugin, 0, plugin.priority);

        plugin._props.forEach(function (name) {
          ptLookup[name] = pt;
        });

        plugin.priority && (hasPriority = 1);
      }

      if (!harness || harnessVars) {
        for (p in cleanVars) {
          if (_plugins[p] && (plugin = _checkPlugin(p, cleanVars, tween, index, target, fullTargets))) {
            plugin.priority && (hasPriority = 1);
          } else {
            ptLookup[p] = pt = _addPropTween.call(tween, target, p, "get", cleanVars[p], index, fullTargets, 0, vars.stringFilter);
          }
        }
      }

      tween._op && tween._op[i] && tween.kill(target, tween._op[i]);

      if (autoOverwrite && tween._pt) {
        _overwritingTween = tween;

        _globalTimeline.killTweensOf(target, ptLookup, tween.globalTime(0)); //Also make sure the overwriting doesn't overwrite THIS tween!!!


        overwritten = !tween.parent;
        _overwritingTween = 0;
      }

      tween._pt && lazy && (_lazyLookup[gsData.id] = 1);
    }

    hasPriority && _sortPropTweensByPriority(tween);
    tween._onInit && tween._onInit(tween); //plugins like RoundProps must wait until ALL of the PropTweens are instantiated. In the plugin's init() function, it sets the _onInit on the tween instance. May not be pretty/intuitive, but it's fast and keeps file size down.
  }

  tween._from = !tl && !!vars.runBackwards; //nested timelines should never run backwards - the backwards-ness is in the child tweens.

  tween._onUpdate = onUpdate;
  tween._initted = (!tween._op || tween._pt) && !overwritten; // if overwrittenProps resulted in the entire tween being killed, do NOT flag it as initted or else it may render for one tick.
},
    _addAliasesToVars = function _addAliasesToVars(targets, vars) {
  var harness = targets[0] ? _getCache(targets[0]).harness : 0,
      propertyAliases = harness && harness.aliases,
      copy,
      p,
      i,
      aliases;

  if (!propertyAliases) {
    return vars;
  }

  copy = _merge({}, vars);

  for (p in propertyAliases) {
    if (p in copy) {
      aliases = propertyAliases[p].split(",");
      i = aliases.length;

      while (i--) {
        copy[aliases[i]] = copy[p];
      }
    }
  }

  return copy;
},
    _parseFuncOrString = function _parseFuncOrString(value, tween, i, target, targets) {
  return _isFunction$4(value) ? value.call(tween, i, target, targets) : _isString$4(value) && ~value.indexOf("random(") ? _replaceRandom(value) : value;
},
    _staggerTweenProps = _callbackNames + "repeat,repeatDelay,yoyo,repeatRefresh,yoyoEase",
    _staggerPropsToSkip = (_staggerTweenProps + ",id,stagger,delay,duration,paused,scrollTrigger").split(",");
/*
 * --------------------------------------------------------------------------------------
 * TWEEN
 * --------------------------------------------------------------------------------------
 */


var Tween = /*#__PURE__*/function (_Animation2) {
  _inheritsLoose$1(Tween, _Animation2);

  function Tween(targets, vars, time, skipInherit) {
    var _this3;

    if (typeof vars === "number") {
      time.duration = vars;
      vars = time;
      time = null;
    }

    _this3 = _Animation2.call(this, skipInherit ? vars : _inheritDefaults(vars), time) || this;
    var _this3$vars = _this3.vars,
        duration = _this3$vars.duration,
        delay = _this3$vars.delay,
        immediateRender = _this3$vars.immediateRender,
        stagger = _this3$vars.stagger,
        overwrite = _this3$vars.overwrite,
        keyframes = _this3$vars.keyframes,
        defaults = _this3$vars.defaults,
        scrollTrigger = _this3$vars.scrollTrigger,
        yoyoEase = _this3$vars.yoyoEase,
        parent = _this3.parent,
        parsedTargets = (_isArray$1(targets) || _isTypedArray(targets) ? _isNumber$2(targets[0]) : "length" in vars) ? [targets] : toArray(targets),
        tl,
        i,
        copy,
        l,
        p,
        curTarget,
        staggerFunc,
        staggerVarsToMerge;
    _this3._targets = parsedTargets.length ? _harness(parsedTargets) : _warn$2("GSAP target " + targets + " not found. https://greensock.com", !_config$1.nullTargetWarn) || [];
    _this3._ptLookup = []; //PropTween lookup. An array containing an object for each target, having keys for each tweening property

    _this3._overwrite = overwrite;

    if (keyframes || stagger || _isFuncOrString(duration) || _isFuncOrString(delay)) {
      vars = _this3.vars;
      tl = _this3.timeline = new Timeline({
        data: "nested",
        defaults: defaults || {}
      });
      tl.kill();
      tl.parent = tl._dp = _assertThisInitialized$1(_this3);
      tl._start = 0;

      if (keyframes) {
        _setDefaults$2(tl.vars.defaults, {
          ease: "none"
        });

        keyframes.forEach(function (frame) {
          return tl.to(parsedTargets, frame, ">");
        });
      } else {
        l = parsedTargets.length;
        staggerFunc = stagger ? distribute(stagger) : _emptyFunc$2;

        if (_isObject$2(stagger)) {
          //users can pass in callbacks like onStart/onComplete in the stagger object. These should fire with each individual tween.
          for (p in stagger) {
            if (~_staggerTweenProps.indexOf(p)) {
              staggerVarsToMerge || (staggerVarsToMerge = {});
              staggerVarsToMerge[p] = stagger[p];
            }
          }
        }

        for (i = 0; i < l; i++) {
          copy = {};

          for (p in vars) {
            if (_staggerPropsToSkip.indexOf(p) < 0) {
              copy[p] = vars[p];
            }
          }

          copy.stagger = 0;
          yoyoEase && (copy.yoyoEase = yoyoEase);
          staggerVarsToMerge && _merge(copy, staggerVarsToMerge);
          curTarget = parsedTargets[i]; //don't just copy duration or delay because if they're a string or function, we'd end up in an infinite loop because _isFuncOrString() would evaluate as true in the child tweens, entering this loop, etc. So we parse the value straight from vars and default to 0.

          copy.duration = +_parseFuncOrString(duration, _assertThisInitialized$1(_this3), i, curTarget, parsedTargets);
          copy.delay = (+_parseFuncOrString(delay, _assertThisInitialized$1(_this3), i, curTarget, parsedTargets) || 0) - _this3._delay;

          if (!stagger && l === 1 && copy.delay) {
            // if someone does delay:"random(1, 5)", repeat:-1, for example, the delay shouldn't be inside the repeat.
            _this3._delay = delay = copy.delay;
            _this3._start += delay;
            copy.delay = 0;
          }

          tl.to(curTarget, copy, staggerFunc(i, curTarget, parsedTargets));
        }

        tl.duration() ? duration = delay = 0 : _this3.timeline = 0; // if the timeline's duration is 0, we don't need a timeline internally!
      }

      duration || _this3.duration(duration = tl.duration());
    } else {
      _this3.timeline = 0; //speed optimization, faster lookups (no going up the prototype chain)
    }

    if (overwrite === true && !_suppressOverwrites$1) {
      _overwritingTween = _assertThisInitialized$1(_this3);

      _globalTimeline.killTweensOf(parsedTargets);

      _overwritingTween = 0;
    }

    parent && _postAddChecks(parent, _assertThisInitialized$1(_this3));

    if (immediateRender || !duration && !keyframes && _this3._start === _round$3(parent._time) && _isNotFalse(immediateRender) && _hasNoPausedAncestors(_assertThisInitialized$1(_this3)) && parent.data !== "nested") {
      _this3._tTime = -_tinyNum; //forces a render without having to set the render() "force" parameter to true because we want to allow lazying by default (using the "force" parameter always forces an immediate full render)

      _this3.render(Math.max(0, -delay)); //in case delay is negative

    }

    scrollTrigger && _scrollTrigger(_assertThisInitialized$1(_this3), scrollTrigger);
    return _this3;
  }

  var _proto3 = Tween.prototype;

  _proto3.render = function render(totalTime, suppressEvents, force) {
    var prevTime = this._time,
        tDur = this._tDur,
        dur = this._dur,
        tTime = totalTime > tDur - _tinyNum && totalTime >= 0 ? tDur : totalTime < _tinyNum ? 0 : totalTime,
        time,
        pt,
        iteration,
        cycleDuration,
        prevIteration,
        isYoyo,
        ratio,
        timeline,
        yoyoEase;

    if (!dur) {
      _renderZeroDurationTween(this, totalTime, suppressEvents, force);
    } else if (tTime !== this._tTime || !totalTime || force || !this._initted && this._tTime || this._startAt && this._zTime < 0 !== totalTime < 0) {
      //this senses if we're crossing over the start time, in which case we must record _zTime and force the render, but we do it in this lengthy conditional way for performance reasons (usually we can skip the calculations): this._initted && (this._zTime < 0) !== (totalTime < 0)
      time = tTime;
      timeline = this.timeline;

      if (this._repeat) {
        //adjust the time for repeats and yoyos
        cycleDuration = dur + this._rDelay;

        if (this._repeat < -1 && totalTime < 0) {
          return this.totalTime(cycleDuration * 100 + totalTime, suppressEvents, force);
        }

        time = _round$3(tTime % cycleDuration); //round to avoid floating point errors. (4 % 0.8 should be 0 but some browsers report it as 0.79999999!)

        if (tTime === tDur) {
          // the tDur === tTime is for edge cases where there's a lengthy decimal on the duration and it may reach the very end but the time is rendered as not-quite-there (remember, tDur is rounded to 4 decimals whereas dur isn't)
          iteration = this._repeat;
          time = dur;
        } else {
          iteration = ~~(tTime / cycleDuration);

          if (iteration && iteration === tTime / cycleDuration) {
            time = dur;
            iteration--;
          }

          time > dur && (time = dur);
        }

        isYoyo = this._yoyo && iteration & 1;

        if (isYoyo) {
          yoyoEase = this._yEase;
          time = dur - time;
        }

        prevIteration = _animationCycle(this._tTime, cycleDuration);

        if (time === prevTime && !force && this._initted) {
          //could be during the repeatDelay part. No need to render and fire callbacks.
          return this;
        }

        if (iteration !== prevIteration) {
          timeline && this._yEase && _propagateYoyoEase(timeline, isYoyo); //repeatRefresh functionality

          if (this.vars.repeatRefresh && !isYoyo && !this._lock) {
            this._lock = force = 1; //force, otherwise if lazy is true, the _attemptInitTween() will return and we'll jump out and get caught bouncing on each tick.

            this.render(_round$3(cycleDuration * iteration), true).invalidate()._lock = 0;
          }
        }
      }

      if (!this._initted) {
        if (_attemptInitTween(this, totalTime < 0 ? totalTime : time, force, suppressEvents)) {
          this._tTime = 0; // in constructor if immediateRender is true, we set _tTime to -_tinyNum to have the playhead cross the starting point but we can't leave _tTime as a negative number.

          return this;
        }

        if (dur !== this._dur) {
          // while initting, a plugin like InertiaPlugin might alter the duration, so rerun from the start to ensure everything renders as it should.
          return this.render(totalTime, suppressEvents, force);
        }
      }

      this._tTime = tTime;
      this._time = time;

      if (!this._act && this._ts) {
        this._act = 1; //as long as it's not paused, force it to be active so that if the user renders independent of the parent timeline, it'll be forced to re-render on the next tick.

        this._lazy = 0;
      }

      this.ratio = ratio = (yoyoEase || this._ease)(time / dur);

      if (this._from) {
        this.ratio = ratio = 1 - ratio;
      }

      time && !prevTime && !suppressEvents && _callback(this, "onStart");
      pt = this._pt;

      while (pt) {
        pt.r(ratio, pt.d);
        pt = pt._next;
      }

      timeline && timeline.render(totalTime < 0 ? totalTime : !time && isYoyo ? -_tinyNum : timeline._dur * ratio, suppressEvents, force) || this._startAt && (this._zTime = totalTime);

      if (this._onUpdate && !suppressEvents) {
        totalTime < 0 && this._startAt && this._startAt.render(totalTime, true, force); //note: for performance reasons, we tuck this conditional logic inside less traveled areas (most tweens don't have an onUpdate). We'd just have it at the end before the onComplete, but the values should be updated before any onUpdate is called, so we ALSO put it here and then if it's not called, we do so later near the onComplete.

        _callback(this, "onUpdate");
      }

      this._repeat && iteration !== prevIteration && this.vars.onRepeat && !suppressEvents && this.parent && _callback(this, "onRepeat");

      if ((tTime === this._tDur || !tTime) && this._tTime === tTime) {
        totalTime < 0 && this._startAt && !this._onUpdate && this._startAt.render(totalTime, true, true);
        (totalTime || !dur) && (tTime === this._tDur && this._ts > 0 || !tTime && this._ts < 0) && _removeFromParent(this, 1); // don't remove if we're rendering at exactly a time of 0, as there could be autoRevert values that should get set on the next tick (if the playhead goes backward beyond the startTime, negative totalTime). Don't remove if the timeline is reversed and the playhead isn't at 0, otherwise tl.progress(1).reverse() won't work. Only remove if the playhead is at the end and timeScale is positive, or if the playhead is at 0 and the timeScale is negative.

        if (!suppressEvents && !(totalTime < 0 && !prevTime) && (tTime || prevTime)) {
          // if prevTime and tTime are zero, we shouldn't fire the onReverseComplete. This could happen if you gsap.to(... {paused:true}).play();
          _callback(this, tTime === tDur ? "onComplete" : "onReverseComplete", true);

          this._prom && !(tTime < tDur && this.timeScale() > 0) && this._prom();
        }
      }
    }

    return this;
  };

  _proto3.targets = function targets() {
    return this._targets;
  };

  _proto3.invalidate = function invalidate() {
    this._pt = this._op = this._startAt = this._onUpdate = this._lazy = this.ratio = 0;
    this._ptLookup = [];
    this.timeline && this.timeline.invalidate();
    return _Animation2.prototype.invalidate.call(this);
  };

  _proto3.kill = function kill(targets, vars) {
    if (vars === void 0) {
      vars = "all";
    }

    if (!targets && (!vars || vars === "all")) {
      this._lazy = this._pt = 0;
      return this.parent ? _interrupt(this) : this;
    }

    if (this.timeline) {
      var tDur = this.timeline.totalDuration();
      this.timeline.killTweensOf(targets, vars, _overwritingTween && _overwritingTween.vars.overwrite !== true)._first || _interrupt(this); // if nothing is left tweening, interrupt.

      this.parent && tDur !== this.timeline.totalDuration() && _setDuration(this, this._dur * this.timeline._tDur / tDur, 0, 1); // if a nested tween is killed that changes the duration, it should affect this tween's duration. We must use the ratio, though, because sometimes the internal timeline is stretched like for keyframes where they don't all add up to whatever the parent tween's duration was set to.

      return this;
    }

    var parsedTargets = this._targets,
        killingTargets = targets ? toArray(targets) : parsedTargets,
        propTweenLookup = this._ptLookup,
        firstPT = this._pt,
        overwrittenProps,
        curLookup,
        curOverwriteProps,
        props,
        p,
        pt,
        i;

    if ((!vars || vars === "all") && _arraysMatch(parsedTargets, killingTargets)) {
      vars === "all" && (this._pt = 0);
      return _interrupt(this);
    }

    overwrittenProps = this._op = this._op || [];

    if (vars !== "all") {
      //so people can pass in a comma-delimited list of property names
      if (_isString$4(vars)) {
        p = {};

        _forEachName(vars, function (name) {
          return p[name] = 1;
        });

        vars = p;
      }

      vars = _addAliasesToVars(parsedTargets, vars);
    }

    i = parsedTargets.length;

    while (i--) {
      if (~killingTargets.indexOf(parsedTargets[i])) {
        curLookup = propTweenLookup[i];

        if (vars === "all") {
          overwrittenProps[i] = vars;
          props = curLookup;
          curOverwriteProps = {};
        } else {
          curOverwriteProps = overwrittenProps[i] = overwrittenProps[i] || {};
          props = vars;
        }

        for (p in props) {
          pt = curLookup && curLookup[p];

          if (pt) {
            if (!("kill" in pt.d) || pt.d.kill(p) === true) {
              _removeLinkedListItem(this, pt, "_pt");
            }

            delete curLookup[p];
          }

          if (curOverwriteProps !== "all") {
            curOverwriteProps[p] = 1;
          }
        }
      }
    }

    this._initted && !this._pt && firstPT && _interrupt(this); //if all tweening properties are killed, kill the tween. Without this line, if there's a tween with multiple targets and then you killTweensOf() each target individually, the tween would technically still remain active and fire its onComplete even though there aren't any more properties tweening.

    return this;
  };

  Tween.to = function to(targets, vars) {
    return new Tween(targets, vars, arguments[2]);
  };

  Tween.from = function from(targets, vars) {
    return new Tween(targets, _parseVars(arguments, 1));
  };

  Tween.delayedCall = function delayedCall(delay, callback, params, scope) {
    return new Tween(callback, 0, {
      immediateRender: false,
      lazy: false,
      overwrite: false,
      delay: delay,
      onComplete: callback,
      onReverseComplete: callback,
      onCompleteParams: params,
      onReverseCompleteParams: params,
      callbackScope: scope
    });
  };

  Tween.fromTo = function fromTo(targets, fromVars, toVars) {
    return new Tween(targets, _parseVars(arguments, 2));
  };

  Tween.set = function set(targets, vars) {
    vars.duration = 0;
    vars.repeatDelay || (vars.repeat = 0);
    return new Tween(targets, vars);
  };

  Tween.killTweensOf = function killTweensOf(targets, props, onlyActive) {
    return _globalTimeline.killTweensOf(targets, props, onlyActive);
  };

  return Tween;
}(Animation);

_setDefaults$2(Tween.prototype, {
  _targets: [],
  _lazy: 0,
  _startAt: 0,
  _op: 0,
  _onInit: 0
}); //add the pertinent timeline methods to Tween instances so that users can chain conveniently and create a timeline automatically. (removed due to concerns that it'd ultimately add to more confusion especially for beginners)
// _forEachName("to,from,fromTo,set,call,add,addLabel,addPause", name => {
// 	Tween.prototype[name] = function() {
// 		let tl = new Timeline();
// 		return _addToTimeline(tl, this)[name].apply(tl, toArray(arguments));
// 	}
// });
//for backward compatibility. Leverage the timeline calls.


_forEachName("staggerTo,staggerFrom,staggerFromTo", function (name) {
  Tween[name] = function () {
    var tl = new Timeline(),
        params = _slice.call(arguments, 0);

    params.splice(name === "staggerFromTo" ? 5 : 4, 0, 0);
    return tl[name].apply(tl, params);
  };
});
/*
 * --------------------------------------------------------------------------------------
 * PROPTWEEN
 * --------------------------------------------------------------------------------------
 */


var _setterPlain = function _setterPlain(target, property, value) {
  return target[property] = value;
},
    _setterFunc = function _setterFunc(target, property, value) {
  return target[property](value);
},
    _setterFuncWithParam = function _setterFuncWithParam(target, property, value, data) {
  return target[property](data.fp, value);
},
    _setterAttribute = function _setterAttribute(target, property, value) {
  return target.setAttribute(property, value);
},
    _getSetter$1 = function _getSetter(target, property) {
  return _isFunction$4(target[property]) ? _setterFunc : _isUndefined$2(target[property]) && target.setAttribute ? _setterAttribute : _setterPlain;
},
    _renderPlain = function _renderPlain(ratio, data) {
  return data.set(data.t, data.p, Math.round((data.s + data.c * ratio) * 10000) / 10000, data);
},
    _renderBoolean = function _renderBoolean(ratio, data) {
  return data.set(data.t, data.p, !!(data.s + data.c * ratio), data);
},
    _renderComplexString = function _renderComplexString(ratio, data) {
  var pt = data._pt,
      s = "";

  if (!ratio && data.b) {
    //b = beginning string
    s = data.b;
  } else if (ratio === 1 && data.e) {
    //e = ending string
    s = data.e;
  } else {
    while (pt) {
      s = pt.p + (pt.m ? pt.m(pt.s + pt.c * ratio) : Math.round((pt.s + pt.c * ratio) * 10000) / 10000) + s; //we use the "p" property for the text inbetween (like a suffix). And in the context of a complex string, the modifier (m) is typically just Math.round(), like for RGB colors.

      pt = pt._next;
    }

    s += data.c; //we use the "c" of the PropTween to store the final chunk of non-numeric text.
  }

  data.set(data.t, data.p, s, data);
},
    _renderPropTweens = function _renderPropTweens(ratio, data) {
  var pt = data._pt;

  while (pt) {
    pt.r(ratio, pt.d);
    pt = pt._next;
  }
},
    _addPluginModifier = function _addPluginModifier(modifier, tween, target, property) {
  var pt = this._pt,
      next;

  while (pt) {
    next = pt._next;
    pt.p === property && pt.modifier(modifier, tween, target);
    pt = next;
  }
},
    _killPropTweensOf = function _killPropTweensOf(property) {
  var pt = this._pt,
      hasNonDependentRemaining,
      next;

  while (pt) {
    next = pt._next;

    if (pt.p === property && !pt.op || pt.op === property) {
      _removeLinkedListItem(this, pt, "_pt");
    } else if (!pt.dep) {
      hasNonDependentRemaining = 1;
    }

    pt = next;
  }

  return !hasNonDependentRemaining;
},
    _setterWithModifier = function _setterWithModifier(target, property, value, data) {
  data.mSet(target, property, data.m.call(data.tween, value, data.mt), data);
},
    _sortPropTweensByPriority = function _sortPropTweensByPriority(parent) {
  var pt = parent._pt,
      next,
      pt2,
      first,
      last; //sorts the PropTween linked list in order of priority because some plugins need to do their work after ALL of the PropTweens were created (like RoundPropsPlugin and ModifiersPlugin)

  while (pt) {
    next = pt._next;
    pt2 = first;

    while (pt2 && pt2.pr > pt.pr) {
      pt2 = pt2._next;
    }

    if (pt._prev = pt2 ? pt2._prev : last) {
      pt._prev._next = pt;
    } else {
      first = pt;
    }

    if (pt._next = pt2) {
      pt2._prev = pt;
    } else {
      last = pt;
    }

    pt = next;
  }

  parent._pt = first;
}; //PropTween key: t = target, p = prop, r = renderer, d = data, s = start, c = change, op = overwriteProperty (ONLY populated when it's different than p), pr = priority, _next/_prev for the linked list siblings, set = setter, m = modifier, mSet = modifierSetter (the original setter, before a modifier was added)


var PropTween$2 = /*#__PURE__*/function () {
  function PropTween(next, target, prop, start, change, renderer, data, setter, priority) {
    this.t = target;
    this.s = start;
    this.c = change;
    this.p = prop;
    this.r = renderer || _renderPlain;
    this.d = data || this;
    this.set = setter || _setterPlain;
    this.pr = priority || 0;
    this._next = next;

    if (next) {
      next._prev = this;
    }
  }

  var _proto4 = PropTween.prototype;

  _proto4.modifier = function modifier(func, tween, target) {
    this.mSet = this.mSet || this.set; //in case it was already set (a PropTween can only have one modifier)

    this.set = _setterWithModifier;
    this.m = func;
    this.mt = target; //modifier target

    this.tween = tween;
  };

  return PropTween;
}(); //Initialization tasks

_forEachName(_callbackNames + "parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger", function (name) {
  return _reservedProps[name] = 1;
});

_globals.TweenMax = _globals.TweenLite = Tween;
_globals.TimelineLite = _globals.TimelineMax = Timeline;
_globalTimeline = new Timeline({
  sortChildren: false,
  defaults: _defaults$1,
  autoRemoveChildren: true,
  id: "root",
  smoothChildTiming: true
});
_config$1.stringFilter = _colorStringFilter;
/*
 * --------------------------------------------------------------------------------------
 * GSAP
 * --------------------------------------------------------------------------------------
 */

var _gsap = {
  registerPlugin: function registerPlugin() {
    for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
      args[_key2] = arguments[_key2];
    }

    args.forEach(function (config) {
      return _createPlugin(config);
    });
  },
  timeline: function timeline(vars) {
    return new Timeline(vars);
  },
  getTweensOf: function getTweensOf(targets, onlyActive) {
    return _globalTimeline.getTweensOf(targets, onlyActive);
  },
  getProperty: function getProperty(target, property, unit, uncache) {
    _isString$4(target) && (target = toArray(target)[0]); //in case selector text or an array is passed in

    var getter = _getCache(target || {}).get,
        format = unit ? _passThrough$1 : _numericIfPossible;

    unit === "native" && (unit = "");
    return !target ? target : !property ? function (property, unit, uncache) {
      return format((_plugins[property] && _plugins[property].get || getter)(target, property, unit, uncache));
    } : format((_plugins[property] && _plugins[property].get || getter)(target, property, unit, uncache));
  },
  quickSetter: function quickSetter(target, property, unit) {
    target = toArray(target);

    if (target.length > 1) {
      var setters = target.map(function (t) {
        return gsap$9.quickSetter(t, property, unit);
      }),
          l = setters.length;
      return function (value) {
        var i = l;

        while (i--) {
          setters[i](value);
        }
      };
    }

    target = target[0] || {};

    var Plugin = _plugins[property],
        cache = _getCache(target),
        p = cache.harness && (cache.harness.aliases || {})[property] || property,
        // in case it's an alias, like "rotate" for "rotation".
    setter = Plugin ? function (value) {
      var p = new Plugin();
      _quickTween._pt = 0;
      p.init(target, unit ? value + unit : value, _quickTween, 0, [target]);
      p.render(1, p);
      _quickTween._pt && _renderPropTweens(1, _quickTween);
    } : cache.set(target, p);

    return Plugin ? setter : function (value) {
      return setter(target, p, unit ? value + unit : value, cache, 1);
    };
  },
  isTweening: function isTweening(targets) {
    return _globalTimeline.getTweensOf(targets, true).length > 0;
  },
  defaults: function defaults(value) {
    value && value.ease && (value.ease = _parseEase(value.ease, _defaults$1.ease));
    return _mergeDeep(_defaults$1, value || {});
  },
  config: function config(value) {
    return _mergeDeep(_config$1, value || {});
  },
  registerEffect: function registerEffect(_ref2) {
    var name = _ref2.name,
        effect = _ref2.effect,
        plugins = _ref2.plugins,
        defaults = _ref2.defaults,
        extendTimeline = _ref2.extendTimeline;
    (plugins || "").split(",").forEach(function (pluginName) {
      return pluginName && !_plugins[pluginName] && !_globals[pluginName] && _warn$2(name + " effect requires " + pluginName + " plugin.");
    });

    _effects[name] = function (targets, vars, tl) {
      return effect(toArray(targets), _setDefaults$2(vars || {}, defaults), tl);
    };

    if (extendTimeline) {
      Timeline.prototype[name] = function (targets, vars, position) {
        return this.add(_effects[name](targets, _isObject$2(vars) ? vars : (position = vars) && {}, this), position);
      };
    }
  },
  registerEase: function registerEase(name, ease) {
    _easeMap[name] = _parseEase(ease);
  },
  parseEase: function parseEase(ease, defaultEase) {
    return arguments.length ? _parseEase(ease, defaultEase) : _easeMap;
  },
  getById: function getById(id) {
    return _globalTimeline.getById(id);
  },
  exportRoot: function exportRoot(vars, includeDelayedCalls) {
    if (vars === void 0) {
      vars = {};
    }

    var tl = new Timeline(vars),
        child,
        next;
    tl.smoothChildTiming = _isNotFalse(vars.smoothChildTiming);

    _globalTimeline.remove(tl);

    tl._dp = 0; //otherwise it'll get re-activated when adding children and be re-introduced into _globalTimeline's linked list (then added to itself).

    tl._time = tl._tTime = _globalTimeline._time;
    child = _globalTimeline._first;

    while (child) {
      next = child._next;

      if (includeDelayedCalls || !(!child._dur && child instanceof Tween && child.vars.onComplete === child._targets[0])) {
        _addToTimeline(tl, child, child._start - child._delay);
      }

      child = next;
    }

    _addToTimeline(_globalTimeline, tl, 0);

    return tl;
  },
  utils: {
    wrap: wrap,
    wrapYoyo: wrapYoyo,
    distribute: distribute,
    random: random,
    snap: snap,
    normalize: normalize,
    getUnit: getUnit,
    clamp: clamp,
    splitColor: splitColor,
    toArray: toArray,
    mapRange: mapRange,
    pipe: pipe,
    unitize: unitize,
    interpolate: interpolate,
    shuffle: shuffle
  },
  install: _install,
  effects: _effects,
  ticker: _ticker,
  updateRoot: Timeline.updateRoot,
  plugins: _plugins,
  globalTimeline: _globalTimeline,
  core: {
    PropTween: PropTween$2,
    globals: _addGlobal,
    Tween: Tween,
    Timeline: Timeline,
    Animation: Animation,
    getCache: _getCache,
    _removeLinkedListItem: _removeLinkedListItem,
    suppressOverwrites: function suppressOverwrites(value) {
      return _suppressOverwrites$1 = value;
    }
  }
};

_forEachName("to,from,fromTo,delayedCall,set,killTweensOf", function (name) {
  return _gsap[name] = Tween[name];
});

_ticker.add(Timeline.updateRoot);

_quickTween = _gsap.to({}, {
  duration: 0
}); // ---- EXTRA PLUGINS --------------------------------------------------------

var _getPluginPropTween = function _getPluginPropTween(plugin, prop) {
  var pt = plugin._pt;

  while (pt && pt.p !== prop && pt.op !== prop && pt.fp !== prop) {
    pt = pt._next;
  }

  return pt;
},
    _addModifiers = function _addModifiers(tween, modifiers) {
  var targets = tween._targets,
      p,
      i,
      pt;

  for (p in modifiers) {
    i = targets.length;

    while (i--) {
      pt = tween._ptLookup[i][p];

      if (pt && (pt = pt.d)) {
        if (pt._pt) {
          // is a plugin
          pt = _getPluginPropTween(pt, p);
        }

        pt && pt.modifier && pt.modifier(modifiers[p], tween, targets[i], p);
      }
    }
  }
},
    _buildModifierPlugin = function _buildModifierPlugin(name, modifier) {
  return {
    name: name,
    rawVars: 1,
    //don't pre-process function-based values or "random()" strings.
    init: function init(target, vars, tween) {
      tween._onInit = function (tween) {
        var temp, p;

        if (_isString$4(vars)) {
          temp = {};

          _forEachName(vars, function (name) {
            return temp[name] = 1;
          }); //if the user passes in a comma-delimited list of property names to roundProps, like "x,y", we round to whole numbers.


          vars = temp;
        }

        if (modifier) {
          temp = {};

          for (p in vars) {
            temp[p] = modifier(vars[p]);
          }

          vars = temp;
        }

        _addModifiers(tween, vars);
      };
    }
  };
}; //register core plugins


var gsap$9 = _gsap.registerPlugin({
  name: "attr",
  init: function init(target, vars, tween, index, targets) {
    var p, pt;

    for (p in vars) {
      pt = this.add(target, "setAttribute", (target.getAttribute(p) || 0) + "", vars[p], index, targets, 0, 0, p);
      pt && (pt.op = p);

      this._props.push(p);
    }
  }
}, {
  name: "endArray",
  init: function init(target, value) {
    var i = value.length;

    while (i--) {
      this.add(target, i, target[i] || 0, value[i]);
    }
  }
}, _buildModifierPlugin("roundProps", _roundModifier), _buildModifierPlugin("modifiers"), _buildModifierPlugin("snap", snap)) || _gsap; //to prevent the core plugins from being dropped via aggressive tree shaking, we must include them in the variable declaration in this way.

Tween.version = Timeline.version = gsap$9.version = "3.6.1";
_coreReady = 1;

if (_windowExists$7()) {
  _wake();
}

var Power1 = _easeMap.Power1;

/*!
 * CSSPlugin 3.6.1
 * https://greensock.com
 *
 * Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

var _win$5,
    _doc$4,
    _docElement$2,
    _pluginInitted,
    _tempDiv$2,
    _recentSetterPlugin,
    _windowExists$6 = function _windowExists() {
  return typeof window !== "undefined";
},
    _transformProps = {},
    _RAD2DEG$2 = 180 / Math.PI,
    _DEG2RAD$3 = Math.PI / 180,
    _atan2$1 = Math.atan2,
    _bigNum$1 = 1e8,
    _capsExp$1 = /([A-Z])/g,
    _horizontalExp = /(?:left|right|width|margin|padding|x)/i,
    _complexExp = /[\s,\(]\S/,
    _propertyAliases = {
  autoAlpha: "opacity,visibility",
  scale: "scaleX,scaleY",
  alpha: "opacity"
},
    _renderCSSProp = function _renderCSSProp(ratio, data) {
  return data.set(data.t, data.p, Math.round((data.s + data.c * ratio) * 10000) / 10000 + data.u, data);
},
    _renderPropWithEnd$1 = function _renderPropWithEnd(ratio, data) {
  return data.set(data.t, data.p, ratio === 1 ? data.e : Math.round((data.s + data.c * ratio) * 10000) / 10000 + data.u, data);
},
    _renderCSSPropWithBeginning = function _renderCSSPropWithBeginning(ratio, data) {
  return data.set(data.t, data.p, ratio ? Math.round((data.s + data.c * ratio) * 10000) / 10000 + data.u : data.b, data);
},
    //if units change, we need a way to render the original unit/value when the tween goes all the way back to the beginning (ratio:0)
_renderRoundedCSSProp = function _renderRoundedCSSProp(ratio, data) {
  var value = data.s + data.c * ratio;
  data.set(data.t, data.p, ~~(value + (value < 0 ? -.5 : .5)) + data.u, data);
},
    _renderNonTweeningValue = function _renderNonTweeningValue(ratio, data) {
  return data.set(data.t, data.p, ratio ? data.e : data.b, data);
},
    _renderNonTweeningValueOnlyAtEnd = function _renderNonTweeningValueOnlyAtEnd(ratio, data) {
  return data.set(data.t, data.p, ratio !== 1 ? data.b : data.e, data);
},
    _setterCSSStyle = function _setterCSSStyle(target, property, value) {
  return target.style[property] = value;
},
    _setterCSSProp = function _setterCSSProp(target, property, value) {
  return target.style.setProperty(property, value);
},
    _setterTransform = function _setterTransform(target, property, value) {
  return target._gsap[property] = value;
},
    _setterScale = function _setterScale(target, property, value) {
  return target._gsap.scaleX = target._gsap.scaleY = value;
},
    _setterScaleWithRender = function _setterScaleWithRender(target, property, value, data, ratio) {
  var cache = target._gsap;
  cache.scaleX = cache.scaleY = value;
  cache.renderTransform(ratio, cache);
},
    _setterTransformWithRender = function _setterTransformWithRender(target, property, value, data, ratio) {
  var cache = target._gsap;
  cache[property] = value;
  cache.renderTransform(ratio, cache);
},
    _transformProp$3 = "transform",
    _transformOriginProp$2 = _transformProp$3 + "Origin",
    _supports3D$1,
    _createElement$1 = function _createElement(type, ns) {
  var e = _doc$4.createElementNS ? _doc$4.createElementNS((ns || "http://www.w3.org/1999/xhtml").replace(/^https/, "http"), type) : _doc$4.createElement(type); //some servers swap in https for http in the namespace which can break things, making "style" inaccessible.

  return e.style ? e : _doc$4.createElement(type); //some environments won't allow access to the element's style when created with a namespace in which case we default to the standard createElement() to work around the issue. Also note that when GSAP is embedded directly inside an SVG file, createElement() won't allow access to the style object in Firefox (see https://greensock.com/forums/topic/20215-problem-using-tweenmax-in-standalone-self-containing-svg-file-err-cannot-set-property-csstext-of-undefined/).
},
    _getComputedProperty = function _getComputedProperty(target, property, skipPrefixFallback) {
  var cs = getComputedStyle(target);
  return cs[property] || cs.getPropertyValue(property.replace(_capsExp$1, "-$1").toLowerCase()) || cs.getPropertyValue(property) || !skipPrefixFallback && _getComputedProperty(target, _checkPropPrefix(property) || property, 1) || ""; //css variables may not need caps swapped out for dashes and lowercase.
},
    _prefixes = "O,Moz,ms,Ms,Webkit".split(","),
    _checkPropPrefix = function _checkPropPrefix(property, element, preferPrefix) {
  var e = element || _tempDiv$2,
      s = e.style,
      i = 5;

  if (property in s && !preferPrefix) {
    return property;
  }

  property = property.charAt(0).toUpperCase() + property.substr(1);

  while (i-- && !(_prefixes[i] + property in s)) {}

  return i < 0 ? null : (i === 3 ? "ms" : i >= 0 ? _prefixes[i] : "") + property;
},
    _initCore$6 = function _initCore() {
  if (_windowExists$6() && window.document) {
    _win$5 = window;
    _doc$4 = _win$5.document;
    _docElement$2 = _doc$4.documentElement;
    _tempDiv$2 = _createElement$1("div") || {
      style: {}
    };
    _createElement$1("div");
    _transformProp$3 = _checkPropPrefix(_transformProp$3);
    _transformOriginProp$2 = _transformProp$3 + "Origin";
    _tempDiv$2.style.cssText = "border-width:0;line-height:0;position:absolute;padding:0"; //make sure to override certain properties that may contaminate measurements, in case the user has overreaching style sheets.

    _supports3D$1 = !!_checkPropPrefix("perspective");
    _pluginInitted = 1;
  }
},
    _getBBoxHack = function _getBBoxHack(swapIfPossible) {
  //works around issues in some browsers (like Firefox) that don't correctly report getBBox() on SVG elements inside a <defs> element and/or <mask>. We try creating an SVG, adding it to the documentElement and toss the element in there so that it's definitely part of the rendering tree, then grab the bbox and if it works, we actually swap out the original getBBox() method for our own that does these extra steps whenever getBBox is needed. This helps ensure that performance is optimal (only do all these extra steps when absolutely necessary...most elements don't need it).
  var svg = _createElement$1("svg", this.ownerSVGElement && this.ownerSVGElement.getAttribute("xmlns") || "http://www.w3.org/2000/svg"),
      oldParent = this.parentNode,
      oldSibling = this.nextSibling,
      oldCSS = this.style.cssText,
      bbox;

  _docElement$2.appendChild(svg);

  svg.appendChild(this);
  this.style.display = "block";

  if (swapIfPossible) {
    try {
      bbox = this.getBBox();
      this._gsapBBox = this.getBBox; //store the original

      this.getBBox = _getBBoxHack;
    } catch (e) {}
  } else if (this._gsapBBox) {
    bbox = this._gsapBBox();
  }

  if (oldParent) {
    if (oldSibling) {
      oldParent.insertBefore(this, oldSibling);
    } else {
      oldParent.appendChild(this);
    }
  }

  _docElement$2.removeChild(svg);

  this.style.cssText = oldCSS;
  return bbox;
},
    _getAttributeFallbacks = function _getAttributeFallbacks(target, attributesArray) {
  var i = attributesArray.length;

  while (i--) {
    if (target.hasAttribute(attributesArray[i])) {
      return target.getAttribute(attributesArray[i]);
    }
  }
},
    _getBBox = function _getBBox(target) {
  var bounds;

  try {
    bounds = target.getBBox(); //Firefox throws errors if you try calling getBBox() on an SVG element that's not rendered (like in a <symbol> or <defs>). https://bugzilla.mozilla.org/show_bug.cgi?id=612118
  } catch (error) {
    bounds = _getBBoxHack.call(target, true);
  }

  bounds && (bounds.width || bounds.height) || target.getBBox === _getBBoxHack || (bounds = _getBBoxHack.call(target, true)); //some browsers (like Firefox) misreport the bounds if the element has zero width and height (it just assumes it's at x:0, y:0), thus we need to manually grab the position in that case.

  return bounds && !bounds.width && !bounds.x && !bounds.y ? {
    x: +_getAttributeFallbacks(target, ["x", "cx", "x1"]) || 0,
    y: +_getAttributeFallbacks(target, ["y", "cy", "y1"]) || 0,
    width: 0,
    height: 0
  } : bounds;
},
    _isSVG = function _isSVG(e) {
  return !!(e.getCTM && (!e.parentNode || e.ownerSVGElement) && _getBBox(e));
},
    //reports if the element is an SVG on which getBBox() actually works
_removeProperty = function _removeProperty(target, property) {
  if (property) {
    var style = target.style;

    if (property in _transformProps && property !== _transformOriginProp$2) {
      property = _transformProp$3;
    }

    if (style.removeProperty) {
      if (property.substr(0, 2) === "ms" || property.substr(0, 6) === "webkit") {
        //Microsoft and some Webkit browsers don't conform to the standard of capitalizing the first prefix character, so we adjust so that when we prefix the caps with a dash, it's correct (otherwise it'd be "ms-transform" instead of "-ms-transform" for IE9, for example)
        property = "-" + property;
      }

      style.removeProperty(property.replace(_capsExp$1, "-$1").toLowerCase());
    } else {
      //note: old versions of IE use "removeAttribute()" instead of "removeProperty()"
      style.removeAttribute(property);
    }
  }
},
    _addNonTweeningPT = function _addNonTweeningPT(plugin, target, property, beginning, end, onlySetAtEnd) {
  var pt = new PropTween$2(plugin._pt, target, property, 0, 1, onlySetAtEnd ? _renderNonTweeningValueOnlyAtEnd : _renderNonTweeningValue);
  plugin._pt = pt;
  pt.b = beginning;
  pt.e = end;

  plugin._props.push(property);

  return pt;
},
    _nonConvertibleUnits = {
  deg: 1,
  rad: 1,
  turn: 1
},
    //takes a single value like 20px and converts it to the unit specified, like "%", returning only the numeric amount.
_convertToUnit = function _convertToUnit(target, property, value, unit) {
  var curValue = parseFloat(value) || 0,
      curUnit = (value + "").trim().substr((curValue + "").length) || "px",
      // some browsers leave extra whitespace at the beginning of CSS variables, hence the need to trim()
  style = _tempDiv$2.style,
      horizontal = _horizontalExp.test(property),
      isRootSVG = target.tagName.toLowerCase() === "svg",
      measureProperty = (isRootSVG ? "client" : "offset") + (horizontal ? "Width" : "Height"),
      amount = 100,
      toPixels = unit === "px",
      toPercent = unit === "%",
      px,
      parent,
      cache,
      isSVG;

  if (unit === curUnit || !curValue || _nonConvertibleUnits[unit] || _nonConvertibleUnits[curUnit]) {
    return curValue;
  }

  curUnit !== "px" && !toPixels && (curValue = _convertToUnit(target, property, value, "px"));
  isSVG = target.getCTM && _isSVG(target);

  if ((toPercent || curUnit === "%") && (_transformProps[property] || ~property.indexOf("adius"))) {
    px = isSVG ? target.getBBox()[horizontal ? "width" : "height"] : target[measureProperty];
    return _round$3(toPercent ? curValue / px * amount : curValue / 100 * px);
  }

  style[horizontal ? "width" : "height"] = amount + (toPixels ? curUnit : unit);
  parent = ~property.indexOf("adius") || unit === "em" && target.appendChild && !isRootSVG ? target : target.parentNode;

  if (isSVG) {
    parent = (target.ownerSVGElement || {}).parentNode;
  }

  if (!parent || parent === _doc$4 || !parent.appendChild) {
    parent = _doc$4.body;
  }

  cache = parent._gsap;

  if (cache && toPercent && cache.width && horizontal && cache.time === _ticker.time) {
    return _round$3(curValue / cache.width * amount);
  } else {
    (toPercent || curUnit === "%") && (style.position = _getComputedProperty(target, "position"));
    parent === target && (style.position = "static"); // like for borderRadius, if it's a % we must have it relative to the target itself but that may not have position: relative or position: absolute in which case it'd go up the chain until it finds its offsetParent (bad). position: static protects against that.

    parent.appendChild(_tempDiv$2);
    px = _tempDiv$2[measureProperty];
    parent.removeChild(_tempDiv$2);
    style.position = "absolute";

    if (horizontal && toPercent) {
      cache = _getCache(parent);
      cache.time = _ticker.time;
      cache.width = parent[measureProperty];
    }
  }

  return _round$3(toPixels ? px * curValue / amount : px && curValue ? amount / px * curValue : 0);
},
    _get = function _get(target, property, unit, uncache) {
  var value;
  _pluginInitted || _initCore$6();

  if (property in _propertyAliases && property !== "transform") {
    property = _propertyAliases[property];

    if (~property.indexOf(",")) {
      property = property.split(",")[0];
    }
  }

  if (_transformProps[property] && property !== "transform") {
    value = _parseTransform(target, uncache);
    value = property !== "transformOrigin" ? value[property] : _firstTwoOnly(_getComputedProperty(target, _transformOriginProp$2)) + " " + value.zOrigin + "px";
  } else {
    value = target.style[property];

    if (!value || value === "auto" || uncache || ~(value + "").indexOf("calc(")) {
      value = _specialProps[property] && _specialProps[property](target, property, unit) || _getComputedProperty(target, property) || _getProperty(target, property) || (property === "opacity" ? 1 : 0); // note: some browsers, like Firefox, don't report borderRadius correctly! Instead, it only reports every corner like  borderTopLeftRadius
    }
  }

  return unit && !~(value + "").trim().indexOf(" ") ? _convertToUnit(target, property, value, unit) + unit : value;
},
    _tweenComplexCSSString = function _tweenComplexCSSString(target, prop, start, end) {
  //note: we call _tweenComplexCSSString.call(pluginInstance...) to ensure that it's scoped properly. We may call it from within a plugin too, thus "this" would refer to the plugin.
  if (!start || start === "none") {
    // some browsers like Safari actually PREFER the prefixed property and mis-report the unprefixed value like clipPath (BUG). In other words, even though clipPath exists in the style ("clipPath" in target.style) and it's set in the CSS properly (along with -webkit-clip-path), Safari reports clipPath as "none" whereas WebkitClipPath reports accurately like "ellipse(100% 0% at 50% 0%)", so in this case we must SWITCH to using the prefixed property instead. See https://greensock.com/forums/topic/18310-clippath-doesnt-work-on-ios/
    var p = _checkPropPrefix(prop, target, 1),
        s = p && _getComputedProperty(target, p, 1);

    if (s && s !== start) {
      prop = p;
      start = s;
    } else if (prop === "borderColor") {
      start = _getComputedProperty(target, "borderTopColor"); // Firefox bug: always reports "borderColor" as "", so we must fall back to borderTopColor. See https://greensock.com/forums/topic/24583-how-to-return-colors-that-i-had-after-reverse/
    }
  }

  var pt = new PropTween$2(this._pt, target.style, prop, 0, 1, _renderComplexString),
      index = 0,
      matchIndex = 0,
      a,
      result,
      startValues,
      startNum,
      color,
      startValue,
      endValue,
      endNum,
      chunk,
      endUnit,
      startUnit,
      relative,
      endValues;
  pt.b = start;
  pt.e = end;
  start += ""; //ensure values are strings

  end += "";

  if (end === "auto") {
    target.style[prop] = end;
    end = _getComputedProperty(target, prop) || end;
    target.style[prop] = start;
  }

  a = [start, end];

  _colorStringFilter(a); //pass an array with the starting and ending values and let the filter do whatever it needs to the values. If colors are found, it returns true and then we must match where the color shows up order-wise because for things like boxShadow, sometimes the browser provides the computed values with the color FIRST, but the user provides it with the color LAST, so flip them if necessary. Same for drop-shadow().


  start = a[0];
  end = a[1];
  startValues = start.match(_numWithUnitExp) || [];
  endValues = end.match(_numWithUnitExp) || [];

  if (endValues.length) {
    while (result = _numWithUnitExp.exec(end)) {
      endValue = result[0];
      chunk = end.substring(index, result.index);

      if (color) {
        color = (color + 1) % 5;
      } else if (chunk.substr(-5) === "rgba(" || chunk.substr(-5) === "hsla(") {
        color = 1;
      }

      if (endValue !== (startValue = startValues[matchIndex++] || "")) {
        startNum = parseFloat(startValue) || 0;
        startUnit = startValue.substr((startNum + "").length);
        relative = endValue.charAt(1) === "=" ? +(endValue.charAt(0) + "1") : 0;

        if (relative) {
          endValue = endValue.substr(2);
        }

        endNum = parseFloat(endValue);
        endUnit = endValue.substr((endNum + "").length);
        index = _numWithUnitExp.lastIndex - endUnit.length;

        if (!endUnit) {
          //if something like "perspective:300" is passed in and we must add a unit to the end
          endUnit = endUnit || _config$1.units[prop] || startUnit;

          if (index === end.length) {
            end += endUnit;
            pt.e += endUnit;
          }
        }

        if (startUnit !== endUnit) {
          startNum = _convertToUnit(target, prop, startValue, endUnit) || 0;
        } //these nested PropTweens are handled in a special way - we'll never actually call a render or setter method on them. We'll just loop through them in the parent complex string PropTween's render method.


        pt._pt = {
          _next: pt._pt,
          p: chunk || matchIndex === 1 ? chunk : ",",
          //note: SVG spec allows omission of comma/space when a negative sign is wedged between two numbers, like 2.5-5.3 instead of 2.5,-5.3 but when tweening, the negative value may switch to positive, so we insert the comma just in case.
          s: startNum,
          c: relative ? relative * endNum : endNum - startNum,
          m: color && color < 4 || prop === "zIndex" ? Math.round : 0
        };
      }
    }

    pt.c = index < end.length ? end.substring(index, end.length) : ""; //we use the "c" of the PropTween to store the final part of the string (after the last number)
  } else {
    pt.r = prop === "display" && end === "none" ? _renderNonTweeningValueOnlyAtEnd : _renderNonTweeningValue;
  }

  _relExp.test(end) && (pt.e = 0); //if the end string contains relative values or dynamic random(...) values, delete the end it so that on the final render we don't actually set it to the string with += or -= characters (forces it to use the calculated value).

  this._pt = pt; //start the linked list with this new PropTween. Remember, we call _tweenComplexCSSString.call(pluginInstance...) to ensure that it's scoped properly. We may call it from within another plugin too, thus "this" would refer to the plugin.

  return pt;
},
    _keywordToPercent = {
  top: "0%",
  bottom: "100%",
  left: "0%",
  right: "100%",
  center: "50%"
},
    _convertKeywordsToPercentages = function _convertKeywordsToPercentages(value) {
  var split = value.split(" "),
      x = split[0],
      y = split[1] || "50%";

  if (x === "top" || x === "bottom" || y === "left" || y === "right") {
    //the user provided them in the wrong order, so flip them
    value = x;
    x = y;
    y = value;
  }

  split[0] = _keywordToPercent[x] || x;
  split[1] = _keywordToPercent[y] || y;
  return split.join(" ");
},
    _renderClearProps = function _renderClearProps(ratio, data) {
  if (data.tween && data.tween._time === data.tween._dur) {
    var target = data.t,
        style = target.style,
        props = data.u,
        cache = target._gsap,
        prop,
        clearTransforms,
        i;

    if (props === "all" || props === true) {
      style.cssText = "";
      clearTransforms = 1;
    } else {
      props = props.split(",");
      i = props.length;

      while (--i > -1) {
        prop = props[i];

        if (_transformProps[prop]) {
          clearTransforms = 1;
          prop = prop === "transformOrigin" ? _transformOriginProp$2 : _transformProp$3;
        }

        _removeProperty(target, prop);
      }
    }

    if (clearTransforms) {
      _removeProperty(target, _transformProp$3);

      if (cache) {
        cache.svg && target.removeAttribute("transform");

        _parseTransform(target, 1); // force all the cached values back to "normal"/identity, otherwise if there's another tween that's already set to render transforms on this element, it could display the wrong values.


        cache.uncache = 1;
      }
    }
  }
},
    // note: specialProps should return 1 if (and only if) they have a non-zero priority. It indicates we need to sort the linked list.
_specialProps = {
  clearProps: function clearProps(plugin, target, property, endValue, tween) {
    if (tween.data !== "isFromStart") {
      var pt = plugin._pt = new PropTween$2(plugin._pt, target, property, 0, 0, _renderClearProps);
      pt.u = endValue;
      pt.pr = -10;
      pt.tween = tween;

      plugin._props.push(property);

      return 1;
    }
  }
  /* className feature (about 0.4kb gzipped).
  , className(plugin, target, property, endValue, tween) {
  	let _renderClassName = (ratio, data) => {
  			data.css.render(ratio, data.css);
  			if (!ratio || ratio === 1) {
  				let inline = data.rmv,
  					target = data.t,
  					p;
  				target.setAttribute("class", ratio ? data.e : data.b);
  				for (p in inline) {
  					_removeProperty(target, p);
  				}
  			}
  		},
  		_getAllStyles = (target) => {
  			let styles = {},
  				computed = getComputedStyle(target),
  				p;
  			for (p in computed) {
  				if (isNaN(p) && p !== "cssText" && p !== "length") {
  					styles[p] = computed[p];
  				}
  			}
  			_setDefaults(styles, _parseTransform(target, 1));
  			return styles;
  		},
  		startClassList = target.getAttribute("class"),
  		style = target.style,
  		cssText = style.cssText,
  		cache = target._gsap,
  		classPT = cache.classPT,
  		inlineToRemoveAtEnd = {},
  		data = {t:target, plugin:plugin, rmv:inlineToRemoveAtEnd, b:startClassList, e:(endValue.charAt(1) !== "=") ? endValue : startClassList.replace(new RegExp("(?:\\s|^)" + endValue.substr(2) + "(?![\\w-])"), "") + ((endValue.charAt(0) === "+") ? " " + endValue.substr(2) : "")},
  		changingVars = {},
  		startVars = _getAllStyles(target),
  		transformRelated = /(transform|perspective)/i,
  		endVars, p;
  	if (classPT) {
  		classPT.r(1, classPT.d);
  		_removeLinkedListItem(classPT.d.plugin, classPT, "_pt");
  	}
  	target.setAttribute("class", data.e);
  	endVars = _getAllStyles(target, true);
  	target.setAttribute("class", startClassList);
  	for (p in endVars) {
  		if (endVars[p] !== startVars[p] && !transformRelated.test(p)) {
  			changingVars[p] = endVars[p];
  			if (!style[p] && style[p] !== "0") {
  				inlineToRemoveAtEnd[p] = 1;
  			}
  		}
  	}
  	cache.classPT = plugin._pt = new PropTween(plugin._pt, target, "className", 0, 0, _renderClassName, data, 0, -11);
  	if (style.cssText !== cssText) { //only apply if things change. Otherwise, in cases like a background-image that's pulled dynamically, it could cause a refresh. See https://greensock.com/forums/topic/20368-possible-gsap-bug-switching-classnames-in-chrome/.
  		style.cssText = cssText; //we recorded cssText before we swapped classes and ran _getAllStyles() because in cases when a className tween is overwritten, we remove all the related tweening properties from that class change (otherwise class-specific stuff can't override properties we've directly set on the target's style object due to specificity).
  	}
  	_parseTransform(target, true); //to clear the caching of transforms
  	data.css = new gsap.plugins.css();
  	data.css.init(target, changingVars, tween);
  	plugin._props.push(...data.css._props);
  	return 1;
  }
  */

},

/*
 * --------------------------------------------------------------------------------------
 * TRANSFORMS
 * --------------------------------------------------------------------------------------
 */
_identity2DMatrix = [1, 0, 0, 1, 0, 0],
    _rotationalProperties = {},
    _isNullTransform = function _isNullTransform(value) {
  return value === "matrix(1, 0, 0, 1, 0, 0)" || value === "none" || !value;
},
    _getComputedTransformMatrixAsArray = function _getComputedTransformMatrixAsArray(target) {
  var matrixString = _getComputedProperty(target, _transformProp$3);

  return _isNullTransform(matrixString) ? _identity2DMatrix : matrixString.substr(7).match(_numExp$1).map(_round$3);
},
    _getMatrix = function _getMatrix(target, force2D) {
  var cache = target._gsap || _getCache(target),
      style = target.style,
      matrix = _getComputedTransformMatrixAsArray(target),
      parent,
      nextSibling,
      temp,
      addedToDOM;

  if (cache.svg && target.getAttribute("transform")) {
    temp = target.transform.baseVal.consolidate().matrix; //ensures that even complex values like "translate(50,60) rotate(135,0,0)" are parsed because it mashes it into a matrix.

    matrix = [temp.a, temp.b, temp.c, temp.d, temp.e, temp.f];
    return matrix.join(",") === "1,0,0,1,0,0" ? _identity2DMatrix : matrix;
  } else if (matrix === _identity2DMatrix && !target.offsetParent && target !== _docElement$2 && !cache.svg) {
    //note: if offsetParent is null, that means the element isn't in the normal document flow, like if it has display:none or one of its ancestors has display:none). Firefox returns null for getComputedStyle() if the element is in an iframe that has display:none. https://bugzilla.mozilla.org/show_bug.cgi?id=548397
    //browsers don't report transforms accurately unless the element is in the DOM and has a display value that's not "none". Firefox and Microsoft browsers have a partial bug where they'll report transforms even if display:none BUT not any percentage-based values like translate(-50%, 8px) will be reported as if it's translate(0, 8px).
    temp = style.display;
    style.display = "block";
    parent = target.parentNode;

    if (!parent || !target.offsetParent) {
      // note: in 3.3.0 we switched target.offsetParent to _doc.body.contains(target) to avoid [sometimes unnecessary] MutationObserver calls but that wasn't adequate because there are edge cases where nested position: fixed elements need to get reparented to accurately sense transforms. See https://github.com/greensock/GSAP/issues/388 and https://github.com/greensock/GSAP/issues/375
      addedToDOM = 1; //flag

      nextSibling = target.nextSibling;

      _docElement$2.appendChild(target); //we must add it to the DOM in order to get values properly

    }

    matrix = _getComputedTransformMatrixAsArray(target);
    temp ? style.display = temp : _removeProperty(target, "display");

    if (addedToDOM) {
      nextSibling ? parent.insertBefore(target, nextSibling) : parent ? parent.appendChild(target) : _docElement$2.removeChild(target);
    }
  }

  return force2D && matrix.length > 6 ? [matrix[0], matrix[1], matrix[4], matrix[5], matrix[12], matrix[13]] : matrix;
},
    _applySVGOrigin = function _applySVGOrigin(target, origin, originIsAbsolute, smooth, matrixArray, pluginToAddPropTweensTo) {
  var cache = target._gsap,
      matrix = matrixArray || _getMatrix(target, true),
      xOriginOld = cache.xOrigin || 0,
      yOriginOld = cache.yOrigin || 0,
      xOffsetOld = cache.xOffset || 0,
      yOffsetOld = cache.yOffset || 0,
      a = matrix[0],
      b = matrix[1],
      c = matrix[2],
      d = matrix[3],
      tx = matrix[4],
      ty = matrix[5],
      originSplit = origin.split(" "),
      xOrigin = parseFloat(originSplit[0]) || 0,
      yOrigin = parseFloat(originSplit[1]) || 0,
      bounds,
      determinant,
      x,
      y;

  if (!originIsAbsolute) {
    bounds = _getBBox(target);
    xOrigin = bounds.x + (~originSplit[0].indexOf("%") ? xOrigin / 100 * bounds.width : xOrigin);
    yOrigin = bounds.y + (~(originSplit[1] || originSplit[0]).indexOf("%") ? yOrigin / 100 * bounds.height : yOrigin);
  } else if (matrix !== _identity2DMatrix && (determinant = a * d - b * c)) {
    //if it's zero (like if scaleX and scaleY are zero), skip it to avoid errors with dividing by zero.
    x = xOrigin * (d / determinant) + yOrigin * (-c / determinant) + (c * ty - d * tx) / determinant;
    y = xOrigin * (-b / determinant) + yOrigin * (a / determinant) - (a * ty - b * tx) / determinant;
    xOrigin = x;
    yOrigin = y;
  }

  if (smooth || smooth !== false && cache.smooth) {
    tx = xOrigin - xOriginOld;
    ty = yOrigin - yOriginOld;
    cache.xOffset = xOffsetOld + (tx * a + ty * c) - tx;
    cache.yOffset = yOffsetOld + (tx * b + ty * d) - ty;
  } else {
    cache.xOffset = cache.yOffset = 0;
  }

  cache.xOrigin = xOrigin;
  cache.yOrigin = yOrigin;
  cache.smooth = !!smooth;
  cache.origin = origin;
  cache.originIsAbsolute = !!originIsAbsolute;
  target.style[_transformOriginProp$2] = "0px 0px"; //otherwise, if someone sets  an origin via CSS, it will likely interfere with the SVG transform attribute ones (because remember, we're baking the origin into the matrix() value).

  if (pluginToAddPropTweensTo) {
    _addNonTweeningPT(pluginToAddPropTweensTo, cache, "xOrigin", xOriginOld, xOrigin);

    _addNonTweeningPT(pluginToAddPropTweensTo, cache, "yOrigin", yOriginOld, yOrigin);

    _addNonTweeningPT(pluginToAddPropTweensTo, cache, "xOffset", xOffsetOld, cache.xOffset);

    _addNonTweeningPT(pluginToAddPropTweensTo, cache, "yOffset", yOffsetOld, cache.yOffset);
  }

  target.setAttribute("data-svg-origin", xOrigin + " " + yOrigin);
},
    _parseTransform = function _parseTransform(target, uncache) {
  var cache = target._gsap || new GSCache(target);

  if ("x" in cache && !uncache && !cache.uncache) {
    return cache;
  }

  var style = target.style,
      invertedScaleX = cache.scaleX < 0,
      px = "px",
      deg = "deg",
      origin = _getComputedProperty(target, _transformOriginProp$2) || "0",
      x,
      y,
      z,
      scaleX,
      scaleY,
      rotation,
      rotationX,
      rotationY,
      skewX,
      skewY,
      perspective,
      xOrigin,
      yOrigin,
      matrix,
      angle,
      cos,
      sin,
      a,
      b,
      c,
      d,
      a12,
      a22,
      t1,
      t2,
      t3,
      a13,
      a23,
      a33,
      a42,
      a43,
      a32;
  x = y = z = rotation = rotationX = rotationY = skewX = skewY = perspective = 0;
  scaleX = scaleY = 1;
  cache.svg = !!(target.getCTM && _isSVG(target));
  matrix = _getMatrix(target, cache.svg);

  if (cache.svg) {
    t1 = !cache.uncache && !uncache && target.getAttribute("data-svg-origin");

    _applySVGOrigin(target, t1 || origin, !!t1 || cache.originIsAbsolute, cache.smooth !== false, matrix);
  }

  xOrigin = cache.xOrigin || 0;
  yOrigin = cache.yOrigin || 0;

  if (matrix !== _identity2DMatrix) {
    a = matrix[0]; //a11

    b = matrix[1]; //a21

    c = matrix[2]; //a31

    d = matrix[3]; //a41

    x = a12 = matrix[4];
    y = a22 = matrix[5]; //2D matrix

    if (matrix.length === 6) {
      scaleX = Math.sqrt(a * a + b * b);
      scaleY = Math.sqrt(d * d + c * c);
      rotation = a || b ? _atan2$1(b, a) * _RAD2DEG$2 : 0; //note: if scaleX is 0, we cannot accurately measure rotation. Same for skewX with a scaleY of 0. Therefore, we default to the previously recorded value (or zero if that doesn't exist).

      skewX = c || d ? _atan2$1(c, d) * _RAD2DEG$2 + rotation : 0;
      skewX && (scaleY *= Math.abs(Math.cos(skewX * _DEG2RAD$3)));

      if (cache.svg) {
        x -= xOrigin - (xOrigin * a + yOrigin * c);
        y -= yOrigin - (xOrigin * b + yOrigin * d);
      } //3D matrix

    } else {
      a32 = matrix[6];
      a42 = matrix[7];
      a13 = matrix[8];
      a23 = matrix[9];
      a33 = matrix[10];
      a43 = matrix[11];
      x = matrix[12];
      y = matrix[13];
      z = matrix[14];
      angle = _atan2$1(a32, a33);
      rotationX = angle * _RAD2DEG$2; //rotationX

      if (angle) {
        cos = Math.cos(-angle);
        sin = Math.sin(-angle);
        t1 = a12 * cos + a13 * sin;
        t2 = a22 * cos + a23 * sin;
        t3 = a32 * cos + a33 * sin;
        a13 = a12 * -sin + a13 * cos;
        a23 = a22 * -sin + a23 * cos;
        a33 = a32 * -sin + a33 * cos;
        a43 = a42 * -sin + a43 * cos;
        a12 = t1;
        a22 = t2;
        a32 = t3;
      } //rotationY


      angle = _atan2$1(-c, a33);
      rotationY = angle * _RAD2DEG$2;

      if (angle) {
        cos = Math.cos(-angle);
        sin = Math.sin(-angle);
        t1 = a * cos - a13 * sin;
        t2 = b * cos - a23 * sin;
        t3 = c * cos - a33 * sin;
        a43 = d * sin + a43 * cos;
        a = t1;
        b = t2;
        c = t3;
      } //rotationZ


      angle = _atan2$1(b, a);
      rotation = angle * _RAD2DEG$2;

      if (angle) {
        cos = Math.cos(angle);
        sin = Math.sin(angle);
        t1 = a * cos + b * sin;
        t2 = a12 * cos + a22 * sin;
        b = b * cos - a * sin;
        a22 = a22 * cos - a12 * sin;
        a = t1;
        a12 = t2;
      }

      if (rotationX && Math.abs(rotationX) + Math.abs(rotation) > 359.9) {
        //when rotationY is set, it will often be parsed as 180 degrees different than it should be, and rotationX and rotation both being 180 (it looks the same), so we adjust for that here.
        rotationX = rotation = 0;
        rotationY = 180 - rotationY;
      }

      scaleX = _round$3(Math.sqrt(a * a + b * b + c * c));
      scaleY = _round$3(Math.sqrt(a22 * a22 + a32 * a32));
      angle = _atan2$1(a12, a22);
      skewX = Math.abs(angle) > 0.0002 ? angle * _RAD2DEG$2 : 0;
      perspective = a43 ? 1 / (a43 < 0 ? -a43 : a43) : 0;
    }

    if (cache.svg) {
      //sense if there are CSS transforms applied on an SVG element in which case we must overwrite them when rendering. The transform attribute is more reliable cross-browser, but we can't just remove the CSS ones because they may be applied in a CSS rule somewhere (not just inline).
      t1 = target.getAttribute("transform");
      cache.forceCSS = target.setAttribute("transform", "") || !_isNullTransform(_getComputedProperty(target, _transformProp$3));
      t1 && target.setAttribute("transform", t1);
    }
  }

  if (Math.abs(skewX) > 90 && Math.abs(skewX) < 270) {
    if (invertedScaleX) {
      scaleX *= -1;
      skewX += rotation <= 0 ? 180 : -180;
      rotation += rotation <= 0 ? 180 : -180;
    } else {
      scaleY *= -1;
      skewX += skewX <= 0 ? 180 : -180;
    }
  }

  cache.x = x - ((cache.xPercent = x && (cache.xPercent || (Math.round(target.offsetWidth / 2) === Math.round(-x) ? -50 : 0))) ? target.offsetWidth * cache.xPercent / 100 : 0) + px;
  cache.y = y - ((cache.yPercent = y && (cache.yPercent || (Math.round(target.offsetHeight / 2) === Math.round(-y) ? -50 : 0))) ? target.offsetHeight * cache.yPercent / 100 : 0) + px;
  cache.z = z + px;
  cache.scaleX = _round$3(scaleX);
  cache.scaleY = _round$3(scaleY);
  cache.rotation = _round$3(rotation) + deg;
  cache.rotationX = _round$3(rotationX) + deg;
  cache.rotationY = _round$3(rotationY) + deg;
  cache.skewX = skewX + deg;
  cache.skewY = skewY + deg;
  cache.transformPerspective = perspective + px;

  if (cache.zOrigin = parseFloat(origin.split(" ")[2]) || 0) {
    style[_transformOriginProp$2] = _firstTwoOnly(origin);
  }

  cache.xOffset = cache.yOffset = 0;
  cache.force3D = _config$1.force3D;
  cache.renderTransform = cache.svg ? _renderSVGTransforms : _supports3D$1 ? _renderCSSTransforms : _renderNon3DTransforms;
  cache.uncache = 0;
  return cache;
},
    _firstTwoOnly = function _firstTwoOnly(value) {
  return (value = value.split(" "))[0] + " " + value[1];
},
    //for handling transformOrigin values, stripping out the 3rd dimension
_addPxTranslate = function _addPxTranslate(target, start, value) {
  var unit = getUnit(start);
  return _round$3(parseFloat(start) + parseFloat(_convertToUnit(target, "x", value + "px", unit))) + unit;
},
    _renderNon3DTransforms = function _renderNon3DTransforms(ratio, cache) {
  cache.z = "0px";
  cache.rotationY = cache.rotationX = "0deg";
  cache.force3D = 0;

  _renderCSSTransforms(ratio, cache);
},
    _zeroDeg = "0deg",
    _zeroPx = "0px",
    _endParenthesis = ") ",
    _renderCSSTransforms = function _renderCSSTransforms(ratio, cache) {
  var _ref = cache || this,
      xPercent = _ref.xPercent,
      yPercent = _ref.yPercent,
      x = _ref.x,
      y = _ref.y,
      z = _ref.z,
      rotation = _ref.rotation,
      rotationY = _ref.rotationY,
      rotationX = _ref.rotationX,
      skewX = _ref.skewX,
      skewY = _ref.skewY,
      scaleX = _ref.scaleX,
      scaleY = _ref.scaleY,
      transformPerspective = _ref.transformPerspective,
      force3D = _ref.force3D,
      target = _ref.target,
      zOrigin = _ref.zOrigin,
      transforms = "",
      use3D = force3D === "auto" && ratio && ratio !== 1 || force3D === true; // Safari has a bug that causes it not to render 3D transform-origin values properly, so we force the z origin to 0, record it in the cache, and then do the math here to offset the translate values accordingly (basically do the 3D transform-origin part manually)


  if (zOrigin && (rotationX !== _zeroDeg || rotationY !== _zeroDeg)) {
    var angle = parseFloat(rotationY) * _DEG2RAD$3,
        a13 = Math.sin(angle),
        a33 = Math.cos(angle),
        cos;

    angle = parseFloat(rotationX) * _DEG2RAD$3;
    cos = Math.cos(angle);
    x = _addPxTranslate(target, x, a13 * cos * -zOrigin);
    y = _addPxTranslate(target, y, -Math.sin(angle) * -zOrigin);
    z = _addPxTranslate(target, z, a33 * cos * -zOrigin + zOrigin);
  }

  if (transformPerspective !== _zeroPx) {
    transforms += "perspective(" + transformPerspective + _endParenthesis;
  }

  if (xPercent || yPercent) {
    transforms += "translate(" + xPercent + "%, " + yPercent + "%) ";
  }

  if (use3D || x !== _zeroPx || y !== _zeroPx || z !== _zeroPx) {
    transforms += z !== _zeroPx || use3D ? "translate3d(" + x + ", " + y + ", " + z + ") " : "translate(" + x + ", " + y + _endParenthesis;
  }

  if (rotation !== _zeroDeg) {
    transforms += "rotate(" + rotation + _endParenthesis;
  }

  if (rotationY !== _zeroDeg) {
    transforms += "rotateY(" + rotationY + _endParenthesis;
  }

  if (rotationX !== _zeroDeg) {
    transforms += "rotateX(" + rotationX + _endParenthesis;
  }

  if (skewX !== _zeroDeg || skewY !== _zeroDeg) {
    transforms += "skew(" + skewX + ", " + skewY + _endParenthesis;
  }

  if (scaleX !== 1 || scaleY !== 1) {
    transforms += "scale(" + scaleX + ", " + scaleY + _endParenthesis;
  }

  target.style[_transformProp$3] = transforms || "translate(0, 0)";
},
    _renderSVGTransforms = function _renderSVGTransforms(ratio, cache) {
  var _ref2 = cache || this,
      xPercent = _ref2.xPercent,
      yPercent = _ref2.yPercent,
      x = _ref2.x,
      y = _ref2.y,
      rotation = _ref2.rotation,
      skewX = _ref2.skewX,
      skewY = _ref2.skewY,
      scaleX = _ref2.scaleX,
      scaleY = _ref2.scaleY,
      target = _ref2.target,
      xOrigin = _ref2.xOrigin,
      yOrigin = _ref2.yOrigin,
      xOffset = _ref2.xOffset,
      yOffset = _ref2.yOffset,
      forceCSS = _ref2.forceCSS,
      tx = parseFloat(x),
      ty = parseFloat(y),
      a11,
      a21,
      a12,
      a22,
      temp;

  rotation = parseFloat(rotation);
  skewX = parseFloat(skewX);
  skewY = parseFloat(skewY);

  if (skewY) {
    //for performance reasons, we combine all skewing into the skewX and rotation values. Remember, a skewY of 10 degrees looks the same as a rotation of 10 degrees plus a skewX of 10 degrees.
    skewY = parseFloat(skewY);
    skewX += skewY;
    rotation += skewY;
  }

  if (rotation || skewX) {
    rotation *= _DEG2RAD$3;
    skewX *= _DEG2RAD$3;
    a11 = Math.cos(rotation) * scaleX;
    a21 = Math.sin(rotation) * scaleX;
    a12 = Math.sin(rotation - skewX) * -scaleY;
    a22 = Math.cos(rotation - skewX) * scaleY;

    if (skewX) {
      skewY *= _DEG2RAD$3;
      temp = Math.tan(skewX - skewY);
      temp = Math.sqrt(1 + temp * temp);
      a12 *= temp;
      a22 *= temp;

      if (skewY) {
        temp = Math.tan(skewY);
        temp = Math.sqrt(1 + temp * temp);
        a11 *= temp;
        a21 *= temp;
      }
    }

    a11 = _round$3(a11);
    a21 = _round$3(a21);
    a12 = _round$3(a12);
    a22 = _round$3(a22);
  } else {
    a11 = scaleX;
    a22 = scaleY;
    a21 = a12 = 0;
  }

  if (tx && !~(x + "").indexOf("px") || ty && !~(y + "").indexOf("px")) {
    tx = _convertToUnit(target, "x", x, "px");
    ty = _convertToUnit(target, "y", y, "px");
  }

  if (xOrigin || yOrigin || xOffset || yOffset) {
    tx = _round$3(tx + xOrigin - (xOrigin * a11 + yOrigin * a12) + xOffset);
    ty = _round$3(ty + yOrigin - (xOrigin * a21 + yOrigin * a22) + yOffset);
  }

  if (xPercent || yPercent) {
    //The SVG spec doesn't support percentage-based translation in the "transform" attribute, so we merge it into the translation to simulate it.
    temp = target.getBBox();
    tx = _round$3(tx + xPercent / 100 * temp.width);
    ty = _round$3(ty + yPercent / 100 * temp.height);
  }

  temp = "matrix(" + a11 + "," + a21 + "," + a12 + "," + a22 + "," + tx + "," + ty + ")";
  target.setAttribute("transform", temp);
  forceCSS && (target.style[_transformProp$3] = temp); //some browsers prioritize CSS transforms over the transform attribute. When we sense that the user has CSS transforms applied, we must overwrite them this way (otherwise some browser simply won't render the  transform attribute changes!)
},
    _addRotationalPropTween$1 = function _addRotationalPropTween(plugin, target, property, startNum, endValue, relative) {
  var cap = 360,
      isString = _isString$4(endValue),
      endNum = parseFloat(endValue) * (isString && ~endValue.indexOf("rad") ? _RAD2DEG$2 : 1),
      change = relative ? endNum * relative : endNum - startNum,
      finalValue = startNum + change + "deg",
      direction,
      pt;

  if (isString) {
    direction = endValue.split("_")[1];

    if (direction === "short") {
      change %= cap;

      if (change !== change % (cap / 2)) {
        change += change < 0 ? cap : -cap;
      }
    }

    if (direction === "cw" && change < 0) {
      change = (change + cap * _bigNum$1) % cap - ~~(change / cap) * cap;
    } else if (direction === "ccw" && change > 0) {
      change = (change - cap * _bigNum$1) % cap - ~~(change / cap) * cap;
    }
  }

  plugin._pt = pt = new PropTween$2(plugin._pt, target, property, startNum, change, _renderPropWithEnd$1);
  pt.e = finalValue;
  pt.u = "deg";

  plugin._props.push(property);

  return pt;
},
    _assign = function _assign(target, source) {
  // Internet Explorer doesn't have Object.assign(), so we recreate it here.
  for (var p in source) {
    target[p] = source[p];
  }

  return target;
},
    _addRawTransformPTs = function _addRawTransformPTs(plugin, transforms, target) {
  //for handling cases where someone passes in a whole transform string, like transform: "scale(2, 3) rotate(20deg) translateY(30em)"
  var startCache = _assign({}, target._gsap),
      exclude = "perspective,force3D,transformOrigin,svgOrigin",
      style = target.style,
      endCache,
      p,
      startValue,
      endValue,
      startNum,
      endNum,
      startUnit,
      endUnit;

  if (startCache.svg) {
    startValue = target.getAttribute("transform");
    target.setAttribute("transform", "");
    style[_transformProp$3] = transforms;
    endCache = _parseTransform(target, 1);

    _removeProperty(target, _transformProp$3);

    target.setAttribute("transform", startValue);
  } else {
    startValue = getComputedStyle(target)[_transformProp$3];
    style[_transformProp$3] = transforms;
    endCache = _parseTransform(target, 1);
    style[_transformProp$3] = startValue;
  }

  for (p in _transformProps) {
    startValue = startCache[p];
    endValue = endCache[p];

    if (startValue !== endValue && exclude.indexOf(p) < 0) {
      //tweening to no perspective gives very unintuitive results - just keep the same perspective in that case.
      startUnit = getUnit(startValue);
      endUnit = getUnit(endValue);
      startNum = startUnit !== endUnit ? _convertToUnit(target, p, startValue, endUnit) : parseFloat(startValue);
      endNum = parseFloat(endValue);
      plugin._pt = new PropTween$2(plugin._pt, endCache, p, startNum, endNum - startNum, _renderCSSProp);
      plugin._pt.u = endUnit || 0;

      plugin._props.push(p);
    }
  }

  _assign(endCache, startCache);
}; // handle splitting apart padding, margin, borderWidth, and borderRadius into their 4 components. Firefox, for example, won't report borderRadius correctly - it will only do borderTopLeftRadius and the other corners. We also want to handle paddingTop, marginLeft, borderRightWidth, etc.


_forEachName("padding,margin,Width,Radius", function (name, index) {
  var t = "Top",
      r = "Right",
      b = "Bottom",
      l = "Left",
      props = (index < 3 ? [t, r, b, l] : [t + l, t + r, b + r, b + l]).map(function (side) {
    return index < 2 ? name + side : "border" + side + name;
  });

  _specialProps[index > 1 ? "border" + name : name] = function (plugin, target, property, endValue, tween) {
    var a, vars;

    if (arguments.length < 4) {
      // getter, passed target, property, and unit (from _get())
      a = props.map(function (prop) {
        return _get(plugin, prop, property);
      });
      vars = a.join(" ");
      return vars.split(a[0]).length === 5 ? a[0] : vars;
    }

    a = (endValue + "").split(" ");
    vars = {};
    props.forEach(function (prop, i) {
      return vars[prop] = a[i] = a[i] || a[(i - 1) / 2 | 0];
    });
    plugin.init(target, vars, tween);
  };
});

var CSSPlugin$1 = {
  name: "css",
  register: _initCore$6,
  targetTest: function targetTest(target) {
    return target.style && target.nodeType;
  },
  init: function init(target, vars, tween, index, targets) {
    var props = this._props,
        style = target.style,
        startAt = tween.vars.startAt,
        startValue,
        endValue,
        endNum,
        startNum,
        type,
        specialProp,
        p,
        startUnit,
        endUnit,
        relative,
        isTransformRelated,
        transformPropTween,
        cache,
        smooth,
        hasPriority;
    _pluginInitted || _initCore$6();

    for (p in vars) {
      if (p === "autoRound") {
        continue;
      }

      endValue = vars[p];

      if (_plugins[p] && _checkPlugin(p, vars, tween, index, target, targets)) {
        // plugins
        continue;
      }

      type = typeof endValue;
      specialProp = _specialProps[p];

      if (type === "function") {
        endValue = endValue.call(tween, index, target, targets);
        type = typeof endValue;
      }

      if (type === "string" && ~endValue.indexOf("random(")) {
        endValue = _replaceRandom(endValue);
      }

      if (specialProp) {
        specialProp(this, target, p, endValue, tween) && (hasPriority = 1);
      } else if (p.substr(0, 2) === "--") {
        //CSS variable
        startValue = (getComputedStyle(target).getPropertyValue(p) + "").trim();
        endValue += "";
        _colorExp.lastIndex = 0;

        if (!_colorExp.test(startValue)) {
          // colors don't have units
          startUnit = getUnit(startValue);
          endUnit = getUnit(endValue);
        }

        endUnit ? startUnit !== endUnit && (startValue = _convertToUnit(target, p, startValue, endUnit) + endUnit) : startUnit && (endValue += startUnit);
        this.add(style, "setProperty", startValue, endValue, index, targets, 0, 0, p);
      } else if (type !== "undefined") {
        if (startAt && p in startAt) {
          // in case someone hard-codes a complex value as the start, like top: "calc(2vh / 2)". Without this, it'd use the computed value (always in px)
          startValue = typeof startAt[p] === "function" ? startAt[p].call(tween, index, target, targets) : startAt[p];
          p in _config$1.units && !getUnit(startValue) && (startValue += _config$1.units[p]); // for cases when someone passes in a unitless value like {x: 100}; if we try setting translate(100, 0px) it won't work.

          (startValue + "").charAt(1) === "=" && (startValue = _get(target, p)); // can't work with relative values
        } else {
          startValue = _get(target, p);
        }

        startNum = parseFloat(startValue);
        relative = type === "string" && endValue.charAt(1) === "=" ? +(endValue.charAt(0) + "1") : 0;
        relative && (endValue = endValue.substr(2));
        endNum = parseFloat(endValue);

        if (p in _propertyAliases) {
          if (p === "autoAlpha") {
            //special case where we control the visibility along with opacity. We still allow the opacity value to pass through and get tweened.
            if (startNum === 1 && _get(target, "visibility") === "hidden" && endNum) {
              //if visibility is initially set to "hidden", we should interpret that as intent to make opacity 0 (a convenience)
              startNum = 0;
            }

            _addNonTweeningPT(this, style, "visibility", startNum ? "inherit" : "hidden", endNum ? "inherit" : "hidden", !endNum);
          }

          if (p !== "scale" && p !== "transform") {
            p = _propertyAliases[p];
            ~p.indexOf(",") && (p = p.split(",")[0]);
          }
        }

        isTransformRelated = p in _transformProps; //--- TRANSFORM-RELATED ---

        if (isTransformRelated) {
          if (!transformPropTween) {
            cache = target._gsap;
            cache.renderTransform && !vars.parseTransform || _parseTransform(target, vars.parseTransform); // if, for example, gsap.set(... {transform:"translateX(50vw)"}), the _get() call doesn't parse the transform, thus cache.renderTransform won't be set yet so force the parsing of the transform here.

            smooth = vars.smoothOrigin !== false && cache.smooth;
            transformPropTween = this._pt = new PropTween$2(this._pt, style, _transformProp$3, 0, 1, cache.renderTransform, cache, 0, -1); //the first time through, create the rendering PropTween so that it runs LAST (in the linked list, we keep adding to the beginning)

            transformPropTween.dep = 1; //flag it as dependent so that if things get killed/overwritten and this is the only PropTween left, we can safely kill the whole tween.
          }

          if (p === "scale") {
            this._pt = new PropTween$2(this._pt, cache, "scaleY", cache.scaleY, relative ? relative * endNum : endNum - cache.scaleY);
            props.push("scaleY", p);
            p += "X";
          } else if (p === "transformOrigin") {
            endValue = _convertKeywordsToPercentages(endValue); //in case something like "left top" or "bottom right" is passed in. Convert to percentages.

            if (cache.svg) {
              _applySVGOrigin(target, endValue, 0, smooth, 0, this);
            } else {
              endUnit = parseFloat(endValue.split(" ")[2]) || 0; //handle the zOrigin separately!

              endUnit !== cache.zOrigin && _addNonTweeningPT(this, cache, "zOrigin", cache.zOrigin, endUnit);

              _addNonTweeningPT(this, style, p, _firstTwoOnly(startValue), _firstTwoOnly(endValue));
            }

            continue;
          } else if (p === "svgOrigin") {
            _applySVGOrigin(target, endValue, 1, smooth, 0, this);

            continue;
          } else if (p in _rotationalProperties) {
            _addRotationalPropTween$1(this, cache, p, startNum, endValue, relative);

            continue;
          } else if (p === "smoothOrigin") {
            _addNonTweeningPT(this, cache, "smooth", cache.smooth, endValue);

            continue;
          } else if (p === "force3D") {
            cache[p] = endValue;
            continue;
          } else if (p === "transform") {
            _addRawTransformPTs(this, endValue, target);

            continue;
          }
        } else if (!(p in style)) {
          p = _checkPropPrefix(p) || p;
        }

        if (isTransformRelated || (endNum || endNum === 0) && (startNum || startNum === 0) && !_complexExp.test(endValue) && p in style) {
          startUnit = (startValue + "").substr((startNum + "").length);
          endNum || (endNum = 0); // protect against NaN

          endUnit = getUnit(endValue) || (p in _config$1.units ? _config$1.units[p] : startUnit);
          startUnit !== endUnit && (startNum = _convertToUnit(target, p, startValue, endUnit));
          this._pt = new PropTween$2(this._pt, isTransformRelated ? cache : style, p, startNum, relative ? relative * endNum : endNum - startNum, !isTransformRelated && (endUnit === "px" || p === "zIndex") && vars.autoRound !== false ? _renderRoundedCSSProp : _renderCSSProp);
          this._pt.u = endUnit || 0;

          if (startUnit !== endUnit) {
            //when the tween goes all the way back to the beginning, we need to revert it to the OLD/ORIGINAL value (with those units). We record that as a "b" (beginning) property and point to a render method that handles that. (performance optimization)
            this._pt.b = startValue;
            this._pt.r = _renderCSSPropWithBeginning;
          }
        } else if (!(p in style)) {
          if (p in target) {
            //maybe it's not a style - it could be a property added directly to an element in which case we'll try to animate that.
            this.add(target, p, target[p], endValue, index, targets);
          } else {
            _missingPlugin(p, endValue);

            continue;
          }
        } else {
          _tweenComplexCSSString.call(this, target, p, startValue, endValue);
        }

        props.push(p);
      }
    }

    hasPriority && _sortPropTweensByPriority(this);
  },
  get: _get,
  aliases: _propertyAliases,
  getSetter: function getSetter(target, property, plugin) {
    //returns a setter function that accepts target, property, value and applies it accordingly. Remember, properties like "x" aren't as simple as target.style.property = value because they've got to be applied to a proxy object and then merged into a transform string in a renderer.
    var p = _propertyAliases[property];
    p && p.indexOf(",") < 0 && (property = p);
    return property in _transformProps && property !== _transformOriginProp$2 && (target._gsap.x || _get(target, "x")) ? plugin && _recentSetterPlugin === plugin ? property === "scale" ? _setterScale : _setterTransform : (_recentSetterPlugin = plugin || {}) && (property === "scale" ? _setterScaleWithRender : _setterTransformWithRender) : target.style && !_isUndefined$2(target.style[property]) ? _setterCSSStyle : ~property.indexOf("-") ? _setterCSSProp : _getSetter$1(target, property);
  },
  core: {
    _removeProperty: _removeProperty,
    _getMatrix: _getMatrix
  }
};
gsap$9.utils.checkPrefix = _checkPropPrefix;

(function (positionAndScale, rotation, others, aliases) {
  var all = _forEachName(positionAndScale + "," + rotation + "," + others, function (name) {
    _transformProps[name] = 1;
  });

  _forEachName(rotation, function (name) {
    _config$1.units[name] = "deg";
    _rotationalProperties[name] = 1;
  });

  _propertyAliases[all[13]] = positionAndScale + "," + rotation;

  _forEachName(aliases, function (name) {
    var split = name.split(":");
    _propertyAliases[split[1]] = all[split[0]];
  });
})("x,y,z,scale,scaleX,scaleY,xPercent,yPercent", "rotation,rotationX,rotationY,skewX,skewY", "transform,transformOrigin,svgOrigin,force3D,smoothOrigin,transformPerspective", "0:translateX,1:translateY,2:translateZ,8:rotate,8:rotationZ,8:rotateZ,9:rotateX,10:rotateY");

_forEachName("x,y,z,top,right,bottom,left,width,height,fontSize,padding,margin,perspective", function (name) {
  _config$1.units[name] = "px";
});

gsap$9.registerPlugin(CSSPlugin$1);

var gsapWithCSS$1 = gsap$9.registerPlugin(CSSPlugin$1) || gsap$9;
    // to protect from tree shaking
gsapWithCSS$1.core.Tween;

function createCommonjsModule(fn) {
  var module = { exports: {} };
	return fn(module, module.exports), module.exports;
}

/*! Hammer.JS - v2.0.7 - 2016-04-22
 * http://hammerjs.github.io/
 *
 * Copyright (c) 2016 Jorik Tangelder;
 * Licensed under the MIT license */

var hammer = createCommonjsModule(function (module) {
(function(window, document, exportName, undefined$1) {

var VENDOR_PREFIXES = ['', 'webkit', 'Moz', 'MS', 'ms', 'o'];
var TEST_ELEMENT = document.createElement('div');

var TYPE_FUNCTION = 'function';

var round = Math.round;
var abs = Math.abs;
var now = Date.now;

/**
 * set a timeout with a given scope
 * @param {Function} fn
 * @param {Number} timeout
 * @param {Object} context
 * @returns {number}
 */
function setTimeoutContext(fn, timeout, context) {
    return setTimeout(bindFn(fn, context), timeout);
}

/**
 * if the argument is an array, we want to execute the fn on each entry
 * if it aint an array we don't want to do a thing.
 * this is used by all the methods that accept a single and array argument.
 * @param {*|Array} arg
 * @param {String} fn
 * @param {Object} [context]
 * @returns {Boolean}
 */
function invokeArrayArg(arg, fn, context) {
    if (Array.isArray(arg)) {
        each(arg, context[fn], context);
        return true;
    }
    return false;
}

/**
 * walk objects and arrays
 * @param {Object} obj
 * @param {Function} iterator
 * @param {Object} context
 */
function each(obj, iterator, context) {
    var i;

    if (!obj) {
        return;
    }

    if (obj.forEach) {
        obj.forEach(iterator, context);
    } else if (obj.length !== undefined$1) {
        i = 0;
        while (i < obj.length) {
            iterator.call(context, obj[i], i, obj);
            i++;
        }
    } else {
        for (i in obj) {
            obj.hasOwnProperty(i) && iterator.call(context, obj[i], i, obj);
        }
    }
}

/**
 * wrap a method with a deprecation warning and stack trace
 * @param {Function} method
 * @param {String} name
 * @param {String} message
 * @returns {Function} A new function wrapping the supplied method.
 */
function deprecate(method, name, message) {
    var deprecationMessage = 'DEPRECATED METHOD: ' + name + '\n' + message + ' AT \n';
    return function() {
        var e = new Error('get-stack-trace');
        var stack = e && e.stack ? e.stack.replace(/^[^\(]+?[\n$]/gm, '')
            .replace(/^\s+at\s+/gm, '')
            .replace(/^Object.<anonymous>\s*\(/gm, '{anonymous}()@') : 'Unknown Stack Trace';

        var log = window.console && (window.console.warn || window.console.log);
        if (log) {
            log.call(window.console, deprecationMessage, stack);
        }
        return method.apply(this, arguments);
    };
}

/**
 * extend object.
 * means that properties in dest will be overwritten by the ones in src.
 * @param {Object} target
 * @param {...Object} objects_to_assign
 * @returns {Object} target
 */
var assign;
if (typeof Object.assign !== 'function') {
    assign = function assign(target) {
        if (target === undefined$1 || target === null) {
            throw new TypeError('Cannot convert undefined or null to object');
        }

        var output = Object(target);
        for (var index = 1; index < arguments.length; index++) {
            var source = arguments[index];
            if (source !== undefined$1 && source !== null) {
                for (var nextKey in source) {
                    if (source.hasOwnProperty(nextKey)) {
                        output[nextKey] = source[nextKey];
                    }
                }
            }
        }
        return output;
    };
} else {
    assign = Object.assign;
}

/**
 * extend object.
 * means that properties in dest will be overwritten by the ones in src.
 * @param {Object} dest
 * @param {Object} src
 * @param {Boolean} [merge=false]
 * @returns {Object} dest
 */
var extend = deprecate(function extend(dest, src, merge) {
    var keys = Object.keys(src);
    var i = 0;
    while (i < keys.length) {
        if (!merge || (merge && dest[keys[i]] === undefined$1)) {
            dest[keys[i]] = src[keys[i]];
        }
        i++;
    }
    return dest;
}, 'extend', 'Use `assign`.');

/**
 * merge the values from src in the dest.
 * means that properties that exist in dest will not be overwritten by src
 * @param {Object} dest
 * @param {Object} src
 * @returns {Object} dest
 */
var merge = deprecate(function merge(dest, src) {
    return extend(dest, src, true);
}, 'merge', 'Use `assign`.');

/**
 * simple class inheritance
 * @param {Function} child
 * @param {Function} base
 * @param {Object} [properties]
 */
function inherit(child, base, properties) {
    var baseP = base.prototype,
        childP;

    childP = child.prototype = Object.create(baseP);
    childP.constructor = child;
    childP._super = baseP;

    if (properties) {
        assign(childP, properties);
    }
}

/**
 * simple function bind
 * @param {Function} fn
 * @param {Object} context
 * @returns {Function}
 */
function bindFn(fn, context) {
    return function boundFn() {
        return fn.apply(context, arguments);
    };
}

/**
 * let a boolean value also be a function that must return a boolean
 * this first item in args will be used as the context
 * @param {Boolean|Function} val
 * @param {Array} [args]
 * @returns {Boolean}
 */
function boolOrFn(val, args) {
    if (typeof val == TYPE_FUNCTION) {
        return val.apply(args ? args[0] || undefined$1 : undefined$1, args);
    }
    return val;
}

/**
 * use the val2 when val1 is undefined
 * @param {*} val1
 * @param {*} val2
 * @returns {*}
 */
function ifUndefined(val1, val2) {
    return (val1 === undefined$1) ? val2 : val1;
}

/**
 * addEventListener with multiple events at once
 * @param {EventTarget} target
 * @param {String} types
 * @param {Function} handler
 */
function addEventListeners(target, types, handler) {
    each(splitStr(types), function(type) {
        target.addEventListener(type, handler, false);
    });
}

/**
 * removeEventListener with multiple events at once
 * @param {EventTarget} target
 * @param {String} types
 * @param {Function} handler
 */
function removeEventListeners(target, types, handler) {
    each(splitStr(types), function(type) {
        target.removeEventListener(type, handler, false);
    });
}

/**
 * find if a node is in the given parent
 * @method hasParent
 * @param {HTMLElement} node
 * @param {HTMLElement} parent
 * @return {Boolean} found
 */
function hasParent(node, parent) {
    while (node) {
        if (node == parent) {
            return true;
        }
        node = node.parentNode;
    }
    return false;
}

/**
 * small indexOf wrapper
 * @param {String} str
 * @param {String} find
 * @returns {Boolean} found
 */
function inStr(str, find) {
    return str.indexOf(find) > -1;
}

/**
 * split string on whitespace
 * @param {String} str
 * @returns {Array} words
 */
function splitStr(str) {
    return str.trim().split(/\s+/g);
}

/**
 * find if a array contains the object using indexOf or a simple polyFill
 * @param {Array} src
 * @param {String} find
 * @param {String} [findByKey]
 * @return {Boolean|Number} false when not found, or the index
 */
function inArray(src, find, findByKey) {
    if (src.indexOf && !findByKey) {
        return src.indexOf(find);
    } else {
        var i = 0;
        while (i < src.length) {
            if ((findByKey && src[i][findByKey] == find) || (!findByKey && src[i] === find)) {
                return i;
            }
            i++;
        }
        return -1;
    }
}

/**
 * convert array-like objects to real arrays
 * @param {Object} obj
 * @returns {Array}
 */
function toArray(obj) {
    return Array.prototype.slice.call(obj, 0);
}

/**
 * unique array with objects based on a key (like 'id') or just by the array's value
 * @param {Array} src [{id:1},{id:2},{id:1}]
 * @param {String} [key]
 * @param {Boolean} [sort=False]
 * @returns {Array} [{id:1},{id:2}]
 */
function uniqueArray(src, key, sort) {
    var results = [];
    var values = [];
    var i = 0;

    while (i < src.length) {
        var val = key ? src[i][key] : src[i];
        if (inArray(values, val) < 0) {
            results.push(src[i]);
        }
        values[i] = val;
        i++;
    }

    if (sort) {
        if (!key) {
            results = results.sort();
        } else {
            results = results.sort(function sortUniqueArray(a, b) {
                return a[key] > b[key];
            });
        }
    }

    return results;
}

/**
 * get the prefixed property
 * @param {Object} obj
 * @param {String} property
 * @returns {String|Undefined} prefixed
 */
function prefixed(obj, property) {
    var prefix, prop;
    var camelProp = property[0].toUpperCase() + property.slice(1);

    var i = 0;
    while (i < VENDOR_PREFIXES.length) {
        prefix = VENDOR_PREFIXES[i];
        prop = (prefix) ? prefix + camelProp : property;

        if (prop in obj) {
            return prop;
        }
        i++;
    }
    return undefined$1;
}

/**
 * get a unique id
 * @returns {number} uniqueId
 */
var _uniqueId = 1;
function uniqueId() {
    return _uniqueId++;
}

/**
 * get the window object of an element
 * @param {HTMLElement} element
 * @returns {DocumentView|Window}
 */
function getWindowForElement(element) {
    var doc = element.ownerDocument || element;
    return (doc.defaultView || doc.parentWindow || window);
}

var MOBILE_REGEX = /mobile|tablet|ip(ad|hone|od)|android/i;

var SUPPORT_TOUCH = ('ontouchstart' in window);
var SUPPORT_POINTER_EVENTS = prefixed(window, 'PointerEvent') !== undefined$1;
var SUPPORT_ONLY_TOUCH = SUPPORT_TOUCH && MOBILE_REGEX.test(navigator.userAgent);

var INPUT_TYPE_TOUCH = 'touch';
var INPUT_TYPE_PEN = 'pen';
var INPUT_TYPE_MOUSE = 'mouse';
var INPUT_TYPE_KINECT = 'kinect';

var COMPUTE_INTERVAL = 25;

var INPUT_START = 1;
var INPUT_MOVE = 2;
var INPUT_END = 4;
var INPUT_CANCEL = 8;

var DIRECTION_NONE = 1;
var DIRECTION_LEFT = 2;
var DIRECTION_RIGHT = 4;
var DIRECTION_UP = 8;
var DIRECTION_DOWN = 16;

var DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT;
var DIRECTION_VERTICAL = DIRECTION_UP | DIRECTION_DOWN;
var DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;

var PROPS_XY = ['x', 'y'];
var PROPS_CLIENT_XY = ['clientX', 'clientY'];

/**
 * create new input type manager
 * @param {Manager} manager
 * @param {Function} callback
 * @returns {Input}
 * @constructor
 */
function Input(manager, callback) {
    var self = this;
    this.manager = manager;
    this.callback = callback;
    this.element = manager.element;
    this.target = manager.options.inputTarget;

    // smaller wrapper around the handler, for the scope and the enabled state of the manager,
    // so when disabled the input events are completely bypassed.
    this.domHandler = function(ev) {
        if (boolOrFn(manager.options.enable, [manager])) {
            self.handler(ev);
        }
    };

    this.init();

}

Input.prototype = {
    /**
     * should handle the inputEvent data and trigger the callback
     * @virtual
     */
    handler: function() { },

    /**
     * bind the events
     */
    init: function() {
        this.evEl && addEventListeners(this.element, this.evEl, this.domHandler);
        this.evTarget && addEventListeners(this.target, this.evTarget, this.domHandler);
        this.evWin && addEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
    },

    /**
     * unbind the events
     */
    destroy: function() {
        this.evEl && removeEventListeners(this.element, this.evEl, this.domHandler);
        this.evTarget && removeEventListeners(this.target, this.evTarget, this.domHandler);
        this.evWin && removeEventListeners(getWindowForElement(this.element), this.evWin, this.domHandler);
    }
};

/**
 * create new input type manager
 * called by the Manager constructor
 * @param {Hammer} manager
 * @returns {Input}
 */
function createInputInstance(manager) {
    var Type;
    var inputClass = manager.options.inputClass;

    if (inputClass) {
        Type = inputClass;
    } else if (SUPPORT_POINTER_EVENTS) {
        Type = PointerEventInput;
    } else if (SUPPORT_ONLY_TOUCH) {
        Type = TouchInput;
    } else if (!SUPPORT_TOUCH) {
        Type = MouseInput;
    } else {
        Type = TouchMouseInput;
    }
    return new (Type)(manager, inputHandler);
}

/**
 * handle input events
 * @param {Manager} manager
 * @param {String} eventType
 * @param {Object} input
 */
function inputHandler(manager, eventType, input) {
    var pointersLen = input.pointers.length;
    var changedPointersLen = input.changedPointers.length;
    var isFirst = (eventType & INPUT_START && (pointersLen - changedPointersLen === 0));
    var isFinal = (eventType & (INPUT_END | INPUT_CANCEL) && (pointersLen - changedPointersLen === 0));

    input.isFirst = !!isFirst;
    input.isFinal = !!isFinal;

    if (isFirst) {
        manager.session = {};
    }

    // source event is the normalized value of the domEvents
    // like 'touchstart, mouseup, pointerdown'
    input.eventType = eventType;

    // compute scale, rotation etc
    computeInputData(manager, input);

    // emit secret event
    manager.emit('hammer.input', input);

    manager.recognize(input);
    manager.session.prevInput = input;
}

/**
 * extend the data with some usable properties like scale, rotate, velocity etc
 * @param {Object} manager
 * @param {Object} input
 */
function computeInputData(manager, input) {
    var session = manager.session;
    var pointers = input.pointers;
    var pointersLength = pointers.length;

    // store the first input to calculate the distance and direction
    if (!session.firstInput) {
        session.firstInput = simpleCloneInputData(input);
    }

    // to compute scale and rotation we need to store the multiple touches
    if (pointersLength > 1 && !session.firstMultiple) {
        session.firstMultiple = simpleCloneInputData(input);
    } else if (pointersLength === 1) {
        session.firstMultiple = false;
    }

    var firstInput = session.firstInput;
    var firstMultiple = session.firstMultiple;
    var offsetCenter = firstMultiple ? firstMultiple.center : firstInput.center;

    var center = input.center = getCenter(pointers);
    input.timeStamp = now();
    input.deltaTime = input.timeStamp - firstInput.timeStamp;

    input.angle = getAngle(offsetCenter, center);
    input.distance = getDistance(offsetCenter, center);

    computeDeltaXY(session, input);
    input.offsetDirection = getDirection(input.deltaX, input.deltaY);

    var overallVelocity = getVelocity(input.deltaTime, input.deltaX, input.deltaY);
    input.overallVelocityX = overallVelocity.x;
    input.overallVelocityY = overallVelocity.y;
    input.overallVelocity = (abs(overallVelocity.x) > abs(overallVelocity.y)) ? overallVelocity.x : overallVelocity.y;

    input.scale = firstMultiple ? getScale(firstMultiple.pointers, pointers) : 1;
    input.rotation = firstMultiple ? getRotation(firstMultiple.pointers, pointers) : 0;

    input.maxPointers = !session.prevInput ? input.pointers.length : ((input.pointers.length >
        session.prevInput.maxPointers) ? input.pointers.length : session.prevInput.maxPointers);

    computeIntervalInputData(session, input);

    // find the correct target
    var target = manager.element;
    if (hasParent(input.srcEvent.target, target)) {
        target = input.srcEvent.target;
    }
    input.target = target;
}

function computeDeltaXY(session, input) {
    var center = input.center;
    var offset = session.offsetDelta || {};
    var prevDelta = session.prevDelta || {};
    var prevInput = session.prevInput || {};

    if (input.eventType === INPUT_START || prevInput.eventType === INPUT_END) {
        prevDelta = session.prevDelta = {
            x: prevInput.deltaX || 0,
            y: prevInput.deltaY || 0
        };

        offset = session.offsetDelta = {
            x: center.x,
            y: center.y
        };
    }

    input.deltaX = prevDelta.x + (center.x - offset.x);
    input.deltaY = prevDelta.y + (center.y - offset.y);
}

/**
 * velocity is calculated every x ms
 * @param {Object} session
 * @param {Object} input
 */
function computeIntervalInputData(session, input) {
    var last = session.lastInterval || input,
        deltaTime = input.timeStamp - last.timeStamp,
        velocity, velocityX, velocityY, direction;

    if (input.eventType != INPUT_CANCEL && (deltaTime > COMPUTE_INTERVAL || last.velocity === undefined$1)) {
        var deltaX = input.deltaX - last.deltaX;
        var deltaY = input.deltaY - last.deltaY;

        var v = getVelocity(deltaTime, deltaX, deltaY);
        velocityX = v.x;
        velocityY = v.y;
        velocity = (abs(v.x) > abs(v.y)) ? v.x : v.y;
        direction = getDirection(deltaX, deltaY);

        session.lastInterval = input;
    } else {
        // use latest velocity info if it doesn't overtake a minimum period
        velocity = last.velocity;
        velocityX = last.velocityX;
        velocityY = last.velocityY;
        direction = last.direction;
    }

    input.velocity = velocity;
    input.velocityX = velocityX;
    input.velocityY = velocityY;
    input.direction = direction;
}

/**
 * create a simple clone from the input used for storage of firstInput and firstMultiple
 * @param {Object} input
 * @returns {Object} clonedInputData
 */
function simpleCloneInputData(input) {
    // make a simple copy of the pointers because we will get a reference if we don't
    // we only need clientXY for the calculations
    var pointers = [];
    var i = 0;
    while (i < input.pointers.length) {
        pointers[i] = {
            clientX: round(input.pointers[i].clientX),
            clientY: round(input.pointers[i].clientY)
        };
        i++;
    }

    return {
        timeStamp: now(),
        pointers: pointers,
        center: getCenter(pointers),
        deltaX: input.deltaX,
        deltaY: input.deltaY
    };
}

/**
 * get the center of all the pointers
 * @param {Array} pointers
 * @return {Object} center contains `x` and `y` properties
 */
function getCenter(pointers) {
    var pointersLength = pointers.length;

    // no need to loop when only one touch
    if (pointersLength === 1) {
        return {
            x: round(pointers[0].clientX),
            y: round(pointers[0].clientY)
        };
    }

    var x = 0, y = 0, i = 0;
    while (i < pointersLength) {
        x += pointers[i].clientX;
        y += pointers[i].clientY;
        i++;
    }

    return {
        x: round(x / pointersLength),
        y: round(y / pointersLength)
    };
}

/**
 * calculate the velocity between two points. unit is in px per ms.
 * @param {Number} deltaTime
 * @param {Number} x
 * @param {Number} y
 * @return {Object} velocity `x` and `y`
 */
function getVelocity(deltaTime, x, y) {
    return {
        x: x / deltaTime || 0,
        y: y / deltaTime || 0
    };
}

/**
 * get the direction between two points
 * @param {Number} x
 * @param {Number} y
 * @return {Number} direction
 */
function getDirection(x, y) {
    if (x === y) {
        return DIRECTION_NONE;
    }

    if (abs(x) >= abs(y)) {
        return x < 0 ? DIRECTION_LEFT : DIRECTION_RIGHT;
    }
    return y < 0 ? DIRECTION_UP : DIRECTION_DOWN;
}

/**
 * calculate the absolute distance between two points
 * @param {Object} p1 {x, y}
 * @param {Object} p2 {x, y}
 * @param {Array} [props] containing x and y keys
 * @return {Number} distance
 */
function getDistance(p1, p2, props) {
    if (!props) {
        props = PROPS_XY;
    }
    var x = p2[props[0]] - p1[props[0]],
        y = p2[props[1]] - p1[props[1]];

    return Math.sqrt((x * x) + (y * y));
}

/**
 * calculate the angle between two coordinates
 * @param {Object} p1
 * @param {Object} p2
 * @param {Array} [props] containing x and y keys
 * @return {Number} angle
 */
function getAngle(p1, p2, props) {
    if (!props) {
        props = PROPS_XY;
    }
    var x = p2[props[0]] - p1[props[0]],
        y = p2[props[1]] - p1[props[1]];
    return Math.atan2(y, x) * 180 / Math.PI;
}

/**
 * calculate the rotation degrees between two pointersets
 * @param {Array} start array of pointers
 * @param {Array} end array of pointers
 * @return {Number} rotation
 */
function getRotation(start, end) {
    return getAngle(end[1], end[0], PROPS_CLIENT_XY) + getAngle(start[1], start[0], PROPS_CLIENT_XY);
}

/**
 * calculate the scale factor between two pointersets
 * no scale is 1, and goes down to 0 when pinched together, and bigger when pinched out
 * @param {Array} start array of pointers
 * @param {Array} end array of pointers
 * @return {Number} scale
 */
function getScale(start, end) {
    return getDistance(end[0], end[1], PROPS_CLIENT_XY) / getDistance(start[0], start[1], PROPS_CLIENT_XY);
}

var MOUSE_INPUT_MAP = {
    mousedown: INPUT_START,
    mousemove: INPUT_MOVE,
    mouseup: INPUT_END
};

var MOUSE_ELEMENT_EVENTS = 'mousedown';
var MOUSE_WINDOW_EVENTS = 'mousemove mouseup';

/**
 * Mouse events input
 * @constructor
 * @extends Input
 */
function MouseInput() {
    this.evEl = MOUSE_ELEMENT_EVENTS;
    this.evWin = MOUSE_WINDOW_EVENTS;

    this.pressed = false; // mousedown state

    Input.apply(this, arguments);
}

inherit(MouseInput, Input, {
    /**
     * handle mouse events
     * @param {Object} ev
     */
    handler: function MEhandler(ev) {
        var eventType = MOUSE_INPUT_MAP[ev.type];

        // on start we want to have the left mouse button down
        if (eventType & INPUT_START && ev.button === 0) {
            this.pressed = true;
        }

        if (eventType & INPUT_MOVE && ev.which !== 1) {
            eventType = INPUT_END;
        }

        // mouse must be down
        if (!this.pressed) {
            return;
        }

        if (eventType & INPUT_END) {
            this.pressed = false;
        }

        this.callback(this.manager, eventType, {
            pointers: [ev],
            changedPointers: [ev],
            pointerType: INPUT_TYPE_MOUSE,
            srcEvent: ev
        });
    }
});

var POINTER_INPUT_MAP = {
    pointerdown: INPUT_START,
    pointermove: INPUT_MOVE,
    pointerup: INPUT_END,
    pointercancel: INPUT_CANCEL,
    pointerout: INPUT_CANCEL
};

// in IE10 the pointer types is defined as an enum
var IE10_POINTER_TYPE_ENUM = {
    2: INPUT_TYPE_TOUCH,
    3: INPUT_TYPE_PEN,
    4: INPUT_TYPE_MOUSE,
    5: INPUT_TYPE_KINECT // see https://twitter.com/jacobrossi/status/480596438489890816
};

var POINTER_ELEMENT_EVENTS = 'pointerdown';
var POINTER_WINDOW_EVENTS = 'pointermove pointerup pointercancel';

// IE10 has prefixed support, and case-sensitive
if (window.MSPointerEvent && !window.PointerEvent) {
    POINTER_ELEMENT_EVENTS = 'MSPointerDown';
    POINTER_WINDOW_EVENTS = 'MSPointerMove MSPointerUp MSPointerCancel';
}

/**
 * Pointer events input
 * @constructor
 * @extends Input
 */
function PointerEventInput() {
    this.evEl = POINTER_ELEMENT_EVENTS;
    this.evWin = POINTER_WINDOW_EVENTS;

    Input.apply(this, arguments);

    this.store = (this.manager.session.pointerEvents = []);
}

inherit(PointerEventInput, Input, {
    /**
     * handle mouse events
     * @param {Object} ev
     */
    handler: function PEhandler(ev) {
        var store = this.store;
        var removePointer = false;

        var eventTypeNormalized = ev.type.toLowerCase().replace('ms', '');
        var eventType = POINTER_INPUT_MAP[eventTypeNormalized];
        var pointerType = IE10_POINTER_TYPE_ENUM[ev.pointerType] || ev.pointerType;

        var isTouch = (pointerType == INPUT_TYPE_TOUCH);

        // get index of the event in the store
        var storeIndex = inArray(store, ev.pointerId, 'pointerId');

        // start and mouse must be down
        if (eventType & INPUT_START && (ev.button === 0 || isTouch)) {
            if (storeIndex < 0) {
                store.push(ev);
                storeIndex = store.length - 1;
            }
        } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
            removePointer = true;
        }

        // it not found, so the pointer hasn't been down (so it's probably a hover)
        if (storeIndex < 0) {
            return;
        }

        // update the event in the store
        store[storeIndex] = ev;

        this.callback(this.manager, eventType, {
            pointers: store,
            changedPointers: [ev],
            pointerType: pointerType,
            srcEvent: ev
        });

        if (removePointer) {
            // remove from the store
            store.splice(storeIndex, 1);
        }
    }
});

var SINGLE_TOUCH_INPUT_MAP = {
    touchstart: INPUT_START,
    touchmove: INPUT_MOVE,
    touchend: INPUT_END,
    touchcancel: INPUT_CANCEL
};

var SINGLE_TOUCH_TARGET_EVENTS = 'touchstart';
var SINGLE_TOUCH_WINDOW_EVENTS = 'touchstart touchmove touchend touchcancel';

/**
 * Touch events input
 * @constructor
 * @extends Input
 */
function SingleTouchInput() {
    this.evTarget = SINGLE_TOUCH_TARGET_EVENTS;
    this.evWin = SINGLE_TOUCH_WINDOW_EVENTS;
    this.started = false;

    Input.apply(this, arguments);
}

inherit(SingleTouchInput, Input, {
    handler: function TEhandler(ev) {
        var type = SINGLE_TOUCH_INPUT_MAP[ev.type];

        // should we handle the touch events?
        if (type === INPUT_START) {
            this.started = true;
        }

        if (!this.started) {
            return;
        }

        var touches = normalizeSingleTouches.call(this, ev, type);

        // when done, reset the started state
        if (type & (INPUT_END | INPUT_CANCEL) && touches[0].length - touches[1].length === 0) {
            this.started = false;
        }

        this.callback(this.manager, type, {
            pointers: touches[0],
            changedPointers: touches[1],
            pointerType: INPUT_TYPE_TOUCH,
            srcEvent: ev
        });
    }
});

/**
 * @this {TouchInput}
 * @param {Object} ev
 * @param {Number} type flag
 * @returns {undefined|Array} [all, changed]
 */
function normalizeSingleTouches(ev, type) {
    var all = toArray(ev.touches);
    var changed = toArray(ev.changedTouches);

    if (type & (INPUT_END | INPUT_CANCEL)) {
        all = uniqueArray(all.concat(changed), 'identifier', true);
    }

    return [all, changed];
}

var TOUCH_INPUT_MAP = {
    touchstart: INPUT_START,
    touchmove: INPUT_MOVE,
    touchend: INPUT_END,
    touchcancel: INPUT_CANCEL
};

var TOUCH_TARGET_EVENTS = 'touchstart touchmove touchend touchcancel';

/**
 * Multi-user touch events input
 * @constructor
 * @extends Input
 */
function TouchInput() {
    this.evTarget = TOUCH_TARGET_EVENTS;
    this.targetIds = {};

    Input.apply(this, arguments);
}

inherit(TouchInput, Input, {
    handler: function MTEhandler(ev) {
        var type = TOUCH_INPUT_MAP[ev.type];
        var touches = getTouches.call(this, ev, type);
        if (!touches) {
            return;
        }

        this.callback(this.manager, type, {
            pointers: touches[0],
            changedPointers: touches[1],
            pointerType: INPUT_TYPE_TOUCH,
            srcEvent: ev
        });
    }
});

/**
 * @this {TouchInput}
 * @param {Object} ev
 * @param {Number} type flag
 * @returns {undefined|Array} [all, changed]
 */
function getTouches(ev, type) {
    var allTouches = toArray(ev.touches);
    var targetIds = this.targetIds;

    // when there is only one touch, the process can be simplified
    if (type & (INPUT_START | INPUT_MOVE) && allTouches.length === 1) {
        targetIds[allTouches[0].identifier] = true;
        return [allTouches, allTouches];
    }

    var i,
        targetTouches,
        changedTouches = toArray(ev.changedTouches),
        changedTargetTouches = [],
        target = this.target;

    // get target touches from touches
    targetTouches = allTouches.filter(function(touch) {
        return hasParent(touch.target, target);
    });

    // collect touches
    if (type === INPUT_START) {
        i = 0;
        while (i < targetTouches.length) {
            targetIds[targetTouches[i].identifier] = true;
            i++;
        }
    }

    // filter changed touches to only contain touches that exist in the collected target ids
    i = 0;
    while (i < changedTouches.length) {
        if (targetIds[changedTouches[i].identifier]) {
            changedTargetTouches.push(changedTouches[i]);
        }

        // cleanup removed touches
        if (type & (INPUT_END | INPUT_CANCEL)) {
            delete targetIds[changedTouches[i].identifier];
        }
        i++;
    }

    if (!changedTargetTouches.length) {
        return;
    }

    return [
        // merge targetTouches with changedTargetTouches so it contains ALL touches, including 'end' and 'cancel'
        uniqueArray(targetTouches.concat(changedTargetTouches), 'identifier', true),
        changedTargetTouches
    ];
}

/**
 * Combined touch and mouse input
 *
 * Touch has a higher priority then mouse, and while touching no mouse events are allowed.
 * This because touch devices also emit mouse events while doing a touch.
 *
 * @constructor
 * @extends Input
 */

var DEDUP_TIMEOUT = 2500;
var DEDUP_DISTANCE = 25;

function TouchMouseInput() {
    Input.apply(this, arguments);

    var handler = bindFn(this.handler, this);
    this.touch = new TouchInput(this.manager, handler);
    this.mouse = new MouseInput(this.manager, handler);

    this.primaryTouch = null;
    this.lastTouches = [];
}

inherit(TouchMouseInput, Input, {
    /**
     * handle mouse and touch events
     * @param {Hammer} manager
     * @param {String} inputEvent
     * @param {Object} inputData
     */
    handler: function TMEhandler(manager, inputEvent, inputData) {
        var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH),
            isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE);

        if (isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents) {
            return;
        }

        // when we're in a touch event, record touches to  de-dupe synthetic mouse event
        if (isTouch) {
            recordTouches.call(this, inputEvent, inputData);
        } else if (isMouse && isSyntheticEvent.call(this, inputData)) {
            return;
        }

        this.callback(manager, inputEvent, inputData);
    },

    /**
     * remove the event listeners
     */
    destroy: function destroy() {
        this.touch.destroy();
        this.mouse.destroy();
    }
});

function recordTouches(eventType, eventData) {
    if (eventType & INPUT_START) {
        this.primaryTouch = eventData.changedPointers[0].identifier;
        setLastTouch.call(this, eventData);
    } else if (eventType & (INPUT_END | INPUT_CANCEL)) {
        setLastTouch.call(this, eventData);
    }
}

function setLastTouch(eventData) {
    var touch = eventData.changedPointers[0];

    if (touch.identifier === this.primaryTouch) {
        var lastTouch = {x: touch.clientX, y: touch.clientY};
        this.lastTouches.push(lastTouch);
        var lts = this.lastTouches;
        var removeLastTouch = function() {
            var i = lts.indexOf(lastTouch);
            if (i > -1) {
                lts.splice(i, 1);
            }
        };
        setTimeout(removeLastTouch, DEDUP_TIMEOUT);
    }
}

function isSyntheticEvent(eventData) {
    var x = eventData.srcEvent.clientX, y = eventData.srcEvent.clientY;
    for (var i = 0; i < this.lastTouches.length; i++) {
        var t = this.lastTouches[i];
        var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
        if (dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE) {
            return true;
        }
    }
    return false;
}

var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction');
var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined$1;

// magical touchAction value
var TOUCH_ACTION_COMPUTE = 'compute';
var TOUCH_ACTION_AUTO = 'auto';
var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented
var TOUCH_ACTION_NONE = 'none';
var TOUCH_ACTION_PAN_X = 'pan-x';
var TOUCH_ACTION_PAN_Y = 'pan-y';
var TOUCH_ACTION_MAP = getTouchActionProps();

/**
 * Touch Action
 * sets the touchAction property or uses the js alternative
 * @param {Manager} manager
 * @param {String} value
 * @constructor
 */
function TouchAction(manager, value) {
    this.manager = manager;
    this.set(value);
}

TouchAction.prototype = {
    /**
     * set the touchAction value on the element or enable the polyfill
     * @param {String} value
     */
    set: function(value) {
        // find out the touch-action by the event handlers
        if (value == TOUCH_ACTION_COMPUTE) {
            value = this.compute();
        }

        if (NATIVE_TOUCH_ACTION && this.manager.element.style && TOUCH_ACTION_MAP[value]) {
            this.manager.element.style[PREFIXED_TOUCH_ACTION] = value;
        }
        this.actions = value.toLowerCase().trim();
    },

    /**
     * just re-set the touchAction value
     */
    update: function() {
        this.set(this.manager.options.touchAction);
    },

    /**
     * compute the value for the touchAction property based on the recognizer's settings
     * @returns {String} value
     */
    compute: function() {
        var actions = [];
        each(this.manager.recognizers, function(recognizer) {
            if (boolOrFn(recognizer.options.enable, [recognizer])) {
                actions = actions.concat(recognizer.getTouchAction());
            }
        });
        return cleanTouchActions(actions.join(' '));
    },

    /**
     * this method is called on each input cycle and provides the preventing of the browser behavior
     * @param {Object} input
     */
    preventDefaults: function(input) {
        var srcEvent = input.srcEvent;
        var direction = input.offsetDirection;

        // if the touch action did prevented once this session
        if (this.manager.session.prevented) {
            srcEvent.preventDefault();
            return;
        }

        var actions = this.actions;
        var hasNone = inStr(actions, TOUCH_ACTION_NONE) && !TOUCH_ACTION_MAP[TOUCH_ACTION_NONE];
        var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_Y];
        var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_X];

        if (hasNone) {
            //do not prevent defaults if this is a tap gesture

            var isTapPointer = input.pointers.length === 1;
            var isTapMovement = input.distance < 2;
            var isTapTouchTime = input.deltaTime < 250;

            if (isTapPointer && isTapMovement && isTapTouchTime) {
                return;
            }
        }

        if (hasPanX && hasPanY) {
            // `pan-x pan-y` means browser handles all scrolling/panning, do not prevent
            return;
        }

        if (hasNone ||
            (hasPanY && direction & DIRECTION_HORIZONTAL) ||
            (hasPanX && direction & DIRECTION_VERTICAL)) {
            return this.preventSrc(srcEvent);
        }
    },

    /**
     * call preventDefault to prevent the browser's default behavior (scrolling in most cases)
     * @param {Object} srcEvent
     */
    preventSrc: function(srcEvent) {
        this.manager.session.prevented = true;
        srcEvent.preventDefault();
    }
};

/**
 * when the touchActions are collected they are not a valid value, so we need to clean things up. *
 * @param {String} actions
 * @returns {*}
 */
function cleanTouchActions(actions) {
    // none
    if (inStr(actions, TOUCH_ACTION_NONE)) {
        return TOUCH_ACTION_NONE;
    }

    var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X);
    var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y);

    // if both pan-x and pan-y are set (different recognizers
    // for different directions, e.g. horizontal pan but vertical swipe?)
    // we need none (as otherwise with pan-x pan-y combined none of these
    // recognizers will work, since the browser would handle all panning
    if (hasPanX && hasPanY) {
        return TOUCH_ACTION_NONE;
    }

    // pan-x OR pan-y
    if (hasPanX || hasPanY) {
        return hasPanX ? TOUCH_ACTION_PAN_X : TOUCH_ACTION_PAN_Y;
    }

    // manipulation
    if (inStr(actions, TOUCH_ACTION_MANIPULATION)) {
        return TOUCH_ACTION_MANIPULATION;
    }

    return TOUCH_ACTION_AUTO;
}

function getTouchActionProps() {
    if (!NATIVE_TOUCH_ACTION) {
        return false;
    }
    var touchMap = {};
    var cssSupports = window.CSS && window.CSS.supports;
    ['auto', 'manipulation', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'].forEach(function(val) {

        // If css.supports is not supported but there is native touch-action assume it supports
        // all values. This is the case for IE 10 and 11.
        touchMap[val] = cssSupports ? window.CSS.supports('touch-action', val) : true;
    });
    return touchMap;
}

/**
 * Recognizer flow explained; *
 * All recognizers have the initial state of POSSIBLE when a input session starts.
 * The definition of a input session is from the first input until the last input, with all it's movement in it. *
 * Example session for mouse-input: mousedown -> mousemove -> mouseup
 *
 * On each recognizing cycle (see Manager.recognize) the .recognize() method is executed
 * which determines with state it should be.
 *
 * If the recognizer has the state FAILED, CANCELLED or RECOGNIZED (equals ENDED), it is reset to
 * POSSIBLE to give it another change on the next cycle.
 *
 *               Possible
 *                  |
 *            +-----+---------------+
 *            |                     |
 *      +-----+-----+               |
 *      |           |               |
 *   Failed      Cancelled          |
 *                          +-------+------+
 *                          |              |
 *                      Recognized       Began
 *                                         |
 *                                      Changed
 *                                         |
 *                                  Ended/Recognized
 */
var STATE_POSSIBLE = 1;
var STATE_BEGAN = 2;
var STATE_CHANGED = 4;
var STATE_ENDED = 8;
var STATE_RECOGNIZED = STATE_ENDED;
var STATE_CANCELLED = 16;
var STATE_FAILED = 32;

/**
 * Recognizer
 * Every recognizer needs to extend from this class.
 * @constructor
 * @param {Object} options
 */
function Recognizer(options) {
    this.options = assign({}, this.defaults, options || {});

    this.id = uniqueId();

    this.manager = null;

    // default is enable true
    this.options.enable = ifUndefined(this.options.enable, true);

    this.state = STATE_POSSIBLE;

    this.simultaneous = {};
    this.requireFail = [];
}

Recognizer.prototype = {
    /**
     * @virtual
     * @type {Object}
     */
    defaults: {},

    /**
     * set options
     * @param {Object} options
     * @return {Recognizer}
     */
    set: function(options) {
        assign(this.options, options);

        // also update the touchAction, in case something changed about the directions/enabled state
        this.manager && this.manager.touchAction.update();
        return this;
    },

    /**
     * recognize simultaneous with an other recognizer.
     * @param {Recognizer} otherRecognizer
     * @returns {Recognizer} this
     */
    recognizeWith: function(otherRecognizer) {
        if (invokeArrayArg(otherRecognizer, 'recognizeWith', this)) {
            return this;
        }

        var simultaneous = this.simultaneous;
        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
        if (!simultaneous[otherRecognizer.id]) {
            simultaneous[otherRecognizer.id] = otherRecognizer;
            otherRecognizer.recognizeWith(this);
        }
        return this;
    },

    /**
     * drop the simultaneous link. it doesnt remove the link on the other recognizer.
     * @param {Recognizer} otherRecognizer
     * @returns {Recognizer} this
     */
    dropRecognizeWith: function(otherRecognizer) {
        if (invokeArrayArg(otherRecognizer, 'dropRecognizeWith', this)) {
            return this;
        }

        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
        delete this.simultaneous[otherRecognizer.id];
        return this;
    },

    /**
     * recognizer can only run when an other is failing
     * @param {Recognizer} otherRecognizer
     * @returns {Recognizer} this
     */
    requireFailure: function(otherRecognizer) {
        if (invokeArrayArg(otherRecognizer, 'requireFailure', this)) {
            return this;
        }

        var requireFail = this.requireFail;
        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
        if (inArray(requireFail, otherRecognizer) === -1) {
            requireFail.push(otherRecognizer);
            otherRecognizer.requireFailure(this);
        }
        return this;
    },

    /**
     * drop the requireFailure link. it does not remove the link on the other recognizer.
     * @param {Recognizer} otherRecognizer
     * @returns {Recognizer} this
     */
    dropRequireFailure: function(otherRecognizer) {
        if (invokeArrayArg(otherRecognizer, 'dropRequireFailure', this)) {
            return this;
        }

        otherRecognizer = getRecognizerByNameIfManager(otherRecognizer, this);
        var index = inArray(this.requireFail, otherRecognizer);
        if (index > -1) {
            this.requireFail.splice(index, 1);
        }
        return this;
    },

    /**
     * has require failures boolean
     * @returns {boolean}
     */
    hasRequireFailures: function() {
        return this.requireFail.length > 0;
    },

    /**
     * if the recognizer can recognize simultaneous with an other recognizer
     * @param {Recognizer} otherRecognizer
     * @returns {Boolean}
     */
    canRecognizeWith: function(otherRecognizer) {
        return !!this.simultaneous[otherRecognizer.id];
    },

    /**
     * You should use `tryEmit` instead of `emit` directly to check
     * that all the needed recognizers has failed before emitting.
     * @param {Object} input
     */
    emit: function(input) {
        var self = this;
        var state = this.state;

        function emit(event) {
            self.manager.emit(event, input);
        }

        // 'panstart' and 'panmove'
        if (state < STATE_ENDED) {
            emit(self.options.event + stateStr(state));
        }

        emit(self.options.event); // simple 'eventName' events

        if (input.additionalEvent) { // additional event(panleft, panright, pinchin, pinchout...)
            emit(input.additionalEvent);
        }

        // panend and pancancel
        if (state >= STATE_ENDED) {
            emit(self.options.event + stateStr(state));
        }
    },

    /**
     * Check that all the require failure recognizers has failed,
     * if true, it emits a gesture event,
     * otherwise, setup the state to FAILED.
     * @param {Object} input
     */
    tryEmit: function(input) {
        if (this.canEmit()) {
            return this.emit(input);
        }
        // it's failing anyway
        this.state = STATE_FAILED;
    },

    /**
     * can we emit?
     * @returns {boolean}
     */
    canEmit: function() {
        var i = 0;
        while (i < this.requireFail.length) {
            if (!(this.requireFail[i].state & (STATE_FAILED | STATE_POSSIBLE))) {
                return false;
            }
            i++;
        }
        return true;
    },

    /**
     * update the recognizer
     * @param {Object} inputData
     */
    recognize: function(inputData) {
        // make a new copy of the inputData
        // so we can change the inputData without messing up the other recognizers
        var inputDataClone = assign({}, inputData);

        // is is enabled and allow recognizing?
        if (!boolOrFn(this.options.enable, [this, inputDataClone])) {
            this.reset();
            this.state = STATE_FAILED;
            return;
        }

        // reset when we've reached the end
        if (this.state & (STATE_RECOGNIZED | STATE_CANCELLED | STATE_FAILED)) {
            this.state = STATE_POSSIBLE;
        }

        this.state = this.process(inputDataClone);

        // the recognizer has recognized a gesture
        // so trigger an event
        if (this.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED | STATE_CANCELLED)) {
            this.tryEmit(inputDataClone);
        }
    },

    /**
     * return the state of the recognizer
     * the actual recognizing happens in this method
     * @virtual
     * @param {Object} inputData
     * @returns {Const} STATE
     */
    process: function(inputData) { }, // jshint ignore:line

    /**
     * return the preferred touch-action
     * @virtual
     * @returns {Array}
     */
    getTouchAction: function() { },

    /**
     * called when the gesture isn't allowed to recognize
     * like when another is being recognized or it is disabled
     * @virtual
     */
    reset: function() { }
};

/**
 * get a usable string, used as event postfix
 * @param {Const} state
 * @returns {String} state
 */
function stateStr(state) {
    if (state & STATE_CANCELLED) {
        return 'cancel';
    } else if (state & STATE_ENDED) {
        return 'end';
    } else if (state & STATE_CHANGED) {
        return 'move';
    } else if (state & STATE_BEGAN) {
        return 'start';
    }
    return '';
}

/**
 * direction cons to string
 * @param {Const} direction
 * @returns {String}
 */
function directionStr(direction) {
    if (direction == DIRECTION_DOWN) {
        return 'down';
    } else if (direction == DIRECTION_UP) {
        return 'up';
    } else if (direction == DIRECTION_LEFT) {
        return 'left';
    } else if (direction == DIRECTION_RIGHT) {
        return 'right';
    }
    return '';
}

/**
 * get a recognizer by name if it is bound to a manager
 * @param {Recognizer|String} otherRecognizer
 * @param {Recognizer} recognizer
 * @returns {Recognizer}
 */
function getRecognizerByNameIfManager(otherRecognizer, recognizer) {
    var manager = recognizer.manager;
    if (manager) {
        return manager.get(otherRecognizer);
    }
    return otherRecognizer;
}

/**
 * This recognizer is just used as a base for the simple attribute recognizers.
 * @constructor
 * @extends Recognizer
 */
function AttrRecognizer() {
    Recognizer.apply(this, arguments);
}

inherit(AttrRecognizer, Recognizer, {
    /**
     * @namespace
     * @memberof AttrRecognizer
     */
    defaults: {
        /**
         * @type {Number}
         * @default 1
         */
        pointers: 1
    },

    /**
     * Used to check if it the recognizer receives valid input, like input.distance > 10.
     * @memberof AttrRecognizer
     * @param {Object} input
     * @returns {Boolean} recognized
     */
    attrTest: function(input) {
        var optionPointers = this.options.pointers;
        return optionPointers === 0 || input.pointers.length === optionPointers;
    },

    /**
     * Process the input and return the state for the recognizer
     * @memberof AttrRecognizer
     * @param {Object} input
     * @returns {*} State
     */
    process: function(input) {
        var state = this.state;
        var eventType = input.eventType;

        var isRecognized = state & (STATE_BEGAN | STATE_CHANGED);
        var isValid = this.attrTest(input);

        // on cancel input and we've recognized before, return STATE_CANCELLED
        if (isRecognized && (eventType & INPUT_CANCEL || !isValid)) {
            return state | STATE_CANCELLED;
        } else if (isRecognized || isValid) {
            if (eventType & INPUT_END) {
                return state | STATE_ENDED;
            } else if (!(state & STATE_BEGAN)) {
                return STATE_BEGAN;
            }
            return state | STATE_CHANGED;
        }
        return STATE_FAILED;
    }
});

/**
 * Pan
 * Recognized when the pointer is down and moved in the allowed direction.
 * @constructor
 * @extends AttrRecognizer
 */
function PanRecognizer() {
    AttrRecognizer.apply(this, arguments);

    this.pX = null;
    this.pY = null;
}

inherit(PanRecognizer, AttrRecognizer, {
    /**
     * @namespace
     * @memberof PanRecognizer
     */
    defaults: {
        event: 'pan',
        threshold: 10,
        pointers: 1,
        direction: DIRECTION_ALL
    },

    getTouchAction: function() {
        var direction = this.options.direction;
        var actions = [];
        if (direction & DIRECTION_HORIZONTAL) {
            actions.push(TOUCH_ACTION_PAN_Y);
        }
        if (direction & DIRECTION_VERTICAL) {
            actions.push(TOUCH_ACTION_PAN_X);
        }
        return actions;
    },

    directionTest: function(input) {
        var options = this.options;
        var hasMoved = true;
        var distance = input.distance;
        var direction = input.direction;
        var x = input.deltaX;
        var y = input.deltaY;

        // lock to axis?
        if (!(direction & options.direction)) {
            if (options.direction & DIRECTION_HORIZONTAL) {
                direction = (x === 0) ? DIRECTION_NONE : (x < 0) ? DIRECTION_LEFT : DIRECTION_RIGHT;
                hasMoved = x != this.pX;
                distance = Math.abs(input.deltaX);
            } else {
                direction = (y === 0) ? DIRECTION_NONE : (y < 0) ? DIRECTION_UP : DIRECTION_DOWN;
                hasMoved = y != this.pY;
                distance = Math.abs(input.deltaY);
            }
        }
        input.direction = direction;
        return hasMoved && distance > options.threshold && direction & options.direction;
    },

    attrTest: function(input) {
        return AttrRecognizer.prototype.attrTest.call(this, input) &&
            (this.state & STATE_BEGAN || (!(this.state & STATE_BEGAN) && this.directionTest(input)));
    },

    emit: function(input) {

        this.pX = input.deltaX;
        this.pY = input.deltaY;

        var direction = directionStr(input.direction);

        if (direction) {
            input.additionalEvent = this.options.event + direction;
        }
        this._super.emit.call(this, input);
    }
});

/**
 * Pinch
 * Recognized when two or more pointers are moving toward (zoom-in) or away from each other (zoom-out).
 * @constructor
 * @extends AttrRecognizer
 */
function PinchRecognizer() {
    AttrRecognizer.apply(this, arguments);
}

inherit(PinchRecognizer, AttrRecognizer, {
    /**
     * @namespace
     * @memberof PinchRecognizer
     */
    defaults: {
        event: 'pinch',
        threshold: 0,
        pointers: 2
    },

    getTouchAction: function() {
        return [TOUCH_ACTION_NONE];
    },

    attrTest: function(input) {
        return this._super.attrTest.call(this, input) &&
            (Math.abs(input.scale - 1) > this.options.threshold || this.state & STATE_BEGAN);
    },

    emit: function(input) {
        if (input.scale !== 1) {
            var inOut = input.scale < 1 ? 'in' : 'out';
            input.additionalEvent = this.options.event + inOut;
        }
        this._super.emit.call(this, input);
    }
});

/**
 * Press
 * Recognized when the pointer is down for x ms without any movement.
 * @constructor
 * @extends Recognizer
 */
function PressRecognizer() {
    Recognizer.apply(this, arguments);

    this._timer = null;
    this._input = null;
}

inherit(PressRecognizer, Recognizer, {
    /**
     * @namespace
     * @memberof PressRecognizer
     */
    defaults: {
        event: 'press',
        pointers: 1,
        time: 251, // minimal time of the pointer to be pressed
        threshold: 9 // a minimal movement is ok, but keep it low
    },

    getTouchAction: function() {
        return [TOUCH_ACTION_AUTO];
    },

    process: function(input) {
        var options = this.options;
        var validPointers = input.pointers.length === options.pointers;
        var validMovement = input.distance < options.threshold;
        var validTime = input.deltaTime > options.time;

        this._input = input;

        // we only allow little movement
        // and we've reached an end event, so a tap is possible
        if (!validMovement || !validPointers || (input.eventType & (INPUT_END | INPUT_CANCEL) && !validTime)) {
            this.reset();
        } else if (input.eventType & INPUT_START) {
            this.reset();
            this._timer = setTimeoutContext(function() {
                this.state = STATE_RECOGNIZED;
                this.tryEmit();
            }, options.time, this);
        } else if (input.eventType & INPUT_END) {
            return STATE_RECOGNIZED;
        }
        return STATE_FAILED;
    },

    reset: function() {
        clearTimeout(this._timer);
    },

    emit: function(input) {
        if (this.state !== STATE_RECOGNIZED) {
            return;
        }

        if (input && (input.eventType & INPUT_END)) {
            this.manager.emit(this.options.event + 'up', input);
        } else {
            this._input.timeStamp = now();
            this.manager.emit(this.options.event, this._input);
        }
    }
});

/**
 * Rotate
 * Recognized when two or more pointer are moving in a circular motion.
 * @constructor
 * @extends AttrRecognizer
 */
function RotateRecognizer() {
    AttrRecognizer.apply(this, arguments);
}

inherit(RotateRecognizer, AttrRecognizer, {
    /**
     * @namespace
     * @memberof RotateRecognizer
     */
    defaults: {
        event: 'rotate',
        threshold: 0,
        pointers: 2
    },

    getTouchAction: function() {
        return [TOUCH_ACTION_NONE];
    },

    attrTest: function(input) {
        return this._super.attrTest.call(this, input) &&
            (Math.abs(input.rotation) > this.options.threshold || this.state & STATE_BEGAN);
    }
});

/**
 * Swipe
 * Recognized when the pointer is moving fast (velocity), with enough distance in the allowed direction.
 * @constructor
 * @extends AttrRecognizer
 */
function SwipeRecognizer() {
    AttrRecognizer.apply(this, arguments);
}

inherit(SwipeRecognizer, AttrRecognizer, {
    /**
     * @namespace
     * @memberof SwipeRecognizer
     */
    defaults: {
        event: 'swipe',
        threshold: 10,
        velocity: 0.3,
        direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL,
        pointers: 1
    },

    getTouchAction: function() {
        return PanRecognizer.prototype.getTouchAction.call(this);
    },

    attrTest: function(input) {
        var direction = this.options.direction;
        var velocity;

        if (direction & (DIRECTION_HORIZONTAL | DIRECTION_VERTICAL)) {
            velocity = input.overallVelocity;
        } else if (direction & DIRECTION_HORIZONTAL) {
            velocity = input.overallVelocityX;
        } else if (direction & DIRECTION_VERTICAL) {
            velocity = input.overallVelocityY;
        }

        return this._super.attrTest.call(this, input) &&
            direction & input.offsetDirection &&
            input.distance > this.options.threshold &&
            input.maxPointers == this.options.pointers &&
            abs(velocity) > this.options.velocity && input.eventType & INPUT_END;
    },

    emit: function(input) {
        var direction = directionStr(input.offsetDirection);
        if (direction) {
            this.manager.emit(this.options.event + direction, input);
        }

        this.manager.emit(this.options.event, input);
    }
});

/**
 * A tap is ecognized when the pointer is doing a small tap/click. Multiple taps are recognized if they occur
 * between the given interval and position. The delay option can be used to recognize multi-taps without firing
 * a single tap.
 *
 * The eventData from the emitted event contains the property `tapCount`, which contains the amount of
 * multi-taps being recognized.
 * @constructor
 * @extends Recognizer
 */
function TapRecognizer() {
    Recognizer.apply(this, arguments);

    // previous time and center,
    // used for tap counting
    this.pTime = false;
    this.pCenter = false;

    this._timer = null;
    this._input = null;
    this.count = 0;
}

inherit(TapRecognizer, Recognizer, {
    /**
     * @namespace
     * @memberof PinchRecognizer
     */
    defaults: {
        event: 'tap',
        pointers: 1,
        taps: 1,
        interval: 300, // max time between the multi-tap taps
        time: 250, // max time of the pointer to be down (like finger on the screen)
        threshold: 9, // a minimal movement is ok, but keep it low
        posThreshold: 10 // a multi-tap can be a bit off the initial position
    },

    getTouchAction: function() {
        return [TOUCH_ACTION_MANIPULATION];
    },

    process: function(input) {
        var options = this.options;

        var validPointers = input.pointers.length === options.pointers;
        var validMovement = input.distance < options.threshold;
        var validTouchTime = input.deltaTime < options.time;

        this.reset();

        if ((input.eventType & INPUT_START) && (this.count === 0)) {
            return this.failTimeout();
        }

        // we only allow little movement
        // and we've reached an end event, so a tap is possible
        if (validMovement && validTouchTime && validPointers) {
            if (input.eventType != INPUT_END) {
                return this.failTimeout();
            }

            var validInterval = this.pTime ? (input.timeStamp - this.pTime < options.interval) : true;
            var validMultiTap = !this.pCenter || getDistance(this.pCenter, input.center) < options.posThreshold;

            this.pTime = input.timeStamp;
            this.pCenter = input.center;

            if (!validMultiTap || !validInterval) {
                this.count = 1;
            } else {
                this.count += 1;
            }

            this._input = input;

            // if tap count matches we have recognized it,
            // else it has began recognizing...
            var tapCount = this.count % options.taps;
            if (tapCount === 0) {
                // no failing requirements, immediately trigger the tap event
                // or wait as long as the multitap interval to trigger
                if (!this.hasRequireFailures()) {
                    return STATE_RECOGNIZED;
                } else {
                    this._timer = setTimeoutContext(function() {
                        this.state = STATE_RECOGNIZED;
                        this.tryEmit();
                    }, options.interval, this);
                    return STATE_BEGAN;
                }
            }
        }
        return STATE_FAILED;
    },

    failTimeout: function() {
        this._timer = setTimeoutContext(function() {
            this.state = STATE_FAILED;
        }, this.options.interval, this);
        return STATE_FAILED;
    },

    reset: function() {
        clearTimeout(this._timer);
    },

    emit: function() {
        if (this.state == STATE_RECOGNIZED) {
            this._input.tapCount = this.count;
            this.manager.emit(this.options.event, this._input);
        }
    }
});

/**
 * Simple way to create a manager with a default set of recognizers.
 * @param {HTMLElement} element
 * @param {Object} [options]
 * @constructor
 */
function Hammer(element, options) {
    options = options || {};
    options.recognizers = ifUndefined(options.recognizers, Hammer.defaults.preset);
    return new Manager(element, options);
}

/**
 * @const {string}
 */
Hammer.VERSION = '2.0.7';

/**
 * default settings
 * @namespace
 */
Hammer.defaults = {
    /**
     * set if DOM events are being triggered.
     * But this is slower and unused by simple implementations, so disabled by default.
     * @type {Boolean}
     * @default false
     */
    domEvents: false,

    /**
     * The value for the touchAction property/fallback.
     * When set to `compute` it will magically set the correct value based on the added recognizers.
     * @type {String}
     * @default compute
     */
    touchAction: TOUCH_ACTION_COMPUTE,

    /**
     * @type {Boolean}
     * @default true
     */
    enable: true,

    /**
     * EXPERIMENTAL FEATURE -- can be removed/changed
     * Change the parent input target element.
     * If Null, then it is being set the to main element.
     * @type {Null|EventTarget}
     * @default null
     */
    inputTarget: null,

    /**
     * force an input class
     * @type {Null|Function}
     * @default null
     */
    inputClass: null,

    /**
     * Default recognizer setup when calling `Hammer()`
     * When creating a new Manager these will be skipped.
     * @type {Array}
     */
    preset: [
        // RecognizerClass, options, [recognizeWith, ...], [requireFailure, ...]
        [RotateRecognizer, {enable: false}],
        [PinchRecognizer, {enable: false}, ['rotate']],
        [SwipeRecognizer, {direction: DIRECTION_HORIZONTAL}],
        [PanRecognizer, {direction: DIRECTION_HORIZONTAL}, ['swipe']],
        [TapRecognizer],
        [TapRecognizer, {event: 'doubletap', taps: 2}, ['tap']],
        [PressRecognizer]
    ],

    /**
     * Some CSS properties can be used to improve the working of Hammer.
     * Add them to this method and they will be set when creating a new Manager.
     * @namespace
     */
    cssProps: {
        /**
         * Disables text selection to improve the dragging gesture. Mainly for desktop browsers.
         * @type {String}
         * @default 'none'
         */
        userSelect: 'none',

        /**
         * Disable the Windows Phone grippers when pressing an element.
         * @type {String}
         * @default 'none'
         */
        touchSelect: 'none',

        /**
         * Disables the default callout shown when you touch and hold a touch target.
         * On iOS, when you touch and hold a touch target such as a link, Safari displays
         * a callout containing information about the link. This property allows you to disable that callout.
         * @type {String}
         * @default 'none'
         */
        touchCallout: 'none',

        /**
         * Specifies whether zooming is enabled. Used by IE10>
         * @type {String}
         * @default 'none'
         */
        contentZooming: 'none',

        /**
         * Specifies that an entire element should be draggable instead of its contents. Mainly for desktop browsers.
         * @type {String}
         * @default 'none'
         */
        userDrag: 'none',

        /**
         * Overrides the highlight color shown when the user taps a link or a JavaScript
         * clickable element in iOS. This property obeys the alpha value, if specified.
         * @type {String}
         * @default 'rgba(0,0,0,0)'
         */
        tapHighlightColor: 'rgba(0,0,0,0)'
    }
};

var STOP = 1;
var FORCED_STOP = 2;

/**
 * Manager
 * @param {HTMLElement} element
 * @param {Object} [options]
 * @constructor
 */
function Manager(element, options) {
    this.options = assign({}, Hammer.defaults, options || {});

    this.options.inputTarget = this.options.inputTarget || element;

    this.handlers = {};
    this.session = {};
    this.recognizers = [];
    this.oldCssProps = {};

    this.element = element;
    this.input = createInputInstance(this);
    this.touchAction = new TouchAction(this, this.options.touchAction);

    toggleCssProps(this, true);

    each(this.options.recognizers, function(item) {
        var recognizer = this.add(new (item[0])(item[1]));
        item[2] && recognizer.recognizeWith(item[2]);
        item[3] && recognizer.requireFailure(item[3]);
    }, this);
}

Manager.prototype = {
    /**
     * set options
     * @param {Object} options
     * @returns {Manager}
     */
    set: function(options) {
        assign(this.options, options);

        // Options that need a little more setup
        if (options.touchAction) {
            this.touchAction.update();
        }
        if (options.inputTarget) {
            // Clean up existing event listeners and reinitialize
            this.input.destroy();
            this.input.target = options.inputTarget;
            this.input.init();
        }
        return this;
    },

    /**
     * stop recognizing for this session.
     * This session will be discarded, when a new [input]start event is fired.
     * When forced, the recognizer cycle is stopped immediately.
     * @param {Boolean} [force]
     */
    stop: function(force) {
        this.session.stopped = force ? FORCED_STOP : STOP;
    },

    /**
     * run the recognizers!
     * called by the inputHandler function on every movement of the pointers (touches)
     * it walks through all the recognizers and tries to detect the gesture that is being made
     * @param {Object} inputData
     */
    recognize: function(inputData) {
        var session = this.session;
        if (session.stopped) {
            return;
        }

        // run the touch-action polyfill
        this.touchAction.preventDefaults(inputData);

        var recognizer;
        var recognizers = this.recognizers;

        // this holds the recognizer that is being recognized.
        // so the recognizer's state needs to be BEGAN, CHANGED, ENDED or RECOGNIZED
        // if no recognizer is detecting a thing, it is set to `null`
        var curRecognizer = session.curRecognizer;

        // reset when the last recognizer is recognized
        // or when we're in a new session
        if (!curRecognizer || (curRecognizer && curRecognizer.state & STATE_RECOGNIZED)) {
            curRecognizer = session.curRecognizer = null;
        }

        var i = 0;
        while (i < recognizers.length) {
            recognizer = recognizers[i];

            // find out if we are allowed try to recognize the input for this one.
            // 1.   allow if the session is NOT forced stopped (see the .stop() method)
            // 2.   allow if we still haven't recognized a gesture in this session, or the this recognizer is the one
            //      that is being recognized.
            // 3.   allow if the recognizer is allowed to run simultaneous with the current recognized recognizer.
            //      this can be setup with the `recognizeWith()` method on the recognizer.
            if (session.stopped !== FORCED_STOP && ( // 1
                    !curRecognizer || recognizer == curRecognizer || // 2
                    recognizer.canRecognizeWith(curRecognizer))) { // 3
                recognizer.recognize(inputData);
            } else {
                recognizer.reset();
            }

            // if the recognizer has been recognizing the input as a valid gesture, we want to store this one as the
            // current active recognizer. but only if we don't already have an active recognizer
            if (!curRecognizer && recognizer.state & (STATE_BEGAN | STATE_CHANGED | STATE_ENDED)) {
                curRecognizer = session.curRecognizer = recognizer;
            }
            i++;
        }
    },

    /**
     * get a recognizer by its event name.
     * @param {Recognizer|String} recognizer
     * @returns {Recognizer|Null}
     */
    get: function(recognizer) {
        if (recognizer instanceof Recognizer) {
            return recognizer;
        }

        var recognizers = this.recognizers;
        for (var i = 0; i < recognizers.length; i++) {
            if (recognizers[i].options.event == recognizer) {
                return recognizers[i];
            }
        }
        return null;
    },

    /**
     * add a recognizer to the manager
     * existing recognizers with the same event name will be removed
     * @param {Recognizer} recognizer
     * @returns {Recognizer|Manager}
     */
    add: function(recognizer) {
        if (invokeArrayArg(recognizer, 'add', this)) {
            return this;
        }

        // remove existing
        var existing = this.get(recognizer.options.event);
        if (existing) {
            this.remove(existing);
        }

        this.recognizers.push(recognizer);
        recognizer.manager = this;

        this.touchAction.update();
        return recognizer;
    },

    /**
     * remove a recognizer by name or instance
     * @param {Recognizer|String} recognizer
     * @returns {Manager}
     */
    remove: function(recognizer) {
        if (invokeArrayArg(recognizer, 'remove', this)) {
            return this;
        }

        recognizer = this.get(recognizer);

        // let's make sure this recognizer exists
        if (recognizer) {
            var recognizers = this.recognizers;
            var index = inArray(recognizers, recognizer);

            if (index !== -1) {
                recognizers.splice(index, 1);
                this.touchAction.update();
            }
        }

        return this;
    },

    /**
     * bind event
     * @param {String} events
     * @param {Function} handler
     * @returns {EventEmitter} this
     */
    on: function(events, handler) {
        if (events === undefined$1) {
            return;
        }
        if (handler === undefined$1) {
            return;
        }

        var handlers = this.handlers;
        each(splitStr(events), function(event) {
            handlers[event] = handlers[event] || [];
            handlers[event].push(handler);
        });
        return this;
    },

    /**
     * unbind event, leave emit blank to remove all handlers
     * @param {String} events
     * @param {Function} [handler]
     * @returns {EventEmitter} this
     */
    off: function(events, handler) {
        if (events === undefined$1) {
            return;
        }

        var handlers = this.handlers;
        each(splitStr(events), function(event) {
            if (!handler) {
                delete handlers[event];
            } else {
                handlers[event] && handlers[event].splice(inArray(handlers[event], handler), 1);
            }
        });
        return this;
    },

    /**
     * emit event to the listeners
     * @param {String} event
     * @param {Object} data
     */
    emit: function(event, data) {
        // we also want to trigger dom events
        if (this.options.domEvents) {
            triggerDomEvent(event, data);
        }

        // no handlers, so skip it all
        var handlers = this.handlers[event] && this.handlers[event].slice();
        if (!handlers || !handlers.length) {
            return;
        }

        data.type = event;
        data.preventDefault = function() {
            data.srcEvent.preventDefault();
        };

        var i = 0;
        while (i < handlers.length) {
            handlers[i](data);
            i++;
        }
    },

    /**
     * destroy the manager and unbinds all events
     * it doesn't unbind dom events, that is the user own responsibility
     */
    destroy: function() {
        this.element && toggleCssProps(this, false);

        this.handlers = {};
        this.session = {};
        this.input.destroy();
        this.element = null;
    }
};

/**
 * add/remove the css properties as defined in manager.options.cssProps
 * @param {Manager} manager
 * @param {Boolean} add
 */
function toggleCssProps(manager, add) {
    var element = manager.element;
    if (!element.style) {
        return;
    }
    var prop;
    each(manager.options.cssProps, function(value, name) {
        prop = prefixed(element.style, name);
        if (add) {
            manager.oldCssProps[prop] = element.style[prop];
            element.style[prop] = value;
        } else {
            element.style[prop] = manager.oldCssProps[prop] || '';
        }
    });
    if (!add) {
        manager.oldCssProps = {};
    }
}

/**
 * trigger dom event
 * @param {String} event
 * @param {Object} data
 */
function triggerDomEvent(event, data) {
    var gestureEvent = document.createEvent('Event');
    gestureEvent.initEvent(event, true, true);
    gestureEvent.gesture = data;
    data.target.dispatchEvent(gestureEvent);
}

assign(Hammer, {
    INPUT_START: INPUT_START,
    INPUT_MOVE: INPUT_MOVE,
    INPUT_END: INPUT_END,
    INPUT_CANCEL: INPUT_CANCEL,

    STATE_POSSIBLE: STATE_POSSIBLE,
    STATE_BEGAN: STATE_BEGAN,
    STATE_CHANGED: STATE_CHANGED,
    STATE_ENDED: STATE_ENDED,
    STATE_RECOGNIZED: STATE_RECOGNIZED,
    STATE_CANCELLED: STATE_CANCELLED,
    STATE_FAILED: STATE_FAILED,

    DIRECTION_NONE: DIRECTION_NONE,
    DIRECTION_LEFT: DIRECTION_LEFT,
    DIRECTION_RIGHT: DIRECTION_RIGHT,
    DIRECTION_UP: DIRECTION_UP,
    DIRECTION_DOWN: DIRECTION_DOWN,
    DIRECTION_HORIZONTAL: DIRECTION_HORIZONTAL,
    DIRECTION_VERTICAL: DIRECTION_VERTICAL,
    DIRECTION_ALL: DIRECTION_ALL,

    Manager: Manager,
    Input: Input,
    TouchAction: TouchAction,

    TouchInput: TouchInput,
    MouseInput: MouseInput,
    PointerEventInput: PointerEventInput,
    TouchMouseInput: TouchMouseInput,
    SingleTouchInput: SingleTouchInput,

    Recognizer: Recognizer,
    AttrRecognizer: AttrRecognizer,
    Tap: TapRecognizer,
    Pan: PanRecognizer,
    Swipe: SwipeRecognizer,
    Pinch: PinchRecognizer,
    Rotate: RotateRecognizer,
    Press: PressRecognizer,

    on: addEventListeners,
    off: removeEventListeners,
    each: each,
    merge: merge,
    extend: extend,
    assign: assign,
    inherit: inherit,
    bindFn: bindFn,
    prefixed: prefixed
});

// this prevents errors when Hammer is loaded in the presence of an AMD
//  style loader but by script tag, not by the loader.
var freeGlobal = (typeof window !== 'undefined' ? window : (typeof self !== 'undefined' ? self : {})); // jshint ignore:line
freeGlobal.Hammer = Hammer;

if (typeof undefined$1 === 'function' && undefined$1.amd) {
    undefined$1(function() {
        return Hammer;
    });
} else if (module.exports) {
    module.exports = Hammer;
} else {
    window[exportName] = Hammer;
}

})(window, document, 'Hammer');
});

const mediaQueries = {
  
  props: {
    classPrefix: 'break--'
  },
  
  state: {
    ['mobile']: false,
    ['tablet-portrait']: false,
    ['tablet-landscape']: false,
    ['desktop']: false,
  },
  
  state2: {
    prevBreakpoint: null,
    current: null,
  },
  
  
  
  init: (signal) => {
  
    mediaQueries.props.signal = signal;
    mediaQueries.props.signal.on(config.eventNames.APP_RESIZE_END, () => {
      mediaQueries.update();
    });
    mediaQueries.update();
  },
  
  getValue: () => {
    
    let breakpoint = mediaQueries.state2.current;
    let hasChanged = mediaQueries.state2.current !== mediaQueries.state2.prevBreakpoint;
    
    // *** fallback default - fixes 3106, 3306. appears to be safe
    if (!breakpoint) {
      breakpoint = 'tablet-portrait';
      hasChanged = true;
    }
    
    return {
      breakpoint,
      hasChanged,
    }
  },
  
  update: () => {
    mediaQueries.state['mobile'] = window.matchMedia(
        `(max-width: ${config.matchMedia.mobile}px)`).matches;
    mediaQueries.state['tablet-portrait'] = window.matchMedia(
        `(min-width: ${config.matchMedia.tablet_portrait}px) and (max-width: ${config.matchMedia.tablet_landscape - 1}px`).matches;
    mediaQueries.state['tablet-landscape'] = window.matchMedia(
        `(min-width: ${config.matchMedia.tablet_landscape}px) and (max-width: ${config.matchMedia.desktop - 1}px)`).matches;
    mediaQueries.state['desktop'] = window.matchMedia(
        `(min-width: ${config.matchMedia.desktop}px)`).matches;
    
    Object.keys(mediaQueries.state).map((item) => {
      const word = `${mediaQueries.props.classPrefix}${item}`;
      if (mediaQueries.state[item]) {
        document.body.classList.add(word);
        mediaQueries.state2.prevBreakpoint = mediaQueries.state2.current;
        mediaQueries.state2.current = item;
      } else {
        document.body.classList.remove(word);
      }
    });
    
    mediaQueries.props.signal.emit(
        config.eventNames.APP_BREAKPOINT_READY,
        mediaQueries.getValue()
    );
  }
};

const actions = {
  CAROUSEL_NAVIGATION_INTENTION: 'CAROUSEL_NAVIGATION_INTENTION',
  CAROUSEL_NAVIGATION_GESTURE: 'CAROUSEL_NAVIGATION_GESTURE',
  CAROUSEL_UPDATING: 'CAROUSEL_UPDATING',
  CAROUSEL_WILL_UPDATE: 'CAROUSEL_WILL_UPDATE', // *** when @deferUpdateRunning in use fires this at start of deferment... make sense?
  CAROUSEL_HAS_UPDATED: 'CAROUSEL_HAS_UPDATED', // *** carousel has finished scroll
  TABSWITCHER_NAVIGATION_INTENTION: 'TABSWITCHER_NAVIGATION_INTENTION', // **ADDED4TIMELINE need this for timeline event
};

/**
 * behaviours are event driven. these utilities add a little sugar to their usage
 */
const utils = {
  // *** ensures binding is received on the correct 'channel'
  eventFilter: (options, payload) => {
    
    if (!options.binding) return false;

    const target = Array.isArray(options.binding.from)
      ? options.binding.from[0]
      : options.binding.from;

    let pass = false;
    if (options.binding.from && target.includes(payload.sender)) {
      pass = true;
    }
    return pass;
  },

  // *** convenience to allow cleaner invocation from within dispatching behaviour
  dispatcher: (options, args) => {
    
    // console.log('/utils/ -dispatcher', options, args);
    
    options.signal.emit(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, {
      sender: options.config.uid,
      type: args.type,
      data: args.data,
    });
  },

  /* @as : TODO - util to essentially determine if children are present in dom or should be dynamically generated */
  hasChildren: (el, type) => {
    const nodes = [...el.querySelectorAll(type)];
    return !!nodes.length;
  },
};

const simpleTweens = {
  defaults: {
    time: config.timings.animation.short(),
    delay: 0,
    ease: 'Sine.inout', // TODO
  },

  // *** prep values with defaults if required
  setValues: (options) => {
    return {
      time: options.time ? options.time : simpleTweens.defaults.time,
      delay: options.delay ? options.delay : simpleTweens.defaults.delay,
      onStart: options.onStart ? options.onStart : () => {},
      onComplete: options.onComplete ? options.onComplete : () => {},
    };
  },

  fade: (options) => {
    const opacity = options.type === 'in' ? 1 : 0;
    const vo = simpleTweens.setValues(options);
    // console.log('/simpleTweens/ -fade', options);
    gsapWithCSS$1.to(options.el, vo.time, {
      opacity,
      delay: vo.delay,
      ease: simpleTweens.defaults.ease,
      onStart: vo.onStart,
      onComplete: vo.onComplete,
      
      onUpdate: () => {
        // console.log('/simpleTweens/ -onUpdate', options.el.style.opacity);
      }
    });
  },
};

/**
 * generic carousel 'behaviour'
 * invoked from html `data-behaviour="carousel"`
 * data-behaviour-config options:
 * > mobile: <int>, tablet-portrait: <int>, tablet-landscape: <int>, desktop: <int> = num items to display in each slide at defined breakpoints
 * > layout: <string> when 'grid' = resizes elements to fit container
 * > async: <bool> when true = expects children to be added via 'inject' method
 * > self-control: <bool> when true = allows built-in gesture control without requiring receipt of external control event
 * > peek: <bool> when true = allows a peek preview of next slide
 * > peekAmount: <int> the amt to peek by for above
 *   (e.g. by having 'carousel-controls' present and bound in dom).
 *
 *   CHANGELOG
 *   @as 10.3.22 exposed state object:
 *   example from a parent component:
 *   :: const instance = Behaviours.getBehaviourByUid(<uid>);
 *   :: instance.options.state.lockNavigation = true etc
 */
const Carousel = (options) => {
  const props = {
    originalOptions: options,
    classes: {
      // *** for dynamically generated elements
      viewport: 'carousel__viewport',
      inner: 'carousel__inner',
      page: 'carousel__page',
      item: 'carousel__item',
      placeholder: 'carousel__item--placeholder',
      releaseHeight: 'release-height',
    },
    redrawDelay: 0, // *** delay before performing actual dom sizing
  };
  
  let state = {
    numPages: 0,
    currentIndex: 0,
    viewportRect: null,
    pageChunks: [], // *** full item list broken into page chunks
    itemsPerPage: 0,
    shouldHardReset: false, // *** hard reset true when breakpoint crossed, involving re-calc of pages
    selfControl: false, // *** sets to true if no binding (i.e. with carouselControls) declared, will use gesture control and bypass normal event to update
    eventsAdded: false,
    pageGap: 14, // page gap of 1em between carousel items
    peek: false,
    peekAmount: 28, // default to 2em if user doesnt enter an amt
    ignoreArtificialEvents: false,
    deferUpdateTime: null,  // TEST
    deferUpdateRunning: null,
    
    lockNavigation: false, // *** prevents navigation - used for hero carousel to block while animation is in progress
    defaultEase: 'power3.inout',
  };
  
  let els = {}; //*** parentEl, el, pages: [] etc
  
  /**
   * when injecting new children and re-invoking init, prepare elements
   */
  const cleanup = () => {
    options.els.el.style.opacity = 0; // *** hide jank
    
    const staleViewport = els.el.querySelector(`.${props.classes.viewport}`);
    const staleInner = els.el.querySelector(`.${props.classes.inner}`);
    
    if (staleViewport) {
      const currHeight = staleViewport.getBoundingClientRect().height;
      options.els.el.style.height = `${currHeight}px`; // *** prevents dom collapse during this process
      staleViewport.remove();
    }
    if (staleInner) staleInner.remove();
    
    [...els.el.children].map((item) => {
      item.style.opacity = 1; // *** cloned items may be faded out by another component
    });
    
    simpleTweens.fade({
      el: els.el,
      type: 'in',
      delay: config.timings.animation.medium(),
      onComplete: () => {
        options.els.el.style.height = 'initial';
      },
    });
  };
  
  // *** facade for injecting dynamic content, add els and restart
  const inject = (data /* index not implemented */) => {
    data.map((item) => {
      options.els.el.appendChild(item);
    });
    init();
  };
  
  const init = () => {
    els = options.els;
    cleanup(); // *** smooths injection of new children
    
    els.pages = [];
    state.items = [...els.el.children]; // *** get items from dom
    state.numItems = state.items.length;
    
    state.selfControl =
        options.config['self-control'] &&
        options.config['self-control'] === 'true';
    
    // *** without binding ensure self control via built-in gestures still available
    if (!options.binding) {
      console.warn(
          '/Carousel/ -init --no control binding declared, only gestures control available'
      );
      state.selfControl = true;
    }
    
    state.ignoreArtificialEvents =
        options.config.ignoreArtificialEvents === 'true';
    
    // state.deferUpdateTime = options.config.deferUpdateTime || null;
    
    // *** when async, block until children present
    if (options.config.async && state.numItems === 0) {
      console.warn('/Carousel/ -init --async, waiting for children');
      return;
    }
    
    // *** set carousel speed
    const defaultSpeed = config.timings.animation.medium();
    let speed = defaultSpeed;
    if (options.config.speed) {
      speed = config.timings.animation[options.config.speed];
      speed = speed ? speed() : defaultSpeed; // *** use html attribute value if valid
    }
    state.speed = speed;
    
    // *** invoked only once on startup
    scaffold();
    addGestures();

    if(options.config && options.config.isAutoSlide == "True") {
      initAutoSlide();
    }
    
    // *** core event for resizing, allows clean rebuild
    options.signal.on(config.eventNames.APP_BREAKPOINT_READY, (payload) => {
      // *** in cases where this event is spoofed to trigger render, i.e. homepage tabs. fixes issue of hero carousel rerendering jankily unnecessarily
      if (payload.isArtificialOrigin && state.ignoreArtificialEvents) {
        return;
      }
      
      bootstrap(payload);
      // console.log('/Carousel/ -STATIC');
      
      // *** @as removing timeout, fingers crossed...
      // signal.emit(config.eventNames.SCROLLFADER_READY);
      /*      setTimeout(() => {
              bootstrap(payload);
              // primarily for hero carousel
              options.signal.emit(config.eventNames.CAROUSEL_READY);
            }, 777); // @ja changed from 500 to 777 - issues on page load, also removed the first call to bootstrap*/
    });
    
    
    const bootstrap = (options) => {
      state.viewportRect = els.el.getBoundingClientRect();
      state.shouldHardReset = options.hasChanged;
      
      if (state.shouldHardReset) {
        destroy();
        calcPages();
        build();
        zero(); // *** ensure zero index on reset
      }
      defineLayout(); // *** calcs not dependent on redraw
      simpleTweens.fade({
        el: els.el,
        type: 'in',
        delay: config.timings.animation.short(),
      }); // *** fade back in after resizing
    };
    
    // *** when async invoke bootstrap function
    if (options.config.async) {
      // @as TODO use mutation observer - may need core util, as image loading also needs to be detected!
      // @as TODO have to investigate this timeout, quick mutation observer test was a failure
      setTimeout(() => {
        bootstrap({ hasChanged: true });
        // console.log('/Carousel/ -ASYNC');
      }, 133);
    }
    
    // *** Should we have peek preview of next slide?
    if (options.config.peek) {
      state.peek = true;
    }
    
    // *** How much to peek the next slide by
    if (options.config.peek && options.config.peekAmount != null) {
      state.peekAmount = options.config.peekAmount;
    }
    // *** pageGap
    if (typeof options.config.pageGap !== 'undefined') {
      state.pageGap = options.config.pageGap;
    }
    
    // *** fade out before resize calcs
    options.signal.on(config.eventNames.APP_RESIZE_START, () => {
      simpleTweens.fade({ el: els.el, type: 'out' });
    });
    
    // *** 'local' behaviour events
    if (!state.eventsAdded) {
      options.signal.on(
          config.eventNames.BEHAVIOUR_BINDING_SIGNAL,
          (payload) => {
            if (!utils.eventFilter(options, payload)) return;
            actionRunner(payload);
          }
      );
    }
    
    state.eventsAdded = true; // *** flag to prevent gestures being added on reinjection, TODO may need to test further
    options.state = state; // *** expose state
  };
  
  // *** send back to first index, available publicly
  const zero = () => {
    navigate(0, null, { index: 0, endStop: 'start', numPages: state.numPages });
  };
  
  // *** process incoming events from bound behaviours
  const actionRunner = (payload) => {
    // console.log('/Carousel/ -actionRunner', payload);
    switch (payload.type) {
      case actions.CAROUSEL_NAVIGATION_INTENTION:
        if (!state.lockNavigation) {
          onNavigation(payload.data.direction, payload.data.velocity);
        }
        break;
        
        // ***ADDED4TIMELINE: CApture events from carousel updates for timeline
      case actions.TABSWITCHER_NAVIGATION_INTENTION:
        navigate(payload.data.index, state.speed, {
          index: payload.data.index,
          endStop: 'start',
          numPages: state.numPages,
        });
        break;
    }
  };
  
  // *** create required dom structure
  const scaffold = () => {
    
    // *** wrap items
    state.items = state.items.map((item) => {
      const wrapper = document.createElement('div');
      wrapper.classList.add(props.classes.item);
      wrapper.appendChild(item);
      return wrapper;
    });
    
    // *** construct viewport and sliding track
    els.viewport = document.createElement('div');
    els.inner = document.createElement('div');
    els.viewport.classList.add(props.classes.viewport);
    els.inner.classList.add(props.classes.inner);
    els.viewport.appendChild(els.inner);
    els.el.appendChild(els.viewport);
  };
  
  // *** gestures handled by TabStack, not directly called here in order to keep controls, paginator etc in sync
  const addGestures = () => {
    if (state.eventsAdded) return;
    
    const h = new hammer(els.el);
    // for mobile and desktop we have to panleft and panright events
    h.on("panleft", function(e) {
      if(!e.isFinal) return;
      dispatch('next', e.velocity);
    });
    h.on("panright", function(e) {
      if(!e.isFinal) return;
      dispatch('prev', e.velocity);
    });
    
    const dispatch = (direction, velocity) => {
      if (state.selfControl) {
        onNavigation(direction, velocity);
        return;
      }
      
      utils.dispatcher(options, {
        type: actions.CAROUSEL_NAVIGATION_GESTURE,
        data: { direction, velocity },
      });
    };
  };

  const initAutoSlide = () => {
    setInterval(()=>{
      let direction = '';
      if(state.currentIndex == state.numPages - 1) {
        state.currentIndex = 0;
        direction = 'prev';
      }else {
        direction = 'next';
      }
      utils.dispatcher(options, {
        type: actions.CAROUSEL_NAVIGATION_GESTURE,
        data: { direction: direction, velocity: 0 },
      });
    }, options.config.autoSlideTimer);
  };
  
  // *** paginate items, relies on dom attribute, e.g `data-behaviour-config="mobile: 1, tablet-portrait: 2, tablet-landscape: 2, desktop: 3"`
  const calcPages = () => {
    const breakpoint = mediaQueries.getValue().breakpoint;
    state.itemsPerPage = options.config[breakpoint];
    
    // console.log(breakpoint);
    
    if (!state.itemsPerPage) {
      console.error(
          '/Carousel/ -calcPages --config error, please check data attributes provided',
          options.config
      );
      return;
    }
    state.numPages = Math.ceil(state.numItems / state.itemsPerPage);
    state.pageChunks = lodash_chunk(state.items, state.itemsPerPage);
    
    // *** add empty placeholders if last page is of different length - keeps item widths consistent
    state.pageChunks.map((item) => {
      const diff = state.itemsPerPage - item.length;
      if (diff === 0) return; // *** no difference, quit
      for (let i = 0; i < diff; i++) {
        const blank = document.createElement('div');
        blank.classList.add(props.classes.placeholder);
        item.push(blank);
      }
    });
  };
  
  // *** pre rebuild remove children and reset chunks
  const destroy = () => {
    state.pageChunks = [];
    els.pages = [];
    while (els.inner.lastChild) {
      els.inner.removeChild(els.inner.lastChild);
    }
    els.inner.style.width = 'auto';
  };
  
  // *** create pages
  const build = () => {
    state.pageChunks.map((set) => {
      const pageEl = document.createElement('div');
      pageEl.classList.add(props.classes.page);
      set.map((item) => {
        pageEl.appendChild(item);
        els.inner.appendChild(pageEl);
      });
      els.pages.push(pageEl);
    });
  };
  
  // *** expensive layout process
  const defineLayout = () => {
    let greatestHeight = 0;
    
    // *** init
    setTimeout(() => {
      setPageWidthAndPositions(); // *** first layout pages
      setItemSizing(); // *** then organise page contents
      setViewportHeight(); // *** now items have naturally flowed we can set main height
    }, props.redrawDelay); // *** but we need to wait before doing this shizzle
    
    // *** layout each page
    const setPageWidthAndPositions = () => {
      // console.log(`state.pageGap: ${state.pageGap}`);
      els.inner.style.width = `${state.numPages * 100}%`;
      els.pages.map((item, index) => {
        // *** ADDED4TIMELINE peek functionality
        const standardLeftValue =
            index * (state.viewportRect.width + state.pageGap);
        const standardWidth = state.viewportRect.width;
        const peekAdjustedLeftValue =
            standardLeftValue - state.peekAmount < 0
                ? 0
                : standardLeftValue - index * state.peekAmount;
        const peekAdjustedWidth = standardWidth - state.peekAmount;
        
        item.style.left = state.peek
            ? `${peekAdjustedLeftValue}px`
            : `${standardLeftValue}px`;
        item.style.width = state.peek
            ? `${peekAdjustedWidth}px`
            : `${standardWidth}px`;
      });
    };
    
    const setItemSizing = () => {
      els.el.classList.add(props.classes.releaseHeight); // *** required for height calcs
      
      // *** use placeholders in grid layout for empty slots
      const usePlaceholders = (bool) => {
        const placeholders = [
          ...els.el.querySelectorAll(`.${props.classes.placeholder}`),
        ];
        placeholders.map((item) => {
          item.style.display = bool ? 'block' : 'none';
        });
      };
      
      // *** find greatest height and 'reset' css
      const release = () => {
        els.pages.map((page) => {
          const height = page.getBoundingClientRect().height;
          if (height > greatestHeight) {
            greatestHeight = height;
          }
        });
        els.el.classList.remove(props.classes.releaseHeight); // *** after height calcs restore
      };
      
      // *** set widths
      if (options.config.layout) {
        // *** use grid layout
        if (options.config.layout === 'grid') {
          const width = 100 / state.itemsPerPage;
          usePlaceholders(true);
          els.pages.map((page) => {
            [...page.childNodes].map((item) => {
              item.style.flexBasis = `${width}%`;
            });
          });
          release();
          return;
        }
      }
      // *** otherwise lauout will be 'natural'
      usePlaceholders(false);
      release();
    };
    
    // *** viewport height must be explicitly set
    const setViewportHeight = () => {
      els.viewport.style.height = `${greatestHeight}px`;
    };
  };
  
  const onNavigation = (direction, velocity = null) => {
    
    // console.log('========== /Carousel/ -onNavigation', options.config.deferUpdateTime);
    
    const exec = () => {
      let newIndex =
          direction === 'next'
              ? (state.currentIndex += 1)
              : (state.currentIndex -= 1);
      let endStop = 'middle';
      
      // *** limit carousel
      if (newIndex < 0) {
        newIndex = 0;
      }
      if (newIndex === state.numPages) {
        newIndex = state.numPages - 1;
      }
      
      // *** determine position within carousel for sake of 'endStop' param
      if (newIndex === 0) {
        endStop = 'start';
      }
      if (newIndex === state.numPages - 1) {
        endStop = 'end';
      }
      
      navigate(newIndex, velocity, {
        index: newIndex,
        endStop,
        direction,
        numPages: state.numPages,
      });
    };
    
    if (state.deferUpdateRunning) {
      console.log('/Carousel/ -onNavigation DEFEAT');
      return;
    }
    
    if (state.deferUpdateTime) {
      state.deferUpdateRunning = true;
      dispatch({direction}, actions.CAROUSEL_WILL_UPDATE);
      const time = state.deferUpdateTime;
      setTimeout(() => {
        state.deferUpdateRunning = false;
        exec();
      }, time);
    } else {
      exec();
    }
    
  };
  
  // *** final navigation call updates state and updates slide position
  const navigate = (index, velocity = null, data) => {
    // console.log('/Carousel/ -navigate >>>', data);
    
    state.currentIndex = index;
    tween(index, velocity);
    dispatch(data);
  };
  
  const dispatch = (data, type = actions.CAROUSEL_UPDATING) => {
    // if (state.selfControl) return; // @as removed - allow update of paginator even when controls are not present
    
    utils.dispatcher(options, {
      type,
      data,
    });
  };
  
  // *** update slide position
  const tween = (index, velocity = null) => {
    
    velocity = (options.config.deferUpdateTime) ? 0 : velocity; // *** clear velocity in this case
    
    let tm = state.speed;
    //console.log(`INDEX: ${index}`);
    const minTm = 0.15; // *** if gesture velocity provided limit min speed
    
    if (velocity) {
      let v = Math.abs(velocity) / 10;
      tm -= v;
      tm = tm < minTm ? minTm : tm;
    }
    let gapFactor = index * state.peekAmount;
    
    const adjustedTweenLeft = state.peek
        ? `calc(${-(index * 100)}% - ${index * state.pageGap}px + ${gapFactor}px)`
        : `calc(${-(index * 100)}% - ${index * state.pageGap}px)`;
    if(options.config.isAutoSlide == "True" && index === 0) {
      gsapWithCSS$1.fromTo(els.inner, tm, {
        left: adjustedTweenLeft,
        ease: state.defaultEase,
      }, {
        left: 0,
        ease: state.defaultEase,
        onComplete: () => {
          dispatch({index}, actions.CAROUSEL_HAS_UPDATED);
        }
      });
    } else {
      gsapWithCSS$1.to(els.inner, tm, {
        left: adjustedTweenLeft,
        ease: state.defaultEase,
        onComplete: () => {
          dispatch({index}, actions.CAROUSEL_HAS_UPDATED);
        }
      });
    }    

  };
  
  const lockNavigation = (bool) => {
    state.lockNavigation = bool;
    console.log('/Carousel/ -lockNavigation', state);
  };
  
  const getState = () => {
    options.getState(state);
  };
  
  return {
    init,
    zero,
    inject,
    lockNavigation,
    getState,
    state,
    // test: Carousel,
    // foobar,
  };
};

const CarouselControls = (options) => {
  
  let els = {}; // *** prevBtn, nextBtn
  
  const props = {
    debounceDelay: 0,
  };
  
  const init = () => {
    els = options.els;
    els.prevBtn = els.el.querySelector('.carousel-controls__btn-prev');
    els.nextBtn = els.el.querySelector('.carousel-controls__btn-next');
    
    els.prevBtn.addEventListener('click', () => {
      dispatch('prev');
    });
    els.nextBtn.addEventListener('click', () => {
      dispatch('next');
    });
    
    // *** @as TODO need to more carefully scope keyboard events
    if (options.config.useKeyboard && options.config.useKeyboard === 'yes') {
      document.addEventListener('keydown', (e) => {
        if (e.key === 'ArrowLeft') {
          dispatch('prev');
        }
        if (e.key === 'ArrowRight') {
          dispatch('next');
        }
      });
    }
    
    options.signal.on(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, (payload) => {
      if (!utils.eventFilter(options, payload)) return;
      actionRunner(payload);
    });
  };
  
  const dispatch = general.debounce((direction, velocity = null) => {
    utils.dispatcher(options, {
      type: actions.CAROUSEL_NAVIGATION_INTENTION,
      data: {direction, velocity}
    });
  }, props.debounceDelay);
  
  // *** process incoming events from bound behaviours
  const actionRunner = (payload)  => {
    switch (payload.type) {
      case actions.CAROUSEL_UPDATING:
        update(
            (payload.data.endStop === 'start'),
            (payload.data.endStop === 'end'),
            (payload.data.numPages > 1)
        );
        break;
        
      case actions.CAROUSEL_NAVIGATION_GESTURE:
        dispatch(payload.data.direction, payload.data.velocity);
        break;
    }
  };
  
  const update = (atStart, atEnd, shouldEnable) => {
    if (shouldEnable) { // *** hidden by default
      els.el.classList.add('is-enabled');
    } else {
      els.el.classList.remove('is-enabled');
      els.parentEl.classList.add('hide');
    }
    
    const exec = (btnEl, bool) => {
      if (bool) {
        btnEl.classList.add('is-disabled');
      } else {
        btnEl.classList.remove('is-disabled');
      }
    };
    exec(els.prevBtn, atStart);
    exec(els.nextBtn, atEnd);
  };
  
  return {
    init
  }
};

/**
 * This element has no html partial api, include via javascript only
 * @return {{init: init, getElement: (function(): HTMLDivElement), update: (function(*=, *=): HTMLDivElement)}}
 * @constructor
 */

const ProgressBar = () => {
  
  const els = {};
  
  const props = {
    classes: {
      main: 'progress-bar',
      markerTrack: 'progress-bar__marker-track',
      marker: 'progress-bar__marker',
      isActive: 'is-active',
    }
  };
  
  const state = {
    length: 0, // *** num 'items'
    index: 0,
  };
  
  const init = () => {
    scaffold();
  };
  
  const scaffold = () => {
    els.el = document.createElement('div');
    els.el.classList.add(props.classes.main);
  
    els.markerTrack = document.createElement('div');
    els.markerTrack.classList.add(props.classes.markerTrack);
    els.el.appendChild(els.markerTrack);
  };
  
  const destroy = () => {
    while (els.markerTrack.lastChild) {
      els.markerTrack.removeChild(els.markerTrack.lastChild);
    }
  };
  
  const build = () => {
    for (let i = 0; i < state.length; i++) {
      const marker = document.createElement('div');
      marker.style.width = `calc(${100 / state.length}% + 4px`;
      marker.classList.add(props.classes.marker);
      
      els.markerTrack.appendChild(marker);
    }
  };
  
  const update = (index, length) => {
    state.index = index;
  
    if (state.length !== length) {
      state.length = length;
      destroy();
      build();
    }
  
    [...els.markerTrack.childNodes].map((item, idx) => {
      if (idx <= state.index) {
        item.classList.add(props.classes.isActive);
      } else {
        item.classList.remove(props.classes.isActive);
      }
    });
    
    return els.el;
  };
  
  const getElement = () => {
    return els.el;
  };
  
  return {
    init,
    update,
    getElement,
  }
};

const CarouselPaginator = (options) => {
  
  let els = {};
  
  const props = {
    classes: {
      inner: 'carousel-paginator__inner',
      numeral: 'carousel-paginator__numeral',
    },
    progressBar: ProgressBar()
  };
  
  const init = () => {
    els = options.els;
    scaffold();
    options.signal.on(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, (payload) => {
      if (!utils.eventFilter(options, payload)) return;
      actionRunner(payload);
    });
  };
  
  // *** process incoming events from bound behaviours
  const actionRunner = (payload)  => {

    // console.log(payload.data);
    switch (payload.type) {
      case actions.CAROUSEL_UPDATING:
        update(
            payload.data.index,
            payload.data.numPages,
            (payload.data.numPages > 1)
        );
        break;
    }
  };
  
  // *** create dom structure
  const scaffold = () => {
    els.inner = document.createElement('div');
    els.inner.classList.add(props.classes.inner);
    els.el.appendChild(els.inner);
    
    els.numeral = document.createElement('div');
    els.numeral.classList.add(props.classes.numeral);
    els.inner.appendChild(els.numeral);
    
    // *** progressbar
    props.progressBar.init();
    els.inner.appendChild(props.progressBar.getElement());
  };
  
  const update = (index, numPages, shouldEnable) => {
  
    if (shouldEnable) { // *** hidden by default
      els.el.classList.add('is-enabled');
    } else {
      els.el.classList.remove('is-enabled');
    }
    els.numeral.innerText = (index < 9) ? `0${index + 1}` : index + 1;
    props.progressBar.update(index, numPages);
  };
  
  return {
    init
  }
};

/**
 * This element has no html partial api, include via javascript only
 * @return {{init: init, getElement: (function(): HTMLDivElement), update: (function(*=, *=): HTMLDivElement)}}
 * @constructor
 */

const SliderDots = () => {
  
    const els = {};
    
    const props = {
      classes: {
        main: 'slider-dots',
        markerTrack: 'slider-dots__marker-track',
        marker: 'slider-dots__marker',
        isActive: 'is-active',
      }
    };
    
    const state = {
      length: 0, // *** num 'items'
      index: 0,
    };
    
    const init = () => {
      scaffold();
    };
    
    const scaffold = () => {
      els.el = document.createElement('div');
      els.el.classList.add(props.classes.main);
    
      els.markerTrack = document.createElement('div');
      els.markerTrack.classList.add(props.classes.markerTrack);
      els.el.appendChild(els.markerTrack);
    };
    
    const destroy = () => {
      while (els.markerTrack.lastChild) {
        els.markerTrack.removeChild(els.markerTrack.lastChild);
      }
    };
    
    const build = () => {
      for (let i = 0; i < state.length; i++) {
        const marker = document.createElement('div');
        // marker.style.width = `calc(${100 / state.length}% + 4px`;
        marker.classList.add(props.classes.marker);
        
        els.markerTrack.appendChild(marker);
      }
    };
  
    const updateDots = (index, length) => {
      state.index = index;
    
      if (state.length !== length) {
        state.length = length;
        destroy();
        build();
      }
    
      [...els.markerTrack.childNodes].map((item, idx) => {
        if (idx == state.index) {
          item.classList.add(props.classes.isActive);
        } else {
          item.classList.remove(props.classes.isActive);
        }
      });
      
      return els.el;
    };
    
    const getElement = () => {
      return els.el;
    };
    
    return {
      init,
      getElement,
      updateDots
    }
  };

const CarouselDots = (options) => {
  
  let els = {};
  
  const props = {
    classes: {
      inner: 'carousel-dots__inner',
    },
    sliderDots: SliderDots()
  };
  
  const init = () => {
    els = options.els;
    scaffold();
    options.signal.on(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, (payload) => {
      if (!utils.eventFilter(options, payload)) return;
      actionRunner(payload);
    });
  };
  
  // *** process incoming events from bound behaviours
  const actionRunner = (payload)  => {

    // console.log(payload.data);
    switch (payload.type) {
      case actions.CAROUSEL_UPDATING:
        update(
            payload.data.index,
            payload.data.numPages,
            (payload.data.numPages > 1)
        );
        break;
    }
  };
  
  // *** create dom structure
  const scaffold = () => {
    els.inner = document.createElement('div');
    els.inner.classList.add(props.classes.inner);
    els.el.appendChild(els.inner);
    
    // *** sliderDots
    props.sliderDots.init();
    els.inner.appendChild(props.sliderDots.getElement());
  };
  
  const update = (index, numPages, shouldEnable) => {
  
    if (shouldEnable) { // *** hidden by default
      els.el.classList.add('is-enabled');
    } else {
      els.el.classList.remove('is-enabled');
    }
    props.sliderDots.updateDots(index, numPages);
  };
  
  return {
    init
  }
};

const tabSwitcherActions = {
  // *** to pass labels to TabSwitcherControls from TabStack which has the data
  // *** NOTE may be circumvented when used with dynamic data, by called public 'build' method on TabSwitcherControls
  TABSWITCHER_CONFIGURATION: 'TABSWITCHER_CONFIGURATION',

  TABSWITCHER_NAVIGATION_INTENTION: 'TABSWITCHER_NAVIGATION_INTENTION',
  TABSWITCHER_UPDATING: 'TABSWITCHER_UPDATING',
  CAROUSEL_UPDATING: 'CAROUSEL_UPDATING', // **ADDED4TIMELINE need this for timeline event
};

/**
 * detect image loading complete of any images found in an array of elements and dispatch notification
 * usage see ref impl: src/core/js/behaviours/tabswitcher/TabStack.js
 * @return {any}
 * @constructor
 */
const ImageLoadDetector = () => {
  
  const events = signal; // *** discrete event bus
  const LOADED = 'LOADED';
  
  const state = {
    data: [],
    loaded: [],
    numItems: 0,
    numItemsLoaded: 0,
  };
  
  /**
   *
   * @param options
   * @options.els: Array
   * @options.notify: String
   *  | 'last-of' - dispatch loaded only on final image in data complete
   * @options.uid: String/Number - optional identifier for a cycle of this process
   */
  const watch = (options) => {
    
    state.data = options.els.map((item) => {
      return item.querySelectorAll('img');
    });
  
    state.data.map((set, setIndex) => {
      set = [...set];  // *** to normal array, not nodelist
      set.map((img, imgIndex) => {
        const url = img.getAttribute('src'); // *** extract @src
        const temp = document.createElement('img'); // *** create test image
        temp.setAttribute('src', url);
        state.numItems ++;
  
        const vo = {
          setIndex,
          imgIndex,
          url,
          loaded: false,
        };
        
        temp.addEventListener('load', (e) => {
          vo.loaded = true;
          state.loaded.push(vo);
          dispatch(options);
        });
      });
    });
  };
  
  const dispatch = (options, vo) => {
    // *** notify only when last img is complete
    if (options.notify === 'last-of') {
      state.numItemsLoaded ++;
      if (state.numItemsLoaded === state.numItems) {
        signal.emit(LOADED, {
          items: state.loaded,
          uid: options.uid || null,
        });
      }
      return;
    }
    // *** other use cases here...
  };
  
  return {
    LOADED,
    events,
    watch,
  }
};

/*
 * config options:
 * > setHeight: bool = disable/enable the setHeight function, used only on generic tabbed module to allow tabstack to wrap naturally around its contents
 * > skipImageDetection : bool = enable/disable image loading detection on startup. set to false if view remains collapsed. NOTE should be false by default but would be a breaking change
 * ... others todo!
 * */
const TabStack = (options) => {
  let els = {}; // *** prevBtn, nextBtn

  const props = {
    classes: {
      isInitialising: 'is-initialising',
      isActive: 'is-active',
      spinner: 'ellipsis-spinner', // *** TODO update with branded spinner
    },
    imageLoadDetector: ImageLoadDetector(),
  };

  const state = {
    items: [],
    currentIndex: 0,
    prevIndex: null,
    isFirstRun: true,
    setHeight: true, // *** manually set a height on tabstack(true) or just let it expand to wrap contents(false)
    persistMaxHeight: false, // *** set view height either to current page height (false) or tallest page in set (true)
    imageLoadedState: {
      indicesReady: [], // *** store pages that have successfully loaded all images
      hasLoadedFirstIndex: false, // *** flag has completely loaded a page (to hide spinner etc)
    },
    selfAnimate: true, // *** tweens own items, if false allows other operation to animate items, i.e. scrollfader TEST
  };

  // *** facade for injecting dynamic content, add els and restart
  const inject = (data, index = state.currentIndex) => {
    state.currentIndex = index;
    data.map((node) => {
      // *** child el sets
      const el = document.createElement('div'); // *** create container for child els
      el.classList.add('cardstack'); // *** extend as necessary
      // el.classList.add('cardstack--large-gutter'); // *** TEST, but may need a hook to inject large-gutter to dynamic cardstacks
      node.nodes.map((item) => {
        el.appendChild(item);
      });
      options.els.el.appendChild(el); // *** append all to tabstack element
    });
    init();
  };

  const init = () => {
    els = options.els;
    els.el.classList.add(props.classes.isInitialising);
    state.items = [...options.els.el.children];
    
    state.imageLoadedState.hasLoadedFirstIndex =
      options.config['skipImageDetection'] === 'true';

    if (options.config['setHeight']) {
      state.setHeight = options.config['setHeight'] === 'true' ? true : false;
    }
  
    if (options.config['selfAnimate']) {
      state.selfAnimate = options.config['selfAnimate'] === 'true' ? true : false;
    }
    
    // *** when async, block until children present
    if (options.config.async && state.numItems === 0) {
      console.warn('/TabStack/ -init --async, waiting for children');
      return;
    }
    
    // console.log('/TabStack/ -init', options);

    update(state.currentIndex);

    options.signal.on(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, (payload) => {
      if (!utils.eventFilter(options, payload)) return;
      actionRunner(payload);
    });

    options.signal.on(config.eventNames.APP_BREAKPOINT_READY, () => {
      setHeight();
    });

    /*setTimeout(() => { // *** @as may need this later...
      setHeight();
    }, config.timings.resizeDebounceDelayTime);*/
  
    options.state = state; // *** expose state
    options.update = update; // *** add update method to options object for easy invocation from elsewhere
  };

  const setHeight = () => {
    
    if (!state.imageLoadedState.hasLoadedFirstIndex) return;

    const setAsDomInitialised = () => {
      state.imageLoadedState.hasLoadedFirstIndex = true;
      els.el.classList.remove(props.classes.isInitialising);
    };
    // *** bail now if dont want to manually set the height
    // *** (used by generic tabstack module)
    if (!state.setHeight) {
      setAsDomInitialised();
      return;
    }
    // *** determine tallest page and persist
    if (state.persistMaxHeight) {
      let greatestHeight = 0;
      state.items.map((item) => {
        const height = item.getBoundingClientRect().height;
        if (height > greatestHeight) {
          greatestHeight = height;
        }
      });
      els.el.style.height = `calc(${greatestHeight}px)`;
      setAsDomInitialised();
      return;
    }

    // *** set view height to current page
    const height =
      state.items[state.currentIndex].getBoundingClientRect().height;
    els.el.style.height = `calc(${height}px)`;
    setAsDomInitialised();
  };

  // *** process incoming events from bound behaviours
  const actionRunner = (payload) => {
    switch (payload.type) {
      case tabSwitcherActions.TABSWITCHER_NAVIGATION_INTENTION:
        update(payload.data.index);
        setHeight();
        break;
    }
  };

  const update = (index) => {
    const tabs = {
      current: null,
      previous: [],
    };

    state.items.map((item, i) => {
      const tab = state.items[i];
      if (i === index) {
        state.prevIndex = state.currentIndex;
        state.currentIndex = index;
        tabs.current = tab;
      } else {
        tabs.previous.push(tab);
      }
    });

    // *** detect image loading and reset height on final image loaded
    if (!state.imageLoadedState.indicesReady.includes(index)) {
      if (state.setHeight) els.el.style.height = 0;
      props.imageLoadDetector.watch({
        notify: 'last-of',
        els: [tabs.current],
        uid: null,
      });
      props.imageLoadDetector.events.on(
        props.imageLoadDetector.LOADED,
        (payload) => {
          /*console.log(
            '/TabStack/ -LOADED IMAGES',
            payload.items,
            'into:',
            options.config.uid
          );*/
          state.imageLoadedState.hasLoadedFirstIndex = true;
          props.imageLoadDetector.events.off(props.imageLoadDetector.LOADED);
          setHeight();
        }
      );
      state.imageLoadedState.indicesReady.push(index);
    }

    if (!state.imageLoadedState.hasLoadedFirstIndex) ;

    utils.dispatcher(options, {
      type: tabSwitcherActions.TABSWITCHER_UPDATING,
      data: { index: state.currentIndex },
    });

    // console.log('/TabStack/ -update TWEEN?', state.selfAnimate);
    // *** show
    if (state.selfAnimate) {
      tween(tabs);
    } else {
      withoutTween(tabs);
    }
    // tween(tabs);
    // withoutTween(tabs);
  };
  
  // *** @as works with scrollfader, but suboptimal imo
  const withoutTween = (tabs) => {
    // console.log('/TabStack/ -withoutTween');
    tabs.current.classList.add(props.classes.isActive);
    tabs.previous.map((item) => {
      item.classList.remove(props.classes.isActive);
    });
  };

  const tween = (tabs) => {
    
    // console.log('/TabStack/ -tween', tabs);
    
    const speedDivisor = 4;
    let crossFadeDelay = 0.3; // *** TODO dynamic value?

    if (state.isFirstRun) {
      crossFadeDelay = 0;
      state.isFirstRun = false;
    }

    const exec = (options) => {
      options.nodes.map((item, index) => {
        const delay = index / options.nodes.length / speedDivisor + options.predelay;
        
        // console.log('/TabStack/ -item', item);
        
        simpleTweens.fade({
          el: item,
          type: options.type,
          delay,
          onStart: options.onStart,
          onComplete: options.onComplete,
        });
      });
    };
    exec({
      nodes: [...tabs.current.children],
      type: 'in',
      predelay: crossFadeDelay,
      onStart: () => {
        tabs.current.classList.add(props.classes.isActive);
        setHeight();
      },
      onComplete: null,
    });

    tabs.previous.map((item) => {
      exec({
        nodes: [...item.children], //.reverse(),
        type: 'out',
        predelay: 0,
        onStart: null,
        onComplete: () => {
          item.classList.remove(props.classes.isActive);
        },
      });
    });
  };
  
  const getState = () => {
    options.getState(state);
  };

  return {
    init,
    inject,
    update,
    getState,
    state,
  };
};

/**
 * accepts raw children in format <any-element>Some text</any-element> (carrier element will be discarded and text injected into a generated <button>)
 * or data payload in format [{label: 'Some text'}]
 * @param options
 * @return {{init: init, build: build, update: update, getOptions: (function(): *), inject: inject}}
 * @constructor
 */

const TabSwitcherControl = (options) => {
  let els = {}; // *** prevBtn, nextBtn
  let medicalEducationTabs = [];
  let medicalEducationNonAvailabilityListItems = [];

  const props = {
    classes: {
      inner: 'tab-switcher-control__inner',
      buttonGroup: 'tab-button-group',
      button: 'tab-button',
      arrow: 'tab-arrow',
      arrowLeft: 'tab-arrow--left',
      arrowRight: 'tab-arrow--right',
      arrowDisabled: 'arrow-disabled',
      label: 'tab-button__label',
      marker: 'tab-marker',
      markerTrack: 'tab-marker-track',
      hasArrows: 'tab-switcher-control--arrows',
      isActive: 'is-active',
      isDisabled: 'is-disabled', // *** ensure in sync with `src/core/scss/ui/_tab-switcher-control.scss` re breakpoints
      hasInitialised: 'tab-switcher-control--has-initialised', // *** hides element until children added and built
    },
  };

  const state = {
    items: [],
    isDisabled: true, // *** mobile use drop down selector
    navArrows: false,
    arrowPadding: 70,
    arrowScrollAmount: 5,
  };

  // *** facade for injecting dynamic content, add els and restart
  const inject = (data, index) => {
    // *** cleanup
    if (els.el) {
      [...els.el.querySelectorAll('.tab-button')].map((item) => {
        item.remove();
      });
    }

    init();
    build(data);
    update(index);
    dispatch({ index });
  };

  const init = () => {
    els = options.els;
    if (options.config.arrows) {
      state.navArrows = true;
    }
    const rawChildren = [...els.el.children];
    if(els.el.dataset.parentTabSwitcherName && els.el.dataset.parentTabSwitcherName === 'MedicalEducation') {
      rawChildren.map((item) => {
        medicalEducationTabs.push(item.innerText);
      });
      if(medicalEducationTabs.length && !medicalEducationNonAvailabilityListItems.length) {
        medicalEducationTabs.map((item) => {
          if(item.toLowerCase() === 'webinars') {
            const noResultsWebinar = document.getElementById('NoResults_Webinar');
            const resultWebinars = document.getElementById('Results_Webinar');
            if(noResultsWebinar && medicalEducationNonAvailabilityListItems.indexOf('webinars') === -1 || !resultWebinars) {
              medicalEducationNonAvailabilityListItems.push('webinars');
            }
          }
          if(item.toLowerCase() === 'courses') {
            const noResultsCourses = document.getElementById('NoResults_Courses');
            const resultCourses = document.getElementById('Results_Courses');
            if(noResultsCourses && medicalEducationNonAvailabilityListItems.indexOf('courses') === -1 || !resultCourses) {
              medicalEducationNonAvailabilityListItems.push('courses');
            }
          }
        });
      }
    }
    scaffold();

    // *** if elements present in markup, generate data from them and then discard elements
    if (rawChildren.length > 0) {
      const data = rawChildren.map((item) => {
        const vo = { label: item.innerText };
        
        try {
          // *** protects against weird bug when running in sitecore experience editor
          item.remove();
        } catch (error) {
          // console.error('--EXP /TabSwitcherControl/ -ERROR removing sub item:', error);
        }

        return vo;
      });
      // quick check here, mitigates a bug where els.inner is added and is wrongly registered as a rawChildren item
      if (data[0].label !== '') {
        build(data);
        update(0); // ***ADDED4TIMELINE: Select 1st tab item onload
      }
    }

    // *** this component is disabled except >= tablet landscape
    // @as TODO consider removing this - may not be the responsibility of this component to decide
    options.signal.on(config.eventNames.APP_BREAKPOINT_READY, (payload) => {
      state.isDisabled =
        payload.breakpoint === 'mobile' ||
        payload.breakpoint === 'tablet-portrait';
      // If we are on homepage tabs we need to recalculate width for arrows, alignment, etc
      if (state.navArrows) insertNavArrows();
      if (payload.isArtificialOrigin) {
        // come in from an async homepage tab click. set to first tab
        update(0);
      }
    });

    options.signal.on(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, (payload) => {
      if (!utils.eventFilter(options, payload)) return;
      actionRunner(payload);
    });
  };

  const getOptions = () => {
    return options;
  };

  const scaffold = () => {
    els.inner = document.createElement('div');
    els.inner.classList.add(props.classes.inner);
    els.el.appendChild(els.inner);

    // *** when used in dynamic context, init is called twice causing inner to be duplicated
    // *** this is a simple fix to clean up, but this class should be refactored to avoid it TODO
    const inners = [...els.el.querySelectorAll(`.${props.classes.inner}`)];
    if (inners.length > 1) {
      els.el.removeChild(inners[0]);
    }

    els.buttonGroup = document.createElement('div');
    els.buttonGroup.classList.add(props.classes.buttonGroup);
    els.inner.appendChild(els.buttonGroup);

    els.markerTrack = document.createElement('div');
    els.markerTrack.classList.add(props.classes.markerTrack);
    els.inner.appendChild(els.markerTrack);

    els.marker = document.createElement('div');
    els.marker.classList.add(props.classes.marker);
    els.markerTrack.appendChild(els.marker);
  };

  const getMedicalEducationLabelUsingDataComponentName = (educationEle) => {
    let medicalEduLabel = null;
    if(educationEle) {
      const medicalEduEl = general.attributeParser(
        educationEle.dataset.anchor,
        'TabSwitcherControl'
      );
      if (medicalEduEl) {
        medicalEduLabel = medicalEduEl.label;
      }
    }
    return medicalEduLabel;
  };

  const checkDataAvailability = ({label}, educationEle) => {
    let isDataAvailable = true;
    if(educationEle) {
      if(medicalEducationTabs.indexOf(label) !== -1) {
        if(medicalEducationNonAvailabilityListItems.indexOf(label.toLowerCase()) !== -1) {
          isDataAvailable = false;
        }      }
      if(medicalEducationNonAvailabilityListItems.length > 0 && medicalEducationNonAvailabilityListItems.length === medicalEducationTabs.length) {
        const tabMedicalEduEle = document.getElementById('tab-medical-education');
        if(tabMedicalEduEle) {
          tabMedicalEduEle.style.display = 'none';
        }
        if(educationEle) {
          educationEle.style.display = 'none';
        }
      }
    }
    
    return isDataAvailable;
  };

  const build = (data) => {
    state.items = []; // *** clear old items if rebuilding
    const educationEle = document.body.querySelector(`[data-component-name="MedicalEducation"]`);
    const medicalEduLabel = getMedicalEducationLabelUsingDataComponentName(educationEle);
    const SkipMedicalEducationDelete = document.getElementById('SkipMedicalEducationDelete');
    let isSkipMedicalEducation = false;
    if(SkipMedicalEducationDelete) {
      isSkipMedicalEducation = SkipMedicalEducationDelete.value === "True" ? true : false;
    }
    data.map((item, index) => {
      const isDataAvailable = SkipMedicalEducationDelete && isSkipMedicalEducation ? true : checkDataAvailability(item, educationEle);
      if(isDataAvailable) {
        if(item.isContactUsBtn && item.contactUsUrl) {
          const anchor = document.createElement('a');
          anchor.classList.add("cta-button", "cta-button--primary", "cta-button--medium", "contact-us-sticky-btn");
          anchor.href = item.contactUsUrl;
          anchor.innerHTML = `<span class="${props.classes.label}">${item.label}</span>`;
          els.buttonGroup.appendChild(anchor);

          state.items.push(anchor);
        } else {
          const button = document.createElement('button');
          if(item.isContactUsBtn) {
            button.classList.add("cta-button", "cta-button--primary", "cta-button--medium", "contact-us-sticky-btn");
          } else {
            button.classList.add(props.classes.button);
          }
          button.setAttribute('data-button-action', `index: ${index}`);
          if(item.label === medicalEduLabel) {
            button.setAttribute('id', 'tab-medical-education');
          }
          button.innerHTML = `<span class="${props.classes.label}">${item.label}</span>`;
          els.buttonGroup.appendChild(button);
    
          button.addEventListener('click', () => {
            dispatch({ index });
          });
    
          state.items.push(button);
        }     
      }
    });

    els.el.classList.add(props.classes.hasInitialised);
    if (state.navArrows) insertNavArrows();
  };

  const insertNavArrows = () => {
    // check that we need the arrows first..
    const controlWidth = els.el.getBoundingClientRect().width;
    const tabsWidth = els.inner.getBoundingClientRect().width;
    const overflow = tabsWidth - controlWidth;
    // if no dimensions its pointless continuing
    // prob coming from artificial event so wait for later to do this step then
    if (controlWidth === 0 && tabsWidth === 0) return;
    // console.log('insertNavArrows');
    // console.log(`controlWidth: ${controlWidth}`);
    // console.log(`tabsWidth: ${controlWidth}`);
    // console.log('insertNavArrows');

    if (tabsWidth < controlWidth) {
      els.el.classList.remove(props.classes.hasArrows);
      return;
    }
    // build the arrows into the dom
    const leftArrow = document.createElement('button');
    const rightArrow = document.createElement('button');
    leftArrow.classList.add(props.classes.arrow);
    leftArrow.classList.add(props.classes.arrowLeft);
    leftArrow.classList.add(props.classes.arrowDisabled);
	leftArrow.setAttribute('title','left-arrow');
    leftArrow.innerHTML = `<span class="sn-icon-navigation-left"></span>`;

    rightArrow.classList.add(props.classes.arrow);
    rightArrow.classList.add(props.classes.arrowRight);
	rightArrow.setAttribute('title','right-arrow');
    rightArrow.innerHTML = `<span class="sn-icon-navigation-right"></span>`;

    els.el.appendChild(leftArrow);
    els.el.appendChild(rightArrow);
    els.inner.style.left = '10px';
    // change flex position on parent
    els.el.classList.add(props.classes.hasArrows);

    let scrollInterval = null;

    const primeScroll = (where) => {
      const el = where === 'left' ? rightArrow : leftArrow;
      el.classList.remove(props.classes.arrowDisabled);
      scrollInterval = setInterval(() => {
        scroll(where);
      }, 10);
    };

    const clearScrolling = () => {
      clearInterval(scrollInterval);
    };

    const scroll = (where) => {
      const current = els.inner.style.left.replace('px', '');
      if (where === 'left') {
        if (+current >= 0) {
          leftArrow.classList.add(props.classes.arrowDisabled);
          clearInterval(scrollInterval);
          return;
        } else {
          els.inner.style.left = `${+current + state.arrowScrollAmount}px`;
        }
      } else {
        if (+current <= -(overflow + state.arrowPadding)) {
          rightArrow.classList.add(props.classes.arrowDisabled);
          clearInterval(scrollInterval);
          return;
        } else {
          els.inner.style.left = `${+current - state.arrowScrollAmount}px`;
        }
      }
    };

    leftArrow.addEventListener('mousedown', () => primeScroll('left'));
    leftArrow.addEventListener('touchstart', () => primeScroll('left'));
    rightArrow.addEventListener('mousedown', () => primeScroll('right'));
    rightArrow.addEventListener('touchstart', () => primeScroll('right'));

    leftArrow.addEventListener('mouseup', () => clearScrolling());
    leftArrow.addEventListener('touchend', () => clearScrolling());
    rightArrow.addEventListener('mouseup', () => clearScrolling());
    rightArrow.addEventListener('touchend', () => clearScrolling());
  };

  const dispatch = (data) => {
    utils.dispatcher(options, {
      type: tabSwitcherActions.TABSWITCHER_NAVIGATION_INTENTION,
      data,
    });
  };

  // *** process incoming events from bound behaviours
  const actionRunner = (payload) => {
    switch (payload.type) {
      case tabSwitcherActions.TABSWITCHER_CONFIGURATION:
        build(payload.data);
        break;

      case tabSwitcherActions.TABSWITCHER_UPDATING:
        update(payload.data.index);
        break;

      // ***ADDED4TIMELINE: CApture events from carousel updates for timeline
      case tabSwitcherActions.CAROUSEL_UPDATING:
        update(payload.data.index);
        break;
    }
  };

  const update = (index) => {
    if (!state.items.length || index >= state.items.length) return;
    const target = state.items[index];
    const offset = els.markerTrack.getBoundingClientRect().left;
    const rect = target.getBoundingClientRect();

    els.marker.style.left = `${rect.left - offset}px`;
    els.marker.style.width = `${rect.width}px`;

    state.items.map((item) => {
      if (item === target) {
        item.classList.add(props.classes.isActive);
      } else {
        item.classList.remove(props.classes.isActive);
      }
    });
  };

  return {
    init,
    build,
    update,
    inject,
    getOptions,
  };
};

// *** NOTE when updating path values, ensure spaces after/around alpha chars
const shapeData = {
  option1: [
    { // *** top
      transition: {
        from: {
          x: 50,
          y: -200,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1368.46 1236.91',
      path: 'M 144.07 577.2 l -2.51 -1.49 C 88.4 544.14 51.36 484.05 30.77 427 C -24.24 274.69 41.93 126 191.49 64 c 327 -135.42 609.25 -28.74 811.49 69.2 s 262.17 182.1 256.25 352.62 s -42.9 431.06 -312.91 660.3 c -183.13 155.48 -434.4 23.44 -507.44 -179.43 c -20.33 -56.48 -33.93 -115.5 -59.22 -169.93 C 327.71 685 244 636.54 144.07 577.2 Z'



    },
    { // *** bottom
      transition: {
        from: {
          x: 50,
          y: -200,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1168.46 1236.91',
      path: 'M 170.39 691.83 l -2.69 -1.12 C 110.59 667 65.38 612.79 36.9 559.28 c -76.09 -143 -31.73 -299.61 107.52 -382.16 C 448.9 -3.4 743.4 62.11 957.5 130.31 s 285.39 143 303.76 312.63 s 18.79 432.79 -215.92 698.07 c -159.18 179.92 -426.65 84.93 -527.78 -105.51 c -28.15 -53 -50 -109.5 -82.77 -159.79 C 367.48 772.41 277.77 736.36 170.39 691.83 Z'

    },
  ],
  option2: [
    { // *** top
      transition: {
        from: {
          x: 50,
          y: -200,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1415.18 1232.95',
      path: 'M 1368.63 763.86 c -84.21 224 -308.33 353.56 -441 426.78 s -282.91 59.24 -365.91 -104.1 C 363.67 696.72 124.23 642 29.86 485.3 c -45.44 -75.43 -33.92 -167.88 0.57 -244.83 c 36.64 -81.72 98.33 -156.06 174.86 -203 C 293.79 -16.77 406.78 2 441.88 8.15 C 557.74 28.31 673.53 49.54 788.23 75.69 c 94.16 21.46 188 46.07 278.62 79.91 c 71.91 26.87 143 59.78 204.45 106.4 c 50.62 38.42 93.72 87.12 118.5 146.05 c 26.41 62.82 30.06 132.11 20.75 198.86 C 1403.05 660.68 1387.7 713.15 1368.63 763.86 Z'
    },
    { // *** bottom
      transition: {
        from: {
          x: 50,
          y: -200,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1415.18 1232.95',
      path: 'M 1315.63 736.46 C 1266.94 940.59 1086.17 1078 978.8 1156.21 s -239.64 82.62 -329.8 -50.4 C 433.84 788.38 219.49 767 120.18 641.08 C 72.36 580.43 72.24 498.72 93.8 428 c 22.91 -75.13 68.43 -146.59 129.86 -195.82 c 71 -56.92 171.42 -53 202.63 -51.5 c 103 4.82 206.11 10.58 308.78 20.74 c 84.28 8.34 168.67 19.44 251.2 38.94 c 65.51 15.49 131 36.31 189.57 70.14 C 1224.1 338.36 1267 376 1295 424.55 c 29.88 51.76 40.66 111.64 39.88 170.75 C 1334.24 642.91 1326.65 690.25 1315.63 736.46 Z'
    },
  ],
  option3: [
    { // *** top
      transition: {
        from: {
          x: 50,
          y: -200,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1383 1170.03',
      path: 'M 775.08 1170 c -214.58 0.87 -404.89 -55.83 -494.24 -298.21 C 172.54 578 -145.74 562.05 77.83 260.16 C 276.72 -8.42 406 -52.9 689.22 53.23 S 1380.6 387.87 1383 645.11 S 1170.24 1168.41 775.08 1170 Z'
    },
    { // *** bottom
      transition: {
        from: {
          x: 50,
          y: -200,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1383 1170.03',
      path: 'M 839.52 1116.45 c -186.6 24.32 -358.42 -4.12 -462.78 -205.21 C 250.26 667.51 -28.43 688.55 133 401.33 C 276.52 145.81 384.13 92.91 642.21 154.16 S 1280.52 369.42 1310.83 593 S 1183.18 1071.66 839.52 1116.45 Z'
    },
  ],
  option4: [
    { // *** top
      transition: {
        from: {
          x: 50,
          y: 50,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1405.64 1419.97',
      path: 'M 1.32 755.24 C -9 613.72 40.86 433.08 160.5 514.12 c 276.68 187.4 716.64 163.81 932.28 343.37 s 248.37 337.68 167.19 441 S 1011.1 1500.64 554 1318.93 S 15.6 951.1 1.32 755.24 Z'
    },
    { // *** bottom
      transition: {
        from: {
          x: 50,
          y: -250,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1313.36 1862.21',
      path: 'M 619.77 497.86 c 101.91 68.3 56.29 190.67 -205.32 40.11 S -202.65 152 149.92 54.88 s 1080.87 -77.16 1217.8 155.75 S 1249.86 576.6 936.85 480 S 516.8 428.85 619.77 497.86 Z'
    },
  ],

  option5: [
    { // *** top
      transition: {
        from: {
          x: 50,
          y: 50,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 991.26 1614.96',
      path: 'M 757.85 1123.49 C 741.85 1026.83 778.74 971 731.1 871 c -53.86 -113.22 -152.75 -85.06 -154.78 -84.5 c -58.2 17 -55.26 127.47 -144.57 175.78 s -192.66 51.79 -257.15 44.81 S 75 997.25 45.8 1017.92 s -48 63.14 -45.59 100.13 a 105.12 105.12 0 0 0 5.67 28.19 c 11.23 30.61 44.86 81 145 93.18 c 43.78 1.82 151.88 18.63 284.32 128 l 0.15 -0.76 c 58.33 46.51 104.53 93 133.39 128.71 c 55 67.9 90.07 112.48 138 118.19 c 25.26 3 43.81 1.11 60.19 -5.09 c 49 -16.16 209.16 -87.19 97.18 -299.74 C 834.16 1249.36 770 1197.61 757.85 1123.49 Z'
    },
    { // *** bottom
      transition: {
        from: {
          x: 50,
          y: -250,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 991.26 1614.96',
      path: 'M 202.73 819.57 c -5.82 43.28 78.36 97.12 123.42 67.13 c 31.72 -21.14 27 -86.56 -12.26 -117.53 S 206.18 794.3 202.73 819.57 Z M 893.49 321.21 c -32.5 20.05 -68.65 37.62 -99.32 59 C 723.92 429 674.69 507.31 661.62 591.79 c -5.42 34.57 -5.46 71.58 -24.44 101 s -66.83 43.49 -87.82 15.85 c -29.07 -38.41 27.34 -91.44 18.17 -138.91 C 558 520 479.21 516.26 447 555.71 s -29.18 96.34 -19 145.92 s 25.26 101.07 11.42 150 c -14.94 52.94 -63.56 92.6 -116.95 106.63 c -53.42 13.81 -110.48 4.9 -161.75 -14.79 C 72.65 909.6 -9.78 823.22 13.13 731.22 c 12 -48.53 50.95 -86.35 93.93 -111.66 s 91.09 -40.41 136.74 -60.86 c 158.86 -71.25 286.07 -211.12 342.2 -376 C 600.26 141.07 610 97 633.81 60.1 C 710.48 -58.34 898.28 22.88 962.23 93.9 c 29.46 32.71 37.16 77.41 20 130.65 C 968.29 268.39 933.51 296.6 893.49 321.21 Z'
    },
  ],
  option6: [
    { // *** top
      transition: {
        from: {
          x: 50,
          y: 50,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1313.36 1885.98',
      path: 'M 1220.26 1006 c -97.1 -139.25 -209 -170 -328 -202.54 C 838.56 788.8 777.86 779.83 722 754.3 c -26.53 -12.12 -51 -32.2 -74.44 -50.62 c -99.65 -78.17 -165.23 -165.34 -302.76 -154.9 c -99 7.52 -197.89 69.62 -256.93 142.43 c -22.92 28.28 -42.55 53.63 -57.67 87.4 c -23 51.49 -48.15 153.71 -12 263.93 c 33.14 101.12 106.19 156.85 150.74 252.65 c 33.65 72.34 56.45 150.07 98.57 218.31 c 10.59 17.17 80 130.77 137.61 186.82 c 165.69 161.32 380.75 241.07 617.91 142.44 C 1180.26 1777.36 1459.54 1349.14 1220.26 1006 Z'
    },
    { // *** bottom
      transition: {
        from: {
          x: 50,
          y: -250,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1313.36 1885.98',
      path: 'M 317.38 479.48 l -1.62 0.11 c -34.2 2.29 -70 -13.49 -97.34 -33 c -73 -52.27 -89.58 -141 -40.76 -216.3 C 284.41 65.56 446.82 25.54 570 6.66 s 177 0.71 227.9 80.43 s 116 210.32 64.75 399.91 c -34.73 128.58 -191 147.3 -288.13 77.63 c -27 -19.39 -51.81 -42.06 -80.48 -59 C 435.12 470.94 381.68 475.14 317.38 479.48 Z'
    },
  ],
  option7: [
    { // *** top
      transition: {
        from: {
          x: 50,
          y: 50,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1094.91 1129.61',
      path: 'M 633.86 479 c -76.74 48.61 -95.83 107.7 -116 170.54 c -9.12 28.35 -15.34 60.58 -30.31 89.84 c -7.11 13.9 -18.42 26.48 -28.82 38.6 c -44.14 51.41 -92.29 84.4 -90 158.17 c 1.68 53.1 32.53 107.43 70.06 140.7 c 14.57 12.93 27.65 24 45.35 32.9 c 27 13.54 81 29.38 140.8 12.68 c 54.83 -15.32 86.35 -53.06 138.61 -74.6 c 39.46 -16.27 81.55 -26.62 119 -47.52 C 892 995 954.37 960.61 985.7 931.16 c 90.15 -84.75 137.87 -197.82 90.76 -326.92 C 1045.23 518.62 822.94 359.2 633.86 479 Z'
    },
    { // *** bottom
      transition: {
        from: {
          x: 50,
          y: -250,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1094.91 1129.61',
      path: 'M 208.35 439.9 c 37.08 80.35 18.31 137.79 83.75 213.48 c 74 85.71 153.38 37.5 155 36.52 c 46.63 -28.7 17.6 -124.23 83.81 -187.69 S 686.32 411 744.15 401.62 s 89.07 -15.27 109.58 -40.27 s 26.69 -66.48 15.72 -98.11 A 95.3 95.3 0 0 0 857.77 240 c -17.11 -24 -58.46 -59.8 -148.55 -46.43 C 670.65 202.5 572.48 213.74 431 150.22 l 0 0.69 C 369.09 124.38 317.72 95 284 70.77 C 219.91 24.8 178.66 -5.63 135.58 0.87 C 112.85 4.28 97.16 10.4 84.37 19.71 C 45.56 45.53 -76.9 145.72 71.51 304 C 111.76 348.55 180 378.26 208.35 439.9 Z'
    },
  ],
  option8: [
    { // *** single blob
      transition: {
        from: {
          x: 50,
          y: 50,
          rotation: -15,
        },
      },
      effects: {
        lavaLamp: {}
      },
      viewBox: '0 0 1020.25 613.58',
      path: 'M682.42 529.03C835.92 429.06 1018.54 335.34 1020.23 171.74C1021.92 8.14023 873.28 -34.3097 724.58 32.1102C497.02 133.75 380.07 83.3802 199.38 18.6202C89.8799 -20.6298 -2.10008 -12.3398 0.0299205 188.19C1.85992 357.72 271.08 796.88 682.42 529.03Z'
    },
  ]
};

const Shapes = (options) => {
  
  let els = {};
  
  const props = {
    classes: {
      inner: 'shapes__inner',
      shape: 'shape',
      option: 'shapes-option',
      colourOption: 'shapes-colour-option',
      group: 'shapes__group',
    }
  };
  
  const state = {
    option: 0,
    colourOption: 0,
    shapes: null
  };
  
  const init = () => {
    els = options.els;
    state.option = options.config.shapeOption;
    state.colourOption = options.config.colourOption;
    
    if (!state.option) {
      console.warn('/Shapes/ -init --no option set, quitting');
      return;
    }
    
    scaffold();
    state.shapes = shapeData[`option${state.option}`];
    state.shapes = state.shapes.map((item, index) => {
      return build(item, index + 1); // *** after creation, update state.shapes with svg el for transition ref
    });
    
    // *** TODO some refactoring required (for all behaviours) to enable methods to be reliably called from outside
    if (options.els.parentEl.classList.contains(config.classes.heroAnimated)) {
      animate();
    }
  };
  
  const scaffold = () => {
    els.inner = document.createElement('div');
    els.inner.classList.add(props.classes.inner);
    els.inner.classList.add(`${props.classes.option}--${state.option}`);
    els.inner.classList.add(`${props.classes.colourOption}--${state.colourOption}`);
    els.el.appendChild(els.inner);
    
    els.group = document.createElement('div');
    els.group.classList.add('shapes__group');
    els.inner.appendChild(els.group);
  };
  
  // *** generate svg and add to dom
  const build = (item, index) => {
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    const svgPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    const svgTitle = document.createElementNS('http://www.w3.org/2000/svg', 'title');
	
    svg.classList.add(props.classes.shape);
    svg.classList.add(`${props.classes.shape}--${index}`);
    svg.setAttribute('viewBox', item.viewBox);
	svg.setAttribute('role', 'img');
	svg.setAttribute('aria-label', `Page hero animated shapes ${props.classes.shape}--${index}`);
	svg.setAttribute('aria-labelledby', `HeroBlobs--${props.classes.shape}--${index}`);
    svgPath.setAttribute('d', item.path);
	svgTitle.setAttribute('id', `HeroBlobs--${props.classes.shape}--${index}`);
	svgTitle.textContent = `Page hero animated shapes ${props.classes.shape}--${index}`;
	svg.appendChild(svgTitle);
    svg.appendChild(svgPath);
    
    els.group.appendChild(svg);
    
    item.colour = window.getComputedStyle(svg ,null).getPropertyValue('fill'); // *** store css set colour value
    item.el = svg; // *** wrap el back into value object
    
    // const effect = LavaLampEffect({item, index});
    // effect.init();
    
    return item;
  };
  
  
  const animate = () => {
    
    const tm = config.timings.animation.longest() * 2; //*** TODO set via attribute?
    const easeParam = 1.5;
    const staggerDivisor = 6;
    
    const exec = (item, index) => {
      const r = item.el.getBoundingClientRect();
      const offsetX = item.transition.from.x;
      const x = `${r.left + offsetX}px`;
      const y = `${r.top + item.transition.from.y}px`;
      
      
      gsap$9.set(item.el, {
        left: x,
        top: y,
        rotate: item.transition.from.rotation,
        scale: 0,
        fill: '#78C3ED', // *** FIXIT
      });
      
      gsap$9.to(item.el, tm, {
        left: r.left,
        top: r.top,
        rotate: 0,
        scale: 1,
        fill: item.colour,
        delay: index / staggerDivisor,
        ease: `back.out(${easeParam})`,
      });
    };
    
    // *** invoke in loop
    state.shapes.map((item, index) => {
      exec(item, index);
    });
  };
  
  
  return {
    init,
    animate,
  }
};

/*!
 * ScrollTrigger 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var gsap$8,
    _coreInitted$4,
    _win$4,
    _doc$3,
    _docEl$1,
    _body$3,
    _root,
    _resizeDelay,
    _raf,
    _request,
    _toArray$3,
    _clamp,
    _time2,
    _syncInterval,
    _refreshing,
    _pointerIsDown,
    _transformProp$2,
    _i,
    _prevWidth,
    _prevHeight,
    _autoRefresh,
    _sort,
    _suppressOverwrites,
    _ignoreResize,
    _limitCallbacks,
    // if true, we'll only trigger callbacks if the active state toggles, so if you scroll immediately past both the start and end positions of a ScrollTrigger (thus inactive to inactive), neither its onEnter nor onLeave will be called. This is useful during startup.
_startup = 1,
    _proxies = [],
    _scrollers = [],
    _getTime$1 = Date.now,
    _time1 = _getTime$1(),
    _lastScrollTime = 0,
    _enabled = 1,
    _passThrough = function _passThrough(v) {
  return v;
},
    _round$2 = function _round(value) {
  return Math.round(value * 100000) / 100000 || 0;
},
    _windowExists$5 = function _windowExists() {
  return typeof window !== "undefined";
},
    _getGSAP$8 = function _getGSAP() {
  return gsap$8 || _windowExists$5() && (gsap$8 = window.gsap) && gsap$8.registerPlugin && gsap$8;
},
    _isViewport = function _isViewport(e) {
  return !!~_root.indexOf(e);
},
    _getProxyProp = function _getProxyProp(element, property) {
  return ~_proxies.indexOf(element) && _proxies[_proxies.indexOf(element) + 1][property];
},
    _getScrollFunc = function _getScrollFunc(element, _ref) {
  var s = _ref.s,
      sc = _ref.sc;

  var i = _scrollers.indexOf(element),
      offset = sc === _vertical.sc ? 1 : 2;

  !~i && (i = _scrollers.push(element) - 1);
  return _scrollers[i + offset] || (_scrollers[i + offset] = _getProxyProp(element, s) || (_isViewport(element) ? sc : function (value) {
    return arguments.length ? element[s] = value : element[s];
  }));
},
    _getBoundsFunc = function _getBoundsFunc(element) {
  return _getProxyProp(element, "getBoundingClientRect") || (_isViewport(element) ? function () {
    _winOffsets.width = _win$4.innerWidth;
    _winOffsets.height = _win$4.innerHeight;
    return _winOffsets;
  } : function () {
    return _getBounds$1(element);
  });
},
    _getSizeFunc = function _getSizeFunc(scroller, isViewport, _ref2) {
  var d = _ref2.d,
      d2 = _ref2.d2,
      a = _ref2.a;
  return (a = _getProxyProp(scroller, "getBoundingClientRect")) ? function () {
    return a()[d];
  } : function () {
    return (isViewport ? _win$4["inner" + d2] : scroller["client" + d2]) || 0;
  };
},
    _getOffsetsFunc = function _getOffsetsFunc(element, isViewport) {
  return !isViewport || ~_proxies.indexOf(element) ? _getBoundsFunc(element) : function () {
    return _winOffsets;
  };
},
    _maxScroll = function _maxScroll(element, _ref3) {
  var s = _ref3.s,
      d2 = _ref3.d2,
      d = _ref3.d,
      a = _ref3.a;
  return (s = "scroll" + d2) && (a = _getProxyProp(element, s)) ? a() - _getBoundsFunc(element)()[d] : _isViewport(element) ? Math.max(_docEl$1[s], _body$3[s]) - (_win$4["inner" + d2] || _docEl$1["client" + d2] || _body$3["client" + d2]) : element[s] - element["offset" + d2];
},
    _iterateAutoRefresh = function _iterateAutoRefresh(func, events) {
  for (var i = 0; i < _autoRefresh.length; i += 3) {
    (!events || ~events.indexOf(_autoRefresh[i + 1])) && func(_autoRefresh[i], _autoRefresh[i + 1], _autoRefresh[i + 2]);
  }
},
    _isString$3 = function _isString(value) {
  return typeof value === "string";
},
    _isFunction$3 = function _isFunction(value) {
  return typeof value === "function";
},
    _isNumber$1 = function _isNumber(value) {
  return typeof value === "number";
},
    _isObject$1 = function _isObject(value) {
  return typeof value === "object";
},
    _callIfFunc = function _callIfFunc(value) {
  return _isFunction$3(value) && value();
},
    _combineFunc = function _combineFunc(f1, f2) {
  return function () {
    var result1 = _callIfFunc(f1),
        result2 = _callIfFunc(f2);

    return function () {
      _callIfFunc(result1);

      _callIfFunc(result2);
    };
  };
},
    _abs$1 = Math.abs,
    _scrollLeft = "scrollLeft",
    _scrollTop = "scrollTop",
    _left = "left",
    _top = "top",
    _right = "right",
    _bottom = "bottom",
    _width = "width",
    _height = "height",
    _Right = "Right",
    _Left = "Left",
    _Top = "Top",
    _Bottom = "Bottom",
    _padding = "padding",
    _margin = "margin",
    _Width = "Width",
    _Height = "Height",
    _px = "px",
    _horizontal = {
  s: _scrollLeft,
  p: _left,
  p2: _Left,
  os: _right,
  os2: _Right,
  d: _width,
  d2: _Width,
  a: "x",
  sc: function sc(value) {
    return arguments.length ? _win$4.scrollTo(value, _vertical.sc()) : _win$4.pageXOffset || _doc$3[_scrollLeft] || _docEl$1[_scrollLeft] || _body$3[_scrollLeft] || 0;
  }
},
    _vertical = {
  s: _scrollTop,
  p: _top,
  p2: _Top,
  os: _bottom,
  os2: _Bottom,
  d: _height,
  d2: _Height,
  a: "y",
  op: _horizontal,
  sc: function sc(value) {
    return arguments.length ? _win$4.scrollTo(_horizontal.sc(), value) : _win$4.pageYOffset || _doc$3[_scrollTop] || _docEl$1[_scrollTop] || _body$3[_scrollTop] || 0;
  }
},
    _getComputedStyle$1 = function _getComputedStyle(element) {
  return _win$4.getComputedStyle(element);
},
    _makePositionable = function _makePositionable(element) {
  return element.style.position = _getComputedStyle$1(element).position === "absolute" ? "absolute" : "relative";
},
    // if the element already has position: absolute, leave that, otherwise make it position: relative
_setDefaults$1 = function _setDefaults(obj, defaults) {
  for (var p in defaults) {
    p in obj || (obj[p] = defaults[p]);
  }

  return obj;
},
    //_isInViewport = element => (element = _getBounds(element)) && !(element.top > (_win.innerHeight || _docEl.clientHeight) || element.bottom < 0 || element.left > (_win.innerWidth || _docEl.clientWidth) || element.right < 0) && element,
_getBounds$1 = function _getBounds(element, withoutTransforms) {
  var tween = withoutTransforms && _getComputedStyle$1(element)[_transformProp$2] !== "matrix(1, 0, 0, 1, 0, 0)" && gsap$8.to(element, {
    x: 0,
    y: 0,
    xPercent: 0,
    yPercent: 0,
    rotation: 0,
    rotationX: 0,
    rotationY: 0,
    scale: 1,
    skewX: 0,
    skewY: 0
  }).progress(1),
      bounds = element.getBoundingClientRect();
  tween && tween.progress(0).kill();
  return bounds;
},
    _getSize = function _getSize(element, _ref4) {
  var d2 = _ref4.d2;
  return element["offset" + d2] || element["client" + d2] || 0;
},
    _getLabelRatioArray = function _getLabelRatioArray(timeline) {
  var a = [],
      labels = timeline.labels,
      duration = timeline.duration(),
      p;

  for (p in labels) {
    a.push(labels[p] / duration);
  }

  return a;
},
    _getClosestLabel = function _getClosestLabel(animation) {
  return function (value) {
    return gsap$8.utils.snap(_getLabelRatioArray(animation), value);
  };
},
    _getLabelAtDirection = function _getLabelAtDirection(timeline) {
  return function (value, st) {
    var a = _getLabelRatioArray(timeline),
        i;

    a.sort(function (a, b) {
      return a - b;
    });

    if (st.direction > 0) {
      value -= 1e-4; // to avoid rounding errors. If we're too strict, it might snap forward, then immediately again, and again.

      for (i = 0; i < a.length; i++) {
        if (a[i] >= value) {
          return a[i];
        }
      }

      return a.pop();
    } else {
      i = a.length;
      value += 1e-4;

      while (i--) {
        if (a[i] <= value) {
          return a[i];
        }
      }
    }

    return a[0];
  };
},
    _multiListener = function _multiListener(func, element, types, callback) {
  return types.split(",").forEach(function (type) {
    return func(element, type, callback);
  });
},
    _addListener$1 = function _addListener(element, type, func) {
  return element.addEventListener(type, func, {
    passive: true
  });
},
    _removeListener$1 = function _removeListener(element, type, func) {
  return element.removeEventListener(type, func);
},
    _markerDefaults = {
  startColor: "green",
  endColor: "red",
  indent: 0,
  fontSize: "16px",
  fontWeight: "normal"
},
    _defaults = {
  toggleActions: "play",
  anticipatePin: 0
},
    _keywords = {
  top: 0,
  left: 0,
  center: 0.5,
  bottom: 1,
  right: 1
},
    _offsetToPx = function _offsetToPx(value, size) {
  if (_isString$3(value)) {
    var eqIndex = value.indexOf("="),
        relative = ~eqIndex ? +(value.charAt(eqIndex - 1) + 1) * parseFloat(value.substr(eqIndex + 1)) : 0;

    if (~eqIndex) {
      value.indexOf("%") > eqIndex && (relative *= size / 100);
      value = value.substr(0, eqIndex - 1);
    }

    value = relative + (value in _keywords ? _keywords[value] * size : ~value.indexOf("%") ? parseFloat(value) * size / 100 : parseFloat(value) || 0);
  }

  return value;
},
    _createMarker = function _createMarker(type, name, container, direction, _ref5, offset, matchWidthEl) {
  var startColor = _ref5.startColor,
      endColor = _ref5.endColor,
      fontSize = _ref5.fontSize,
      indent = _ref5.indent,
      fontWeight = _ref5.fontWeight;

  var e = _doc$3.createElement("div"),
      useFixedPosition = _isViewport(container) || _getProxyProp(container, "pinType") === "fixed",
      isScroller = type.indexOf("scroller") !== -1,
      parent = useFixedPosition ? _body$3 : container,
      isStart = type.indexOf("start") !== -1,
      color = isStart ? startColor : endColor,
      css = "border-color:" + color + ";font-size:" + fontSize + ";color:" + color + ";font-weight:" + fontWeight + ";pointer-events:none;white-space:nowrap;font-family:sans-serif,Arial;z-index:1000;padding:4px 8px;border-width:0;border-style:solid;";

  css += "position:" + (isScroller && useFixedPosition ? "fixed;" : "absolute;");
  (isScroller || !useFixedPosition) && (css += (direction === _vertical ? _right : _bottom) + ":" + (offset + parseFloat(indent)) + "px;");
  matchWidthEl && (css += "box-sizing:border-box;text-align:left;width:" + matchWidthEl.offsetWidth + "px;");
  e._isStart = isStart;
  e.setAttribute("class", "gsap-marker-" + type);
  e.style.cssText = css;
  e.innerText = name || name === 0 ? type + "-" + name : type;
  parent.children[0] ? parent.insertBefore(e, parent.children[0]) : parent.appendChild(e);
  e._offset = e["offset" + direction.op.d2];

  _positionMarker(e, 0, direction, isStart);

  return e;
},
    _positionMarker = function _positionMarker(marker, start, direction, flipped) {
  var vars = {
    display: "block"
  },
      side = direction[flipped ? "os2" : "p2"],
      oppositeSide = direction[flipped ? "p2" : "os2"];
  marker._isFlipped = flipped;
  vars[direction.a + "Percent"] = flipped ? -100 : 0;
  vars[direction.a] = flipped ? "1px" : 0;
  vars["border" + side + _Width] = 1;
  vars["border" + oppositeSide + _Width] = 0;
  vars[direction.p] = start + "px";
  gsap$8.set(marker, vars);
},
    _triggers = [],
    _ids = {},
    _sync = function _sync() {
  return _request || (_request = _raf(_updateAll));
},
    _onScroll = function _onScroll() {
  if (!_request) {
    _request = _raf(_updateAll);
    _lastScrollTime || _dispatch("scrollStart");
    _lastScrollTime = _getTime$1();
  }
},
    _onResize = function _onResize() {
  return !_refreshing && !_ignoreResize && !_doc$3.fullscreenElement && _resizeDelay.restart(true);
},
    // ignore resizes triggered by refresh()
_listeners = {},
    _emptyArray = [],
    _media = [],
    _creatingMedia,
    // when ScrollTrigger.matchMedia() is called, we record the current media key here (like "(min-width: 800px)") so that we can assign it to everything that's created during that call. Then we can revert just those when necessary. In the ScrollTrigger's init() call, the _creatingMedia is recorded as a "media" property on the instance.
_lastMediaTick,
    _onMediaChange = function _onMediaChange(e) {
  var tick = gsap$8.ticker.frame,
      matches = [],
      i = 0,
      index;

  if (_lastMediaTick !== tick || _startup) {
    _revertAll();

    for (; i < _media.length; i += 4) {
      index = _win$4.matchMedia(_media[i]).matches;

      if (index !== _media[i + 3]) {
        // note: some browsers fire the matchMedia event multiple times, like when going full screen, so we shouldn't call the function multiple times. Check to see if it's already matched.
        _media[i + 3] = index;
        index ? matches.push(i) : _revertAll(1, _media[i]) || _isFunction$3(_media[i + 2]) && _media[i + 2](); // Firefox doesn't update the "matches" property of the MediaQueryList object correctly - it only does so as it calls its change handler - so we must re-create a media query here to ensure it's accurate.
      }
    }

    _revertRecorded(); // in case killing/reverting any of the animations actually added inline styles back.


    for (i = 0; i < matches.length; i++) {
      index = matches[i];
      _creatingMedia = _media[index];
      _media[index + 2] = _media[index + 1](e);
    }

    _creatingMedia = 0;
    _coreInitted$4 && _refreshAll(0, 1);
    _lastMediaTick = tick;

    _dispatch("matchMedia");
  }
},
    _softRefresh = function _softRefresh() {
  return _removeListener$1(ScrollTrigger, "scrollEnd", _softRefresh) || _refreshAll(true);
},
    _dispatch = function _dispatch(type) {
  return _listeners[type] && _listeners[type].map(function (f) {
    return f();
  }) || _emptyArray;
},
    _savedStyles = [],
    // when ScrollTrigger.saveStyles() is called, the inline styles are recorded in this Array in a sequential format like [element, cssText, gsCache, media]. This keeps it very memory-efficient and fast to iterate through.
_revertRecorded = function _revertRecorded(media) {
  for (var i = 0; i < _savedStyles.length; i += 4) {
    if (!media || _savedStyles[i + 3] === media) {
      _savedStyles[i].style.cssText = _savedStyles[i + 1];
      _savedStyles[i + 2].uncache = 1;
    }
  }
},
    _revertAll = function _revertAll(kill, media) {
  var trigger;

  for (_i = 0; _i < _triggers.length; _i++) {
    trigger = _triggers[_i];

    if (!media || trigger.media === media) {
      if (kill) {
        trigger.kill(1);
      } else {
        trigger.scroll.rec || (trigger.scroll.rec = trigger.scroll()); // record the scroll positions so that in each refresh() we can ensure that it doesn't shift. Remember, pinning can make things change around, especially if the same element is pinned multiple times. If one was already recorded, don't re-record because unpinning may have occurred and made it shorter.

        trigger.revert();
      }
    }
  }

  _revertRecorded(media);

  media || _dispatch("revert");
},
    _refreshAll = function _refreshAll(force, skipRevert) {
  if (_lastScrollTime && !force) {
    _addListener$1(ScrollTrigger, "scrollEnd", _softRefresh);

    return;
  }

  var refreshInits = _dispatch("refreshInit");

  _sort && ScrollTrigger.sort();
  skipRevert || _revertAll();

  for (_i = 0; _i < _triggers.length; _i++) {
    _triggers[_i].refresh();
  }

  refreshInits.forEach(function (result) {
    return result && result.render && result.render(-1);
  }); // if the onRefreshInit() returns an animation (typically a gsap.set()), revert it. This makes it easy to put things in a certain spot before refreshing for measurement purposes, and then put things back.

  _i = _triggers.length;

  while (_i--) {
    _triggers[_i].scroll.rec = 0;
  }

  _resizeDelay.pause();

  _dispatch("refresh");
},
    _lastScroll = 0,
    _direction = 1,
    _updateAll = function _updateAll() {
  var l = _triggers.length,
      time = _getTime$1(),
      recordVelocity = time - _time1 >= 50,
      scroll = l && _triggers[0].scroll();

  _direction = _lastScroll > scroll ? -1 : 1;
  _lastScroll = scroll;

  if (recordVelocity) {
    if (_lastScrollTime && !_pointerIsDown && time - _lastScrollTime > 200) {
      _lastScrollTime = 0;

      _dispatch("scrollEnd");
    }

    _time2 = _time1;
    _time1 = time;
  }

  if (_direction < 0) {
    _i = l;

    while (_i-- > 0) {
      _triggers[_i] && _triggers[_i].update(0, recordVelocity);
    }

    _direction = 1;
  } else {
    for (_i = 0; _i < l; _i++) {
      _triggers[_i] && _triggers[_i].update(0, recordVelocity);
    }
  }

  _request = 0;
},
    _propNamesToCopy = [_left, _top, _bottom, _right, _margin + _Bottom, _margin + _Right, _margin + _Top, _margin + _Left, "display", "flexShrink", "float", "zIndex"],
    _stateProps = _propNamesToCopy.concat([_width, _height, "boxSizing", "max" + _Width, "max" + _Height, "position", _margin, _padding, _padding + _Top, _padding + _Right, _padding + _Bottom, _padding + _Left]),
    _swapPinOut = function _swapPinOut(pin, spacer, state) {
  _setState(state);

  if (pin.parentNode === spacer) {
    var parent = spacer.parentNode;

    if (parent) {
      parent.insertBefore(pin, spacer);
      parent.removeChild(spacer);
    }
  }
},
    _swapPinIn = function _swapPinIn(pin, spacer, cs, spacerState) {
  if (pin.parentNode !== spacer) {
    var i = _propNamesToCopy.length,
        spacerStyle = spacer.style,
        pinStyle = pin.style,
        p;

    while (i--) {
      p = _propNamesToCopy[i];
      spacerStyle[p] = cs[p];
    }

    spacerStyle.position = cs.position === "absolute" ? "absolute" : "relative";
    cs.display === "inline" && (spacerStyle.display = "inline-block");
    pinStyle[_bottom] = pinStyle[_right] = "auto";
    spacerStyle.overflow = "visible";
    spacerStyle.boxSizing = "border-box";
    spacerStyle[_width] = _getSize(pin, _horizontal) + _px;
    spacerStyle[_height] = _getSize(pin, _vertical) + _px;
    spacerStyle[_padding] = pinStyle[_margin] = pinStyle[_top] = pinStyle[_left] = "0";

    _setState(spacerState);

    pinStyle[_width] = pinStyle["max" + _Width] = cs[_width];
    pinStyle[_height] = pinStyle["max" + _Height] = cs[_height];
    pinStyle[_padding] = cs[_padding];
    pin.parentNode.insertBefore(spacer, pin);
    spacer.appendChild(pin);
  }
},
    _capsExp = /([A-Z])/g,
    _setState = function _setState(state) {
  if (state) {
    var style = state.t.style,
        l = state.length,
        i = 0,
        p,
        value;
    (state.t._gsap || gsap$8.core.getCache(state.t)).uncache = 1; // otherwise transforms may be off

    for (; i < l; i += 2) {
      value = state[i + 1];
      p = state[i];

      if (value) {
        style[p] = value;
      } else if (style[p]) {
        style.removeProperty(p.replace(_capsExp, "-$1").toLowerCase());
      }
    }
  }
},
    _getState = function _getState(element) {
  // returns an Array with alternating values like [property, value, property, value] and a "t" property pointing to the target (element). Makes it fast and cheap.
  var l = _stateProps.length,
      style = element.style,
      state = [],
      i = 0;

  for (; i < l; i++) {
    state.push(_stateProps[i], style[_stateProps[i]]);
  }

  state.t = element;
  return state;
},
    _copyState = function _copyState(state, override, omitOffsets) {
  var result = [],
      l = state.length,
      i = omitOffsets ? 8 : 0,
      // skip top, left, right, bottom if omitOffsets is true
  p;

  for (; i < l; i += 2) {
    p = state[i];
    result.push(p, p in override ? override[p] : state[i + 1]);
  }

  result.t = state.t;
  return result;
},
    _winOffsets = {
  left: 0,
  top: 0
},
    _parsePosition = function _parsePosition(value, trigger, scrollerSize, direction, scroll, marker, markerScroller, self, scrollerBounds, borderWidth, useFixedPosition, scrollerMax) {
  _isFunction$3(value) && (value = value(self));

  if (_isString$3(value) && value.substr(0, 3) === "max") {
    value = scrollerMax + (value.charAt(4) === "=" ? _offsetToPx("0" + value.substr(3), scrollerSize) : 0);
  }

  if (!_isNumber$1(value)) {
    _isFunction$3(trigger) && (trigger = trigger(self));

    var element = _toArray$3(trigger)[0] || _body$3,
        bounds = _getBounds$1(element) || {},
        offsets = value.split(" "),
        localOffset,
        globalOffset,
        display;

    if ((!bounds || !bounds.left && !bounds.top) && _getComputedStyle$1(element).display === "none") {
      // if display is "none", it won't report getBoundingClientRect() properly
      display = element.style.display;
      element.style.display = "block";
      bounds = _getBounds$1(element);
      display ? element.style.display = display : element.style.removeProperty("display");
    }

    localOffset = _offsetToPx(offsets[0], bounds[direction.d]);
    globalOffset = _offsetToPx(offsets[1] || "0", scrollerSize);
    value = bounds[direction.p] - scrollerBounds[direction.p] - borderWidth + localOffset + scroll - globalOffset;
    markerScroller && _positionMarker(markerScroller, globalOffset, direction, scrollerSize - globalOffset < 20 || markerScroller._isStart && globalOffset > 20);
    scrollerSize -= scrollerSize - globalOffset; // adjust for the marker
  } else if (markerScroller) {
    _positionMarker(markerScroller, scrollerSize, direction, true);
  }

  if (marker) {
    var position = value + scrollerSize,
        isStart = marker._isStart;
    scrollerMax = "scroll" + direction.d2;

    _positionMarker(marker, position, direction, isStart && position > 20 || !isStart && (useFixedPosition ? Math.max(_body$3[scrollerMax], _docEl$1[scrollerMax]) : marker.parentNode[scrollerMax]) <= position + 1);

    if (useFixedPosition) {
      scrollerBounds = _getBounds$1(markerScroller);
      useFixedPosition && (marker.style[direction.op.p] = scrollerBounds[direction.op.p] - direction.op.m - marker._offset + _px);
    }
  }

  return Math.round(value);
},
    _prefixExp = /(?:webkit|moz|length|cssText|inset)/i,
    _reparent = function _reparent(element, parent, top, left) {
  if (element.parentNode !== parent) {
    var style = element.style,
        p,
        cs;

    if (parent === _body$3) {
      element._stOrig = style.cssText; // record original inline styles so we can revert them later

      cs = _getComputedStyle$1(element);

      for (p in cs) {
        // must copy all relevant styles to ensure that nothing changes visually when we reparent to the <body>. Skip the vendor prefixed ones.
        if (!+p && !_prefixExp.test(p) && cs[p] && typeof style[p] === "string" && p !== "0") {
          style[p] = cs[p];
        }
      }

      style.top = top;
      style.left = left;
    } else {
      style.cssText = element._stOrig;
    }

    gsap$8.core.getCache(element).uncache = 1;
    parent.appendChild(element);
  }
},
    // returns a function that can be used to tween the scroll position in the direction provided, and when doing so it'll add a .tween property to the FUNCTION itself, and remove it when the tween completes or gets killed. This gives us a way to have multiple ScrollTriggers use a central function for any given scroller and see if there's a scroll tween running (which would affect if/how things get updated)
_getTweenCreator = function _getTweenCreator(scroller, direction) {
  var getScroll = _getScrollFunc(scroller, direction),
      prop = "_scroll" + direction.p2,
      // add a tweenable property to the scroller that's a getter/setter function, like _scrollTop or _scrollLeft. This way, if someone does gsap.killTweensOf(scroller) it'll kill the scroll tween.
  lastScroll1,
      lastScroll2,
      getTween = function getTween(scrollTo, vars, initialValue, change1, change2) {
    var tween = getTween.tween,
        onComplete = vars.onComplete,
        modifiers = {};
    tween && tween.kill();
    lastScroll1 = Math.round(initialValue);
    vars[prop] = scrollTo;
    vars.modifiers = modifiers;

    modifiers[prop] = function (value) {
      value = _round$2(getScroll()); // round because in some [very uncommon] Windows environments, it can get reported with decimals even though it was set without.

      if (value !== lastScroll1 && value !== lastScroll2 && Math.abs(value - lastScroll1) > 2) {
        // if the user scrolls, kill the tween. iOS Safari intermittently misreports the scroll position, it may be the most recently-set one or the one before that! When Safari is zoomed (CMD-+), it often misreports as 1 pixel off too! So if we set the scroll position to 125, for example, it'll actually report it as 124.
        tween.kill();
        getTween.tween = 0;
      } else {
        value = initialValue + change1 * tween.ratio + change2 * tween.ratio * tween.ratio;
      }

      lastScroll2 = lastScroll1;
      return lastScroll1 = _round$2(value);
    };

    vars.onComplete = function () {
      getTween.tween = 0;
      onComplete && onComplete.call(tween);
    };

    tween = getTween.tween = gsap$8.to(scroller, vars);
    return tween;
  };

  scroller[prop] = getScroll;
  scroller.addEventListener("wheel", function () {
    return getTween.tween && getTween.tween.kill() && (getTween.tween = 0);
  }); // Windows machines handle mousewheel scrolling in chunks (like "3 lines per scroll") meaning the typical strategy for cancelling the scroll isn't as sensitive. It's much more likely to match one of the previous 2 scroll event positions. So we kill any snapping as soon as there's a wheel event.

  return getTween;
};

_horizontal.op = _vertical;
var ScrollTrigger = /*#__PURE__*/function () {
  function ScrollTrigger(vars, animation) {
    _coreInitted$4 || ScrollTrigger.register(gsap$8) || console.warn("Please gsap.registerPlugin(ScrollTrigger)");
    this.init(vars, animation);
  }

  var _proto = ScrollTrigger.prototype;

  _proto.init = function init(vars, animation) {
    this.progress = this.start = 0;
    this.vars && this.kill(1); // in case it's being initted again

    if (!_enabled) {
      this.update = this.refresh = this.kill = _passThrough;
      return;
    }

    vars = _setDefaults$1(_isString$3(vars) || _isNumber$1(vars) || vars.nodeType ? {
      trigger: vars
    } : vars, _defaults);

    var direction = vars.horizontal ? _horizontal : _vertical,
        _vars = vars,
        onUpdate = _vars.onUpdate,
        toggleClass = _vars.toggleClass,
        id = _vars.id,
        onToggle = _vars.onToggle,
        onRefresh = _vars.onRefresh,
        scrub = _vars.scrub,
        trigger = _vars.trigger,
        pin = _vars.pin,
        pinSpacing = _vars.pinSpacing,
        invalidateOnRefresh = _vars.invalidateOnRefresh,
        anticipatePin = _vars.anticipatePin,
        onScrubComplete = _vars.onScrubComplete,
        onSnapComplete = _vars.onSnapComplete,
        once = _vars.once,
        snap = _vars.snap,
        pinReparent = _vars.pinReparent,
        isToggle = !scrub && scrub !== 0,
        scroller = _toArray$3(vars.scroller || _win$4)[0],
        scrollerCache = gsap$8.core.getCache(scroller),
        isViewport = _isViewport(scroller),
        useFixedPosition = "pinType" in vars ? vars.pinType === "fixed" : isViewport || _getProxyProp(scroller, "pinType") === "fixed",
        callbacks = [vars.onEnter, vars.onLeave, vars.onEnterBack, vars.onLeaveBack],
        toggleActions = isToggle && vars.toggleActions.split(" "),
        markers = "markers" in vars ? vars.markers : _defaults.markers,
        borderWidth = isViewport ? 0 : parseFloat(_getComputedStyle$1(scroller)["border" + direction.p2 + _Width]) || 0,
        self = this,
        onRefreshInit = vars.onRefreshInit && function () {
      return vars.onRefreshInit(self);
    },
        getScrollerSize = _getSizeFunc(scroller, isViewport, direction),
        getScrollerOffsets = _getOffsetsFunc(scroller, isViewport),
        tweenTo,
        pinCache,
        snapFunc,
        isReverted,
        scroll1,
        scroll2,
        start,
        end,
        markerStart,
        markerEnd,
        markerStartTrigger,
        markerEndTrigger,
        markerVars,
        change,
        pinOriginalState,
        pinActiveState,
        pinState,
        spacer,
        offset,
        pinGetter,
        pinSetter,
        pinStart,
        pinChange,
        spacingStart,
        spacerState,
        markerStartSetter,
        markerEndSetter,
        cs,
        snap1,
        snap2,
        scrubTween,
        scrubSmooth,
        snapDurClamp,
        snapDelayedCall,
        prevProgress,
        prevScroll,
        prevAnimProgress;

    self.media = _creatingMedia;
    anticipatePin *= 45;

    _triggers.push(self);

    self.scroller = scroller;
    self.scroll = _getScrollFunc(scroller, direction);
    scroll1 = self.scroll();
    self.vars = vars;
    animation = animation || vars.animation;
    "refreshPriority" in vars && (_sort = 1);
    scrollerCache.tweenScroll = scrollerCache.tweenScroll || {
      top: _getTweenCreator(scroller, _vertical),
      left: _getTweenCreator(scroller, _horizontal)
    };
    self.tweenTo = tweenTo = scrollerCache.tweenScroll[direction.p];

    if (animation) {
      animation.vars.lazy = false;
      animation._initted || animation.vars.immediateRender !== false && vars.immediateRender !== false && animation.render(0, true, true);
      self.animation = animation.pause();
      animation.scrollTrigger = self;
      scrubSmooth = _isNumber$1(scrub) && scrub;
      scrubSmooth && (scrubTween = gsap$8.to(animation, {
        ease: "power3",
        duration: scrubSmooth,
        onComplete: function onComplete() {
          return onScrubComplete && onScrubComplete(self);
        }
      }));
      snap1 = 0;
      id || (id = animation.vars.id);
    }

    if (snap) {
      _isObject$1(snap) || (snap = {
        snapTo: snap
      });
      "scrollBehavior" in _body$3.style && gsap$8.set(isViewport ? [_body$3, _docEl$1] : scroller, {
        scrollBehavior: "auto"
      }); // smooth scrolling doesn't work with snap.

      snapFunc = _isFunction$3(snap.snapTo) ? snap.snapTo : snap.snapTo === "labels" ? _getClosestLabel(animation) : snap.snapTo === "labelsDirectional" ? _getLabelAtDirection(animation) : gsap$8.utils.snap(snap.snapTo);
      snapDurClamp = snap.duration || {
        min: 0.1,
        max: 2
      };
      snapDurClamp = _isObject$1(snapDurClamp) ? _clamp(snapDurClamp.min, snapDurClamp.max) : _clamp(snapDurClamp, snapDurClamp);
      snapDelayedCall = gsap$8.delayedCall(snap.delay || scrubSmooth / 2 || 0.1, function () {
        if (Math.abs(self.getVelocity()) < 10 && !_pointerIsDown) {
          var totalProgress = animation && !isToggle ? animation.totalProgress() : self.progress,
              velocity = (totalProgress - snap2) / (_getTime$1() - _time2) * 1000 || 0,
              change1 = _abs$1(velocity / 2) * velocity / 0.185,
              naturalEnd = totalProgress + (snap.inertia === false ? 0 : change1),
              endValue = _clamp(0, 1, snapFunc(naturalEnd, self)),
              scroll = self.scroll(),
              endScroll = Math.round(start + endValue * change),
              _snap = snap,
              onStart = _snap.onStart,
              _onInterrupt = _snap.onInterrupt,
              _onComplete = _snap.onComplete,
              tween = tweenTo.tween;

          if (scroll <= end && scroll >= start && endScroll !== scroll) {
            if (tween && !tween._initted && tween.data <= Math.abs(endScroll - scroll)) {
              // there's an overlapping snap! So we must figure out which one is closer and let that tween live.
              return;
            }

            tweenTo(endScroll, {
              duration: snapDurClamp(_abs$1(Math.max(_abs$1(naturalEnd - totalProgress), _abs$1(endValue - totalProgress)) * 0.185 / velocity / 0.05 || 0)),
              ease: snap.ease || "power3",
              data: Math.abs(endScroll - scroll),
              // record the distance so that if another snap tween occurs (conflict) we can prioritize the closest snap.
              onInterrupt: function onInterrupt() {
                return snapDelayedCall.restart(true) && _onInterrupt && _onInterrupt(self);
              },
              onComplete: function onComplete() {
                snap1 = snap2 = animation && !isToggle ? animation.totalProgress() : self.progress;
                onSnapComplete && onSnapComplete(self);
                _onComplete && _onComplete(self);
              }
            }, scroll, change1 * change, endScroll - scroll - change1 * change);
            onStart && onStart(self, tweenTo.tween);
          }
        } else if (self.isActive) {
          snapDelayedCall.restart(true);
        }
      }).pause();
    }

    id && (_ids[id] = self);
    trigger = self.trigger = _toArray$3(trigger || pin)[0];
    pin = pin === true ? trigger : _toArray$3(pin)[0];
    _isString$3(toggleClass) && (toggleClass = {
      targets: trigger,
      className: toggleClass
    });

    if (pin) {
      pinSpacing === false || pinSpacing === _margin || (pinSpacing = !pinSpacing && _getComputedStyle$1(pin.parentNode).display === "flex" ? false : _padding); // if the parent is display: flex, don't apply pinSpacing by default.

      self.pin = pin;
      vars.force3D !== false && gsap$8.set(pin, {
        force3D: true
      });
      pinCache = gsap$8.core.getCache(pin);

      if (!pinCache.spacer) {
        // record the spacer and pinOriginalState on the cache in case someone tries pinning the same element with MULTIPLE ScrollTriggers - we don't want to have multiple spacers or record the "original" pin state after it has already been affected by another ScrollTrigger.
        pinCache.spacer = spacer = _doc$3.createElement("div");
        spacer.setAttribute("class", "pin-spacer" + (id ? " pin-spacer-" + id : ""));
        pinCache.pinState = pinOriginalState = _getState(pin);
      } else {
        pinOriginalState = pinCache.pinState;
      }

      self.spacer = spacer = pinCache.spacer;
      cs = _getComputedStyle$1(pin);
      spacingStart = cs[pinSpacing + direction.os2];
      pinGetter = gsap$8.getProperty(pin);
      pinSetter = gsap$8.quickSetter(pin, direction.a, _px); // pin.firstChild && !_maxScroll(pin, direction) && (pin.style.overflow = "hidden"); // protects from collapsing margins, but can have unintended consequences as demonstrated here: https://codepen.io/GreenSock/pen/1e42c7a73bfa409d2cf1e184e7a4248d so it was removed in favor of just telling people to set up their CSS to avoid the collapsing margins (overflow: hidden | auto is just one option. Another is border-top: 1px solid transparent).

      _swapPinIn(pin, spacer, cs);

      pinState = _getState(pin);
    }

    if (markers) {
      markerVars = _isObject$1(markers) ? _setDefaults$1(markers, _markerDefaults) : _markerDefaults;
      markerStartTrigger = _createMarker("scroller-start", id, scroller, direction, markerVars, 0);
      markerEndTrigger = _createMarker("scroller-end", id, scroller, direction, markerVars, 0, markerStartTrigger);
      offset = markerStartTrigger["offset" + direction.op.d2];
      markerStart = _createMarker("start", id, scroller, direction, markerVars, offset);
      markerEnd = _createMarker("end", id, scroller, direction, markerVars, offset);

      if (!useFixedPosition) {
        _makePositionable(isViewport ? _body$3 : scroller);

        gsap$8.set([markerStartTrigger, markerEndTrigger], {
          force3D: true
        });
        markerStartSetter = gsap$8.quickSetter(markerStartTrigger, direction.a, _px);
        markerEndSetter = gsap$8.quickSetter(markerEndTrigger, direction.a, _px);
      }
    }

    self.revert = function (revert) {
      var r = revert !== false || !self.enabled,
          prevRefreshing = _refreshing;

      if (r !== isReverted) {
        if (r) {
          prevScroll = Math.max(self.scroll(), self.scroll.rec || 0); // record the scroll so we can revert later (repositioning/pinning things can affect scroll position). In the static refresh() method, we first record all the scroll positions as a reference.

          prevProgress = self.progress;
          prevAnimProgress = animation && animation.progress();
        }

        markerStart && [markerStart, markerEnd, markerStartTrigger, markerEndTrigger].forEach(function (m) {
          return m.style.display = r ? "none" : "block";
        });
        r && (_refreshing = 1);
        self.update(r); // make sure the pin is back in its original position so that all the measurements are correct.

        _refreshing = prevRefreshing;
        pin && (r ? _swapPinOut(pin, spacer, pinOriginalState) : (!pinReparent || !self.isActive) && _swapPinIn(pin, spacer, _getComputedStyle$1(pin), spacerState));
        isReverted = r;
      }
    };

    self.refresh = function (soft, force) {
      if ((_refreshing || !self.enabled) && !force) {
        return;
      }

      if (pin && soft && _lastScrollTime) {
        _addListener$1(ScrollTrigger, "scrollEnd", _softRefresh);

        return;
      }

      _refreshing = 1;
      scrubTween && scrubTween.pause();
      invalidateOnRefresh && animation && animation.progress(0).invalidate();
      isReverted || self.revert();

      var size = getScrollerSize(),
          scrollerBounds = getScrollerOffsets(),
          max = _maxScroll(scroller, direction),
          offset = 0,
          otherPinOffset = 0,
          parsedEnd = vars.end,
          parsedEndTrigger = vars.endTrigger || trigger,
          parsedStart = vars.start || (vars.start === 0 || !trigger ? 0 : pin ? "0 0" : "0 100%"),
          triggerIndex = trigger && Math.max(0, _triggers.indexOf(self)) || 0,
          i = triggerIndex,
          cs,
          bounds,
          scroll,
          isVertical,
          override,
          curTrigger,
          curPin,
          oppositeScroll,
          initted;

      while (i--) {
        // user might try to pin the same element more than once, so we must find any prior triggers with the same pin, revert them, and determine how long they're pinning so that we can offset things appropriately. Make sure we revert from last to first so that things "rewind" properly.
        curTrigger = _triggers[i];
        curTrigger.end || curTrigger.refresh(0, 1) || (_refreshing = 1); // if it's a timeline-based trigger that hasn't been fully initialized yet because it's waiting for 1 tick, just force the refresh() here, otherwise if it contains a pin that's supposed to affect other ScrollTriggers further down the page, they won't be adjusted properly.

        curPin = curTrigger.pin;
        curPin && (curPin === trigger || curPin === pin) && curTrigger.revert();
      }

      start = _parsePosition(parsedStart, trigger, size, direction, self.scroll(), markerStart, markerStartTrigger, self, scrollerBounds, borderWidth, useFixedPosition, max) || (pin ? -0.001 : 0);
      _isFunction$3(parsedEnd) && (parsedEnd = parsedEnd(self));

      if (_isString$3(parsedEnd) && !parsedEnd.indexOf("+=")) {
        if (~parsedEnd.indexOf(" ")) {
          parsedEnd = (_isString$3(parsedStart) ? parsedStart.split(" ")[0] : "") + parsedEnd;
        } else {
          offset = _offsetToPx(parsedEnd.substr(2), size);
          parsedEnd = _isString$3(parsedStart) ? parsedStart : start + offset; // _parsePosition won't factor in the offset if the start is a number, so do it here.

          parsedEndTrigger = trigger;
        }
      }

      end = Math.max(start, _parsePosition(parsedEnd || (parsedEndTrigger ? "100% 0" : max), parsedEndTrigger, size, direction, self.scroll() + offset, markerEnd, markerEndTrigger, self, scrollerBounds, borderWidth, useFixedPosition, max)) || -0.001;
      change = end - start || (start -= 0.01) && 0.001;
      offset = 0;
      i = triggerIndex;

      while (i--) {
        curTrigger = _triggers[i];
        curPin = curTrigger.pin;

        if (curPin && curTrigger.start - curTrigger._pinPush < start) {
          cs = curTrigger.end - curTrigger.start;
          curPin === trigger && (offset += cs);
          curPin === pin && (otherPinOffset += cs);
        }
      }

      start += offset;
      end += offset;
      self._pinPush = otherPinOffset;

      if (markerStart && offset) {
        // offset the markers if necessary
        cs = {};
        cs[direction.a] = "+=" + offset;
        gsap$8.set([markerStart, markerEnd], cs);
      }

      if (pin) {
        cs = _getComputedStyle$1(pin);
        isVertical = direction === _vertical;
        scroll = self.scroll(); // recalculate because the triggers can affect the scroll

        pinStart = parseFloat(pinGetter(direction.a)) + otherPinOffset;
        !max && end > 1 && ((isViewport ? _body$3 : scroller).style["overflow-" + direction.a] = "scroll"); // makes sure the scroller has a scrollbar, otherwise if something has width: 100%, for example, it would be too big (exclude the scrollbar). See https://greensock.com/forums/topic/25182-scrolltrigger-width-of-page-increase-where-markers-are-set-to-false/

        _swapPinIn(pin, spacer, cs);

        pinState = _getState(pin); // transforms will interfere with the top/left/right/bottom placement, so remove them temporarily. getBoundingClientRect() factors in transforms.

        bounds = _getBounds$1(pin, true);
        oppositeScroll = useFixedPosition && _getScrollFunc(scroller, isVertical ? _horizontal : _vertical)();

        if (pinSpacing) {
          spacerState = [pinSpacing + direction.os2, change + otherPinOffset + _px];
          spacerState.t = spacer;
          i = pinSpacing === _padding ? _getSize(pin, direction) + change + otherPinOffset : 0;
          i && spacerState.push(direction.d, i + _px); // for box-sizing: border-box (must include padding).

          _setState(spacerState);

          useFixedPosition && self.scroll(prevScroll);
        }

        if (useFixedPosition) {
          override = {
            top: bounds.top + (isVertical ? scroll - start : oppositeScroll) + _px,
            left: bounds.left + (isVertical ? oppositeScroll : scroll - start) + _px,
            boxSizing: "border-box",
            position: "fixed"
          };
          override[_width] = override["max" + _Width] = Math.ceil(bounds.width) + _px;
          override[_height] = override["max" + _Height] = Math.ceil(bounds.height) + _px;
          override[_margin] = override[_margin + _Top] = override[_margin + _Right] = override[_margin + _Bottom] = override[_margin + _Left] = "0";
          override[_padding] = cs[_padding];
          override[_padding + _Top] = cs[_padding + _Top];
          override[_padding + _Right] = cs[_padding + _Right];
          override[_padding + _Bottom] = cs[_padding + _Bottom];
          override[_padding + _Left] = cs[_padding + _Left];
          pinActiveState = _copyState(pinOriginalState, override, pinReparent);
        }

        if (animation) {
          // the animation might be affecting the transform, so we must jump to the end, check the value, and compensate accordingly. Otherwise, when it becomes unpinned, the pinSetter() will get set to a value that doesn't include whatever the animation did.
          initted = animation._initted; // if not, we must invalidate() after this step, otherwise it could lock in starting values prematurely.

          _suppressOverwrites(1);

          animation.progress(1, true);
          pinChange = pinGetter(direction.a) - pinStart + change + otherPinOffset;
          change !== pinChange && pinActiveState.splice(pinActiveState.length - 2, 2); // transform is the last property/value set in the state Array. Since the animation is controlling that, we should omit it.

          animation.progress(0, true);
          initted || animation.invalidate();

          _suppressOverwrites(0);
        } else {
          pinChange = change;
        }
      } else if (trigger && self.scroll()) {
        // it may be INSIDE a pinned element, so walk up the tree and look for any elements with _pinOffset to compensate because anything with pinSpacing that's already scrolled would throw off the measurements in getBoundingClientRect()
        bounds = trigger.parentNode;

        while (bounds && bounds !== _body$3) {
          if (bounds._pinOffset) {
            start -= bounds._pinOffset;
            end -= bounds._pinOffset;
          }

          bounds = bounds.parentNode;
        }
      }

      for (i = 0; i < triggerIndex; i++) {
        // make sure we revert from first to last to make sure things reach their end state properly
        curTrigger = _triggers[i].pin;
        curTrigger && (curTrigger === trigger || curTrigger === pin) && _triggers[i].revert(false);
      }

      self.start = start;
      self.end = end;
      scroll1 = scroll2 = self.scroll(); // reset velocity

      scroll1 < prevScroll && self.scroll(prevScroll);
      self.revert(false);
      _refreshing = 0;
      animation && isToggle && animation._initted && animation.progress(prevAnimProgress, true).render(animation.time(), true, true); // must force a re-render because if saveStyles() was used on the target(s), the styles could have been wiped out during the refresh().

      if (prevProgress !== self.progress) {
        // ensures that the direction is set properly (when refreshing, progress is set back to 0 initially, then back again to wherever it needs to be) and that callbacks are triggered.
        scrubTween && animation.totalProgress(prevProgress, true); // to avoid issues where animation callbacks like onStart aren't triggered.

        self.progress = prevProgress;
        self.update();
      }

      pin && pinSpacing && (spacer._pinOffset = Math.round(self.progress * pinChange));
      onRefresh && onRefresh(self);
    };

    self.getVelocity = function () {
      return (self.scroll() - scroll2) / (_getTime$1() - _time2) * 1000 || 0;
    };

    self.update = function (reset, recordVelocity) {
      var scroll = self.scroll(),
          p = reset ? 0 : (scroll - start) / change,
          clipped = p < 0 ? 0 : p > 1 ? 1 : p || 0,
          prevProgress = self.progress,
          isActive,
          wasActive,
          toggleState,
          action,
          stateChanged,
          toggled;

      if (recordVelocity) {
        scroll2 = scroll1;
        scroll1 = scroll;

        if (snap) {
          snap2 = snap1;
          snap1 = animation && !isToggle ? animation.totalProgress() : clipped;
        }
      } // anticipate the pinning a few ticks ahead of time based on velocity to avoid a visual glitch due to the fact that most browsers do scrolling on a separate thread (not synced with requestAnimationFrame).


      anticipatePin && !clipped && pin && !_refreshing && !_startup && _lastScrollTime && start < scroll + (scroll - scroll2) / (_getTime$1() - _time2) * anticipatePin && (clipped = 0.0001);

      if (clipped !== prevProgress && self.enabled) {
        isActive = self.isActive = !!clipped && clipped < 1;
        wasActive = !!prevProgress && prevProgress < 1;
        toggled = isActive !== wasActive;
        stateChanged = toggled || !!clipped !== !!prevProgress; // could go from start all the way to end, thus it didn't toggle but it did change state in a sense (may need to fire a callback)

        self.direction = clipped > prevProgress ? 1 : -1;
        self.progress = clipped;

        if (!isToggle) {
          if (scrubTween && !_refreshing && !_startup) {
            scrubTween.vars.totalProgress = clipped;
            scrubTween.invalidate().restart();
          } else if (animation) {
            animation.totalProgress(clipped, !!_refreshing);
          }
        }

        if (pin) {
          reset && pinSpacing && (spacer.style[pinSpacing + direction.os2] = spacingStart);

          if (!useFixedPosition) {
            pinSetter(pinStart + pinChange * clipped);
          } else if (stateChanged) {
            action = !reset && clipped > prevProgress && end + 1 > scroll && scroll + 1 >= _maxScroll(scroller, direction); // if it's at the VERY end of the page, don't switch away from position: fixed because it's pointless and it could cause a brief flash when the user scrolls back up (when it gets pinned again)

            if (pinReparent) {
              if (!reset && (isActive || action)) {
                var bounds = _getBounds$1(pin, true),
                    _offset = scroll - start;

                _reparent(pin, _body$3, bounds.top + (direction === _vertical ? _offset : 0) + _px, bounds.left + (direction === _vertical ? 0 : _offset) + _px);
              } else {
                _reparent(pin, spacer);
              }
            }

            _setState(isActive || action ? pinActiveState : pinState);

            pinChange !== change && clipped < 1 && isActive || pinSetter(pinStart + (clipped === 1 && !action ? pinChange : 0));
          }
        }

        snap && !tweenTo.tween && !_refreshing && !_startup && snapDelayedCall.restart(true);
        toggleClass && (toggled || once && clipped && (clipped < 1 || !_limitCallbacks)) && _toArray$3(toggleClass.targets).forEach(function (el) {
          return el.classList[isActive || once ? "add" : "remove"](toggleClass.className);
        }); // classes could affect positioning, so do it even if reset or refreshing is true.

        onUpdate && !isToggle && !reset && onUpdate(self);

        if (stateChanged && !_refreshing) {
          toggleState = clipped && !prevProgress ? 0 : clipped === 1 ? 1 : prevProgress === 1 ? 2 : 3; // 0 = enter, 1 = leave, 2 = enterBack, 3 = leaveBack (we prioritize the FIRST encounter, thus if you scroll really fast past the onEnter and onLeave in one tick, it'd prioritize onEnter.

          if (isToggle) {
            action = !toggled && toggleActions[toggleState + 1] !== "none" && toggleActions[toggleState + 1] || toggleActions[toggleState]; // if it didn't toggle, that means it shot right past and since we prioritize the "enter" action, we should switch to the "leave" in this case (but only if one is defined)

            if (animation && (action === "complete" || action === "reset" || action in animation)) {
              if (action === "complete") {
                animation.pause().totalProgress(1);
              } else if (action === "reset") {
                animation.restart(true).pause();
              } else {
                animation[action]();
              }
            }

            onUpdate && onUpdate(self);
          }

          if (toggled || !_limitCallbacks) {
            // on startup, the page could be scrolled and we don't want to fire callbacks that didn't toggle. For example onEnter shouldn't fire if the ScrollTrigger isn't actually entered.
            onToggle && toggled && onToggle(self);
            callbacks[toggleState] && callbacks[toggleState](self);
            once && (clipped === 1 ? self.kill(false, 1) : callbacks[toggleState] = 0); // a callback shouldn't be called again if once is true.

            if (!toggled) {
              // it's possible to go completely past, like from before the start to after the end (or vice-versa) in which case BOTH callbacks should be fired in that order
              toggleState = clipped === 1 ? 1 : 3;
              callbacks[toggleState] && callbacks[toggleState](self);
            }
          }
        } else if (isToggle && onUpdate && !_refreshing) {
          onUpdate(self);
        }
      } // update absolutely-positioned markers (only if the scroller isn't the viewport)


      if (markerEndSetter) {
        markerStartSetter(scroll + (markerStartTrigger._isFlipped ? 1 : 0));
        markerEndSetter(scroll);
      }
    };

    self.enable = function () {
      if (!self.enabled) {
        self.enabled = true;

        _addListener$1(scroller, "resize", _onResize);

        _addListener$1(scroller, "scroll", _onScroll);

        onRefreshInit && _addListener$1(ScrollTrigger, "refreshInit", onRefreshInit);
        !animation || !animation.add ? self.refresh() : gsap$8.delayedCall(0.01, function () {
          return start || end || self.refresh();
        }) && (change = 0.01) && (start = end = 0); // if the animation is a timeline, it may not have been populated yet, so it wouldn't render at the proper place on the first refresh(), thus we should schedule one for the next tick.
      }
    };

    self.disable = function (reset, allowAnimation) {
      if (self.enabled) {
        reset !== false && self.revert();
        self.enabled = self.isActive = false;
        allowAnimation || scrubTween && scrubTween.pause();
        prevScroll = 0;
        pinCache && (pinCache.uncache = 1);
        onRefreshInit && _removeListener$1(ScrollTrigger, "refreshInit", onRefreshInit);

        if (snapDelayedCall) {
          snapDelayedCall.pause();
          tweenTo.tween && tweenTo.tween.kill() && (tweenTo.tween = 0);
        }

        if (!isViewport) {
          var i = _triggers.length;

          while (i--) {
            if (_triggers[i].scroller === scroller && _triggers[i] !== self) {
              return; //don't remove the listeners if there are still other triggers referencing it.
            }
          }

          _removeListener$1(scroller, "resize", _onResize);

          _removeListener$1(scroller, "scroll", _onScroll);
        }
      }
    };

    self.kill = function (revert, allowAnimation) {
      self.disable(revert, allowAnimation);
      id && delete _ids[id];

      var i = _triggers.indexOf(self);

      _triggers.splice(i, 1);

      i === _i && _direction > 0 && _i--; // if we're in the middle of a refresh() or update(), splicing would cause skips in the index, so adjust...

      if (animation) {
        animation.scrollTrigger = null;
        revert && animation.render(-1);
        allowAnimation || animation.kill();
      }

      markerStart && [markerStart, markerEnd, markerStartTrigger, markerEndTrigger].forEach(function (m) {
        return m.parentNode.removeChild(m);
      });

      if (pin) {
        pinCache && (pinCache.uncache = 1);
        i = 0;

        _triggers.forEach(function (t) {
          return t.pin === pin && i++;
        });

        i || (pinCache.spacer = 0); // if there aren't any more ScrollTriggers with the same pin, remove the spacer, otherwise it could be contaminated with old/stale values if the user re-creates a ScrollTrigger for the same element.
      }
    };

    self.enable();
  };

  ScrollTrigger.register = function register(core) {
    if (!_coreInitted$4) {
      gsap$8 = core || _getGSAP$8();

      if (_windowExists$5() && window.document) {
        _win$4 = window;
        _doc$3 = document;
        _docEl$1 = _doc$3.documentElement;
        _body$3 = _doc$3.body;
      }

      if (gsap$8) {
        _toArray$3 = gsap$8.utils.toArray;
        _clamp = gsap$8.utils.clamp;
        _suppressOverwrites = gsap$8.core.suppressOverwrites || _passThrough;
        gsap$8.core.globals("ScrollTrigger", ScrollTrigger); // must register the global manually because in Internet Explorer, functions (classes) don't have a "name" property.

        if (_body$3) {
          _raf = _win$4.requestAnimationFrame || function (f) {
            return setTimeout(f, 16);
          };

          _addListener$1(_win$4, "wheel", _onScroll);

          _root = [_win$4, _doc$3, _docEl$1, _body$3];

          _addListener$1(_doc$3, "scroll", _onScroll); // some browsers (like Chrome), the window stops dispatching scroll events on the window if you scroll really fast, but it's consistent on the document!


          var bodyStyle = _body$3.style,
              border = bodyStyle.borderTop,
              bounds;
          bodyStyle.borderTop = "1px solid #000"; // works around an issue where a margin of a child element could throw off the bounds of the _body, making it seem like there's a margin when there actually isn't. The border ensures that the bounds are accurate.

          bounds = _getBounds$1(_body$3);
          _vertical.m = Math.round(bounds.top + _vertical.sc()) || 0; // accommodate the offset of the <body> caused by margins and/or padding

          _horizontal.m = Math.round(bounds.left + _horizontal.sc()) || 0;
          border ? bodyStyle.borderTop = border : bodyStyle.removeProperty("border-top");
          _syncInterval = setInterval(_sync, 200);
          gsap$8.delayedCall(0.5, function () {
            return _startup = 0;
          });

          _addListener$1(_doc$3, "touchcancel", _passThrough); // some older Android devices intermittently stop dispatching "touchmove" events if we don't listen for "touchcancel" on the document.


          _addListener$1(_body$3, "touchstart", _passThrough); //works around Safari bug: https://greensock.com/forums/topic/21450-draggable-in-iframe-on-mobile-is-buggy/


          _multiListener(_addListener$1, _doc$3, "pointerdown,touchstart,mousedown", function () {
            return _pointerIsDown = 1;
          });

          _multiListener(_addListener$1, _doc$3, "pointerup,touchend,mouseup", function () {
            return _pointerIsDown = 0;
          });

          _transformProp$2 = gsap$8.utils.checkPrefix("transform");

          _stateProps.push(_transformProp$2);

          _coreInitted$4 = _getTime$1();
          _resizeDelay = gsap$8.delayedCall(0.2, _refreshAll).pause();
          _autoRefresh = [_doc$3, "visibilitychange", function () {
            var w = _win$4.innerWidth,
                h = _win$4.innerHeight;

            if (_doc$3.hidden) {
              _prevWidth = w;
              _prevHeight = h;
            } else if (_prevWidth !== w || _prevHeight !== h) {
              _onResize();
            }
          }, _doc$3, "DOMContentLoaded", _refreshAll, _win$4, "load", function () {
            return _lastScrollTime || _refreshAll();
          }, _win$4, "resize", _onResize];

          _iterateAutoRefresh(_addListener$1);
        }
      }
    }

    return _coreInitted$4;
  };

  ScrollTrigger.defaults = function defaults(config) {
    for (var p in config) {
      _defaults[p] = config[p];
    }
  };

  ScrollTrigger.kill = function kill() {
    _enabled = 0;

    _triggers.slice(0).forEach(function (trigger) {
      return trigger.kill(1);
    });
  };

  ScrollTrigger.config = function config(vars) {
    "limitCallbacks" in vars && (_limitCallbacks = !!vars.limitCallbacks);
    var ms = vars.syncInterval;
    ms && clearInterval(_syncInterval) || (_syncInterval = ms) && setInterval(_sync, ms);

    if ("autoRefreshEvents" in vars) {
      _iterateAutoRefresh(_removeListener$1) || _iterateAutoRefresh(_addListener$1, vars.autoRefreshEvents || "none");
      _ignoreResize = (vars.autoRefreshEvents + "").indexOf("resize") === -1;
    }
  };

  ScrollTrigger.scrollerProxy = function scrollerProxy(target, vars) {
    var t = _toArray$3(target)[0],
        i = _scrollers.indexOf(t),
        isViewport = _isViewport(t);

    if (~i) {
      _scrollers.splice(i, isViewport ? 6 : 2);
    }

    isViewport ? _proxies.unshift(_win$4, vars, _body$3, vars, _docEl$1, vars) : _proxies.unshift(t, vars);
  };

  ScrollTrigger.matchMedia = function matchMedia(vars) {
    // _media is populated in the following order: mediaQueryString, onMatch, onUnmatch, isMatched. So if there are two media queries, the Array would have a length of 8
    var mq, p, i, func, result;

    for (p in vars) {
      i = _media.indexOf(p);
      func = vars[p];
      _creatingMedia = p;

      if (p === "all") {
        func();
      } else {
        mq = _win$4.matchMedia(p);

        if (mq) {
          mq.matches && (result = func());

          if (~i) {
            _media[i + 1] = _combineFunc(_media[i + 1], func);
            _media[i + 2] = _combineFunc(_media[i + 2], result);
          } else {
            i = _media.length;

            _media.push(p, func, result);

            mq.addListener ? mq.addListener(_onMediaChange) : mq.addEventListener("change", _onMediaChange);
          }

          _media[i + 3] = mq.matches;
        }
      }

      _creatingMedia = 0;
    }

    return _media;
  };

  ScrollTrigger.clearMatchMedia = function clearMatchMedia(query) {
    query || (_media.length = 0);
    query = _media.indexOf(query);
    query >= 0 && _media.splice(query, 4);
  };

  return ScrollTrigger;
}();
ScrollTrigger.version = "3.6.1";

ScrollTrigger.saveStyles = function (targets) {
  return targets ? _toArray$3(targets).forEach(function (target) {
    if (target && target.style) {
      var i = _savedStyles.indexOf(target);

      i >= 0 && _savedStyles.splice(i, 4);

      _savedStyles.push(target, target.style.cssText, gsap$8.core.getCache(target), _creatingMedia);
    }
  }) : _savedStyles;
};

ScrollTrigger.revert = function (soft, media) {
  return _revertAll(!soft, media);
};

ScrollTrigger.create = function (vars, animation) {
  return new ScrollTrigger(vars, animation);
};

ScrollTrigger.refresh = function (safe) {
  return safe ? _onResize() : _refreshAll(true);
};

ScrollTrigger.update = _updateAll;

ScrollTrigger.maxScroll = function (element, horizontal) {
  return _maxScroll(element, horizontal ? _horizontal : _vertical);
};

ScrollTrigger.getScrollFunc = function (element, horizontal) {
  return _getScrollFunc(_toArray$3(element)[0], horizontal ? _horizontal : _vertical);
};

ScrollTrigger.getById = function (id) {
  return _ids[id];
};

ScrollTrigger.getAll = function () {
  return _triggers.slice(0);
};

ScrollTrigger.isScrolling = function () {
  return !!_lastScrollTime;
};

ScrollTrigger.addEventListener = function (type, callback) {
  var a = _listeners[type] || (_listeners[type] = []);
  ~a.indexOf(callback) || a.push(callback);
};

ScrollTrigger.removeEventListener = function (type, callback) {
  var a = _listeners[type],
      i = a && a.indexOf(callback);
  i >= 0 && a.splice(i, 1);
};

ScrollTrigger.batch = function (targets, vars) {
  var result = [],
      varsCopy = {},
      interval = vars.interval || 0.016,
      batchMax = vars.batchMax || 1e9,
      proxyCallback = function proxyCallback(type, callback) {
    var elements = [],
        triggers = [],
        delay = gsap$8.delayedCall(interval, function () {
      callback(elements, triggers);
      elements = [];
      triggers = [];
    }).pause();
    return function (self) {
      elements.length || delay.restart(true);
      elements.push(self.trigger);
      triggers.push(self);
      batchMax <= elements.length && delay.progress(1);
    };
  },
      p;

  for (p in vars) {
    varsCopy[p] = p.substr(0, 2) === "on" && _isFunction$3(vars[p]) && p !== "onRefreshInit" ? proxyCallback(p, vars[p]) : vars[p];
  }

  if (_isFunction$3(batchMax)) {
    batchMax = batchMax();

    _addListener$1(ScrollTrigger, "refresh", function () {
      return batchMax = vars.batchMax();
    });
  }

  _toArray$3(targets).forEach(function (target) {
    var config = {};

    for (p in varsCopy) {
      config[p] = varsCopy[p];
    }

    config.trigger = target;
    result.push(ScrollTrigger.create(config));
  });

  return result;
};

ScrollTrigger.sort = function (func) {
  return _triggers.sort(func || function (a, b) {
    return (a.vars.refreshPriority || 0) * -1e6 + a.start - (b.start + (b.vars.refreshPriority || 0) * -1e6);
  });
};

_getGSAP$8() && gsap$8.registerPlugin(ScrollTrigger);

gsapWithCSS$1.registerPlugin(ScrollTrigger);

/* abstract gsap scrolltrigger usage */
const scrollWatcher = {
  attach: (options, callbacks) => {
    // *** simplify passing in callbacks from instantiator
    const defaultCallbacks = {
      onEnter: callbacks.onEnter || function () {},
      onEnterBack: callbacks.onEnterBack || function () {},
      onLeave: callbacks.onLeave || function () {},
      onLeaveBack: callbacks.onLeaveBack || function () {},
      onUpdate: callbacks.onUpdate || function () {},
    };

    // console.log('/scrollWatcher/ -attach', options, defaultCallbacks);

    ScrollTrigger.create({
      ...options,
      onEnter: () => {
        //console.log('onEnter');
        defaultCallbacks.onEnter();
      },
      onEnterBack: () => {
        //console.log('onEnterBack');
        defaultCallbacks.onEnterBack();
      },
      onLeave: () => {
        //console.log('onLeave');
        defaultCallbacks.onLeave();
      },
      onLeaveBack: () => {
        //console.log('onLeaveBack');
        defaultCallbacks.onLeaveBack();
      },
      onUpdate: (self) => {
        defaultCallbacks.onUpdate(self.progress, self.direction);
      },
    });
  },
};

/*!
 * ScrollToPlugin 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var gsap$7,
    _coreInitted$3,
    _window,
    _docEl,
    _body$2,
    _toArray$2,
    _config,
    _windowExists$4 = function _windowExists() {
  return typeof window !== "undefined";
},
    _getGSAP$7 = function _getGSAP() {
  return gsap$7 || _windowExists$4() && (gsap$7 = window.gsap) && gsap$7.registerPlugin && gsap$7;
},
    _isString$2 = function _isString(value) {
  return typeof value === "string";
},
    _isFunction$2 = function _isFunction(value) {
  return typeof value === "function";
},
    _max = function _max(element, axis) {
  var dim = axis === "x" ? "Width" : "Height",
      scroll = "scroll" + dim,
      client = "client" + dim;
  return element === _window || element === _docEl || element === _body$2 ? Math.max(_docEl[scroll], _body$2[scroll]) - (_window["inner" + dim] || _docEl[client] || _body$2[client]) : element[scroll] - element["offset" + dim];
},
    _buildGetter = function _buildGetter(e, axis) {
  //pass in an element and an axis ("x" or "y") and it'll return a getter function for the scroll position of that element (like scrollTop or scrollLeft, although if the element is the window, it'll use the pageXOffset/pageYOffset or the documentElement's scrollTop/scrollLeft or document.body's. Basically this streamlines things and makes a very fast getter across browsers.
  var p = "scroll" + (axis === "x" ? "Left" : "Top");

  if (e === _window) {
    if (e.pageXOffset != null) {
      p = "page" + axis.toUpperCase() + "Offset";
    } else {
      e = _docEl[p] != null ? _docEl : _body$2;
    }
  }

  return function () {
    return e[p];
  };
},
    _clean = function _clean(value, index, target, targets) {
  _isFunction$2(value) && (value = value(index, target, targets));

  if (typeof value !== "object") {
    return _isString$2(value) && value !== "max" && value.charAt(1) !== "=" ? {
      x: value,
      y: value
    } : {
      y: value
    }; //if we don't receive an object as the parameter, assume the user intends "y".
  } else if (value.nodeType) {
    return {
      y: value,
      x: value
    };
  } else {
    var result = {},
        p;

    for (p in value) {
      result[p] = p !== "onAutoKill" && _isFunction$2(value[p]) ? value[p](index, target, targets) : value[p];
    }

    return result;
  }
},
    _getOffset = function _getOffset(element, container) {
  element = _toArray$2(element)[0];

  if (!element || !element.getBoundingClientRect) {
    return console.warn("scrollTo target doesn't exist. Using 0") || {
      x: 0,
      y: 0
    };
  }

  var rect = element.getBoundingClientRect(),
      isRoot = !container || container === _window || container === _body$2,
      cRect = isRoot ? {
    top: _docEl.clientTop - (_window.pageYOffset || _docEl.scrollTop || _body$2.scrollTop || 0),
    left: _docEl.clientLeft - (_window.pageXOffset || _docEl.scrollLeft || _body$2.scrollLeft || 0)
  } : container.getBoundingClientRect(),
      offsets = {
    x: rect.left - cRect.left,
    y: rect.top - cRect.top
  };

  if (!isRoot && container) {
    //only add the current scroll position if it's not the window/body.
    offsets.x += _buildGetter(container, "x")();
    offsets.y += _buildGetter(container, "y")();
  }

  return offsets;
},
    _parseVal = function _parseVal(value, target, axis, currentVal, offset) {
  return !isNaN(value) && typeof value !== "object" ? parseFloat(value) - offset : _isString$2(value) && value.charAt(1) === "=" ? parseFloat(value.substr(2)) * (value.charAt(0) === "-" ? -1 : 1) + currentVal - offset : value === "max" ? _max(target, axis) - offset : Math.min(_max(target, axis), _getOffset(value, target)[axis] - offset);
},
    _initCore$5 = function _initCore() {
  gsap$7 = _getGSAP$7();

  if (_windowExists$4() && gsap$7 && document.body) {
    _window = window;
    _body$2 = document.body;
    _docEl = document.documentElement;
    _toArray$2 = gsap$7.utils.toArray;
    gsap$7.config({
      autoKillThreshold: 7
    });
    _config = gsap$7.config();
    _coreInitted$3 = 1;
  }
};

var ScrollToPlugin = {
  version: "3.6.1",
  name: "scrollTo",
  rawVars: 1,
  register: function register(core) {
    gsap$7 = core;

    _initCore$5();
  },
  init: function init(target, value, tween, index, targets) {
    _coreInitted$3 || _initCore$5();
    var data = this;
    data.isWin = target === _window;
    data.target = target;
    data.tween = tween;
    value = _clean(value, index, target, targets);
    data.vars = value;
    data.autoKill = !!value.autoKill;
    data.getX = _buildGetter(target, "x");
    data.getY = _buildGetter(target, "y");
    data.x = data.xPrev = data.getX();
    data.y = data.yPrev = data.getY();

    if (value.x != null) {
      data.add(data, "x", data.x, _parseVal(value.x, target, "x", data.x, value.offsetX || 0), index, targets);

      data._props.push("scrollTo_x");
    } else {
      data.skipX = 1;
    }

    if (value.y != null) {
      data.add(data, "y", data.y, _parseVal(value.y, target, "y", data.y, value.offsetY || 0), index, targets);

      data._props.push("scrollTo_y");
    } else {
      data.skipY = 1;
    }
  },
  render: function render(ratio, data) {
    var pt = data._pt,
        target = data.target,
        tween = data.tween,
        autoKill = data.autoKill,
        xPrev = data.xPrev,
        yPrev = data.yPrev,
        isWin = data.isWin,
        x,
        y,
        yDif,
        xDif,
        threshold;

    while (pt) {
      pt.r(ratio, pt.d);
      pt = pt._next;
    }

    x = isWin || !data.skipX ? data.getX() : xPrev;
    y = isWin || !data.skipY ? data.getY() : yPrev;
    yDif = y - yPrev;
    xDif = x - xPrev;
    threshold = _config.autoKillThreshold;

    if (data.x < 0) {
      //can't scroll to a position less than 0! Might happen if someone uses a Back.easeOut or Elastic.easeOut when scrolling back to the top of the page (for example)
      data.x = 0;
    }

    if (data.y < 0) {
      data.y = 0;
    }

    if (autoKill) {
      //note: iOS has a bug that throws off the scroll by several pixels, so we need to check if it's within 7 pixels of the previous one that we set instead of just looking for an exact match.
      if (!data.skipX && (xDif > threshold || xDif < -threshold) && x < _max(target, "x")) {
        data.skipX = 1; //if the user scrolls separately, we should stop tweening!
      }

      if (!data.skipY && (yDif > threshold || yDif < -threshold) && y < _max(target, "y")) {
        data.skipY = 1; //if the user scrolls separately, we should stop tweening!
      }

      if (data.skipX && data.skipY) {
        tween.kill();
        data.vars.onAutoKill && data.vars.onAutoKill.apply(tween, data.vars.onAutoKillParams || []);
      }
    }

    if (isWin) {
      _window.scrollTo(!data.skipX ? data.x : x, !data.skipY ? data.y : y);
    } else {
      data.skipY || (target.scrollTop = data.y);
      data.skipX || (target.scrollLeft = data.x);
    }

    data.xPrev = data.x;
    data.yPrev = data.y;
  },
  kill: function kill(property) {
    var both = property === "scrollTo";

    if (both || property === "scrollTo_x") {
      this.skipX = 1;
    }

    if (both || property === "scrollTo_y") {
      this.skipY = 1;
    }
  }
};
ScrollToPlugin.max = _max;
ScrollToPlugin.getOffset = _getOffset;
ScrollToPlugin.buildGetter = _buildGetter;
_getGSAP$7() && gsap$7.registerPlugin(ScrollToPlugin);

const cookieManager = {
  
  state: {
    hasCookie: false,
  },
  
  strings: {
    alchemyRouteOrigin: 'alchemyRouteOrigin',
    alchemyRouteCurrent: 'alchemyRouteCurrent',
  },
  
  props: {
    routeDelimiter: '+',
  },
  
  init: () => {
    console.log('/cookieManager/ -init');
    
    cookieManager.state.hasCookie = !!document.cookie.length;
    
    if (!cookieManager.state.hasCookie) {
      // *** could initialise cookie here, but this util is late to the party
      
      // *** gracefully break public methods, returns 'undefined'
      // cookieManager.setRoute = () => console.warn('/cookieManager/ -setRoute INVALIDATED');
      // cookieManager.getRouteOrigin = () => console.warn('/cookieManager/ -getRouteOrigin INVALIDATED');
      return;
    }
  },
  
  set: (key, value, expiration = 0, path = '/') => {
    // console.log('/cookieManager/ -set', key, value);
    document.cookie = `${key}=${value};expires=${expiration};path=${path}`;
  },
  
  get: (key) => {
    
    if (document.cookie.indexOf(key) === -1) {
      console.log('/cookieManager/ -get --create', key);
      cookieManager.set(key, '');
    }
    
    const parsed = general.attributeParser(document.cookie, null, 'cookie');
    return parsed[key] || null;
  },
  
  // *** common macro type things
  setRoute: () => {

    // *** for testing only, clear cookie
    // cookieManager.set(cookieManager.strings.alchemyRouteOrigin, '', 0); // TEST ONLY REMOVE
    // return;
    
    
    let origString = cookieManager.get(cookieManager.strings.alchemyRouteOrigin) || '?';
    let origPaths = origString.split(cookieManager.props.routeDelimiter);
  
    console.log('/cookieManager/ -setRoute AAA', origPaths);
  
    const currentPath = window.location.pathname;
    const lastPath = origPaths[origPaths.length - 1];
  
    if (lastPath === currentPath) {
      console.log('/cookieManager/ -setRoute SAME, QUIT');
      return;
    }
  
  
    // TODO cleanup, keep length minimum
    if (origPaths.length > 2) {
      
      console.log('/cookieManager/ -setRoute SHOULD CLIP');
      // return;
      origPaths.shift();
      origString = origPaths.join('===');
      console.log('/cookieManager/ -setRoute CLIP B', origPaths, origString);
    }
    
    // origPaths = origPaths.splice(0, origPaths.length - 3);
    // console.log('/cookieManager/ -setRoute TEST', origPaths);
    
    cookieManager.set(cookieManager.strings.alchemyRouteOrigin,
        `${origString}${cookieManager.props.routeDelimiter}${window.location.pathname}`);
  
    // *** debug
    let s = cookieManager.get(cookieManager.strings.alchemyRouteOrigin);
    let p = s.split(cookieManager.props.routeDelimiter);
    console.log('/cookieManager/ -setRoute  RESULT', p);
  },
  
  getRouteOrigin: () => {
    const newString = cookieManager.get(cookieManager.strings.alchemyRouteOrigin);
    const newPaths = newString.split(cookieManager.props.routeDelimiter);
    // return (newPaths.length > 1) ? `${window.location.origin}${newPaths[newPaths.length - 2]}` : null;
    return (newPaths.length > 1) ? `${newPaths[newPaths.length - 2]}` : null;
  }
};

const pageState = {
  
  types: {
    popoverType: 'popoverType',
    popoverLaunch: 'popoverLaunch',
    referrer: 'referrer',
  },
  
  props: {
    signal: null,
  },
  
  state: {
    referrer: null, // *** set via @data-page-referrer on body, used by sticky nav for back button restoration
  },
  
  init: (signal) => {
    pageState.props.signal = signal;
  },
  
  examine: (props) => {
    if (!props) return;
    
    Object.keys(props).map((key) => {
      switch (key) {
        case pageState.types.popoverLaunch:
          pageState.handlePopOver(props);
          break;
  
        case pageState.types.referrer:
          pageState.state[pageState.types.referrer] = props[pageState.types.referrer];
          //console.log('/pageState/ -setting referrer:', pageState.state);
          break;
      }
    });
  },
  
  handlePopOver: (options) => {
    const {popoverType, popoverLaunch} = options;
    
    if (popoverLaunch === 'onload') {
      pageState.dispatch(config.eventNames.LAUNCH_POPOVER, {
        type: popoverType,
        ...options
        // ...
      });
    }
  },
  
  dispatch: (eventName, payload) => {
    // console.log('/pageState/ -dispatch', eventName, payload);
    pageState.props.signal.emit(eventName, payload);
  }
  
};

gsapWithCSS$1.registerPlugin(ScrollToPlugin);

const StickyNav = (options) => {
  const state = {
    anchors: null,
    currentIndex: 0,
    previousIndex: 0,
    previousRepositionIndex: 0, // *** for horizontal tracking
    isClickAction: false, // *** when true, prevent scroll position detection from retriggering 'update'
    isScrolling: false,
    isUserHovering: false,
    initialHash: null,
    currentUpdateOrigin: null, // *** null || observer || deeplink || tabswitcher, origin of thing that ultimately causes a hash change

    shouldScrollToDeeplinkOnPageload: true, // *** enable/disable scrolling to deeplink

    // *** ding ding. deprecated this for now in favour of @data-page-referrer
    routeInspector: {
      initialHistoryLength: null, // *** history.length on pageload
      manualUpdateHistoryDiff: 0, // *** capture weird jump in length on first manual update - chrome, safari, ff = +3, ios = 0. fkw :(
      hasTriggeredManualUpdate: false, // *** i.e. 'is first run'
      localHistory: [], // *** this routine's store, not browser histroy
    },
  };

  const props = {
    classes: {
      viewport: 'viewport__inner',
      inner: 'sticky-nav__inner',
      isAttached: 'sticky-nav--is-attached',
      offsetAnchor: 'offset-anchor',
      hasOffsetAnchor: 'alchemy--has-offset-anchor',
      arrowButtons: 'sticky-nav__arrow-buttons',
      arrowButtonGroupIsActive: 'sticky-nav__arrow-buttons--is-active',
      arrowButtonPrev: 'arrow-button--prev',
      arrowButtonNext: 'arrow-button--next',
      arrowButtonIsActive: 'arrow-button--is-active',
      isEnabled: 'sticky-nav--is-enabled',

      tabSwitcherInner: 'tab-switcher-control__inner',
    },
    tabSwitcher: null, // *** nested behaviour in this component
    arrowButtonScrollValueDivisor: 1, // *** higher value = lower scroll change. 1 scrolls to either end, 2 halfway etc
    tempHashSuffix: '__temp',
  };

  let els = {};

  const updateRouteInspectorState = (type, hash) => {
    // console.log('/StickyNav/ -updateRouteInspectorState', type, hash);

    const add = (value) => {
      // console.log('/StickyNav/ -add', value);
      // *** defense if required
      state.routeInspector.localHistory.push(value);
    };

    //console.log('type here...', type);

    if (type === 'onPageLoad') {
      if (window.location.hash) {
        add(window.location.hash);
      }
    }

    if (type === 'manual') {
      add(window.location.hash);
      if (!state.routeInspector.hasTriggeredManualUpdate) {
        state.routeInspector.manualUpdateHistoryDiff =
          window.history.length - state.routeInspector.initialHistoryLength;
        state.routeInspector.hasTriggeredManualUpdate = true;
      }
    }

    if (type === 'exit') {
      if (!pageState.state.referrer) {
        console.warn(
          '/StickyNav/ -updateRouteInspectorState --referrer not set, staying put'
        );
        return;
      }
      console.warn(
        '/StickyNav/ -updateRouteInspectorState --should exit to:',
        pageState.state.referrer
      );
      //console.log('exiting.....');

      // return;
      window.location.replace(pageState.state.referrer);
    }

    // *** debugging
    /*    console.log('\n/StickyNav/ -updateRouteInspectorState +++++++++++++++++++++++++++++++++++');
    console.log('/StickyNav/ -updateRouteInspectorState --state.routeInspector.localHistory', state.routeInspector.localHistory);
    console.log('/StickyNav/ -updateRouteInspectorState --startup', state.routeInspector.initialHistoryLength);
    console.log('/StickyNav/ -updateRouteInspectorState --current', window.history.length);
    console.log('/StickyNav/ -updateRouteInspectorState --app', state.routeInspector.localHistory.length);
    console.warn('/StickyNav/ -updateRouteInspectorState --DIFF:', diff);
    console.log('/StickyNav/ -updateRouteInspectorState +++++++++++++++++++++++++++++++++++\n');*/
  };

  const addRouteListener = () => {
    window.onpopstate = (e) => {
      // console.log('/StickyNav/ -onpopstate', window.history.length, pageState.state);

      //   console.log('state.isScrolling', state.isScrolling);
      //   console.log('state.currentUpdateOrigin', state.currentUpdateOrigin);
      //   console.log('state.previousIndex', state.previousIndex);
      //   console.log(
      //     'state.previousRepositionIndex',
      //     state.previousRepositionIndex
      //   );

      //   // *** this condition is only true on page load
      //   if (state.previousIndex === state.previousRepositionIndex) return;

      // *** infers back button

      //   console.log('state.isScrolling', state.isScrolling);
      //   console.log('state.currentUpdateOrigin', state.currentUpdateOrigin);

      if (!state.isScrolling && state.currentUpdateOrigin === null) {
        updateRouteInspectorState('exit');
      } else {
        updateRouteInspectorState('manual');
      }

      //console.log('/StickyNav/ -onpopstate', state.currentUpdateOrigin, e);

      state.currentUpdateOrigin = null;
    };
  };

  const init = () => {
    els = { ...options.els };
    captureDeepLinkHash();

    state.anchors = getAnchors();

    state.routeInspector.initialHistoryLength = window.history.length;
    updateRouteInspectorState('onPageLoad');
    addRouteListener();

    //console.log('/StickyNav/ -init ANCHORS', state.anchors);

    if (state.anchors.length > 0) {
      //console.log('/StickyNav/ -init SHOULD ENABLE');
      els.parentEl.classList.add(props.classes.isEnabled);
    }

    // *** workaround to detect end of smooth scrolling. delay may need adjustment
    // @ja leave at 50 to avoid problems with manual scrolling and tracking
    const onScrollComplete = general.debounce(() => {
      // @ja tiny timeout here fixes bug where scroller gets confused when going
      // across really long portion of page, gets confusedwhen stops and jumps back
      setTimeout(() => {
        state.isScrolling = false;
        state.isClickAction = false;
      }, 200);
    }, 50);

    // *** watch body scrolling
    scrollWatcher.attach(
      {
        trigger: document.body,
      },
      {
        onUpdate: (a) => {
          state.isScrolling = true;
          onScrollComplete();
        },
      }
    );

    // *** watch scroll position of stickynav for attach/detach
    scrollWatcher.attach(
      {
        trigger: options.els.parentEl,
        start: 'top',
        end: 'bottom',
      },
      {
        onEnter: () => attach(true),
        onLeaveBack: () => attach(false),
      }
    );

    // *** get tabswitchercontrol behaviour after it has been cooked
    options.signal.on(config.eventNames.BEHAVIOUR_ADDED, (payload) => {
      els.tabSwitcherEl = els.parentEl.querySelector(
        '[data-behaviour="tab-switcher-control"]'
      );
      const uid = els.tabSwitcherEl.getAttribute('data-behaviour-uid');
      if (payload.uid === uid) {
        els.el.appendChild(els.tabSwitcherEl); // *** can't actually nest behaviours, so shift el after instantiation
        const instance = options.getBehaviourByUid(uid); // *** get behaviour by uid
        props.tabSwitcher = instance.Behaviour(instance.options);
        initTabSwitcher();
        initArrowButtons();
      }
    });

    // *** standard behaviour channel events
    options.signal.on(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, (payload) => {
      if (!utils.eventFilter(options, payload)) return;
      actionRunner(payload);
    });
    // listen for scroll signal from another script
    // if it comes, treat it like a deeplink
    options.signal.on(config.eventNames.SCROLL_ANCHOR, (payload) => {
      if (payload.id) {
        state.initialHash = payload.id;
        scrollToDeepLink();
      }
    });
    // listen for signal to remove a particulr id/item from nav
    options.signal.on(config.eventNames.STICKYNAV_REMOVE_ITEM, (payload) => {
      //console.log(payload);
      if (payload && payload.id) {
        removeAnchor(payload.id);
      }
    });
  };

  const captureDeepLinkHash = () => {
    if (window.location.hash) {
      // lets capture the initial hash if its a deep link
      state.initialHash = window.location.hash.replace('#', '');
    }
  };

  const removeAnchor = (id) => {
    let itemIndex;
    state.anchors.map((item, index) => {
      if (item.id === id) itemIndex = index;
    });
    state.anchors.filter((item) => item.id !== id);
    // now we remove the actual dom element in nav
    els.tabButtons.map((item, index) => {
      if (index === itemIndex) {
        item.parentElement.removeChild(item);
      }
    });
  };

  const scrollToDeepLink = () => {
    //console.log('/StickyNav/ -scrollToDeepLink', state.initialHash);
    if (!state.initialHash) return;
    // clear the actual hash, we have it stored in a var now
    state.isScrolling = true;
    if(window.location.hash) {
      window.location.hash = '';
      const url = window.location.href;
      window.location.replace(url);
    }
    
    // here we need to check that the hash actually exists in the anchors
    // if so, we scroll to it as if it were  a normal button click
    let anchorIndex = null;
    state.anchors.map((item, index) => {
      if (item.id === state.initialHash) anchorIndex = item.index;
    });
    if (anchorIndex !== null) {
      setIndex(anchorIndex);
      state.isClickAction = true;
      update({ origin: 'deeplink' });
    }
  };

  // *** process incoming events from bound behaviours
  const actionRunner = (payload) => {
    switch (payload.type) {
      case tabSwitcherActions.TABSWITCHER_NAVIGATION_INTENTION:
        setIndex(payload.data.index);
        state.isClickAction = true;
        update({ origin: 'tabswitcher' });
        break;
    }
  };

  // *** init when tabSwitcher dom ready
  const initTabSwitcher = () => {
    props.tabSwitcher.init();

    els.tabSwitcherInner = els.el.querySelector(
      `.${props.classes.tabSwitcherInner}`
    );

    props.tabSwitcher.build(
      // *** populate with scraped data
      state.anchors.map((item) => {
        const tabData = {
          label: item.label
        };
        if(item.el.dataset.isContactUsStickyNav) {
          tabData['isContactUsBtn'] = true;
          tabData['contactUsUrl'] = item.el.dataset.redirectionUrl;
          
          // added class for contact us btn to provide space behind the contact us button
          els.tabSwitcherInner.classList.add('sticky-nav-right-space');
        }
        return tabData;
      })
    );

    els.tabButtonGroup = els.tabSwitcherEl.querySelector('.tab-button-group');
    els.tabButtons = [...els.tabSwitcherEl.querySelectorAll('.tab-button')];
    els.arrowButtonPrev = els.el.querySelector(
      `.${props.classes.arrowButtonPrev}`
    );
    els.arrowButtonNext = els.el.querySelector(
      `.${props.classes.arrowButtonNext}`
    );

    // NB: Put a timeout on this to prevent instant scrolling
    // we also need to wait for pageload to account for really
    // slow network speeds on mobile
    window.addEventListener('load', () => {
      if (state.initialHash) setTimeout(scrollToDeepLink, 1000);
      setTimeout(trackScrolling, 2000);
    });

    // *** detect enter/leave and set state var, used to prevent horizontal scrolling tracking
    els.tabSwitcherEl.addEventListener('mouseenter', () => {
      state.isUserHovering = true;
    });
    els.tabSwitcherEl.addEventListener('mouseleave', () => {
      state.isUserHovering = false;
    });
  };

  const initArrowButtons = () => {
    els.arrowButtons = els.el.querySelector(`.${props.classes.arrowButtons}`);

    els.arrowButtonPrev.addEventListener('mousedown', () => {
      arrowButtonScroll(false);
      setArrowButtonUiState(false);
    });
    els.arrowButtonNext.addEventListener('mousedown', () => {
      arrowButtonScroll(true);
      setArrowButtonUiState(true);
    });

    setArrowButtonUiState(false);

    // *** show hide arrow buttons
    options.signal.on(config.eventNames.APP_BREAKPOINT_READY, (payload) => {
      if (els.tabSwitcherEl.offsetWidth >= els.tabSwitcherEl.scrollWidth) {
        // console.log('/StickyNav/ -initArrowButtons SHOULD HIDE');
        els.arrowButtons.classList.remove(
          props.classes.arrowButtonGroupIsActive
        );
        setArrowButtonUiState(false); // *** reset
      } else {
        // console.log('/StickyNav/ -initArrowButtons SHOULD SHOW');
        els.arrowButtons.classList.add(props.classes.arrowButtonGroupIsActive);
      }
    });
  };

  const setIndex = (index) => {
    state.previousIndex = state.currentIndex;
    state.currentIndex = index;
  };

  const update = (info) => {
    // *** @as fixes SMIT-3513, allow reposition to top of section on click when already in selected state
    if (state.currentIndex === state.previousIndex) {
      if (state.isClickAction) {
        // window.location.hash = `${state.anchors[state.currentIndex].id}${props.tempHashSuffix}`; // *** allows retrigger, doesn't choke safari
        window.history.pushState(
          null,
          null,
          `#${state.anchors[state.currentIndex].id}${props.tempHashSuffix}`
        );
      }
    }

    // console.log('/StickyNav/ -update', info.origin);
    state.currentUpdateOrigin = info.origin;

    // *** @as SMIT-3472..! changed to pushState which appears to be more reliable, but doesn't auto scroll
    // *** so manually setting scroll (smooth scrolling set on body in css)
    // window.location.hash = state.anchors[state.currentIndex].id;
    window.history.pushState(
      null,
      null,
      `#${state.anchors[state.currentIndex].id}`
    );

    // *** scroll to deeplink?
    const scrollToDeeplink =
      state.currentUpdateOrigin === 'deeplink';

    // *** scroll on tabswitcher action
    if (state.currentUpdateOrigin === 'tabswitcher' || scrollToDeeplink) {
      const el = state.anchors[state.currentIndex].el;
      const offset = els.el.getBoundingClientRect().height; // TODO - possibly account for different offset for item at index 0
      const targetY = el.getBoundingClientRect().top;
      window.scrollTo(0, targetY + window.scrollY - offset);
    }

    // *** update tabswitcher
    utils.dispatcher(options, {
      type: tabSwitcherActions.TABSWITCHER_UPDATING,
      data: { index: state.currentIndex },
    });

    setTimeout(() => {
      state.currentUpdateOrigin = null;
    }, 0);
  };

  const arrowButtonScroll = (isNext) => {
    const w = els.tabSwitcherEl.getBoundingClientRect().width;
    let x = w / props.arrowButtonScrollValueDivisor;
    x = !isNext ? -x : x;

    els.tabSwitcherEl.scrollBy({
      top: 0,
      left: x,
      behavior: 'smooth',
    });
  };

  // *** update highlight state
  const setArrowButtonUiState = (isNext) => {
    if (!isNext) {
      els.arrowButtonPrev.classList.remove(props.classes.arrowButtonIsActive);
      els.arrowButtonNext.classList.add(props.classes.arrowButtonIsActive);
    } else {
      els.arrowButtonPrev.classList.add(props.classes.arrowButtonIsActive);
      els.arrowButtonNext.classList.remove(props.classes.arrowButtonIsActive);
    }
  };

  // *** scrape page for anchor data
  const getAnchors = () => {
    const result = [];
    let count = 0;

    // *** NOTE does not not unique values
    // [...document.body.querySelectorAll('.alchemy')].map((item) => { // *** all alchemy components
    [...document.body.querySelectorAll(`[data-anchor]`)].map((item, index) => {
      // *** @as : any element with @data-anchor
      const vo = {
        el: item,
        id: item.getAttribute('id'),
        label: null,
        offsetAnchor: null,
      };
      if (!vo.id) return; // *** pointless without an id
      vo.label = general.attributeParser(
        item.getAttribute('data-anchor'),
        'StickyNav'
      ).label;
      if (!vo.label) return; // *** also pointless without a label

      // *** create offset anchor points
      vo.offsetAnchor = document.createElement('div');
      vo.offsetAnchor.classList.add(props.classes.offsetAnchor);
      vo.offsetAnchor.classList.add(`${props.classes.offsetAnchor}__${index}`); // *** @as : added indexed classname
      vo.el.prepend(vo.offsetAnchor);
      vo.offsetAnchor.id = vo.id;
      vo.el.removeAttribute('id');
      vo.el.classList.add(props.classes.hasOffsetAnchor);

      vo.index = count; // *** can't rely on map index if item was not added
      count += 1;

      result.push(vo);
      return true;
    });

    return result;
  };

  const trackScrolling = () => {
    const watch = (item) => {
      const observer = new IntersectionObserver(
        (entries) => {
          if (entries[0].isIntersecting) {
            // console.log('/StickyNav/ -=========', entries[0]);
            if (!state.isClickAction) {
              setIndex(item.index);
              update({ origin: 'observer' });
              reposition();
            }
          } else {
            state.currentUpdateOrigin = null; // *** part of state to determine back button action
          }
        },
        {
          root: null, // *** resolves to body
          threshold: 0,
          rootMargin: '0px 0px -100% 0px', // *** triggers at top of viewport
        }
      );

      observer.observe(item.offsetAnchor);
    };

    state.anchors.map((item) => {
      watch(item);
    });
  };

  const reposition = () => {
    // if (state.isUserHovering) return; // FIXIT always true on touch device

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].intersectionRatio >= 1) return;

        entries[0].intersectionRatio;
        const r1 = entries[0].boundingClientRect;
        const r2 = entries[0].intersectionRect;
        const r3 = entries[0].rootBounds;

        const nx = r1.width * 2;

        observer.unobserve(target);
        observer.disconnect();

        const rhsOverlap = r1.left + r1.width > r3.width; // *** is partially visible : true = right, false = left

        if (r1.left < r2.left) {
          scroll(-nx);
          return;
        }

        if (r1.left > r2.left) {
          scroll(nx);
          return;
        }

        if (!rhsOverlap) {
          scroll(-nx);
          return;
        }

        if (rhsOverlap) {
          scroll(nx);
          return;
        }
      },
      {
        root: els.tabSwitcherEl,
        threshold: 0.5,
        rootMargin: '16px',
      }
    );

    const scroll = (x) => {
      if (state.currentIndex === state.previousRepositionIndex) {
        // console.log('/StickyNav/ -scroll --ERROR? defeated...', x, state.currentIndex, state.previousIndex, state.previousRepositionIndex);
        return;
      }

      const url = window.location.href;
      window.location.replace(url);

      state.previousRepositionIndex = state.currentIndex;

      els.tabSwitcherEl.scrollBy({
        top: 0,
        left: x,
        // behavior: 'smooth', // *** looks nice but causes positioning issues - TODO revisit
      });
    };

    const target = els.tabButtons[state.currentIndex];
    if(target) {
      observer.observe(target);
    }
  };

  // *** sticky ui behaviour
  const attach = (bool) => {
    if (bool) {
      els.parentEl.classList.add(props.classes.isAttached);
    } else {
      els.parentEl.classList.remove(props.classes.isAttached);
    }
  };

  return {
    init,
  };
};

/*
@as
'behaviour' implementation of vanilla dropdown used elsewhere
- tbc to replace vanilla version with this
*/

const SelectControl = (options) => {
  let els = {};
  let medicalEducationOptions = [];
  let medicalEducationNonAvailabilityListItems = [];

  const props = {
    classes: {
      selectDropdown: 'select-dropdown',
      selectOption: 'select-option',
    },
  };

  const state = {
    items: [],
  };

  // *** facade for injecting dynamic content, add els and restart
  const inject = (data, index) => {
  
    // *** cleanup
    if (els.el) {
      [...els.el.querySelectorAll('option')].map((item) => {
        item.remove();
      });
    }
    
    init();
    build(data);
    update(index);
  };

  const init = () => {
    els = options.els;
    const rawChildren = [...els.el.querySelectorAll('option')];
    if(els.el.dataset.parentSelectControlName && els.el.dataset.parentSelectControlName === 'MedicalEducation') {
      rawChildren.map((item) => {
        medicalEducationOptions.push(item.innerText);
      });
      if(medicalEducationOptions.length && !medicalEducationNonAvailabilityListItems.length) {
        medicalEducationOptions.map((item) => {
          if(item.toLowerCase() === 'webinars') {
            const noResultsWebinar = document.getElementById('NoResults_Webinar');
            const resultWebinars = document.getElementById('Results_Webinar');
            if(noResultsWebinar && medicalEducationNonAvailabilityListItems.indexOf('webinars') === -1 || !resultWebinars) {
              medicalEducationNonAvailabilityListItems.push('webinars');
            }
          }
          if(item.toLowerCase() === 'courses') {
            const noResultsCourses = document.getElementById('NoResults_Courses');
            const resultCourses = document.getElementById('Results_Courses');
            if(noResultsCourses && medicalEducationNonAvailabilityListItems.indexOf('courses') === -1 || !resultCourses) {
              medicalEducationNonAvailabilityListItems.push('courses');
            }
          }
        });
      }
    }
    scaffold();

    // *** if <option>s present in markup, generate data from them and then discard originals
    if (rawChildren.length > 0) {
      const data = rawChildren.map((item) => {
        const vo = { label: item.innerText };
        item.remove();
        return vo;
      });
      build(data);
    }

    options.signal.on(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, (payload) => {
      if (!utils.eventFilter(options, payload)) return;
      actionRunner(payload);
    });
  };

  const getOptions = () => {
    return options;
  };

  const scaffold = () => {
    if (els.select) return; // *** blocks re invocation when init called twice, i.e. from async data

    els.select = document.createElement('select');
    els.select.classList.add(props.classes.selectDropdown);
    els.el.prepend(els.select);
  };

  const checkDataAvailability = ({label}, educationEle) => {
    let isDataAvailable = true;
    
    if(educationEle && medicalEducationOptions.indexOf(label) !== -1) {
      if(medicalEducationNonAvailabilityListItems.indexOf(label.toLowerCase()) !== -1) {
        isDataAvailable = false;
      }    }
    return isDataAvailable;
  };

  const build = (data) => {
  
    state.items = []; // *** clear old items if rebuilding
    const SkipMedicalEducationDelete = document.getElementById('SkipMedicalEducationDelete');
    const educationEle = document.body.querySelector(`[data-component-name="MedicalEducation"]`);
    let isSkipMedicalEducation = false;
    if(SkipMedicalEducationDelete) {
      isSkipMedicalEducation = SkipMedicalEducationDelete.value === "True" ? true : false;
    }
    
    data.map((item, index) => {
      const isDataAvailable = SkipMedicalEducationDelete && isSkipMedicalEducation ? true : checkDataAvailability(item, educationEle);
      if(isDataAvailable) {
        const option = document.createElement('option');
        option.classList.add(props.classes.selectOption);
        option.setAttribute('value', index);
        option.innerHTML = item.label;
        els.select.appendChild(option);

        state.items.push(option);
      }
    });

    els.select.addEventListener('change', (e) => {
      dispatch({ index: parseInt(e.target.value, 10) });
    });
  };

  const dispatch = (data) => {
    utils.dispatcher(options, {
      type: tabSwitcherActions.TABSWITCHER_NAVIGATION_INTENTION,
      data,
    });
  };

  // *** process incoming events from bound behaviours
  const actionRunner = (payload) => {
    // console.log('/SelectControl/ -actionRunner', payload);
    switch (payload.type) {
      case tabSwitcherActions.TABSWITCHER_CONFIGURATION:
        build(payload.data);
        break;

      case tabSwitcherActions.TABSWITCHER_UPDATING:
        update(payload.data.index);
        break;

      // ***ADDED4TIMELINE: CApture events from carousel updates for timeline
      case tabSwitcherActions.CAROUSEL_UPDATING:
        update(payload.data.index);
        break;
    }
  };

  // *** updates value from other controls
  const update = (index) => {
    if (!state.items.length) return;
    els.select.value = index;
  };

  return {
    init,
    build,
    update,
    inject,
    getOptions,
  };
};

const classes = {
  hasError: 'videoplayer--has-error',
  playing: 'videoplayer--playing',
  paused: 'videoplayer--paused',
  hidden: 'videoplayer--hidden',
  fullscreen: 'videoplayer--fullscreen',
  fauxfullscreen: 'videoplayer--fauxfullscreen',
  cta: 'videoplayer__cta',
  ctaPlay: 'videoplayer__cta--play',
  ctaFull: 'videoplayer__cta--full',
  close: 'videoplayer__close',
  closeIcon: 'sn-icon-close',
  fauxPlay: 'sn-icon-play',
  fauxPause: 'icon-pause',
  titlebox: {
    box: 'videoplayer__titlebox',
    duration: 'videoplayer__duration',
    title: 'videoplayer__title',
  },
  video: {
    iframe: 'videoplayer__iframe',
    html5: 'videoplayer__html5',
    container: 'videoplayer',
    containerInner: 'videoplayer__inner',
  },
  controls: {
    wrapper: 'videoplayer__controls',
    progress: 'videoplayer__progress',
    progressTrack: 'videoplayer__progress-track',
    progressTooltip: 'videoplayer__progress-tooltip',
    buttons: {
      container: 'videoplayer__buttons',
      playPause: 'playbutton',
      playPausePlaying: 'playbutton--playing',
    },
    time: {
      container: 'videoplayer__time',
    },
    volume: {
      container: 'videoplayer__volume',
      iconMute: 'icon-volume-mute',
      iconHigh: 'icon-volume-high',
      iconLow: 'icon-volume-low',
      slider: 'videoplayer__volume-slider',
    },
    fullscreen: {
      container: 'videoplayer__fullscreen',
      iconExpand: 'sn-icon-full-Screen',
      iconShrink: 'sn-icon-exit-full-screen',
    },
  },
};

const TYPE_HTML5 = 'html5';
const TYPE_YOUTUBE = 'youtube';
const TYPE_VIMEO = 'vimeo';
const TYPE_BRIGHTCOVE = 'brightcove';

class Player {
  constructor(type, config) {
    this._els = {
      el: null,
      container: null, // top level container that hosts the video
      containerInner: null, // inner wrapper to wrap video or iframe
      video: null, // html5 video tag with event listeners, etc
      cta: null, // used for faux play button with fauxPlay below
      close: null, // close fullscreen mode button
      fauxPlay: null, // used for faux play button with cta above
      titlebox: {
        box: null,
        duration: null,
        title: null,
        cta: null,
      },
      controls: {
        wrapper: null,
        progress: null,
        progressTrack: null,
        progressTooltip: null,
        buttons: {
          container: null,
          playPause: null,
        },
        time: {
          container: null,
          elapsed: null,
          duration: null,
        },
        volume: {
          container: null,
          icon: null,
          slider: null,
        },
        fullscreen: {
          container: null,
          icon: null,
        },
      },
    };
    this._config = config;
    this._type = type;
    this._player = null;
    this._fullscreen = false;
    this._playing = false;
    this._timer = null;
    this._poster = null;
    this._title = null;
    this._trackingVideoTitle=null;
    this._genericPosterImage =
      '/-/media/project/smithandnephew/examples/genericvideoposterimage.jpg';
    this._playerIndex = null;
    this._host = null;
    this._proxy = false;
    this._src = null;
    /* Youtube */
    this._ytPlayerId = null; // youtube player ID
    this._ytVideoId = null; // youtube video ID
    /* Vimeo */
    this._vimPlayerRefId = null; // reference to Vimeo player instance in DOM
    this._vimVideoId = null; // vimeo video ID
    this._vimAccountId = null; // vimeo hash param for private account videos only
    this._currentTime = 0; // player: for vimeo only
    this._duration = 0; // player: for vimeo only
    this._muted = false; // player: for vimeo only
    this._volume = 1; // player: for vimeo only
    /* Brightcove */
    this._bcPlayerRefId = null; // reference to brightcove player instance in DOM
    this._bcPlayerId = null; // brightcove player ID (from account)
    this._bcVideoId = null; // brightcove video ID (from account)
    this._bcAccountId = null; // brightcove account ID (from account)
    this._bcDurationFix = true; // flag that signals to do a fix for bug in retrieving duration
    // NB we need to bind context to these
    this.onPlayerReady = this.onPlayerReady.bind(this);
    this.keyboardShortcuts = this.keyboardShortcuts.bind(this);
    this.trackingObject = {}; // for tracking ga analytics
    this.videoPlayOption = false;
    this.autoPlay = false;
    this.isCarouselCardVideo = false;
  }

  get playing() {
    return this._playing;
  }

  set playing(state) {
    return (this._playing = state);
  }

  get type() {
    return this._type;
  }

  get player() {
    return this._player;
  }

  set player(player) {
    return (this._player = player);
  }

  get muted() {
    switch (this._type) {
      case TYPE_HTML5: {
        return this._player.muted;
      }
      case TYPE_YOUTUBE: {
        return this._player.isMuted();
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.muted();
      }
      case TYPE_VIMEO: {
        return this._muted;
      }
    }
  }

  set muted(which) {
    switch (this._type) {
      case TYPE_HTML5: {
        return (this._player.muted = which);
      }
      case TYPE_YOUTUBE: {
        if (which === false) {
          return this._player.unMute();
        } else {
          return this._player.mute();
        }
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.muted(which);
      }
      case TYPE_VIMEO: {
        if (which === false) {
          return this._player.setVolume(this._volume);
        } else {
          return this._player.setVolume(0);
        }
      }
    }
  }

  /* returns true/false for player.muted state */
  toggleMute() {
    switch (this.type) {
      case TYPE_HTML5: {
        return (this._player.muted = !this._player.muted);
      }
      case TYPE_YOUTUBE: {
        if (this._player.isMuted()) {
          this._player.unMute();
          return false;
        } else {
          this._player.mute();
          return true;
        }
      }
      case TYPE_BRIGHTCOVE: {
        const toggled = !this._player.muted();
        return this._player.muted(toggled);
      }
      case TYPE_VIMEO: {
        this._muted = !this._muted;
        this.muted = this._muted;
        return this.muted;
      }
    }
  }

  get volume() {
    switch (this._type) {
      case TYPE_HTML5: {
        return this._player.volume;
      }
      case TYPE_YOUTUBE: {
        return this._player.getVolume();
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.volume();
      }
      case TYPE_VIMEO: {
        return this._volume;
      }
    }
  }

  set volume(level) {
    switch (this._type) {
      case TYPE_HTML5: {
        return (this._player.volume = level);
      }
      case TYPE_YOUTUBE: {
        return this._player.setVolume(level);
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.volume(level);
      }
      case TYPE_VIMEO: {
        return this._player.setVolume(level);
      }
    }
  }

  get volumeAmount() {
    switch (this._type) {
      case TYPE_HTML5: {
        return 1;
      }
      case TYPE_YOUTUBE: {
        return 100;
      }
      case TYPE_BRIGHTCOVE: {
        return 1;
      }
      case TYPE_VIMEO: {
        return 1;
      }
    }
  }

  get volumeStepAmount() {
    switch (this._type) {
      case TYPE_HTML5: {
        return 0.01;
      }
      case TYPE_YOUTUBE: {
        return 1;
      }
      case TYPE_BRIGHTCOVE: {
        return 0.01;
      }
      case TYPE_VIMEO: {
        return 0.01;
      }
    }
  }

  get duration() {
    switch (this._type) {
      case TYPE_HTML5: {
        return this._player.duration;
      }
      case TYPE_YOUTUBE: {
        return this._player.getDuration();
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.duration();
      }
      case TYPE_VIMEO: {
        return this._duration;
      }
    }
  }

  get currentTime() {
    switch (this._type) {
      case TYPE_HTML5: {
        return this._player.currentTime || 0;
      }
      case TYPE_YOUTUBE: {
        return this._player.getCurrentTime();
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.currentTime();
      }
      case TYPE_VIMEO: {
        return this._currentTime;
      }
    }
  }

  set currentTime(secs) {
    // set both locally and on vimeo player
    this._currentTime = secs;
  }

  get ended() {
    switch (this._type) {
      case TYPE_HTML5: {
        return this._player.ended;
      }
      case TYPE_YOUTUBE: {
        return this._player.getPlayerState() === 0;
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.ended();
      }
      case TYPE_VIMEO: {
        return false;
      }
    }
  }

  // methods
  play() {
    switch (this._type) {
      case TYPE_HTML5: {
        return this._player.play();
      }
      case TYPE_YOUTUBE: {
        return this._player.playVideo();
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.play();
      }
      case TYPE_VIMEO: {
        return this._player.play();
      }
    }
  }

  pause() {
    switch (this._type) {
      case TYPE_HTML5: {
        return this._player.pause();
      }
      case TYPE_YOUTUBE: {
        return this._player.pauseVideo();
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.pause();
      }
      case TYPE_VIMEO: {
        return this._player.pause();
      }
    }
  }

  seek(secs) {
    switch (this._type) {
      case TYPE_HTML5: {
        return (this._player.currentTime = secs);
      }
      case TYPE_YOUTUBE: {
        return this._player.seekTo(secs, true);
      }
      case TYPE_BRIGHTCOVE: {
        return this._player.currentTime(secs);
      }
      case TYPE_VIMEO: {
        this._currentTime = secs;
        return this._player.setCurrentTime(secs);
      }
    }
  }

  setup(videoObject, playerIndex, options) {
    this.options = options;
    this._playerIndex = playerIndex;
    if (videoObject.host) this._host = videoObject.host;
    if (videoObject.poster) this._poster = videoObject.poster;
    if (videoObject.title) this._title = videoObject.title;
    if (videoObject.videoTitle) this._trackingVideoTitle = videoObject.videoTitle;
    if (videoObject.ctaTitle) this._ctaTitle = videoObject.ctaTitle;
    if (videoObject.vimVideoId) this._vimVideoId = videoObject.vimVideoId;
    if (videoObject.vimAccountId)
      this._vimAccountId = videoObject.vimAccountId.replace('%', '');
    if (videoObject.ytVideoId) this._ytVideoId = videoObject.ytVideoId;
    if (videoObject.bcPlayerId) this._bcPlayerId = videoObject.bcPlayerId;
    if (videoObject.bcAccountId) this._bcAccountId = videoObject.bcAccountId;
    if (videoObject.bcVideoId) this._bcVideoId = videoObject.bcVideoId;
    if (videoObject.src) this._src = videoObject.src;
    if (videoObject.proxy) this._proxy = videoObject.proxy; // if this is set we are playing a thumb through a host element on the page
    if (!videoObject.host) {
      // if this is a fullscreen player, lets first make sure there isnt alreay a fullscreen player present
      const leftoverPlayers = [
        ...document.querySelectorAll('.videoplayer--fullscreen'),
      ];
      if (leftoverPlayers.length) return;
    } 
    if (videoObject.carouselVideoTimer) this.isCarouselCardVideo = true;
    if(videoObject.videoPlayOption === 'True') this.videoPlayOption = true;
    if(videoObject.autoPlay === 'True') this.autoPlay = true;
    this.buildTrackingObject();
    this.prepareContainer(this._host, this._proxy);
    this.scaffold(videoObject);
    this.listen();
  }

  buildTrackingObject() {
    this.trackingObject['event'] = null; // set this later
    this.trackingObject['video_provider'] = this._type;
    this.trackingObject['video_url'] = null; // set this later
    this.trackingObject['video_title'] = this._trackingVideoTitle ? this._trackingVideoTitle : '';
  }

  doTrackingEvent(event) {
    this.trackingObject['event'] = event;
    const data = { ...this.trackingObject };
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push(data);
  }

  getTrackingVideoType() {
    if (!this._host) return 'thumbnail video';
    if (this._host && this._proxy) {
      return 'carousel video';
    } else {
      return 'in page video';
    }
  }

  prepareContainer(elementID, proxy) {
    // if swapping host player src, we must kill previous version first
    if (proxy) {
      this.killHostPlayer(elementID);
      // give focus to scroll to container on thumb click on carousel
      this._els.container.focus();
      return;
    }
    if (elementID) {
      this._els.container = document.getElementById(elementID);
    } else {
      this._els.container = document.createElement('div');
    }
  
    this._els.container.classList.remove(`${classes.hasError}`); // *** clear error
  
    // add paused class to begin..
    this._els.container.classList.add(classes.video.container);
    this._els.container.classList.add(classes.paused);
    // create an innner wrapper for fullscreen mode
    this._els.containerInner = document.createElement('div');
    this._els.containerInner.classList.add(classes.video.containerInner);
    // create a close button for fullscreen mode
    this._els.close = document.createElement('div');
    this._els.close.classList.add(classes.close);
    const button = document.createElement('span');
    button.classList.add(classes.closeIcon);
    this._els.close.appendChild(button);
    // and append to main container..
    this._els.container.appendChild(this._els.containerInner);
    // and append to main container..
    this._els.container.appendChild(this._els.close);
    // now write index to dom element so we can use as a reference later
    this._els.container.dataset.videoindex = this._playerIndex;
    // if we are not attaching to an existing container then we will attach to document body when played
    // nb we will add a hidden class first, so player only display fullscreen when thumb is clicked and
    // will disappear when exiting fullscreen
    if (!elementID) {
      this._els.container.classList.add(classes.hidden, classes.fullscreen);
      //this._els.container.classList.add(classes.fullscreen);
      document.body.appendChild(this._els.container);
      // this.broadcastThumbClicked();
      this.broadcastPlaying();
    }
    // make it able to accept focus for more targeted keyboard listeners
    this._els.container.setAttribute('tabIndex', `5${this._playerIndex}`);
  }

  scaffold(videoObj) {
    switch (this._type) {
      case TYPE_HTML5:
        this.scaffoldHtml5(videoObj);
        this.scaffoldControls();
        this.initControlEvents();
        this.initVideoEvents();
        this.initScrollWatch();
        // if we are not attached to a host element then lets go fullscreen now
        if (!this._host) this.makeFullScreen();
        break;
      case TYPE_YOUTUBE:
        this.scaffoldYoutube();
        break;
      case TYPE_VIMEO:
        this.scaffoldVimeo();
        break;
      case TYPE_BRIGHTCOVE:
        this.scaffoldBrightCove();
        break;
    }
  }

  // create video markup
  scaffoldHtml5(videoObj) {
    this._els.video = document.createElement('video');
    // i guess no point in appending to dom if we cant play the video ?
    if (this._els.video.canPlayType('video/mp4')) {
      // set src, type and attributes..
      this._els.video.setAttribute('src', videoObj.src);
      this._els.video.setAttribute('type', 'video/mp4');
      this._els.video.setAttribute('width', '100%');
      this._els.video.setAttribute('preload', 'metadata');
      this._els.video.setAttribute('playsinline', 1);
      // commenting this code to hide poster on video because already that poster image rendering as background image.
      // if (videoObj.poster) {
        //this._els.video.setAttribute('poster', videoObj.poster);
      // }
      this._els.video.setAttribute('controls', 'controls');
      // add correct class
      this._els.video.classList.add(classes.video.html5);
      // append to container
      this._els.containerInner.appendChild(this._els.video);
    }
    // now we reference the Player class
    this._player = this._els.video;
    if (videoObj.poster) this._poster = videoObj.poster;
    // disable native controls first..
    const supportsVideo = !!this._els.video.canPlayType;
    if (supportsVideo) {
      this._els.video.controls = false;
    }
    // set tracking url for ga analytics
    this.trackingObject['video_url'] = videoObj.src;
  }

  // create YOutube video markup
  scaffoldYoutube() {
    // first create a div with unique id
    const iframeContainer = document.createElement('div');
    iframeContainer.classList.add(classes.video.iframe);
    const videoContainer = document.createElement('div');
    const uuid = this.createUUID(); // TODO keep as UUID ? pass ID in behaviour maybe ??
    this._ytPlayerId = 'yt-player-' + uuid;
    videoContainer.id = this._ytPlayerId;
    // append iframe to container
    iframeContainer.appendChild(videoContainer);
    this._els.containerInner.appendChild(iframeContainer);
    // insert the yt IFrame Player API script
    this.addAPIScript('https://www.youtube.com/iframe_api');
    // and we call YouTubeAPI Promise to fire global load event for new video item
    var that = this;
    YouTubeAPI.then(function (YT) {
      that.initYoutubePlayer();
    });
  }

  // create YOutube video markup
  scaffoldVimeo() {
    // first create a div with unique id
    const iframeContainer = document.createElement('div');
    iframeContainer.classList.add(classes.video.iframe);
    const videoContainer = document.createElement('div');
    const uuid = this.createUUID(); // TODO keep as UUID ? pass ID in behaviour maybe ??
    this._vimPlayerRefId = 'vimeo-player-' + uuid;
    videoContainer.id = this._vimPlayerRefId;
    // append iframe to container
    iframeContainer.appendChild(videoContainer);
    this._els.containerInner.appendChild(iframeContainer);
    // insert the Vimeo API script
    this.addAPIScript('https://player.vimeo.com/api/player.js');
    setTimeout(() => {
      this.initVimeoPlayer();
    }, 1000);
  }

  scaffoldBrightCove() {
    // first create a div with unique id
    const uuid = this.createUUID(); // TODO keep as UUID ? pass ID in behaviour maybe ??
    this._bcPlayerRefId = 'bc-player-' + uuid; // ID for this specific player instance on page
    const videoContainer = document.createElement('video-js');
    videoContainer.id = this._bcPlayerRefId;
    videoContainer.dataset.account = this._bcAccountId; // Brightcove client account ID
    videoContainer.dataset.player = this._bcPlayerId; // Brightcove ID for player created in account
    videoContainer.dataset.embed = 'default';
    videoContainer.dataset.videoId = this._bcVideoId; // Brightcove video ID from video media hosted in account
    videoContainer.classList.add('video-js');
    // append video to container
    this._els.containerInner.appendChild(videoContainer);
    // insert the Vimeo API script
    this.addAPIScript(
      `https://players.brightcove.net/${this._bcAccountId}/${this._bcPlayerId}_default/index.min.js`
    );

    setTimeout(() => {
      this.initBrightCovePlayer();
    }, 2000);
  }

  changeBrightCoveVideo(videoID) {
    this.scaffoldControls();
    // and we can safely prime the events
    this.initBrightCoveEvents();
    this.initControlEvents();
    this.initScrollWatch();
    // if we are not attached to a host element then lets go fullscreen now
    if (!this._host) this.makeFullScreen();
  }

  initBrightCovePlayer() {
    var that = this;
    var myPlayerEl = document.getElementById(this._bcPlayerRefId);
    if (typeof videojs.getPlayer(myPlayerEl) === 'undefined') {
      this._player = bc(myPlayerEl);
      this._player.on('loadedmetadata', function () {
        that.changeBrightCoveVideo(that._bcVideoId);
      });
    } else {
      const player = videojs.getPlayer(myPlayerEl);
      // nb: make sure player is ready first
      player.ready(function () {
        that._player = this;
        that.changeBrightCoveVideo(that._bcVideoId);
      });
    }
  }

  addAPIScript(script) {
    // first check this isnt already in the page
    let alreadyPresent = false;
    const scriptUrl = script;
    const scripts = document.getElementsByTagName('script');
    for (let i = scripts.length; i--; ) {
      if (scripts[i].src == scriptUrl) alreadyPresent = true;
    }
    // if not, append to head..
    if (!alreadyPresent) {
      const tag = document.createElement('script');
      
      // tag.src = `${scriptUrl}x`; // *** @as DEBUGGING, will cause error
      tag.src = scriptUrl;
      tag.defer = true;
      tag.onerror = (e) => {
        console.error('/Player/ -addAPIScript --failed', e);
        this._els.container.classList.add(`${classes.hasError}`);
        return;
      };
      
      const firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    }
  }

  initYoutubePlayer() {
    new YT.Player(this._ytPlayerId, {
      videoId: this._ytVideoId,
      playerVars: {
        playsinline: 1,
        enablejsapi: 1,
        controls: 0,
        origin: window.location.href,
        host: 'https://www.youtube.com',
      },
      events: {
        onReady: this.onPlayerReady,
      },
    });
  }

  initVimeoPlayer() {
    const options = {
      title: false,
      controls: false,
    };
    if (this._vimVideoId && this._vimAccountId) {
      options.url = `https://player.vimeo.com/video/${this._vimVideoId}?h=${this._vimAccountId}`;
    } else {
      options.id = this._vimVideoId;
    }
    this._player = new Vimeo.Player(this._vimPlayerRefId, options);
    // and we can safely prime the events
    this.initVimeoEvents();
    this.scaffoldControls();
    this.initControlEvents();
    this.initScrollWatch();
    // if we are not attached to a host element then lets go fullscreen now
    if (!this._host) this.makeFullScreen();
    // set tracking url for ga analytics
    if (this._vimVideoId && this._vimAccountId) {
      this.trackingObject[
        'video_url'
      ] = `https://player.vimeo.com/video/${this._vimVideoId}?h=${this._vimAccountId}`;
    } else {
      this.trackingObject[
        'video_url'
      ] = `https://player.vimeo.com/video/${this._vimVideoId}`;
    }
  }

  onPlayerReady(event) {
    this._player = event.target;
    this.scaffoldControls();
    // and we can safely prime the events
    this.initYoutubeEvents(event.target);
    this.initControlEvents();
    this.initScrollWatch();
    // if we are not attached to a host element then lets go fullscreen now
    if (!this._host) this.makeFullScreen();
  }

  killStandalonePlayer() {
    this.pause();
    this._els.container.classList.add(classes.hidden);
    // this._els.container.classList.remove(classes.fullscreen);
    this.destroyEvents();
    // and remove dom element as well
    const container = document.querySelector(
      `div[data-videoindex='${this._playerIndex}']`
    );
    if (container) {
      container.parentElement.removeChild(container);
      this._els.container = null;
      this._player = null;
      this._els.video = null;
    }
    // remove variable reference to this player
    this.broadcastDestroyThumb();
  }

  killHostPlayer(id) {
    // first get reference to host container
    const container = document.getElementById(id);
    const containerClone = container.cloneNode();
    // capture the videoindex of original item
    const containerPlayerIndex = container.dataset.videoindex;
    // and replace container el witha cloned version of itself
    if (container) {
      container.replaceWith(containerClone);
    }
    // remove variable reference to this player
    this.broadcastDestroyThumb(containerPlayerIndex);
    // now we go back and create the new container, setting the proxy to false
    this.prepareContainer(id, false);
  }

  createUUID() {
    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
      (
        c ^
        (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
      ).toString(16)
    );
  }

  scaffoldControls() {
    const supportsVideo = !!document.createElement('video').canPlayType;
    if (supportsVideo) {
      // create custom controls container
      this._els.controls.wrapper = document.createElement('div');
      this._els.controls.wrapper.classList.add(classes.controls.wrapper);
      // create progress bar
      this.buildProgressBar();
      // create buttons container div
      this.buildButtons();
      // create volume controls
      this.buildVolumeControls();
      // create div with time display
      this.buildTimeDisplay();
      // create fullscreen toggle
      this.buildFullScreenToggle();
      // make the screen for poster, pause, etc
      this.buildPosterOverlay();
      // append controls to video container
      this._els.containerInner.appendChild(this._els.controls.wrapper);
    }
  }

  buildProgressBar() {
    this._els.controls.progress = document.createElement('div');
    this._els.controls.progress.classList.add(classes.controls.progress);
    // add inner progress track
    this._els.controls.progressTrack = document.createElement('div');
    this._els.controls.progressTrack.classList.add(
      classes.controls.progressTrack
    );
    // add seek tooltip
    this._els.controls.progressTooltip = document.createElement('div');
    this._els.controls.progressTooltip.classList.add(
      classes.controls.progressTooltip
    );
    this._els.controls.progressTooltip.innerHTML = '00:00';
    // append progress track to progress bar
    this._els.controls.progress.appendChild(this._els.controls.progressTrack);
    // append progress tooltip to progress bar
    this._els.controls.progress.appendChild(this._els.controls.progressTooltip);
    // append progress bar to wrapper
    this._els.controls.wrapper.appendChild(this._els.controls.progress);
  }

  buildButtons() {
    this._els.controls.buttons.container = document.createElement('div');
    this._els.controls.buttons.container.classList.add(
      classes.controls.buttons.container
    );
    // create play-pause button
    this._els.controls.buttons.playPause = document.createElement('button');
	this._els.controls.buttons.playPause.setAttribute('title','play-button');
    this._els.controls.buttons.playPause.classList.add(
      classes.controls.buttons.playPause
    );
    // append playPause button to button container div
    this._els.controls.buttons.container.appendChild(
      this._els.controls.buttons.playPause
    );
    // append button container to wrapper
    this._els.controls.wrapper.appendChild(
      this._els.controls.buttons.container
    );
    // set data title on play button
    this.setButtonTitle();
  }

  buildTimeDisplay() {
    this._els.controls.time.container = document.createElement('div');
    this._els.controls.time.container.classList.add(
      classes.controls.time.container
    );
    // build time-elapsed div
    this._els.controls.time.elapsed = document.createElement('time');
    this._els.controls.time.elapsed.innerHTML = '00:00';
    // build divider span
    const divider = document.createElement('span');
    divider.innerHTML = '/';
    // build time-duration div
    this._els.controls.time.duration = document.createElement('time');
    this._els.controls.time.duration.innerHTML = '00:00';
    // populate time container
    this._els.controls.time.container.appendChild(
      this._els.controls.time.elapsed
    );
    this._els.controls.time.container.appendChild(divider);
    this._els.controls.time.container.appendChild(
      this._els.controls.time.duration
    );
    // append time container to wrapper
    this._els.controls.wrapper.appendChild(this._els.controls.time.container);
  }

  setButtonTitle() {
    const title = !this._playing ? 'Play [space]' : 'Pause [space]';
    this._els.controls.buttons.playPause.setAttribute('data-title', title);
  }

  buildVolumeControls() {
    this._els.controls.volume.container = document.createElement('div');
    this._els.controls.volume.container.classList.add(
      classes.controls.volume.container
    );
    // create icon
    this._els.controls.volume.icon = document.createElement('span');
    this._els.controls.volume.icon.classList.add(
      classes.controls.volume.iconHigh
    );
    // create volume slider
    this._els.controls.volume.slider = document.createElement('input');
    this._els.controls.volume.slider.type = 'range';
	this._els.controls.volume.slider.setAttribute('title','volume-slider-control');
    this._els.controls.volume.slider.value = this.volumeAmount;
    this._els.controls.volume.slider.setAttribute(
      'data-volume',
      this.volumeAmount
    );
    this._els.controls.volume.slider.max = this.volumeAmount;
    this._els.controls.volume.slider.min = '0';
    this._els.controls.volume.slider.step = this.volumeStepAmount;
    this._els.controls.volume.slider.classList.add(
      classes.controls.volume.slider
    );
    // append icon to volume container
    this._els.controls.volume.container.appendChild(
      this._els.controls.volume.icon
    );
    // append slider to volume container
    this._els.controls.volume.container.appendChild(
      this._els.controls.volume.slider
    );
    // append volume container to main controls container
    this._els.controls.wrapper.appendChild(this._els.controls.volume.container);
  }

  buildPosterOverlay() {
    this._els.cta = document.createElement('div');
    this._els.cta.classList.add(classes.cta);
    // append cta container to main container
    this._els.containerInner.appendChild(this._els.cta);
    // sort out poster..
    this.doPosterImage();
    // based on config, create titlebox OR just play button
    if (this._title) {
      this.buildTitleBox();
    } else {
      this.buildFauxPlayButton();
    }
  }

  buildFauxPlayButton() {
    // create main faux play button
    this._els.fauxPlay = document.createElement('span');
    this._els.fauxPlay.classList.add(classes.fauxPlay);
    // append faux play button to cta container
    this._els.cta.appendChild(this._els.fauxPlay);
    this._els.cta.classList.add(classes.ctaFull);
  }

  buildTitleBox() {
    // create title box overlays
    this._els.titlebox.box = document.createElement('div');
    this._els.titlebox.box.classList.add(classes.titlebox.box);
    // duration label
    this._els.titlebox.duration = document.createElement('div');
    this._els.titlebox.duration.classList.add(classes.titlebox.duration);
    this._els.titlebox.duration = document.createElement('button');
    this._els.titlebox.duration.classList.add(
      'cta-button',
      'cta-button--secondary',
      'cta-button--small'
    );
    this._els.titlebox.duration.insertAdjacentHTML(
      'afterbegin',
      '<span class="cta-button__icon sn-icon-clock"></span><span class="cta-button__label">12m 47s</span>'
    );
    // title
    this._els.titlebox.title = document.createElement('h2');
    this._els.titlebox.title.classList.add(classes.titlebox.title);
    this._els.titlebox.title.innerText = this._title;
    // cta
    this._els.titlebox.cta = document.createElement('button');
    this._els.titlebox.cta.classList.add(
      'cta-button',
      'cta-button--primary',
      'cta-button--medium'
    );
    this._els.titlebox.cta.insertAdjacentHTML(
      'afterbegin',
      `<span class="cta-button__label">${this._ctaTitle}</span><span class="cta-button__icon sn-icon-play"></span>`
    );

    // append elements into box
    this._els.titlebox.box.appendChild(this._els.titlebox.duration);
    this._els.titlebox.box.appendChild(this._els.titlebox.title);
    this._els.titlebox.box.appendChild(this._els.titlebox.cta);
    // append box container to cta container
    this._els.cta.appendChild(this._els.titlebox.box);
  }

  handleVolumeInputChange() {
    let target = this._els.controls.volume.slider;
    const min = target.min;
    const max = target.max;
    const val = target.value;
    target.style.backgroundSize = ((val - min) * 100) / (max - min) + '% 100%';
  }

  doPosterImage() {
    // If we have poster set in markup, apply it as background, then bail out..
    if (this._poster) {
      this.addPosterImageBackground(this._poster);
      return;
    }
    // verify what poster image is first..
    switch (this._type) {
      case TYPE_HTML5:
        this.addPosterImageBackground(this._genericPosterImage);
        break;
      case TYPE_YOUTUBE:
        const ytPoster = `https://img.youtube.com/vi/${this._ytVideoId}/maxresdefault.jpg`;
        this.checkPosterImageExists(ytPoster).then((img) => {
          this.addPosterImageBackground(img);
        });
        break;
      case TYPE_BRIGHTCOVE:
        const bcPoster = this._player.poster_ || '';
        this.checkPosterImageExists(bcPoster).then((img) => {
          this.addPosterImageBackground(img);
        });
        break;
      case TYPE_VIMEO:
        this.getVimeoPoster();
        break;
    }
  }

  getVimeoPoster() {
    const accountHash = this._vimAccountId ? `/${this._vimAccountId}` : '';
    fetch(
      `https://vimeo.com/api/oembed.json?url=https%3A//vimeo.com/${this._vimVideoId}${accountHash}`
    )
      .then((response) => response.json())
      .then((data) => {
        const vimPoster = data && data.thumbnail_url ? data.thumbnail_url : '';
        this.checkPosterImageExists(vimPoster).then((img) => {
          this.addPosterImageBackground(img);
        });
      })
      .catch(() => {
        // if the actual call fails, then just set default img
        this.addPosterImageBackground(this._genericPosterImage);
      });
  }

  addPosterImageBackground(img) {
    this._poster = img;
    this._els.cta.style.backgroundImage = `url('${img}')`;
  }

  buildFullScreenToggle() {
    this._els.controls.fullscreen.container = document.createElement('div');
    this._els.controls.fullscreen.container.classList.add(
      classes.controls.fullscreen.container
    );
    // add icon
    this._els.controls.fullscreen.icon = document.createElement('span');
    this._els.controls.fullscreen.icon.classList.add(
      classes.controls.fullscreen.iconExpand
    );
    // add icon to container
    this._els.controls.fullscreen.container.appendChild(
      this._els.controls.fullscreen.icon
    );
    // add container to main wrapper
    this._els.controls.wrapper.appendChild(
      this._els.controls.fullscreen.container
    );
  }

  initControlEvents() {
    // click play/pause
    this._els.controls.buttons.playPause.addEventListener(
      'click',
      (e) => {
        e.stopPropagation();
        this.togglePlayPause();
      },
      true
    );
    // also, click anywhere on the video container to toggle play
    this._els.containerInner.addEventListener('click', (e) => {
      e.preventDefault();
      this.togglePlayPause();
    });
    // close fullscreen mode
    this._els.close.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      // this.toggleFullScreen();
      this.closeFullScreen();
    });
    // display scrub/seek time in tooltip when hovering the progressbar
    this._els.controls.progress.addEventListener('mousemove', (e) =>
      this.updateProgressTooltip(e)
    );
    // jump to scrub/seek time on clicking progress bar
    this._els.controls.progress.addEventListener('click', (e) => {
      e.stopPropagation();
      this.skipAhead();
    });
    // toggle fullscreen video click
    this._els.controls.fullscreen.icon.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      this.toggleFullScreen();
    });
    // add volume events
    this._els.controls.volume.slider.addEventListener('input', (e) => {
      e.stopPropagation();
      this.handleVolumeInputChange();
      this.updateVolume();
    });
    this._els.controls.volume.slider.addEventListener('click', (e) => {
      e.stopPropagation();
    });
    this._els.controls.volume.icon.addEventListener('click', (e) => {
      e.stopPropagation();
      this.toggleVolumeMute();
    });
    // general catchall for clicking on items within the wrapper
    // stops the toggle play/pause from firing
    this._els.controls.wrapper.addEventListener('click', (e) => {
      e.stopPropagation();
    });
    // support for keyboard events
    this._els.container.addEventListener('keydown', (e) => {
      e.preventDefault();
      this.keyboardShortcuts(e);
    });
    
    // exit fullscreen while press on esc button
    const exitHandler = () => {
      if (!document.fullscreenElement && !document.webkitIsFullScreen && !document.mozFullScreen && !document.msFullscreenElement) {
        this.closeFullScreen();
      }
    };  
    document.addEventListener('fullscreenchange', exitHandler);
    document.addEventListener('webkitfullscreenchange', exitHandler);
    document.addEventListener('mozfullscreenchange', exitHandler);
    document.addEventListener('MSFullscreenChange', exitHandler);
   
  }

  /* only for html5 video */
  initVideoEvents() {
    // HTML5 video, listen for volume change
    this._els.video.addEventListener('volumechange', (e) =>
      this.updateVolumeIcon(e)
    );
    // progress bar update
    this._els.video.addEventListener('timeupdate', (e) =>
      this.updateProgressBar(e)
    );
    // time display update
    this._els.video.addEventListener('loadedmetadata', (e) =>
      this.setVideoDuration(e)
    );
  }

  initBrightCoveEvents() {
    this.setVideoDuration();
    // do this again in case there are any race conditions..
    this.doPosterImage();
    // we set this just to get ref to physical dom element for later
    this._els.video = this._els.containerInner.querySelector('video');
    // HTML5 video, listen for volume change
    this._els.video.addEventListener('volumechange', (e) =>
      this.updateVolumeIcon(e)
    );
    // progress bar update
    this._els.video.addEventListener('timeupdate', (e) => {
      // nb: we run this again once here as previous ready state doesnt give correct duration
      if (this._bcDurationFix) {
        this.setVideoDuration();
        this._bcDurationFix = false;
      }
      this.updateProgressBar(e);
    });
    // set tracking url for ga analytics
    this.trackingObject[
      'video_url'
    ] = `https://players.brightcove.net/${this._bcAccountId}/${this._bcPlayerId}_default/index.html?videoId=${this._bcVideoId}`;
  }

  initVimeoEvents() {
    // we set this just to get ref to physical dom element for later
    this._els.video = this._els.containerInner.querySelector(
      '.videoplayer__iframe'
    );
    // make the player ready first..
    this._player.ready().then(() => {
      // then get the duration via promise and set to a var for later use..
      this._player.getDuration().then((data) => {
        this._duration = data;
        // now we can safely call this..
        this.setVideoDuration();
        // and call this one again...
        this.doPosterImage();
      });
    });
    this._player.on('ended', () => {
      this.currentTime = 0;
      this.stopVideo();
    });
    this._player.on('volumechange', (data) => {
      if (!this._muted) {
        this._volume = data.volume;
      }
      this.updateVolumeIcon();
    });
  }

  initYoutubeEvents(playerInstance) {
    const iframe = playerInstance.getIframe();

    // NB We need to add this to get volume changes from Youtube iframe
    // This is the source "window" that will emit the events.
    const iframeWindow = iframe.contentWindow;
    // Listen to events triggered by postMessage.
    var that = this;
    window.addEventListener('message', function (event) {
      // Check that the event was sent from the YouTube IFrame.
      if (event.source === iframeWindow) {
        const data = JSON.parse(event.data);
        // The "infoDelivery" event is used by YT to transmit any kind of information change in the player,
        // such as the current time or a volume change.
        if (data.event === 'infoDelivery' && data.info && data.info.volume) {
          that.updateVolumeIcon();
        }
      }
    });
    // we should have access to this metadata so can call this now...
    this.setVideoDuration();
    // we set this just to get ref to physical dom element for later
    this._els.video = iframe;
    // set tracking url for ga analytics
    this.trackingObject['video_url'] = iframe.src;
  }

  initScrollWatch() {
    // play and pause current video when out of viewport
    scrollWatcher.attach(
      {
        trigger: this._els.video,
      },
      {
        onEnter: () => {
          if(this.autoPlay && !this._playing && this._type == TYPE_HTML5 && !this.isCarouselCardVideo) {
            this.muted = true;
            this.playsinline = 1;
            if(this.videoPlayOption) {
              this._player.loop = true;
            } else {
              this._player.loop = false;
            } 
            this.togglePlayPause();
          }
        },
        onEnterBack: () => {
          if(this.autoPlay && !this._playing && this._type == TYPE_HTML5 && !this.isCarouselCardVideo) {
            this.togglePlayPause();
          }
        },
        onLeave: () => {
          if (this._playing) {
            this.togglePlayPause();
            this.doTrackingEvent('scroll away from video');
          }
        },
        onLeaveBack: () => {
          if (this._playing) {
            this.togglePlayPause();
          }
        },
      }
    );
  }

  // returns time in minutes and seconds
  formatTime(timeInSeconds) {
    if (isNaN(timeInSeconds)) {
      return {
        minutes: 0,
        seconds: 0,
      };
    }
    const result = new Date(timeInSeconds * 1000).toISOString().substr(11, 8);
    return {
      minutes: result.substr(3, 2),
      seconds: result.substr(6, 2),
    };
  }

  // set video duration from video metadata
  setVideoDuration() {
    const time = this.formatTime(Math.round(this.duration));
    this._els.controls.time.duration.innerText = `${time.minutes}:${time.seconds}`;
    this._els.controls.time.duration.setAttribute(
      'datetime',
      `${time.minutes}m ${time.seconds}s`
    );
    if (this._title) this.setPosterDuration();
  }

  // set video duration on poster image
  setPosterDuration() {
    const time = this.formatTime(Math.round(this.duration));
    if (time.minutes === '00') time.minutes = '0';
    if (time.seconds === '00') time.seconds = '0';
    const timeLabel =
      this._els.titlebox.duration.querySelector('.cta-button__label');
    timeLabel.innerText = `${time.minutes}m ${time.seconds}s`;
  }

  updateTimeElapsed() {
    const time = this.formatTime(Math.floor(this.currentTime));
    this._els.controls.time.elapsed.innerText = `${time.minutes}:${time.seconds}`;
    this._els.controls.time.elapsed.setAttribute(
      'datetime',
      `${time.minutes}m ${time.seconds}s`
    );
  }

  updateProgressBar() {
    if (!this._player) return; //fix for html5 err on destroy
    const position = this.currentTime / this.duration;
    // update progress bar
    this._els.controls.progressTrack.style.width = position * 100 + '%';
    const formatedVideoProgress = Math.floor(position * 100);
    if(formatedVideoProgress == 0 && formatedVideoProgress !== this.trackedVideoProgress) {
      this.trackedVideoProgress = formatedVideoProgress;
      this.doTrackingProgressEvent('video_start');
    }
    if([10, 25, 50, 75, 90].indexOf(formatedVideoProgress) !== -1 && formatedVideoProgress !== this.trackedVideoProgress) {
      this.trackedVideoProgress = formatedVideoProgress;
      this.doTrackingProgressEvent('video_progress', formatedVideoProgress);
    }
    if(formatedVideoProgress == 100 && formatedVideoProgress !== this.trackedVideoProgress) {
      this.trackedVideoProgress = formatedVideoProgress;
      this.doTrackingProgressEvent('video_complete');
    }
    // update time counter
    this.updateTimeElapsed();
    if (this.ended) {
      this.stopVideo();

    }
  }

  doTrackingProgressEvent(event, progress) {
    this.trackingObject['event'] = event;
    let data = {};
    if(progress) {
    data = { ...this.trackingObject,'video_percent' :progress};
    } else {
      if('video_percent' in this.trackingObject) {
        delete this.trackingObject['video_percent'];
      }
      data = { ...this.trackingObject };
    }
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push(data);
  }

  // uses position of the mouse on the progress bar to indicate time at that point
  updateProgressTooltip(event) {
    const skipTo = Math.round(
      (event.offsetX / event.target.clientWidth) * parseInt(this.duration, 10)
    );
    // set these now as refs for css and when jump to seek/scrub position later
    this._els.controls.progressTooltip.setAttribute('data-seek', skipTo);
    this._els.controls.progress.setAttribute('data-seek', skipTo);
    const t = this.formatTime(skipTo);
    this._els.controls.progressTooltip.textContent = `${t.minutes}:${t.seconds}`;
    const rect = this._els.video.getBoundingClientRect();
    this._els.controls.progressTooltip.style.left = `${
      event.pageX - rect.left
    }px`;
  }

  skipAhead(amt) {
    let skipTo = 0; // prevents err if not present
    if (amt) {
      skipTo = this.currentTime + parseInt(amt);
    } else {
      skipTo = parseInt(event.target.dataset.seek);
    }
    this.seek(skipTo);
    this.updateProgressBar();
  }

  toggleVolumeMute() {
    const isMuted = this.toggleMute();
    if (isMuted) {
      this._els.controls.volume.slider.setAttribute(
        'data-volume',
        this._els.controls.volume.slider.value
      );
      this._els.controls.volume.slider.value = 0;
    } else {
      this._els.controls.volume.slider.value =
        this._els.controls.volume.slider.dataset.volume;
    }
  }

  updateVolume() {
    if (this.muted) {
      this.muted = false;
    }
    this.volume = this._els.controls.volume.slider.value;
  }

  updateVolumeIcon() {
    this._els.controls.volume.icon.classList.remove(
      ...this._els.controls.volume.icon.classList
    );
    this._els.controls.volume.icon.setAttribute('data-title', 'Mute (m)');
    if (this.muted || this.volume === 0) {
      this._els.controls.volume.icon.classList.add(
        classes.controls.volume.iconMute
      );
      this._els.controls.volume.icon.setAttribute('data-title', 'Unmute (m)');
      // need to manually set the range slider as well
      this._els.controls.volume.slider.style.backgroundSize = '0% 100%';
    } else if (this.volume > 0 && this.volume <= this.volumeAmount / 2) {
      this._els.controls.volume.icon.classList.add(
        classes.controls.volume.iconLow
      );
      // need to manually set the range slider as well
      this.handleVolumeInputChange();
    } else {
      this._els.controls.volume.icon.classList.add(
        classes.controls.volume.iconHigh
      );
      // need to manually set the range slider as well
      this.handleVolumeInputChange();
    }
  }

  stopVideo() {
    this._els.controls.buttons.playPause.classList.remove(
      classes.controls.buttons.playPausePlaying
    );
    this._els.container.classList.add(classes.paused);
    this.togglePlayerState();
  }

  togglePlayPause() {
    if (!this._player || !this._els.container) return;
    if (!this._playing) {
      this._els.controls.buttons.playPause.classList.add(
        classes.controls.buttons.playPausePlaying
      );
      this._els.container.classList.remove('videoplayer--paused');
      this.play();
      this.broadcastPlaying();
      this.doTrackingEvent('video_play');
    } else {
      this.pausePlayer();
      this.doTrackingEvent('video_pause');
    }
    this.togglePlayerState();
  }

  pausePlayer() {
    this._els.controls.buttons.playPause.classList.remove(
      classes.controls.buttons.playPausePlaying
    );
    this._els.container.classList.add(classes.paused);
    this.pause();
  }

  togglePlayerState() {
    this._playing = !this._playing;
    // for youtube we need to run our own timer to see timeupdate
    if (this._type === TYPE_YOUTUBE || this._type === TYPE_VIMEO)
      this.setTimer(this._playing);
    this.setButtonTitle();
  }

  setTimer(isPlaying) {
    if (isPlaying) {
      if (this.type === TYPE_VIMEO) {
        this._timer = setInterval((e) => {
          const newTime = this.currentTime + 0.1;
          this.currentTime = newTime;
          this.updateProgressBar(e);
        }, 100);
      } else {
        this._timer = setInterval((e) => this.updateProgressBar(e), 100);
      }
    } else {
      clearTimeout(this._timer);
    }
  }

  // nb: this only runs when a video thumb without a
  // host is clicked and made to go fullscreen
  makeFullScreen() {
    this._fullscreen = true;
    // ios requires vendor prefix..
    if (this._els.container.webkitRequestFullscreen) {
      this._els.container.webkitRequestFullscreen();
    } else if (this._els.container.requestFullscreen) {
      this._els.container.requestFullscreen();
    }
    // set a delay here as above request is async promise
    setTimeout(() => {
      // if for any reason the fullscreen api doesnt kick in,
      // we need to fake/style the fullscreen eg (safari on ios)
      if (!document.fullscreenElement && !document.webkitFullscreenElement) {
        this._els.container.classList.add(classes.fauxfullscreen);
      }
      this._els.container.classList.remove(classes.hidden);
      this._els.container.classList.add(classes.fullscreen);
      // this._els.container.classList.add(classes.fauxfullscreen);
      this._els.container.focus();
      this.updateFullscreenButton();
    }, 300);
  }

  closeFullScreen() {
    this._fullscreen = false;
    if (document.webkitFullscreenElement) {
      document.webkitExitFullscreen();
    }
    if (document.fullscreenElement) {
      document.exitFullscreen();
    }
    this._els.container.classList.remove(classes.fullscreen);
    this._els.container.classList.remove(classes.fauxfullscreen);
    // lastly, lets nuke the container if its thumbnail video
    if (!this._host) this.killStandalonePlayer();
    if (this._host) {
      this.updateFullscreenButton();
    }
  }

  toggleFullScreen() {
    if (this._fullscreen) {
      this.closeFullScreen();
    } else {
      this.makeFullScreen();
    }
  }

  updateFullscreenButton() {
    this._els.controls.fullscreen.icon.classList.remove(
      ...this._els.controls.fullscreen.icon.classList
    );
    if (!this._fullscreen) {
      this._els.controls.fullscreen.icon.classList.add(
        classes.controls.fullscreen.iconExpand
      );
      this._els.controls.fullscreen.icon.setAttribute(
        'data-title',
        'Full screen (f)'
      );
    } else {
      this._els.controls.fullscreen.icon.classList.add(
        classes.controls.fullscreen.iconShrink
      );
      this._els.controls.fullscreen.icon.setAttribute(
        'data-title',
        'Exit full screen (f)'
      );
    }
  }

  keyboardShortcuts(event) {
    const { keyCode } = event;
    switch (keyCode) {
      case 32: // space
        this.togglePlayPause();
        break;
      case 77: // 'm'
        this.toggleVolumeMute();
        break;
      case 37: // left arrow
        this.skipAhead(-5);
        break;
      case 39: // right arrow
        this.skipAhead(5);
        break;
      case 70: // f
        this.toggleFullScreen();
        break;
    }
  }

  listen() {
    // *** listen for play signal and pause player if needed
    this.options.signal.on(config.eventNames.PAUSE_OTHER_VIDEOS, (payload) => {
      if (this._player && this._playerIndex !== payload.player) {
        this.pausePlayer();
        // we need to do this else play/pause button sticks..
        this._playing = false;
        // stop timer for these ones, just to be sure..
        if (this._type === TYPE_YOUTUBE || this._type === TYPE_VIMEO)
          this.setTimer(false);
      }
    });
  }

  destroyEvents() {
    // remove these ones to prevent any unwanted errors
    this._els.video.removeEventListener('volumechange', this.updateVolumeIcon);
    this._els.video.removeEventListener('timeupdate', this.updateProgressBar);
    this._els.video.removeEventListener(
      'loadedmetadata',
      this.setVideoDuration
    );
    clearTimeout(this._timer);
  }

  broadcastPlaying() {
    this.options.signal.emit(config.eventNames.PAUSE_OTHER_VIDEOS, {
      player: this._playerIndex,
    });
  }

  broadcastDestroyThumb(index = this._playerIndex) {
    if (!this._host || (this._host && this._proxy)) {
      this.options.signal.emit(config.eventNames.DESTROY_THUMB_VIDEO, {
        player: index,
      });
    }
  }

  checkPosterImageExists(url) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.src = url;
      if (img.complete) {
        resolve(url);
      } else {
        img.onload = () => {
          if (img.width < 250) resolve(this._genericPosterImage);
          resolve(url);
        };
        img.onerror = () => {
          resolve(this._genericPosterImage);
        };
      }
    });
  }
}

/*
 3 player variants
 -----------------
 1. players that have their own host element, they are loaded into element on init
 2. thumbs players that have no host element, they are loaded fullscreen on thumb click, and torn down afterwards
 3. thumbs players that share a host element, (eg carousel items) . First item is loaded on init, others on click
*/

window.YouTubeAPI = new Promise(function (resolve, reject) {
  window.onYouTubeIframeAPIReady = function () {
    resolve(YT);
  };
});

window.VideoPlayerStack = {
  players: [], // array reference to player items
};

// this is specifically for the tabbed video thumb carousels
window.thumbElements = {};

const VideoPlayer = (options) => {
  const els = { ...options.els };
  const init = () => {
    // console.log('/VideoPlayer/ -init', options);
    listen();
  };

  const listen = () => {
    // *** get behaviours as they are added and delegate an action
    options.signal.on(config.eventNames.BEHAVIOUR_ADDED, (payload) => {
      const uid = els.el.getAttribute('data-behaviour-uid');
      if (payload.uid === uid) {
        const instance = options.getBehaviourByUid(payload.uid);
        const element = instance.options.els.el;
        const config = instance.options.config;
        if (config.host && !config.proxy) {
          // in-situ vids and video-carousel(shared host)
          // to play carousel card video inline we need some time to prepare dom
          if(config.carouselVideoTimer) {
            setTimeout(() => {
              setupInstance(config);
            }, config.carouselVideoTimer);
          } else {
            setupInstance(config);
          }
        } else {
          // add to thumbs obj if not already in there...
          window.thumbElements[uid] = {
            el: element,
            config: config,
          };
          // standalone thumbs and video-carousel-thumbs(full screen)
          setupThumbListener(
            window.thumbElements[uid].el,
            window.thumbElements[uid].config
          );
        }
      }
    });
    // *** listen for destroy signal and remove player from stack when called
    options.signal.on(config.eventNames.DESTROY_THUMB_VIDEO, (payload) => {
      const lastItem = window.VideoPlayerStack.players.findIndex(
        (item) => item._playerIndex == payload.player
      );

      if (lastItem !== -1) {
        window.VideoPlayerStack.players.splice(lastItem, 1);
        // console.log(
        //   `VideoPlayer:DESTROY_THUMB_VIDEO:payload.player: ${payload.player}`
        // );
      }
      // Do an extra sweep here - there shouldnt be any full-screen players attached to the dom now
      const leftoverPlayers = [
        ...document.querySelectorAll('.videoplayer--fullscreen'),
      ];
      leftoverPlayers.map((item) => {
        item.parentNode.removeChild(item);
      });
    });
  };

  const setupInstance = (videoObject) => {
    removePlayerByUID(videoObject.uid);
    // setup player instance
    const playerIndex = makePlayerIndex(videoObject);
    const player = new Player(videoObject.type, options.config);
    window.VideoPlayerStack.players.push(player);
    // pass in options so we can communicate with class
    player.setup(videoObject, playerIndex, options);
    // view player stack from class if needed
    window.PlayerStack = window.VideoPlayerStack.players;
  };

  const makePlayerIndex = (obj) => {
    if (obj.proxy) {
      // increment highest value by 1
      return Math.max.apply(
        Math,
        window.VideoPlayerStack.players.map(function (o) {
          return o._playerIndex + 1;
        })
      );
    } else {
      return window.VideoPlayerStack.players.length;
    }
  };

  const removePlayerByUID = (uid) => {
    const duplicateItemIndex = window.VideoPlayerStack.players.findIndex(
      (item) => {
        return item.options.config.uid === uid;
      }
    );
    if (duplicateItemIndex !== -1) {
      window.VideoPlayerStack.players.splice(duplicateItemIndex, 1);
    }
  };

  const setupThumbListener = (el, data) => {
    const clickHandler = function (e) {
      e.preventDefault();
      setupInstance(data);
    };
    // first remove any previous events
    el.removeEventListener('click', clickHandler);
    // scaffold instances that DO NOT have a host element
    // OR thumbs that play within a shared host element
    el.addEventListener('click', clickHandler);
  };

  return {
    init,
  };
};

/*
  Notifications
  requires notifications include on page
  receives payload from EVENT
  payload.notificationType - information / success / error
  payload.text - notification text
  payload.duration - seconds / numeric
  payload.dismissable - boolean

  component/element attributes:

  e.g.

  data-component-notification-text="Link copied"
  data-component-notification-duration="10"
  data-component-notification-type="information"
  data-component-notification-dismissable="true">
*/


const Notification = (options) => {

  const els = {
      el: null,
      targetDiv: null,
      content: null,
      closeBtn: null,
      iconTarget: null,
  };

  const iconClasses = {
      shareUrl: 'sn-icon-Link',
      information: 'sn-icon-info',
      error: 'sn-icon-error',
      success: 'sn-icon-check'
    };

  const init = () => {
    //console.log('/notification/ -init');
    options.signal.on(config.eventNames.TRIGGER_NOTIFICATION, (payload) => {
        generateNotification(payload);
      });
  };

  const generateNotification = (payload) =>{
    
    console.log('/notification/ -generateNotification', payload);

    els.targetDiv = document.querySelector(".notifications");
    els.content = els.targetDiv.querySelector(".content");
    els.iconTarget = els.targetDiv.querySelector(".iconTarget");
    els.closeBtn = els.targetDiv.querySelector(".sn-icon-close");

    if (!els.targetDiv.classList.contains('show')) {
        els.iconTarget.className = '';
        els.iconTarget.classList.add('iconTarget',iconClasses[payload.notificationType]);
        els.targetDiv.className = '';
        els.targetDiv.classList.add("notifications","notifications--"+payload.notificationType);
        els.targetDiv.style.animationDuration = payload.duration+"s"; // maybe remove seems to stop click interuption
        els.content.innerHTML  = payload.text;

        els.targetDiv.classList.add('notifications--show');
    }


    // dismissable if set

    if (payload.dismissable=='true') {
      els.closeBtn.classList.add('active');
      els.targetDiv.addEventListener("click",()=>{
        resetNotification();
      });
    } else {
      els.closeBtn.classList.remove('active');
    }

    // remove animation
    setTimeout(function() {
        resetNotification();
    }, payload.duration*1000);

  };

  const resetNotification = () =>{
    els.targetDiv.classList.remove('notifications--show');
    els.targetDiv.style="";
  };


  return {
    init,
  }
};

/*!
 * matrix 3.6.1
 * https://greensock.com
 *
 * Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var _doc$2,
    _win$3,
    _docElement$1,
    _body$1,
    _divContainer,
    _svgContainer,
    _identityMatrix$1,
    _transformProp$1 = "transform",
    _transformOriginProp$1 = _transformProp$1 + "Origin",
    _hasOffsetBug,
    _setDoc = function _setDoc(element) {
  var doc = element.ownerDocument || element;

  if (!(_transformProp$1 in element.style) && "msTransform" in element.style) {
    //to improve compatibility with old Microsoft browsers
    _transformProp$1 = "msTransform";
    _transformOriginProp$1 = _transformProp$1 + "Origin";
  }

  while (doc.parentNode && (doc = doc.parentNode)) {}

  _win$3 = window;
  _identityMatrix$1 = new Matrix2D();

  if (doc) {
    _doc$2 = doc;
    _docElement$1 = doc.documentElement;
    _body$1 = doc.body; // now test for the offset reporting bug. Use feature detection instead of browser sniffing to make things more bulletproof and future-proof. Hopefully Safari will fix their bug soon but it's 2020 and it's still not fixed.

    var d1 = doc.createElement("div"),
        d2 = doc.createElement("div");

    _body$1.appendChild(d1);

    d1.appendChild(d2);
    d1.style.position = "static";
    d1.style[_transformProp$1] = "translate3d(0,0,1px)";
    _hasOffsetBug = d2.offsetParent !== d1;

    _body$1.removeChild(d1);
  }

  return doc;
},
    _forceNonZeroScale = function _forceNonZeroScale(e) {
  // walks up the element's ancestors and finds any that had their scale set to 0 via GSAP, and changes them to 0.0001 to ensure that measurements work. Firefox has a bug that causes it to incorrectly report getBoundingClientRect() when scale is 0.
  var a, cache;

  while (e && e !== _body$1) {
    cache = e._gsap;
    cache && cache.uncache && cache.get(e, "x"); // force re-parsing of transforms if necessary

    if (cache && !cache.scaleX && !cache.scaleY && cache.renderTransform) {
      cache.scaleX = cache.scaleY = 1e-4;
      cache.renderTransform(1, cache);
      a ? a.push(cache) : a = [cache];
    }

    e = e.parentNode;
  }

  return a;
},
    // possible future addition: pass an element to _forceDisplay() and it'll walk up all its ancestors and make sure anything with display: none is set to display: block, and if there's no parentNode, it'll add it to the body. It returns an Array that you can then feed to _revertDisplay() to have it revert all the changes it made.
// _forceDisplay = e => {
// 	let a = [],
// 		parent;
// 	while (e && e !== _body) {
// 		parent = e.parentNode;
// 		(_win.getComputedStyle(e).display === "none" || !parent) && a.push(e, e.style.display, parent) && (e.style.display = "block");
// 		parent || _body.appendChild(e);
// 		e = parent;
// 	}
// 	return a;
// },
// _revertDisplay = a => {
// 	for (let i = 0; i < a.length; i+=3) {
// 		a[i+1] ? (a[i].style.display = a[i+1]) : a[i].style.removeProperty("display");
// 		a[i+2] || a[i].parentNode.removeChild(a[i]);
// 	}
// },
_svgTemps = [],
    //we create 3 elements for SVG, and 3 for other DOM elements and cache them for performance reasons. They get nested in _divContainer and _svgContainer so that just one element is added to the DOM on each successive attempt. Again, performance is key.
_divTemps = [],
    _getDocScrollTop$1 = function _getDocScrollTop() {
  return _win$3.pageYOffset || _doc$2.scrollTop || _docElement$1.scrollTop || _body$1.scrollTop || 0;
},
    _getDocScrollLeft$1 = function _getDocScrollLeft() {
  return _win$3.pageXOffset || _doc$2.scrollLeft || _docElement$1.scrollLeft || _body$1.scrollLeft || 0;
},
    _svgOwner = function _svgOwner(element) {
  return element.ownerSVGElement || ((element.tagName + "").toLowerCase() === "svg" ? element : null);
},
    _isFixed$1 = function _isFixed(element) {
  if (_win$3.getComputedStyle(element).position === "fixed") {
    return true;
  }

  element = element.parentNode;

  if (element && element.nodeType === 1) {
    // avoid document fragments which will throw an error.
    return _isFixed(element);
  }
},
    _createSibling = function _createSibling(element, i) {
  if (element.parentNode && (_doc$2 || _setDoc(element))) {
    var svg = _svgOwner(element),
        ns = svg ? svg.getAttribute("xmlns") || "http://www.w3.org/2000/svg" : "http://www.w3.org/1999/xhtml",
        type = svg ? i ? "rect" : "g" : "div",
        x = i !== 2 ? 0 : 100,
        y = i === 3 ? 100 : 0,
        css = "position:absolute;display:block;pointer-events:none;margin:0;padding:0;",
        e = _doc$2.createElementNS ? _doc$2.createElementNS(ns.replace(/^https/, "http"), type) : _doc$2.createElement(type);

    if (i) {
      if (!svg) {
        if (!_divContainer) {
          _divContainer = _createSibling(element);
          _divContainer.style.cssText = css;
        }

        e.style.cssText = css + "width:0.1px;height:0.1px;top:" + y + "px;left:" + x + "px";

        _divContainer.appendChild(e);
      } else {
        _svgContainer || (_svgContainer = _createSibling(element));
        e.setAttribute("width", 0.01);
        e.setAttribute("height", 0.01);
        e.setAttribute("transform", "translate(" + x + "," + y + ")");

        _svgContainer.appendChild(e);
      }
    }

    return e;
  }

  throw "Need document and parent.";
},
    _consolidate = function _consolidate(m) {
  // replaces SVGTransformList.consolidate() because a bug in Firefox causes it to break pointer events. See https://greensock.com/forums/topic/23248-touch-is-not-working-on-draggable-in-firefox-windows-v324/?tab=comments#comment-109800
  var c = new Matrix2D(),
      i = 0;

  for (; i < m.numberOfItems; i++) {
    c.multiply(m.getItem(i).matrix);
  }

  return c;
},
    _placeSiblings = function _placeSiblings(element, adjustGOffset) {
  var svg = _svgOwner(element),
      isRootSVG = element === svg,
      siblings = svg ? _svgTemps : _divTemps,
      parent = element.parentNode,
      container,
      m,
      b,
      x,
      y,
      cs;

  if (element === _win$3) {
    return element;
  }

  siblings.length || siblings.push(_createSibling(element, 1), _createSibling(element, 2), _createSibling(element, 3));
  container = svg ? _svgContainer : _divContainer;

  if (svg) {
    b = isRootSVG ? {
      x: 0,
      y: 0
    } : element.getBBox();
    m = element.transform ? element.transform.baseVal : {}; // IE11 doesn't follow the spec.

    if (m.numberOfItems) {
      m = m.numberOfItems > 1 ? _consolidate(m) : m.getItem(0).matrix; // don't call m.consolidate().matrix because a bug in Firefox makes pointer events not work when consolidate() is called on the same tick as getBoundingClientRect()! See https://greensock.com/forums/topic/23248-touch-is-not-working-on-draggable-in-firefox-windows-v324/?tab=comments#comment-109800

      x = m.a * b.x + m.c * b.y;
      y = m.b * b.x + m.d * b.y;
    } else {
      m = _identityMatrix$1;
      x = b.x;
      y = b.y;
    }

    if (adjustGOffset && element.tagName.toLowerCase() === "g") {
      x = y = 0;
    }

    (isRootSVG ? svg : parent).appendChild(container);
    container.setAttribute("transform", "matrix(" + m.a + "," + m.b + "," + m.c + "," + m.d + "," + (m.e + x) + "," + (m.f + y) + ")");
  } else {
    x = y = 0;

    if (_hasOffsetBug) {
      // some browsers (like Safari) have a bug that causes them to misreport offset values. When an ancestor element has a transform applied, it's supposed to treat it as if it's position: relative (new context). Safari botches this, so we need to find the closest ancestor (between the element and its offsetParent) that has a transform applied and if one is found, grab its offsetTop/Left and subtract them to compensate.
      m = element.offsetParent;
      b = element;

      while (b && (b = b.parentNode) && b !== m && b.parentNode) {
        if ((_win$3.getComputedStyle(b)[_transformProp$1] + "").length > 4) {
          x = b.offsetLeft;
          y = b.offsetTop;
          b = 0;
        }
      }
    }

    cs = _win$3.getComputedStyle(element);

    if (cs.position !== "absolute") {
      m = element.offsetParent;

      while (parent && parent !== m) {
        // if there's an ancestor element between the element and its offsetParent that's scrolled, we must factor that in.
        x += parent.scrollLeft || 0;
        y += parent.scrollTop || 0;
        parent = parent.parentNode;
      }
    }

    b = container.style;
    b.top = element.offsetTop - y + "px";
    b.left = element.offsetLeft - x + "px";
    b[_transformProp$1] = cs[_transformProp$1];
    b[_transformOriginProp$1] = cs[_transformOriginProp$1]; // b.border = m.border;
    // b.borderLeftStyle = m.borderLeftStyle;
    // b.borderTopStyle = m.borderTopStyle;
    // b.borderLeftWidth = m.borderLeftWidth;
    // b.borderTopWidth = m.borderTopWidth;

    b.position = cs.position === "fixed" ? "fixed" : "absolute";
    element.parentNode.appendChild(container);
  }

  return container;
},
    _setMatrix = function _setMatrix(m, a, b, c, d, e, f) {
  m.a = a;
  m.b = b;
  m.c = c;
  m.d = d;
  m.e = e;
  m.f = f;
  return m;
};

var Matrix2D = /*#__PURE__*/function () {
  function Matrix2D(a, b, c, d, e, f) {
    if (a === void 0) {
      a = 1;
    }

    if (b === void 0) {
      b = 0;
    }

    if (c === void 0) {
      c = 0;
    }

    if (d === void 0) {
      d = 1;
    }

    if (e === void 0) {
      e = 0;
    }

    if (f === void 0) {
      f = 0;
    }

    _setMatrix(this, a, b, c, d, e, f);
  }

  var _proto = Matrix2D.prototype;

  _proto.inverse = function inverse() {
    var a = this.a,
        b = this.b,
        c = this.c,
        d = this.d,
        e = this.e,
        f = this.f,
        determinant = a * d - b * c || 1e-10;
    return _setMatrix(this, d / determinant, -b / determinant, -c / determinant, a / determinant, (c * f - d * e) / determinant, -(a * f - b * e) / determinant);
  };

  _proto.multiply = function multiply(matrix) {
    var a = this.a,
        b = this.b,
        c = this.c,
        d = this.d,
        e = this.e,
        f = this.f,
        a2 = matrix.a,
        b2 = matrix.c,
        c2 = matrix.b,
        d2 = matrix.d,
        e2 = matrix.e,
        f2 = matrix.f;
    return _setMatrix(this, a2 * a + c2 * c, a2 * b + c2 * d, b2 * a + d2 * c, b2 * b + d2 * d, e + e2 * a + f2 * c, f + e2 * b + f2 * d);
  };

  _proto.clone = function clone() {
    return new Matrix2D(this.a, this.b, this.c, this.d, this.e, this.f);
  };

  _proto.equals = function equals(matrix) {
    var a = this.a,
        b = this.b,
        c = this.c,
        d = this.d,
        e = this.e,
        f = this.f;
    return a === matrix.a && b === matrix.b && c === matrix.c && d === matrix.d && e === matrix.e && f === matrix.f;
  };

  _proto.apply = function apply(point, decoratee) {
    if (decoratee === void 0) {
      decoratee = {};
    }

    var x = point.x,
        y = point.y,
        a = this.a,
        b = this.b,
        c = this.c,
        d = this.d,
        e = this.e,
        f = this.f;
    decoratee.x = x * a + y * c + e || 0;
    decoratee.y = x * b + y * d + f || 0;
    return decoratee;
  };

  return Matrix2D;
}(); // Feed in an element and it'll return a 2D matrix (optionally inverted) so that you can translate between coordinate spaces.
// Inverting lets you translate a global point into a local coordinate space. No inverting lets you go the other way.
// We needed this to work around various browser bugs, like Firefox doesn't accurately report getScreenCTM() when there
// are transforms applied to ancestor elements.
// The matrix math to convert any x/y coordinate is as follows, which is wrapped in a convenient apply() method of Matrix2D above:
//     tx = m.a * x + m.c * y + m.e
//     ty = m.b * x + m.d * y + m.f

function getGlobalMatrix(element, inverse, adjustGOffset, includeScrollInFixed) {
  // adjustGOffset is typically used only when grabbing an element's PARENT's global matrix, and it ignores the x/y offset of any SVG <g> elements because they behave in a special way.
  if (!element || !element.parentNode || (_doc$2 || _setDoc(element)).documentElement === element) {
    return new Matrix2D();
  }

  var zeroScales = _forceNonZeroScale(element),
      svg = _svgOwner(element),
      temps = svg ? _svgTemps : _divTemps,
      container = _placeSiblings(element, adjustGOffset),
      b1 = temps[0].getBoundingClientRect(),
      b2 = temps[1].getBoundingClientRect(),
      b3 = temps[2].getBoundingClientRect(),
      parent = container.parentNode,
      isFixed = !includeScrollInFixed && _isFixed$1(element),
      m = new Matrix2D((b2.left - b1.left) / 100, (b2.top - b1.top) / 100, (b3.left - b1.left) / 100, (b3.top - b1.top) / 100, b1.left + (isFixed ? 0 : _getDocScrollLeft$1()), b1.top + (isFixed ? 0 : _getDocScrollTop$1()));

  parent.removeChild(container);

  if (zeroScales) {
    b1 = zeroScales.length;

    while (b1--) {
      b2 = zeroScales[b1];
      b2.scaleX = b2.scaleY = 0;
      b2.renderTransform(1, b2);
    }
  }

  return inverse ? m.inverse() : m;
}
// 	_doc || _setDoc(element);
// 	let m = (_win.getComputedStyle(element)[_transformProp] + "").substr(7).match(/[-.]*\d+[.e\-+]*\d*[e\-\+]*\d*/g),
// 		is2D = m && m.length === 6;
// 	return !m || m.length < 6 ? new Matrix2D() : new Matrix2D(+m[0], +m[1], +m[is2D ? 2 : 4], +m[is2D ? 3 : 5], +m[is2D ? 4 : 12], +m[is2D ? 5 : 13]);
// }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _inheritsLoose(subClass, superClass) { subClass.prototype = Object.create(superClass.prototype); subClass.prototype.constructor = subClass; subClass.__proto__ = superClass; }

var gsap$6,
    _win$2,
    _doc$1,
    _docElement,
    _body,
    _tempDiv$1,
    _placeholderDiv,
    _coreInitted$2,
    _checkPrefix,
    _toArray$1,
    _supportsPassive,
    _isTouchDevice,
    _touchEventLookup,
    _dragCount,
    _isMultiTouching,
    _isAndroid,
    InertiaPlugin,
    _defaultCursor,
    _supportsPointer,
    _windowExists$3 = function _windowExists() {
  return typeof window !== "undefined";
},
    _getGSAP$6 = function _getGSAP() {
  return gsap$6 || _windowExists$3() && (gsap$6 = window.gsap) && gsap$6.registerPlugin && gsap$6;
},
    _isFunction$1 = function _isFunction(value) {
  return typeof value === "function";
},
    _isObject = function _isObject(value) {
  return typeof value === "object";
},
    _isUndefined$1 = function _isUndefined(value) {
  return typeof value === "undefined";
},
    _emptyFunc$1 = function _emptyFunc() {
  return false;
},
    _transformProp = "transform",
    _transformOriginProp = "transformOrigin",
    _round$1 = function _round(value) {
  return Math.round(value * 10000) / 10000;
},
    _isArray = Array.isArray,
    _createElement = function _createElement(type, ns) {
  var e = _doc$1.createElementNS ? _doc$1.createElementNS((ns || "http://www.w3.org/1999/xhtml").replace(/^https/, "http"), type) : _doc$1.createElement(type); //some servers swap in https for http in the namespace which can break things, making "style" inaccessible.

  return e.style ? e : _doc$1.createElement(type); //some environments won't allow access to the element's style when created with a namespace in which case we default to the standard createElement() to work around the issue. Also note that when GSAP is embedded directly inside an SVG file, createElement() won't allow access to the style object in Firefox (see https://greensock.com/forums/topic/20215-problem-using-tweenmax-in-standalone-self-containing-svg-file-err-cannot-set-property-csstext-of-undefined/).
},
    _RAD2DEG$1 = 180 / Math.PI,
    _bigNum = 1e20,
    _identityMatrix = new Matrix2D(),
    _getTime = Date.now || function () {
  return new Date().getTime();
},
    _renderQueue = [],
    _lookup = {},
    //when a Draggable is created, the target gets a unique _gsDragID property that allows gets associated with the Draggable instance for quick lookups in Draggable.get(). This avoids circular references that could cause gc problems.
_lookupCount = 0,
    _clickableTagExp = /^(?:a|input|textarea|button|select)$/i,
    _lastDragTime = 0,
    _temp1 = {},
    // a simple object we reuse and populate (usually x/y properties) to conserve memory and improve performance.
_windowProxy = {},
    //memory/performance optimization - we reuse this object during autoScroll to store window-related bounds/offsets.
_copy$1 = function _copy(obj, factor) {
  var copy = {},
      p;

  for (p in obj) {
    copy[p] = factor ? obj[p] * factor : obj[p];
  }

  return copy;
},
    _extend = function _extend(obj, defaults) {
  for (var p in defaults) {
    if (!(p in obj)) {
      obj[p] = defaults[p];
    }
  }

  return obj;
},
    _setTouchActionForAllDescendants = function _setTouchActionForAllDescendants(elements, value) {
  var i = elements.length,
      children;

  while (i--) {
    value ? elements[i].style.touchAction = value : elements[i].style.removeProperty("touch-action");
    children = elements[i].children;
    children && children.length && _setTouchActionForAllDescendants(children, value);
  }
},
    _renderQueueTick = function _renderQueueTick() {
  return _renderQueue.forEach(function (func) {
    return func();
  });
},
    _addToRenderQueue = function _addToRenderQueue(func) {
  _renderQueue.push(func);

  if (_renderQueue.length === 1) {
    gsap$6.ticker.add(_renderQueueTick);
  }
},
    _renderQueueTimeout = function _renderQueueTimeout() {
  return !_renderQueue.length && gsap$6.ticker.remove(_renderQueueTick);
},
    _removeFromRenderQueue = function _removeFromRenderQueue(func) {
  var i = _renderQueue.length;

  while (i--) {
    if (_renderQueue[i] === func) {
      _renderQueue.splice(i, 1);
    }
  }

  gsap$6.to(_renderQueueTimeout, {
    overwrite: true,
    delay: 15,
    duration: 0,
    onComplete: _renderQueueTimeout,
    data: "_draggable"
  }); //remove the "tick" listener only after the render queue is empty for 15 seconds (to improve performance). Adding/removing it constantly for every click/touch wouldn't deliver optimal speed, and we also don't want the ticker to keep calling the render method when things are idle for long periods of time (we want to improve battery life on mobile devices).
},
    _setDefaults = function _setDefaults(obj, defaults) {
  for (var p in defaults) {
    if (!(p in obj)) {
      obj[p] = defaults[p];
    }
  }

  return obj;
},
    _addListener = function _addListener(element, type, func, capture) {
  if (element.addEventListener) {
    var touchType = _touchEventLookup[type];
    capture = capture || (_supportsPassive ? {
      passive: false
    } : null);
    element.addEventListener(touchType || type, func, capture);
    touchType && type !== touchType && element.addEventListener(type, func, capture); //some browsers actually support both, so must we. But pointer events cover all.
  }
},
    _removeListener = function _removeListener(element, type, func) {
  if (element.removeEventListener) {
    var touchType = _touchEventLookup[type];
    element.removeEventListener(touchType || type, func);
    touchType && type !== touchType && element.removeEventListener(type, func);
  }
},
    _preventDefault = function _preventDefault(event) {
  event.preventDefault && event.preventDefault();
  event.preventManipulation && event.preventManipulation(); //for some Microsoft browsers
},
    _hasTouchID = function _hasTouchID(list, ID) {
  var i = list.length;

  while (i--) {
    if (list[i].identifier === ID) {
      return true;
    }
  }
},
    _onMultiTouchDocumentEnd = function _onMultiTouchDocumentEnd(event) {
  _isMultiTouching = event.touches && _dragCount < event.touches.length;

  _removeListener(event.target, "touchend", _onMultiTouchDocumentEnd);
},
    _onMultiTouchDocument = function _onMultiTouchDocument(event) {
  _isMultiTouching = event.touches && _dragCount < event.touches.length;

  _addListener(event.target, "touchend", _onMultiTouchDocumentEnd);
},
    _getDocScrollTop = function _getDocScrollTop(doc) {
  return _win$2.pageYOffset || doc.scrollTop || doc.documentElement.scrollTop || doc.body.scrollTop || 0;
},
    _getDocScrollLeft = function _getDocScrollLeft(doc) {
  return _win$2.pageXOffset || doc.scrollLeft || doc.documentElement.scrollLeft || doc.body.scrollLeft || 0;
},
    _addScrollListener = function _addScrollListener(e, callback) {
  _addListener(e, "scroll", callback);

  if (!_isRoot(e.parentNode)) {
    _addScrollListener(e.parentNode, callback);
  }
},
    _removeScrollListener = function _removeScrollListener(e, callback) {
  _removeListener(e, "scroll", callback);

  if (!_isRoot(e.parentNode)) {
    _removeScrollListener(e.parentNode, callback);
  }
},
    _isRoot = function _isRoot(e) {
  return !!(!e || e === _docElement || e.nodeType === 9 || e === _doc$1.body || e === _win$2 || !e.nodeType || !e.parentNode);
},
    _getMaxScroll = function _getMaxScroll(element, axis) {
  var dim = axis === "x" ? "Width" : "Height",
      scroll = "scroll" + dim,
      client = "client" + dim;
  return Math.max(0, _isRoot(element) ? Math.max(_docElement[scroll], _body[scroll]) - (_win$2["inner" + dim] || _docElement[client] || _body[client]) : element[scroll] - element[client]);
},
    _recordMaxScrolls = function _recordMaxScrolls(e, skipCurrent) {
  //records _gsMaxScrollX and _gsMaxScrollY properties for the element and all ancestors up the chain so that we can cap it, otherwise dragging beyond the edges with autoScroll on can endlessly scroll.
  var x = _getMaxScroll(e, "x"),
      y = _getMaxScroll(e, "y");

  if (_isRoot(e)) {
    e = _windowProxy;
  } else {
    _recordMaxScrolls(e.parentNode, skipCurrent);
  }

  e._gsMaxScrollX = x;
  e._gsMaxScrollY = y;

  if (!skipCurrent) {
    e._gsScrollX = e.scrollLeft || 0;
    e._gsScrollY = e.scrollTop || 0;
  }
},
    _setStyle = function _setStyle(element, property, value) {
  var style = element.style;

  if (!style) {
    return;
  }

  if (_isUndefined$1(style[property])) {
    property = _checkPrefix(property, element) || property;
  }

  if (value == null) {
    style.removeProperty && style.removeProperty(property.replace(/([A-Z])/g, "-$1").toLowerCase());
  } else {
    style[property] = value;
  }
},
    _getComputedStyle = function _getComputedStyle(element) {
  return _win$2.getComputedStyle(element instanceof Element ? element : element.host || (element.parentNode || {}).host || element);
},
    //the "host" stuff helps to accommodate ShadowDom objects.
_tempRect = {},
    //reuse to reduce garbage collection tasks
_parseRect = function _parseRect(e) {
  //accepts a DOM element, a mouse event, or a rectangle object and returns the corresponding rectangle with left, right, width, height, top, and bottom properties
  if (e === _win$2) {
    _tempRect.left = _tempRect.top = 0;
    _tempRect.width = _tempRect.right = _docElement.clientWidth || e.innerWidth || _body.clientWidth || 0;
    _tempRect.height = _tempRect.bottom = (e.innerHeight || 0) - 20 < _docElement.clientHeight ? _docElement.clientHeight : e.innerHeight || _body.clientHeight || 0;
    return _tempRect;
  }

  var doc = e.ownerDocument || _doc$1,
      r = !_isUndefined$1(e.pageX) ? {
    left: e.pageX - _getDocScrollLeft(doc),
    top: e.pageY - _getDocScrollTop(doc),
    right: e.pageX - _getDocScrollLeft(doc) + 1,
    bottom: e.pageY - _getDocScrollTop(doc) + 1
  } : !e.nodeType && !_isUndefined$1(e.left) && !_isUndefined$1(e.top) ? e : _toArray$1(e)[0].getBoundingClientRect();

  if (_isUndefined$1(r.right) && !_isUndefined$1(r.width)) {
    r.right = r.left + r.width;
    r.bottom = r.top + r.height;
  } else if (_isUndefined$1(r.width)) {
    //some browsers don't include width and height properties. We can't just set them directly on r because some browsers throw errors, so create a new generic object.
    r = {
      width: r.right - r.left,
      height: r.bottom - r.top,
      right: r.right,
      left: r.left,
      bottom: r.bottom,
      top: r.top
    };
  }

  return r;
},
    _dispatchEvent = function _dispatchEvent(target, type, callbackName) {
  var vars = target.vars,
      callback = vars[callbackName],
      listeners = target._listeners[type],
      result;

  if (_isFunction$1(callback)) {
    result = callback.apply(vars.callbackScope || target, vars[callbackName + "Params"] || [target.pointerEvent]);
  }

  if (listeners && target.dispatchEvent(type) === false) {
    result = false;
  }

  return result;
},
    _getBounds = function _getBounds(target, context) {
  //accepts any of the following: a DOM element, jQuery object, selector text, or an object defining bounds as {top, left, width, height} or {minX, maxX, minY, maxY}. Returns an object with left, top, width, and height properties.
  var e = _toArray$1(target)[0],
      top,
      left,
      offset;

  if (!e.nodeType && e !== _win$2) {
    if (!_isUndefined$1(target.left)) {
      offset = {
        x: 0,
        y: 0
      }; //_getOffsetTransformOrigin(context); //the bounds should be relative to the origin

      return {
        left: target.left - offset.x,
        top: target.top - offset.y,
        width: target.width,
        height: target.height
      };
    }

    left = target.min || target.minX || target.minRotation || 0;
    top = target.min || target.minY || 0;
    return {
      left: left,
      top: top,
      width: (target.max || target.maxX || target.maxRotation || 0) - left,
      height: (target.max || target.maxY || 0) - top
    };
  }

  return _getElementBounds(e, context);
},
    _point1 = {},
    //we reuse to minimize garbage collection tasks.
_getElementBounds = function _getElementBounds(element, context) {
  context = _toArray$1(context)[0];
  var isSVG = element.getBBox && element.ownerSVGElement,
      doc = element.ownerDocument || _doc$1,
      left,
      right,
      top,
      bottom,
      matrix,
      p1,
      p2,
      p3,
      p4,
      bbox,
      width,
      height,
      cs,
      contextParent;

  if (element === _win$2) {
    top = _getDocScrollTop(doc);
    left = _getDocScrollLeft(doc);
    right = left + (doc.documentElement.clientWidth || element.innerWidth || doc.body.clientWidth || 0);
    bottom = top + ((element.innerHeight || 0) - 20 < doc.documentElement.clientHeight ? doc.documentElement.clientHeight : element.innerHeight || doc.body.clientHeight || 0); //some browsers (like Firefox) ignore absolutely positioned elements, and collapse the height of the documentElement, so it could be 8px, for example, if you have just an absolutely positioned div. In that case, we use the innerHeight to resolve this.
  } else if (context === _win$2 || _isUndefined$1(context)) {
    return element.getBoundingClientRect();
  } else {
    left = top = 0;

    if (isSVG) {
      bbox = element.getBBox();
      width = bbox.width;
      height = bbox.height;
    } else {
      if (element.viewBox && (bbox = element.viewBox.baseVal)) {
        left = bbox.x || 0;
        top = bbox.y || 0;
        width = bbox.width;
        height = bbox.height;
      }

      if (!width) {
        cs = _getComputedStyle(element);
        bbox = cs.boxSizing === "border-box";
        width = (parseFloat(cs.width) || element.clientWidth || 0) + (bbox ? 0 : parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth));
        height = (parseFloat(cs.height) || element.clientHeight || 0) + (bbox ? 0 : parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth));
      }
    }

    right = width;
    bottom = height;
  }

  if (element === context) {
    return {
      left: left,
      top: top,
      width: right - left,
      height: bottom - top
    };
  }

  matrix = getGlobalMatrix(context, true).multiply(getGlobalMatrix(element));
  p1 = matrix.apply({
    x: left,
    y: top
  });
  p2 = matrix.apply({
    x: right,
    y: top
  });
  p3 = matrix.apply({
    x: right,
    y: bottom
  });
  p4 = matrix.apply({
    x: left,
    y: bottom
  });
  left = Math.min(p1.x, p2.x, p3.x, p4.x);
  top = Math.min(p1.y, p2.y, p3.y, p4.y);
  contextParent = context.parentNode || {};
  return {
    left: left + (contextParent.scrollLeft || 0),
    top: top + (contextParent.scrollTop || 0),
    width: Math.max(p1.x, p2.x, p3.x, p4.x) - left,
    height: Math.max(p1.y, p2.y, p3.y, p4.y) - top
  };
},
    _parseInertia = function _parseInertia(draggable, snap, max, min, factor, forceZeroVelocity) {
  var vars = {},
      a,
      i,
      l;

  if (snap) {
    if (factor !== 1 && snap instanceof Array) {
      //some data must be altered to make sense, like if the user passes in an array of rotational values in degrees, we must convert it to radians. Or for scrollLeft and scrollTop, we invert the values.
      vars.end = a = [];
      l = snap.length;

      if (_isObject(snap[0])) {
        //if the array is populated with objects, like points ({x:100, y:200}), make copies before multiplying by the factor, otherwise we'll mess up the originals and the user may reuse it elsewhere.
        for (i = 0; i < l; i++) {
          a[i] = _copy$1(snap[i], factor);
        }
      } else {
        for (i = 0; i < l; i++) {
          a[i] = snap[i] * factor;
        }
      }

      max += 1.1; //allow 1.1 pixels of wiggle room when snapping in order to work around some browser inconsistencies in the way bounds are reported which can make them roughly a pixel off. For example, if "snap:[-$('#menu').width(), 0]" was defined and #menu had a wrapper that was used as the bounds, some browsers would be one pixel off, making the minimum -752 for example when snap was [-753,0], thus instead of snapping to -753, it would snap to 0 since -753 was below the minimum.

      min -= 1.1;
    } else if (_isFunction$1(snap)) {
      vars.end = function (value) {
        var result = snap.call(draggable, value),
            copy,
            p;

        if (factor !== 1) {
          if (_isObject(result)) {
            copy = {};

            for (p in result) {
              copy[p] = result[p] * factor;
            }

            result = copy;
          } else {
            result *= factor;
          }
        }

        return result; //we need to ensure that we can scope the function call to the Draggable instance itself so that users can access important values like maxX, minX, maxY, minY, x, and y from within that function.
      };
    } else {
      vars.end = snap;
    }
  }

  if (max || max === 0) {
    vars.max = max;
  }

  if (min || min === 0) {
    vars.min = min;
  }

  if (forceZeroVelocity) {
    vars.velocity = 0;
  }

  return vars;
},
    _isClickable = function _isClickable(element) {
  //sometimes it's convenient to mark an element as clickable by adding a data-clickable="true" attribute (in which case we won't preventDefault() the mouse/touch event). This method checks if the element is an <a>, <input>, or <button> or has an onclick or has the data-clickable or contentEditable attribute set to true (or any of its parent elements).
  var data;
  return !element || !element.getAttribute || element === _body ? false : (data = element.getAttribute("data-clickable")) === "true" || data !== "false" && (element.onclick || _clickableTagExp.test(element.nodeName + "") || element.getAttribute("contentEditable") === "true") ? true : _isClickable(element.parentNode);
},
    _setSelectable = function _setSelectable(elements, selectable) {
  var i = elements.length,
      e;

  while (i--) {
    e = elements[i];
    e.ondragstart = e.onselectstart = selectable ? null : _emptyFunc$1;
    gsap$6.set(e, {
      lazy: true,
      userSelect: selectable ? "text" : "none"
    });
  }
},
    _isFixed = function _isFixed(element) {
  if (_getComputedStyle(element).position === "fixed") {
    return true;
  }

  element = element.parentNode;

  if (element && element.nodeType === 1) {
    // avoid document fragments which will throw an error.
    return _isFixed(element);
  }
},
    _supports3D,
    _addPaddingBR,
    //The ScrollProxy class wraps an element's contents into another div (we call it "content") that we either add padding when necessary or apply a translate3d() transform in order to overscroll (scroll past the boundaries). This allows us to simply set the scrollTop/scrollLeft (or top/left for easier reverse-axis orientation, which is what we do in Draggable) and it'll do all the work for us. For example, if we tried setting scrollTop to -100 on a normal DOM element, it wouldn't work - it'd look the same as setting it to 0, but if we set scrollTop of a ScrollProxy to -100, it'll give the correct appearance by either setting paddingTop of the wrapper to 100 or applying a 100-pixel translateY.
ScrollProxy = function ScrollProxy(element, vars) {
  element = gsap$6.utils.toArray(element)[0];
  vars = vars || {};
  var content = document.createElement("div"),
      style = content.style,
      node = element.firstChild,
      offsetTop = 0,
      offsetLeft = 0,
      prevTop = element.scrollTop,
      prevLeft = element.scrollLeft,
      scrollWidth = element.scrollWidth,
      scrollHeight = element.scrollHeight,
      extraPadRight = 0,
      maxLeft = 0,
      maxTop = 0,
      elementWidth,
      elementHeight,
      contentHeight,
      nextNode,
      transformStart,
      transformEnd;

  if (_supports3D && vars.force3D !== false) {
    transformStart = "translate3d(";
    transformEnd = "px,0px)";
  } else if (_transformProp) {
    transformStart = "translate(";
    transformEnd = "px)";
  }

  this.scrollTop = function (value, force) {
    if (!arguments.length) {
      return -this.top();
    }

    this.top(-value, force);
  };

  this.scrollLeft = function (value, force) {
    if (!arguments.length) {
      return -this.left();
    }

    this.left(-value, force);
  };

  this.left = function (value, force) {
    if (!arguments.length) {
      return -(element.scrollLeft + offsetLeft);
    }

    var dif = element.scrollLeft - prevLeft,
        oldOffset = offsetLeft;

    if ((dif > 2 || dif < -2) && !force) {
      //if the user interacts with the scrollbar (or something else scrolls it, like the mouse wheel), we should kill any tweens of the ScrollProxy.
      prevLeft = element.scrollLeft;
      gsap$6.killTweensOf(this, {
        left: 1,
        scrollLeft: 1
      });
      this.left(-prevLeft);

      if (vars.onKill) {
        vars.onKill();
      }

      return;
    }

    value = -value; //invert because scrolling works in the opposite direction

    if (value < 0) {
      offsetLeft = value - 0.5 | 0;
      value = 0;
    } else if (value > maxLeft) {
      offsetLeft = value - maxLeft | 0;
      value = maxLeft;
    } else {
      offsetLeft = 0;
    }

    if (offsetLeft || oldOffset) {
      if (!this._skip) {
        style[_transformProp] = transformStart + -offsetLeft + "px," + -offsetTop + transformEnd;
      }

      if (offsetLeft + extraPadRight >= 0) {
        style.paddingRight = offsetLeft + extraPadRight + "px";
      }
    }

    element.scrollLeft = value | 0;
    prevLeft = element.scrollLeft; //don't merge this with the line above because some browsers adjust the scrollLeft after it's set, so in order to be 100% accurate in tracking it, we need to ask the browser to report it.
  };

  this.top = function (value, force) {
    if (!arguments.length) {
      return -(element.scrollTop + offsetTop);
    }

    var dif = element.scrollTop - prevTop,
        oldOffset = offsetTop;

    if ((dif > 2 || dif < -2) && !force) {
      //if the user interacts with the scrollbar (or something else scrolls it, like the mouse wheel), we should kill any tweens of the ScrollProxy.
      prevTop = element.scrollTop;
      gsap$6.killTweensOf(this, {
        top: 1,
        scrollTop: 1
      });
      this.top(-prevTop);

      if (vars.onKill) {
        vars.onKill();
      }

      return;
    }

    value = -value; //invert because scrolling works in the opposite direction

    if (value < 0) {
      offsetTop = value - 0.5 | 0;
      value = 0;
    } else if (value > maxTop) {
      offsetTop = value - maxTop | 0;
      value = maxTop;
    } else {
      offsetTop = 0;
    }

    if (offsetTop || oldOffset) {
      if (!this._skip) {
        style[_transformProp] = transformStart + -offsetLeft + "px," + -offsetTop + transformEnd;
      }
    }

    element.scrollTop = value | 0;
    prevTop = element.scrollTop;
  };

  this.maxScrollTop = function () {
    return maxTop;
  };

  this.maxScrollLeft = function () {
    return maxLeft;
  };

  this.disable = function () {
    node = content.firstChild;

    while (node) {
      nextNode = node.nextSibling;
      element.appendChild(node);
      node = nextNode;
    }

    if (element === content.parentNode) {
      //in case disable() is called when it's already disabled.
      element.removeChild(content);
    }
  };

  this.enable = function () {
    node = element.firstChild;

    if (node === content) {
      return;
    }

    while (node) {
      nextNode = node.nextSibling;
      content.appendChild(node);
      node = nextNode;
    }

    element.appendChild(content);
    this.calibrate();
  };

  this.calibrate = function (force) {
    var widthMatches = element.clientWidth === elementWidth,
        cs,
        x,
        y;
    prevTop = element.scrollTop;
    prevLeft = element.scrollLeft;

    if (widthMatches && element.clientHeight === elementHeight && content.offsetHeight === contentHeight && scrollWidth === element.scrollWidth && scrollHeight === element.scrollHeight && !force) {
      return; //no need to recalculate things if the width and height haven't changed.
    }

    if (offsetTop || offsetLeft) {
      x = this.left();
      y = this.top();
      this.left(-element.scrollLeft);
      this.top(-element.scrollTop);
    }

    cs = _getComputedStyle(element); //first, we need to remove any width constraints to see how the content naturally flows so that we can see if it's wider than the containing element. If so, we've got to record the amount of overage so that we can apply that as padding in order for browsers to correctly handle things. Then we switch back to a width of 100% (without that, some browsers don't flow the content correctly)

    if (!widthMatches || force) {
      style.display = "block";
      style.width = "auto";
      style.paddingRight = "0px";
      extraPadRight = Math.max(0, element.scrollWidth - element.clientWidth); //if the content is wider than the container, we need to add the paddingLeft and paddingRight in order for things to behave correctly.

      if (extraPadRight) {
        extraPadRight += parseFloat(cs.paddingLeft) + (_addPaddingBR ? parseFloat(cs.paddingRight) : 0);
      }
    }

    style.display = "inline-block";
    style.position = "relative";
    style.overflow = "visible";
    style.verticalAlign = "top";
    style.boxSizing = "content-box";
    style.width = "100%";
    style.paddingRight = extraPadRight + "px"; //some browsers neglect to factor in the bottom padding when calculating the scrollHeight, so we need to add that padding to the content when that happens. Allow a 2px margin for error

    if (_addPaddingBR) {
      style.paddingBottom = cs.paddingBottom;
    }

    elementWidth = element.clientWidth;
    elementHeight = element.clientHeight;
    scrollWidth = element.scrollWidth;
    scrollHeight = element.scrollHeight;
    maxLeft = element.scrollWidth - elementWidth;
    maxTop = element.scrollHeight - elementHeight;
    contentHeight = content.offsetHeight;
    style.display = "block";

    if (x || y) {
      this.left(x);
      this.top(y);
    }
  };

  this.content = content;
  this.element = element;
  this._skip = false;
  this.enable();
},
    _initCore$4 = function _initCore(required) {
  if (_windowExists$3() && document.body) {
    var nav = window && window.navigator;
    _win$2 = window;
    _doc$1 = document;
    _docElement = _doc$1.documentElement;
    _body = _doc$1.body;
    _tempDiv$1 = _createElement("div");
    _supportsPointer = !!window.PointerEvent;
    _placeholderDiv = _createElement("div");
    _placeholderDiv.style.cssText = "visibility:hidden;height:1px;top:-1px;pointer-events:none;position:relative;clear:both;cursor:grab";
    _defaultCursor = _placeholderDiv.style.cursor === "grab" ? "grab" : "move";
    _isAndroid = nav && nav.userAgent.toLowerCase().indexOf("android") !== -1; //Android handles touch events in an odd way and it's virtually impossible to "feature test" so we resort to UA sniffing

    _isTouchDevice = "ontouchstart" in _docElement && "orientation" in _win$2 || nav && (nav.MaxTouchPoints > 0 || nav.msMaxTouchPoints > 0);

    _addPaddingBR = function () {
      //this function is in charge of analyzing browser behavior related to padding. It sets the _addPaddingBR to true if the browser doesn't normally factor in the bottom or right padding on the element inside the scrolling area, and it sets _addPaddingLeft to true if it's a browser that requires the extra offset (offsetLeft) to be added to the paddingRight (like Opera).
      var div = _createElement("div"),
          child = _createElement("div"),
          childStyle = child.style,
          parent = _body,
          val;

      childStyle.display = "inline-block";
      childStyle.position = "relative";
      div.style.cssText = child.innerHTML = "width:90px;height:40px;padding:10px;overflow:auto;visibility:hidden";
      div.appendChild(child);
      parent.appendChild(div);
      val = child.offsetHeight + 18 > div.scrollHeight; //div.scrollHeight should be child.offsetHeight + 20 because of the 10px of padding on each side, but some browsers ignore one side. We allow a 2px margin of error.

      parent.removeChild(div);
      return val;
    }();

    _touchEventLookup = function (types) {
      //we create an object that makes it easy to translate touch event types into their "pointer" counterparts if we're in a browser that uses those instead. Like IE10 uses "MSPointerDown" instead of "touchstart", for example.
      var standard = types.split(","),
          converted = ("onpointerdown" in _tempDiv$1 ? "pointerdown,pointermove,pointerup,pointercancel" : "onmspointerdown" in _tempDiv$1 ? "MSPointerDown,MSPointerMove,MSPointerUp,MSPointerCancel" : types).split(","),
          obj = {},
          i = 4;

      while (--i > -1) {
        obj[standard[i]] = converted[i];
        obj[converted[i]] = standard[i];
      } //to avoid problems in iOS 9, test to see if the browser supports the "passive" option on addEventListener().


      try {
        _docElement.addEventListener("test", null, Object.defineProperty({}, "passive", {
          get: function get() {
            _supportsPassive = 1;
          }
        }));
      } catch (e) {}

      return obj;
    }("touchstart,touchmove,touchend,touchcancel");

    _addListener(_doc$1, "touchcancel", _emptyFunc$1); //some older Android devices intermittently stop dispatching "touchmove" events if we don't listen for "touchcancel" on the document. Very strange indeed.


    _addListener(_win$2, "touchmove", _emptyFunc$1); //works around Safari bugs that still allow the page to scroll even when we preventDefault() on the touchmove event.


    _body && _body.addEventListener("touchstart", _emptyFunc$1); //works around Safari bug: https://greensock.com/forums/topic/21450-draggable-in-iframe-on-mobile-is-buggy/

    _addListener(_doc$1, "contextmenu", function () {
      for (var p in _lookup) {
        if (_lookup[p].isPressed) {
          _lookup[p].endDrag();
        }
      }
    });

    gsap$6 = _coreInitted$2 = _getGSAP$6();
  }

  if (gsap$6) {
    InertiaPlugin = gsap$6.plugins.inertia;
    _checkPrefix = gsap$6.utils.checkPrefix;
    _transformProp = _checkPrefix(_transformProp);
    _transformOriginProp = _checkPrefix(_transformOriginProp);
    _toArray$1 = gsap$6.utils.toArray;
    _supports3D = !!_checkPrefix("perspective");
  } else if (required) {
    console.warn("Please gsap.registerPlugin(Draggable)");
  }
};

var EventDispatcher = /*#__PURE__*/function () {
  function EventDispatcher(target) {
    this._listeners = {};
    this.target = target || this;
  }

  var _proto = EventDispatcher.prototype;

  _proto.addEventListener = function addEventListener(type, callback) {
    var list = this._listeners[type] || (this._listeners[type] = []);

    if (!~list.indexOf(callback)) {
      list.push(callback);
    }
  };

  _proto.removeEventListener = function removeEventListener(type, callback) {
    var list = this._listeners[type],
        i = list && list.indexOf(callback) || -1;
    i > -1 && list.splice(i, 1);
  };

  _proto.dispatchEvent = function dispatchEvent(type) {
    var _this = this;

    var result;
    (this._listeners[type] || []).forEach(function (callback) {
      return callback.call(_this, {
        type: type,
        target: _this.target
      }) === false && (result = false);
    });
    return result; //if any of the callbacks return false, pass that along.
  };

  return EventDispatcher;
}();

var Draggable = /*#__PURE__*/function (_EventDispatcher) {
  _inheritsLoose(Draggable, _EventDispatcher);

  function Draggable(target, vars) {
    var _this2;

    _this2 = _EventDispatcher.call(this) || this;
    _coreInitted$2 || _initCore$4(1);
    target = _toArray$1(target)[0]; //in case the target is a selector object or selector text

    if (!InertiaPlugin) {
      InertiaPlugin = gsap$6.plugins.inertia;
    }

    _this2.vars = vars = _copy$1(vars || {});
    _this2.target = target;
    _this2.x = _this2.y = _this2.rotation = 0;
    _this2.dragResistance = parseFloat(vars.dragResistance) || 0;
    _this2.edgeResistance = isNaN(vars.edgeResistance) ? 1 : parseFloat(vars.edgeResistance) || 0;
    _this2.lockAxis = vars.lockAxis;
    _this2.autoScroll = vars.autoScroll || 0;
    _this2.lockedAxis = null;
    _this2.allowEventDefault = !!vars.allowEventDefault;
    gsap$6.getProperty(target, "x"); // to ensure that transforms are instantiated.

    var type = (vars.type || "x,y").toLowerCase(),
        xyMode = ~type.indexOf("x") || ~type.indexOf("y"),
        rotationMode = type.indexOf("rotation") !== -1,
        xProp = rotationMode ? "rotation" : xyMode ? "x" : "left",
        yProp = xyMode ? "y" : "top",
        allowX = !!(~type.indexOf("x") || ~type.indexOf("left") || type === "scroll"),
        allowY = !!(~type.indexOf("y") || ~type.indexOf("top") || type === "scroll"),
        minimumMovement = vars.minimumMovement || 2,
        self = _assertThisInitialized(_this2),
        triggers = _toArray$1(vars.trigger || vars.handle || target),
        killProps = {},
        dragEndTime = 0,
        checkAutoScrollBounds = false,
        autoScrollMarginTop = vars.autoScrollMarginTop || 40,
        autoScrollMarginRight = vars.autoScrollMarginRight || 40,
        autoScrollMarginBottom = vars.autoScrollMarginBottom || 40,
        autoScrollMarginLeft = vars.autoScrollMarginLeft || 40,
        isClickable = vars.clickableTest || _isClickable,
        clickTime = 0,
        gsCache = target._gsap || gsap$6.core.getCache(target),
        isFixed = _isFixed(target),
        getPropAsNum = function getPropAsNum(property, unit) {
      return parseFloat(gsCache.get(target, property, unit));
    },
        ownerDoc = target.ownerDocument || _doc$1,
        enabled,
        scrollProxy,
        startPointerX,
        startPointerY,
        startElementX,
        startElementY,
        hasBounds,
        hasDragCallback,
        hasMoveCallback,
        maxX,
        minX,
        maxY,
        minY,
        touch,
        touchID,
        rotationOrigin,
        dirty,
        old,
        snapX,
        snapY,
        snapXY,
        isClicking,
        touchEventTarget,
        matrix,
        interrupted,
        allowNativeTouchScrolling,
        touchDragAxis,
        isDispatching,
        clickDispatch,
        trustedClickDispatch,
        isPreventingDefault,
        onContextMenu = function onContextMenu(e) {
      //used to prevent long-touch from triggering a context menu.
      // (self.isPressed && e.which < 2) && self.endDrag() // previously ended drag when context menu was triggered, but instead we should just stop propagation and prevent the default event behavior.
      _preventDefault(e);

      e.stopImmediatePropagation && e.stopImmediatePropagation();
      return false;
    },
        //this method gets called on every tick of TweenLite.ticker which allows us to synchronize the renders to the core engine (which is typically synchronized with the display refresh via requestAnimationFrame). This is an optimization - it's better than applying the values inside the "mousemove" or "touchmove" event handler which may get called many times inbetween refreshes.
    render = function render(suppressEvents) {
      if (self.autoScroll && self.isDragging && (checkAutoScrollBounds || dirty)) {
        var e = target,
            autoScrollFactor = self.autoScroll * 15,
            //multiplying by 15 just gives us a better "feel" speed-wise.
        parent,
            isRoot,
            rect,
            pointerX,
            pointerY,
            changeX,
            changeY,
            gap;
        checkAutoScrollBounds = false;
        _windowProxy.scrollTop = _win$2.pageYOffset != null ? _win$2.pageYOffset : ownerDoc.documentElement.scrollTop != null ? ownerDoc.documentElement.scrollTop : ownerDoc.body.scrollTop;
        _windowProxy.scrollLeft = _win$2.pageXOffset != null ? _win$2.pageXOffset : ownerDoc.documentElement.scrollLeft != null ? ownerDoc.documentElement.scrollLeft : ownerDoc.body.scrollLeft;
        pointerX = self.pointerX - _windowProxy.scrollLeft;
        pointerY = self.pointerY - _windowProxy.scrollTop;

        while (e && !isRoot) {
          //walk up the chain and sense wherever the pointer is within 40px of an edge that's scrollable.
          isRoot = _isRoot(e.parentNode);
          parent = isRoot ? _windowProxy : e.parentNode;
          rect = isRoot ? {
            bottom: Math.max(_docElement.clientHeight, _win$2.innerHeight || 0),
            right: Math.max(_docElement.clientWidth, _win$2.innerWidth || 0),
            left: 0,
            top: 0
          } : parent.getBoundingClientRect();
          changeX = changeY = 0;

          if (allowY) {
            gap = parent._gsMaxScrollY - parent.scrollTop;

            if (gap < 0) {
              changeY = gap;
            } else if (pointerY > rect.bottom - autoScrollMarginBottom && gap) {
              checkAutoScrollBounds = true;
              changeY = Math.min(gap, autoScrollFactor * (1 - Math.max(0, rect.bottom - pointerY) / autoScrollMarginBottom) | 0);
            } else if (pointerY < rect.top + autoScrollMarginTop && parent.scrollTop) {
              checkAutoScrollBounds = true;
              changeY = -Math.min(parent.scrollTop, autoScrollFactor * (1 - Math.max(0, pointerY - rect.top) / autoScrollMarginTop) | 0);
            }

            if (changeY) {
              parent.scrollTop += changeY;
            }
          }

          if (allowX) {
            gap = parent._gsMaxScrollX - parent.scrollLeft;

            if (gap < 0) {
              changeX = gap;
            } else if (pointerX > rect.right - autoScrollMarginRight && gap) {
              checkAutoScrollBounds = true;
              changeX = Math.min(gap, autoScrollFactor * (1 - Math.max(0, rect.right - pointerX) / autoScrollMarginRight) | 0);
            } else if (pointerX < rect.left + autoScrollMarginLeft && parent.scrollLeft) {
              checkAutoScrollBounds = true;
              changeX = -Math.min(parent.scrollLeft, autoScrollFactor * (1 - Math.max(0, pointerX - rect.left) / autoScrollMarginLeft) | 0);
            }

            if (changeX) {
              parent.scrollLeft += changeX;
            }
          }

          if (isRoot && (changeX || changeY)) {
            _win$2.scrollTo(parent.scrollLeft, parent.scrollTop);

            setPointerPosition(self.pointerX + changeX, self.pointerY + changeY);
          }

          e = parent;
        }
      }

      if (dirty) {
        var x = self.x,
            y = self.y;

        if (rotationMode) {
          self.deltaX = x - parseFloat(gsCache.rotation);
          self.rotation = x;
          gsCache.rotation = x + "deg";
          gsCache.renderTransform(1, gsCache);
        } else {
          if (scrollProxy) {
            if (allowY) {
              self.deltaY = y - scrollProxy.top();
              scrollProxy.top(y);
            }

            if (allowX) {
              self.deltaX = x - scrollProxy.left();
              scrollProxy.left(x);
            }
          } else if (xyMode) {
            if (allowY) {
              self.deltaY = y - parseFloat(gsCache.y);
              gsCache.y = y + "px";
            }

            if (allowX) {
              self.deltaX = x - parseFloat(gsCache.x);
              gsCache.x = x + "px";
            }

            gsCache.renderTransform(1, gsCache);
          } else {
            if (allowY) {
              self.deltaY = y - parseFloat(target.style.top || 0);
              target.style.top = y + "px";
            }

            if (allowX) {
              self.deltaX = x - parseFloat(target.style.left || 0);
              target.style.left = x + "px";
            }
          }
        }

        if (hasDragCallback && !suppressEvents && !isDispatching) {
          isDispatching = true; //in case onDrag has an update() call (avoid endless loop)

          if (_dispatchEvent(self, "drag", "onDrag") === false) {
            if (allowX) {
              self.x -= self.deltaX;
            }

            if (allowY) {
              self.y -= self.deltaY;
            }

            render(true);
          }

          isDispatching = false;
        }
      }

      dirty = false;
    },
        //copies the x/y from the element (whether that be transforms, top/left, or ScrollProxy's top/left) to the Draggable's x and y (and rotation if necessary) properties so that they reflect reality and it also (optionally) applies any snapping necessary. This is used by the InertiaPlugin tween in an onUpdate to ensure things are synced and snapped.
    syncXY = function syncXY(skipOnUpdate, skipSnap) {
      var x = self.x,
          y = self.y,
          snappedValue,
          cs;

      if (!target._gsap) {
        //just in case the _gsap cache got wiped, like if the user called clearProps on the transform or something (very rare).
        gsCache = gsap$6.core.getCache(target);
      }

      gsCache.uncache && gsap$6.getProperty(target, "x"); // trigger a re-cache

      if (xyMode) {
        self.x = parseFloat(gsCache.x);
        self.y = parseFloat(gsCache.y);
      } else if (rotationMode) {
        self.x = self.rotation = parseFloat(gsCache.rotation);
      } else if (scrollProxy) {
        self.y = scrollProxy.top();
        self.x = scrollProxy.left();
      } else {
        self.y = parseFloat(target.style.top || (cs = _getComputedStyle(target)) && cs.top) || 0;
        self.x = parseFloat(target.style.left || (cs || {}).left) || 0;
      }

      if ((snapX || snapY || snapXY) && !skipSnap && (self.isDragging || self.isThrowing)) {
        if (snapXY) {
          _temp1.x = self.x;
          _temp1.y = self.y;
          snappedValue = snapXY(_temp1);

          if (snappedValue.x !== self.x) {
            self.x = snappedValue.x;
            dirty = true;
          }

          if (snappedValue.y !== self.y) {
            self.y = snappedValue.y;
            dirty = true;
          }
        }

        if (snapX) {
          snappedValue = snapX(self.x);

          if (snappedValue !== self.x) {
            self.x = snappedValue;

            if (rotationMode) {
              self.rotation = snappedValue;
            }

            dirty = true;
          }
        }

        if (snapY) {
          snappedValue = snapY(self.y);

          if (snappedValue !== self.y) {
            self.y = snappedValue;
          }

          dirty = true;
        }
      }

      dirty && render(true);

      if (!skipOnUpdate) {
        self.deltaX = self.x - x;
        self.deltaY = self.y - y;

        _dispatchEvent(self, "throwupdate", "onThrowUpdate");
      }
    },
        buildSnapFunc = function buildSnapFunc(snap, min, max, factor) {
      if (min == null) {
        min = -_bigNum;
      }

      if (max == null) {
        max = _bigNum;
      }

      if (_isFunction$1(snap)) {
        return function (n) {
          var edgeTolerance = !self.isPressed ? 1 : 1 - self.edgeResistance; //if we're tweening, disable the edgeTolerance because it's already factored into the tweening values (we don't want to apply it multiple times)

          return snap.call(self, n > max ? max + (n - max) * edgeTolerance : n < min ? min + (n - min) * edgeTolerance : n) * factor;
        };
      }

      if (_isArray(snap)) {
        return function (n) {
          var i = snap.length,
              closest = 0,
              absDif = _bigNum,
              val,
              dif;

          while (--i > -1) {
            val = snap[i];
            dif = val - n;

            if (dif < 0) {
              dif = -dif;
            }

            if (dif < absDif && val >= min && val <= max) {
              closest = i;
              absDif = dif;
            }
          }

          return snap[closest];
        };
      }

      return isNaN(snap) ? function (n) {
        return n;
      } : function () {
        return snap * factor;
      };
    },
        buildPointSnapFunc = function buildPointSnapFunc(snap, minX, maxX, minY, maxY, radius, factor) {
      radius = radius && radius < _bigNum ? radius * radius : _bigNum; //so we don't have to Math.sqrt() in the functions. Performance optimization.

      if (_isFunction$1(snap)) {
        return function (point) {
          var edgeTolerance = !self.isPressed ? 1 : 1 - self.edgeResistance,
              x = point.x,
              y = point.y,
              result,
              dx,
              dy; //if we're tweening, disable the edgeTolerance because it's already factored into the tweening values (we don't want to apply it multiple times)

          point.x = x = x > maxX ? maxX + (x - maxX) * edgeTolerance : x < minX ? minX + (x - minX) * edgeTolerance : x;
          point.y = y = y > maxY ? maxY + (y - maxY) * edgeTolerance : y < minY ? minY + (y - minY) * edgeTolerance : y;
          result = snap.call(self, point);

          if (result !== point) {
            point.x = result.x;
            point.y = result.y;
          }

          if (factor !== 1) {
            point.x *= factor;
            point.y *= factor;
          }

          if (radius < _bigNum) {
            dx = point.x - x;
            dy = point.y - y;

            if (dx * dx + dy * dy > radius) {
              point.x = x;
              point.y = y;
            }
          }

          return point;
        };
      }

      if (_isArray(snap)) {
        return function (p) {
          var i = snap.length,
              closest = 0,
              minDist = _bigNum,
              x,
              y,
              point,
              dist;

          while (--i > -1) {
            point = snap[i];
            x = point.x - p.x;
            y = point.y - p.y;
            dist = x * x + y * y;

            if (dist < minDist) {
              closest = i;
              minDist = dist;
            }
          }

          return minDist <= radius ? snap[closest] : p;
        };
      }

      return function (n) {
        return n;
      };
    },
        calculateBounds = function calculateBounds() {
      var bounds, targetBounds, snap, snapIsRaw;
      hasBounds = false;

      if (scrollProxy) {
        scrollProxy.calibrate();
        self.minX = minX = -scrollProxy.maxScrollLeft();
        self.minY = minY = -scrollProxy.maxScrollTop();
        self.maxX = maxX = self.maxY = maxY = 0;
        hasBounds = true;
      } else if (!!vars.bounds) {
        bounds = _getBounds(vars.bounds, target.parentNode); //could be a selector/jQuery object or a DOM element or a generic object like {top:0, left:100, width:1000, height:800} or {minX:100, maxX:1100, minY:0, maxY:800}

        if (rotationMode) {
          self.minX = minX = bounds.left;
          self.maxX = maxX = bounds.left + bounds.width;
          self.minY = minY = self.maxY = maxY = 0;
        } else if (!_isUndefined$1(vars.bounds.maxX) || !_isUndefined$1(vars.bounds.maxY)) {
          bounds = vars.bounds;
          self.minX = minX = bounds.minX;
          self.minY = minY = bounds.minY;
          self.maxX = maxX = bounds.maxX;
          self.maxY = maxY = bounds.maxY;
        } else {
          targetBounds = _getBounds(target, target.parentNode);
          self.minX = minX = Math.round(getPropAsNum(xProp, "px") + bounds.left - targetBounds.left - 0.5);
          self.minY = minY = Math.round(getPropAsNum(yProp, "px") + bounds.top - targetBounds.top - 0.5);
          self.maxX = maxX = Math.round(minX + (bounds.width - targetBounds.width));
          self.maxY = maxY = Math.round(minY + (bounds.height - targetBounds.height));
        }

        if (minX > maxX) {
          self.minX = maxX;
          self.maxX = maxX = minX;
          minX = self.minX;
        }

        if (minY > maxY) {
          self.minY = maxY;
          self.maxY = maxY = minY;
          minY = self.minY;
        }

        if (rotationMode) {
          self.minRotation = minX;
          self.maxRotation = maxX;
        }

        hasBounds = true;
      }

      if (vars.liveSnap) {
        snap = vars.liveSnap === true ? vars.snap || {} : vars.liveSnap;
        snapIsRaw = _isArray(snap) || _isFunction$1(snap);

        if (rotationMode) {
          snapX = buildSnapFunc(snapIsRaw ? snap : snap.rotation, minX, maxX, 1);
          snapY = null;
        } else {
          if (snap.points) {
            snapXY = buildPointSnapFunc(snapIsRaw ? snap : snap.points, minX, maxX, minY, maxY, snap.radius, scrollProxy ? -1 : 1);
          } else {
            if (allowX) {
              snapX = buildSnapFunc(snapIsRaw ? snap : snap.x || snap.left || snap.scrollLeft, minX, maxX, scrollProxy ? -1 : 1);
            }

            if (allowY) {
              snapY = buildSnapFunc(snapIsRaw ? snap : snap.y || snap.top || snap.scrollTop, minY, maxY, scrollProxy ? -1 : 1);
            }
          }
        }
      }
    },
        onThrowComplete = function onThrowComplete() {
      self.isThrowing = false;

      _dispatchEvent(self, "throwcomplete", "onThrowComplete");
    },
        onThrowInterrupt = function onThrowInterrupt() {
      self.isThrowing = false;
    },
        animate = function animate(inertia, forceZeroVelocity) {
      var snap, snapIsRaw, tween, overshootTolerance;

      if (inertia && InertiaPlugin) {
        if (inertia === true) {
          snap = vars.snap || vars.liveSnap || {};
          snapIsRaw = _isArray(snap) || _isFunction$1(snap);
          inertia = {
            resistance: (vars.throwResistance || vars.resistance || 1000) / (rotationMode ? 10 : 1)
          };

          if (rotationMode) {
            inertia.rotation = _parseInertia(self, snapIsRaw ? snap : snap.rotation, maxX, minX, 1, forceZeroVelocity);
          } else {
            if (allowX) {
              inertia[xProp] = _parseInertia(self, snapIsRaw ? snap : snap.points || snap.x || snap.left, maxX, minX, scrollProxy ? -1 : 1, forceZeroVelocity || self.lockedAxis === "x");
            }

            if (allowY) {
              inertia[yProp] = _parseInertia(self, snapIsRaw ? snap : snap.points || snap.y || snap.top, maxY, minY, scrollProxy ? -1 : 1, forceZeroVelocity || self.lockedAxis === "y");
            }

            if (snap.points || _isArray(snap) && _isObject(snap[0])) {
              inertia.linkedProps = xProp + "," + yProp;
              inertia.radius = snap.radius; //note: we also disable liveSnapping while throwing if there's a "radius" defined, otherwise it looks weird to have the item thrown past a snapping point but live-snapping mid-tween. We do this by altering the onUpdateParams so that "skipSnap" parameter is true for syncXY.
            }
          }
        }

        self.isThrowing = true;
        overshootTolerance = !isNaN(vars.overshootTolerance) ? vars.overshootTolerance : vars.edgeResistance === 1 ? 0 : 1 - self.edgeResistance + 0.2;

        if (!inertia.duration) {
          inertia.duration = {
            max: Math.max(vars.minDuration || 0, "maxDuration" in vars ? vars.maxDuration : 2),
            min: !isNaN(vars.minDuration) ? vars.minDuration : overshootTolerance === 0 || _isObject(inertia) && inertia.resistance > 1000 ? 0 : 0.5,
            overshoot: overshootTolerance
          };
        }

        self.tween = tween = gsap$6.to(scrollProxy || target, {
          inertia: inertia,
          data: "_draggable",
          onComplete: onThrowComplete,
          onInterrupt: onThrowInterrupt,
          onUpdate: vars.fastMode ? _dispatchEvent : syncXY,
          onUpdateParams: vars.fastMode ? [self, "onthrowupdate", "onThrowUpdate"] : snap && snap.radius ? [false, true] : []
        });

        if (!vars.fastMode) {
          if (scrollProxy) {
            scrollProxy._skip = true; // Microsoft browsers have a bug that causes them to briefly render the position incorrectly (it flashes to the end state when we seek() the tween even though we jump right back to the current position, and this only seems to happen when we're affecting both top and left), so we set a _suspendTransforms flag to prevent it from actually applying the values in the ScrollProxy.
          }

          tween.render(1e9, true, true); // force to the end. Remember, the duration will likely change upon initting because that's when InertiaPlugin calculates it.

          syncXY(true, true);
          self.endX = self.x;
          self.endY = self.y;

          if (rotationMode) {
            self.endRotation = self.x;
          }

          tween.play(0);
          syncXY(true, true);

          if (scrollProxy) {
            scrollProxy._skip = false; //Microsoft browsers have a bug that causes them to briefly render the position incorrectly (it flashes to the end state when we seek() the tween even though we jump right back to the current position, and this only seems to happen when we're affecting both top and left), so we set a _suspendTransforms flag to prevent it from actually applying the values in the ScrollProxy.
          }
        }
      } else if (hasBounds) {
        self.applyBounds();
      }
    },
        updateMatrix = function updateMatrix(shiftStart) {
      var start = matrix,
          p;
      matrix = getGlobalMatrix(target.parentNode, true);

      if (shiftStart && self.isPressed && !matrix.equals(start || new Matrix2D())) {
        //if the matrix changes WHILE the element is pressed, we must adjust the startPointerX and startPointerY accordingly, so we invert the original matrix and figure out where the pointerX and pointerY were in the global space, then apply the new matrix to get the updated coordinates.
        p = start.inverse().apply({
          x: startPointerX,
          y: startPointerY
        });
        matrix.apply(p, p);
        startPointerX = p.x;
        startPointerY = p.y;
      }

      if (matrix.equals(_identityMatrix)) {
        //if there are no transforms, we can optimize performance by not factoring in the matrix
        matrix = null;
      }
    },
        recordStartPositions = function recordStartPositions() {
      var edgeTolerance = 1 - self.edgeResistance,
          offsetX = isFixed ? _getDocScrollLeft(ownerDoc) : 0,
          offsetY = isFixed ? _getDocScrollTop(ownerDoc) : 0,
          parsedOrigin,
          x,
          y;
      updateMatrix(false);
      _point1.x = self.pointerX - offsetX;
      _point1.y = self.pointerY - offsetY;
      matrix && matrix.apply(_point1, _point1);
      startPointerX = _point1.x; //translate to local coordinate system

      startPointerY = _point1.y;

      if (dirty) {
        setPointerPosition(self.pointerX, self.pointerY);
        render(true);
      }

      if (scrollProxy) {
        calculateBounds();
        startElementY = scrollProxy.top();
        startElementX = scrollProxy.left();
      } else {
        //if the element is in the process of tweening, don't force snapping to occur because it could make it jump. Imagine the user throwing, then before it's done, clicking on the element in its inbetween state.
        if (isTweening()) {
          syncXY(true, true);
          calculateBounds();
        } else {
          self.applyBounds();
        }

        if (rotationMode) {
          parsedOrigin = target.ownerSVGElement ? [gsCache.xOrigin - target.getBBox().x, gsCache.yOrigin - target.getBBox().y] : (_getComputedStyle(target)[_transformOriginProp] || "0 0").split(" ");
          rotationOrigin = self.rotationOrigin = getGlobalMatrix(target).apply({
            x: parseFloat(parsedOrigin[0]) || 0,
            y: parseFloat(parsedOrigin[1]) || 0
          });
          syncXY(true, true);
          x = self.pointerX - rotationOrigin.x - offsetX;
          y = rotationOrigin.y - self.pointerY + offsetY;
          startElementX = self.x; //starting rotation (x always refers to rotation in type:"rotation", measured in degrees)

          startElementY = self.y = Math.atan2(y, x) * _RAD2DEG$1;
        } else {
          //parent = !isFixed && target.parentNode;
          //startScrollTop = parent ? parent.scrollTop || 0 : 0;
          //startScrollLeft = parent ? parent.scrollLeft || 0 : 0;
          startElementY = getPropAsNum(yProp, "px"); //record the starting top and left values so that we can just add the mouse's movement to them later.

          startElementX = getPropAsNum(xProp, "px");
        }
      }

      if (hasBounds && edgeTolerance) {
        if (startElementX > maxX) {
          startElementX = maxX + (startElementX - maxX) / edgeTolerance;
        } else if (startElementX < minX) {
          startElementX = minX - (minX - startElementX) / edgeTolerance;
        }

        if (!rotationMode) {
          if (startElementY > maxY) {
            startElementY = maxY + (startElementY - maxY) / edgeTolerance;
          } else if (startElementY < minY) {
            startElementY = minY - (minY - startElementY) / edgeTolerance;
          }
        }
      }

      self.startX = startElementX = _round$1(startElementX);
      self.startY = startElementY = _round$1(startElementY);
    },
        isTweening = function isTweening() {
      return self.tween && self.tween.isActive();
    },
        removePlaceholder = function removePlaceholder() {
      if (_placeholderDiv.parentNode && !isTweening() && !self.isDragging) {
        //_placeholderDiv just props open auto-scrolling containers so they don't collapse as the user drags left/up. We remove it after dragging (and throwing, if necessary) finishes.
        _placeholderDiv.parentNode.removeChild(_placeholderDiv);
      }
    },
        //called when the mouse is pressed (or touch starts)
    onPress = function onPress(e, force) {
      var i;

      if (!enabled || self.isPressed || !e || (e.type === "mousedown" || e.type === "pointerdown") && !force && _getTime() - clickTime < 30 && _touchEventLookup[self.pointerEvent.type]) {
        //when we DON'T preventDefault() in order to accommodate touch-scrolling and the user just taps, many browsers also fire a mousedown/mouseup sequence AFTER the touchstart/touchend sequence, thus it'd result in two quick "click" events being dispatched. This line senses that condition and halts it on the subsequent mousedown.
        isPreventingDefault && e && enabled && _preventDefault(e); // in some browsers, we must listen for multiple event types like touchstart, pointerdown, mousedown. The first time this function is called, we record whether or not we _preventDefault() so that on duplicate calls, we can do the same if necessary.

        return;
      }

      interrupted = isTweening();
      self.pointerEvent = e;

      if (_touchEventLookup[e.type]) {
        //note: on iOS, BOTH touchmove and mousemove are dispatched, but the mousemove has pageY and pageX of 0 which would mess up the calculations and needlessly hurt performance.
        touchEventTarget = ~e.type.indexOf("touch") ? e.currentTarget || e.target : ownerDoc; //pointer-based touches (for Microsoft browsers) don't remain locked to the original target like other browsers, so we must use the document instead. The event type would be "MSPointerDown" or "pointerdown".

        _addListener(touchEventTarget, "touchend", onRelease);

        _addListener(touchEventTarget, "touchmove", onMove);

        _addListener(touchEventTarget, "touchcancel", onRelease);

        _addListener(ownerDoc, "touchstart", _onMultiTouchDocument);
      } else {
        touchEventTarget = null;

        _addListener(ownerDoc, "mousemove", onMove); //attach these to the document instead of the box itself so that if the user's mouse moves too quickly (and off of the box), things still work.

      }

      touchDragAxis = null;

      if (!_supportsPointer || !touchEventTarget) {
        _addListener(ownerDoc, "mouseup", onRelease);

        e && e.target && _addListener(e.target, "mouseup", onRelease); //we also have to listen directly on the element because some browsers don't bubble up the event to the _doc on elements with contentEditable="true"
      }

      isClicking = isClickable.call(self, e.target) && vars.dragClickables === false && !force;

      if (isClicking) {
        _addListener(e.target, "change", onRelease); //in some browsers, when you mousedown on a <select> element, no mouseup gets dispatched! So we listen for a "change" event instead.


        _dispatchEvent(self, "pressInit", "onPressInit");

        _dispatchEvent(self, "press", "onPress");

        _setSelectable(triggers, true); //accommodates things like inputs and elements with contentEditable="true" (otherwise user couldn't drag to select text)


        isPreventingDefault = false;
        return;
      }

      allowNativeTouchScrolling = !touchEventTarget || allowX === allowY || self.vars.allowNativeTouchScrolling === false || self.vars.allowContextMenu && e && (e.ctrlKey || e.which > 2) ? false : allowX ? "y" : "x"; //note: in Chrome, right-clicking (for a context menu) fires onPress and it doesn't have the event.which set properly, so we must look for event.ctrlKey. If the user wants to allow context menus we should of course sense it here and not allow native touch scrolling.

      isPreventingDefault = !allowNativeTouchScrolling && !self.allowEventDefault;

      if (isPreventingDefault) {
        _preventDefault(e);

        _addListener(_win$2, "touchforcechange", _preventDefault); //works around safari bug: https://greensock.com/forums/topic/21450-draggable-in-iframe-on-mobile-is-buggy/

      }

      if (e.changedTouches) {
        //touch events store the data slightly differently
        e = touch = e.changedTouches[0];
        touchID = e.identifier;
      } else if (e.pointerId) {
        touchID = e.pointerId; //for some Microsoft browsers
      } else {
        touch = touchID = null;
      }

      _dragCount++;

      _addToRenderQueue(render); //causes the Draggable to render on each "tick" of TweenLite.ticker (performance optimization - updating values in a mousemove can cause them to happen too frequently, like multiple times between frame redraws which is wasteful, and it also prevents values from updating properly in IE8)


      startPointerY = self.pointerY = e.pageY; //record the starting x and y so that we can calculate the movement from the original in _onMouseMove

      startPointerX = self.pointerX = e.pageX;

      _dispatchEvent(self, "pressInit", "onPressInit");

      if (allowNativeTouchScrolling || self.autoScroll) {
        _recordMaxScrolls(target.parentNode);
      }

      if (target.parentNode && self.autoScroll && !scrollProxy && !rotationMode && target.parentNode._gsMaxScrollX && !_placeholderDiv.parentNode && !target.getBBox) {
        //add a placeholder div to prevent the parent container from collapsing when the user drags the element left.
        _placeholderDiv.style.width = target.parentNode.scrollWidth + "px";
        target.parentNode.appendChild(_placeholderDiv);
      }

      recordStartPositions();
      self.tween && self.tween.kill();
      self.isThrowing = false;
      gsap$6.killTweensOf(scrollProxy || target, killProps, true); //in case the user tries to drag it before the last tween is done.

      scrollProxy && gsap$6.killTweensOf(target, {
        scrollTo: 1
      }, true); //just in case the original target's scroll position is being tweened somewhere else.

      self.tween = self.lockedAxis = null;

      if (vars.zIndexBoost || !rotationMode && !scrollProxy && vars.zIndexBoost !== false) {
        target.style.zIndex = Draggable.zIndex++;
      }

      self.isPressed = true;
      hasDragCallback = !!(vars.onDrag || self._listeners.drag);
      hasMoveCallback = !!(vars.onMove || self._listeners.move);

      if (!rotationMode && (vars.cursor !== false || vars.activeCursor)) {
        i = triggers.length;

        while (--i > -1) {
          gsap$6.set(triggers[i], {
            cursor: vars.activeCursor || vars.cursor || (_defaultCursor === "grab" ? "grabbing" : _defaultCursor)
          });
        }
      }

      _dispatchEvent(self, "press", "onPress");
    },
        //called every time the mouse/touch moves
    onMove = function onMove(e) {
      var originalEvent = e,
          touches,
          pointerX,
          pointerY,
          i,
          dx,
          dy;

      if (!enabled || _isMultiTouching || !self.isPressed || !e) {
        isPreventingDefault && e && enabled && _preventDefault(e); // in some browsers, we must listen for multiple event types like touchmove, pointermove, mousemove. The first time this function is called, we record whether or not we _preventDefault() so that on duplicate calls, we can do the same if necessary.

        return;
      }

      self.pointerEvent = e;
      touches = e.changedTouches;

      if (touches) {
        //touch events store the data slightly differently
        e = touches[0];

        if (e !== touch && e.identifier !== touchID) {
          //Usually changedTouches[0] will be what we're looking for, but in case it's not, look through the rest of the array...(and Android browsers don't reuse the event like iOS)
          i = touches.length;

          while (--i > -1 && (e = touches[i]).identifier !== touchID && e.target !== target) {} // Some Android devices dispatch a touchstart AND pointerdown initially, and then only pointermove thus the touchID may not match because it was grabbed from the touchstart event whereas the pointer event is the one that the browser dispatches for move, so if the event target matches this Draggable's target, let it through.


          if (i < 0) {
            return;
          }
        }
      } else if (e.pointerId && touchID && e.pointerId !== touchID) {
        //for some Microsoft browsers, we must attach the listener to the doc rather than the trigger so that when the finger moves outside the bounds of the trigger, things still work. So if the event we're receiving has a pointerId that doesn't match the touchID, ignore it (for multi-touch)
        return;
      }

      if (touchEventTarget && allowNativeTouchScrolling && !touchDragAxis) {
        //Android browsers force us to decide on the first "touchmove" event if we should allow the default (scrolling) behavior or preventDefault(). Otherwise, a "touchcancel" will be fired and then no "touchmove" or "touchend" will fire during the scrolling (no good).
        _point1.x = e.pageX;
        _point1.y = e.pageY;
        matrix && matrix.apply(_point1, _point1);
        pointerX = _point1.x;
        pointerY = _point1.y;
        dx = Math.abs(pointerX - startPointerX);
        dy = Math.abs(pointerY - startPointerY);

        if (dx !== dy && (dx > minimumMovement || dy > minimumMovement) || _isAndroid && allowNativeTouchScrolling === touchDragAxis) {
          touchDragAxis = dx > dy && allowX ? "x" : "y";

          if (allowNativeTouchScrolling && touchDragAxis !== allowNativeTouchScrolling) {
            _addListener(_win$2, "touchforcechange", _preventDefault); // prevents native touch scrolling from taking over if the user started dragging in the other direction in iOS Safari

          }

          if (self.vars.lockAxisOnTouchScroll !== false && allowX && allowY) {
            self.lockedAxis = touchDragAxis === "x" ? "y" : "x";
            _isFunction$1(self.vars.onLockAxis) && self.vars.onLockAxis.call(self, originalEvent);
          }

          if (_isAndroid && allowNativeTouchScrolling === touchDragAxis) {
            onRelease(originalEvent);
            return;
          }
        }
      }

      if (!self.allowEventDefault && (!allowNativeTouchScrolling || touchDragAxis && allowNativeTouchScrolling !== touchDragAxis) && originalEvent.cancelable !== false) {
        _preventDefault(originalEvent);

        isPreventingDefault = true;
      } else if (isPreventingDefault) {
        isPreventingDefault = false;
      }

      if (self.autoScroll) {
        checkAutoScrollBounds = true;
      }

      setPointerPosition(e.pageX, e.pageY, hasMoveCallback);
    },
        setPointerPosition = function setPointerPosition(pointerX, pointerY, invokeOnMove) {
      var dragTolerance = 1 - self.dragResistance,
          edgeTolerance = 1 - self.edgeResistance,
          prevPointerX = self.pointerX,
          prevPointerY = self.pointerY,
          prevStartElementY = startElementY,
          prevX = self.x,
          prevY = self.y,
          prevEndX = self.endX,
          prevEndY = self.endY,
          prevEndRotation = self.endRotation,
          prevDirty = dirty,
          xChange,
          yChange,
          x,
          y,
          dif,
          temp;
      self.pointerX = pointerX;
      self.pointerY = pointerY;

      if (isFixed) {
        pointerX -= _getDocScrollLeft(ownerDoc);
        pointerY -= _getDocScrollTop(ownerDoc);
      }

      if (rotationMode) {
        y = Math.atan2(rotationOrigin.y - pointerY, pointerX - rotationOrigin.x) * _RAD2DEG$1;
        dif = self.y - y;

        if (dif > 180) {
          startElementY -= 360;
          self.y = y;
        } else if (dif < -180) {
          startElementY += 360;
          self.y = y;
        }

        if (self.x !== startElementX || Math.abs(startElementY - y) > minimumMovement) {
          self.y = y;
          x = startElementX + (startElementY - y) * dragTolerance;
        } else {
          x = startElementX;
        }
      } else {
        if (matrix) {
          temp = pointerX * matrix.a + pointerY * matrix.c + matrix.e;
          pointerY = pointerX * matrix.b + pointerY * matrix.d + matrix.f;
          pointerX = temp;
        }

        yChange = pointerY - startPointerY;
        xChange = pointerX - startPointerX;

        if (yChange < minimumMovement && yChange > -minimumMovement) {
          yChange = 0;
        }

        if (xChange < minimumMovement && xChange > -minimumMovement) {
          xChange = 0;
        }

        if ((self.lockAxis || self.lockedAxis) && (xChange || yChange)) {
          temp = self.lockedAxis;

          if (!temp) {
            self.lockedAxis = temp = allowX && Math.abs(xChange) > Math.abs(yChange) ? "y" : allowY ? "x" : null;

            if (temp && _isFunction$1(self.vars.onLockAxis)) {
              self.vars.onLockAxis.call(self, self.pointerEvent);
            }
          }

          if (temp === "y") {
            yChange = 0;
          } else if (temp === "x") {
            xChange = 0;
          }
        }

        x = _round$1(startElementX + xChange * dragTolerance);
        y = _round$1(startElementY + yChange * dragTolerance);
      }

      if ((snapX || snapY || snapXY) && (self.x !== x || self.y !== y && !rotationMode)) {
        if (snapXY) {
          _temp1.x = x;
          _temp1.y = y;
          temp = snapXY(_temp1);
          x = _round$1(temp.x);
          y = _round$1(temp.y);
        }

        if (snapX) {
          x = _round$1(snapX(x));
        }

        if (snapY) {
          y = _round$1(snapY(y));
        }
      } else if (hasBounds) {
        if (x > maxX) {
          x = maxX + Math.round((x - maxX) * edgeTolerance);
        } else if (x < minX) {
          x = minX + Math.round((x - minX) * edgeTolerance);
        }

        if (!rotationMode) {
          if (y > maxY) {
            y = Math.round(maxY + (y - maxY) * edgeTolerance);
          } else if (y < minY) {
            y = Math.round(minY + (y - minY) * edgeTolerance);
          }
        }
      }

      if (self.x !== x || self.y !== y && !rotationMode) {
        if (rotationMode) {
          self.endRotation = self.x = self.endX = x;
          dirty = true;
        } else {
          if (allowY) {
            self.y = self.endY = y;
            dirty = true; //a flag that indicates we need to render the target next time the TweenLite.ticker dispatches a "tick" event (typically on a requestAnimationFrame) - this is a performance optimization (we shouldn't render on every move because sometimes many move events can get dispatched between screen refreshes, and that'd be wasteful to render every time)
          }

          if (allowX) {
            self.x = self.endX = x;
            dirty = true;
          }
        }

        if (!invokeOnMove || _dispatchEvent(self, "move", "onMove") !== false) {
          if (!self.isDragging && self.isPressed) {
            self.isDragging = true;

            _dispatchEvent(self, "dragstart", "onDragStart");
          }
        } else {
          //revert because the onMove returned false!
          self.pointerX = prevPointerX;
          self.pointerY = prevPointerY;
          startElementY = prevStartElementY;
          self.x = prevX;
          self.y = prevY;
          self.endX = prevEndX;
          self.endY = prevEndY;
          self.endRotation = prevEndRotation;
          dirty = prevDirty;
        }
      }
    },
        //called when the mouse/touch is released
    onRelease = function onRelease(e, force) {
      if (!enabled || !self.isPressed || e && touchID != null && !force && (e.pointerId && e.pointerId !== touchID && e.target !== target || e.changedTouches && !_hasTouchID(e.changedTouches, touchID))) {
        //for some Microsoft browsers, we must attach the listener to the doc rather than the trigger so that when the finger moves outside the bounds of the trigger, things still work. So if the event we're receiving has a pointerId that doesn't match the touchID, ignore it (for multi-touch)
        isPreventingDefault && e && enabled && _preventDefault(e); // in some browsers, we must listen for multiple event types like touchend, pointerup, mouseup. The first time this function is called, we record whether or not we _preventDefault() so that on duplicate calls, we can do the same if necessary.

        return;
      }

      self.isPressed = false;
      var originalEvent = e,
          wasDragging = self.isDragging,
          isContextMenuRelease = self.vars.allowContextMenu && e && (e.ctrlKey || e.which > 2),
          placeholderDelayedCall = gsap$6.delayedCall(0.001, removePlaceholder),
          touches,
          i,
          syntheticEvent,
          eventTarget,
          syntheticClick;

      if (touchEventTarget) {
        _removeListener(touchEventTarget, "touchend", onRelease);

        _removeListener(touchEventTarget, "touchmove", onMove);

        _removeListener(touchEventTarget, "touchcancel", onRelease);

        _removeListener(ownerDoc, "touchstart", _onMultiTouchDocument);
      } else {
        _removeListener(ownerDoc, "mousemove", onMove);
      }

      _removeListener(_win$2, "touchforcechange", _preventDefault);

      if (!_supportsPointer || !touchEventTarget) {
        _removeListener(ownerDoc, "mouseup", onRelease);

        e && e.target && _removeListener(e.target, "mouseup", onRelease);
      }

      dirty = false;

      if (wasDragging) {
        dragEndTime = _lastDragTime = _getTime();
        self.isDragging = false;
      }

      if (isClicking && !isContextMenuRelease) {
        if (e) {
          _removeListener(e.target, "change", onRelease);

          self.pointerEvent = originalEvent;
        }

        _setSelectable(triggers, false);

        _dispatchEvent(self, "release", "onRelease");

        _dispatchEvent(self, "click", "onClick");

        isClicking = false;
        return;
      }

      _removeFromRenderQueue(render);

      if (!rotationMode) {
        i = triggers.length;

        while (--i > -1) {
          _setStyle(triggers[i], "cursor", vars.cursor || (vars.cursor !== false ? _defaultCursor : null));
        }
      }

      _dragCount--;

      if (e) {
        touches = e.changedTouches;

        if (touches) {
          //touch events store the data slightly differently
          e = touches[0];

          if (e !== touch && e.identifier !== touchID) {
            //Usually changedTouches[0] will be what we're looking for, but in case it's not, look through the rest of the array...(and Android browsers don't reuse the event like iOS)
            i = touches.length;

            while (--i > -1 && (e = touches[i]).identifier !== touchID && e.target !== target) {}

            if (i < 0) {
              return;
            }
          }
        }

        self.pointerEvent = originalEvent;
        self.pointerX = e.pageX;
        self.pointerY = e.pageY;
      }

      if (isContextMenuRelease && originalEvent) {
        _preventDefault(originalEvent);

        isPreventingDefault = true;

        _dispatchEvent(self, "release", "onRelease");
      } else if (originalEvent && !wasDragging) {
        isPreventingDefault = false;

        if (interrupted && (vars.snap || vars.bounds)) {
          //otherwise, if the user clicks on the object while it's animating to a snapped position, and then releases without moving 3 pixels, it will just stay there (it should animate/snap)
          animate(vars.inertia || vars.throwProps);
        }

        _dispatchEvent(self, "release", "onRelease");

        if ((!_isAndroid || originalEvent.type !== "touchmove") && originalEvent.type.indexOf("cancel") === -1) {
          //to accommodate native scrolling on Android devices, we have to immediately call onRelease() on the first touchmove event, but that shouldn't trigger a "click".
          _dispatchEvent(self, "click", "onClick");

          if (_getTime() - clickTime < 300) {
            _dispatchEvent(self, "doubleclick", "onDoubleClick");
          }

          eventTarget = originalEvent.target || target; //old IE uses srcElement

          clickTime = _getTime();

          syntheticClick = function syntheticClick() {
            // some browsers (like Firefox) won't trust script-generated clicks, so if the user tries to click on a video to play it, for example, it simply won't work. Since a regular "click" event will most likely be generated anyway (one that has its isTrusted flag set to true), we must slightly delay our script-generated click so that the "real"/trusted one is prioritized. Remember, when there are duplicate events in quick succession, we suppress all but the first one. Some browsers don't even trigger the "real" one at all, so our synthetic one is a safety valve that ensures that no matter what, a click event does get dispatched.
            if (clickTime !== clickDispatch && self.enabled() && !self.isPressed && !originalEvent.defaultPrevented) {
              if (eventTarget.click) {
                //some browsers (like mobile Safari) don't properly trigger the click event
                eventTarget.click();
              } else if (ownerDoc.createEvent) {
                syntheticEvent = ownerDoc.createEvent("MouseEvents");
                syntheticEvent.initMouseEvent("click", true, true, _win$2, 1, self.pointerEvent.screenX, self.pointerEvent.screenY, self.pointerX, self.pointerY, false, false, false, false, 0, null);
                eventTarget.dispatchEvent(syntheticEvent);
              }
            }
          };

          if (!_isAndroid && !originalEvent.defaultPrevented) {
            //iOS Safari requires the synthetic click to happen immediately or else it simply won't work, but Android doesn't play nice.
            gsap$6.delayedCall(0.05, syntheticClick); //in addition to the iOS bug workaround, there's a Firefox issue with clicking on things like a video to play, so we must fake a click event in a slightly delayed fashion. Previously, we listened for the "click" event with "capture" false which solved the video-click-to-play issue, but it would allow the "click" event to be dispatched twice like if you were using a jQuery.click() because that was handled in the capture phase, thus we had to switch to the capture phase to avoid the double-dispatching, but do the delayed synthetic click. Don't fire it too fast (like 0.00001) because we want to give the native event a chance to fire first as it's "trusted".
          }
        }
      } else {
        animate(vars.inertia || vars.throwProps); //will skip if inertia/throwProps isn't defined or IntertiaPlugin isn't loaded.

        if (!self.allowEventDefault && originalEvent && (vars.dragClickables !== false || !isClickable.call(self, originalEvent.target)) && wasDragging && (!allowNativeTouchScrolling || touchDragAxis && allowNativeTouchScrolling === touchDragAxis) && originalEvent.cancelable !== false) {
          isPreventingDefault = true;

          _preventDefault(originalEvent);
        } else {
          isPreventingDefault = false;
        }

        _dispatchEvent(self, "release", "onRelease");
      }

      isTweening() && placeholderDelayedCall.duration(self.tween.duration()); //sync the timing so that the placeholder DIV gets

      wasDragging && _dispatchEvent(self, "dragend", "onDragEnd");
      return true;
    },
        updateScroll = function updateScroll(e) {
      if (e && self.isDragging && !scrollProxy) {
        var parent = e.target || target.parentNode,
            deltaX = parent.scrollLeft - parent._gsScrollX,
            deltaY = parent.scrollTop - parent._gsScrollY;

        if (deltaX || deltaY) {
          if (matrix) {
            startPointerX -= deltaX * matrix.a + deltaY * matrix.c;
            startPointerY -= deltaY * matrix.d + deltaX * matrix.b;
          } else {
            startPointerX -= deltaX;
            startPointerY -= deltaY;
          }

          parent._gsScrollX += deltaX;
          parent._gsScrollY += deltaY;
          setPointerPosition(self.pointerX, self.pointerY);
        }
      }
    },
        onClick = function onClick(e) {
      //this was a huge pain in the neck to align all the various browsers and their behaviors. Chrome, Firefox, Safari, Opera, Android, and Microsoft Edge all handle events differently! Some will only trigger native behavior (like checkbox toggling) from trusted events. Others don't even support isTrusted, but require 2 events to flow through before triggering native behavior. Edge treats everything as trusted but also mandates that 2 flow through to trigger the correct native behavior.
      var time = _getTime(),
          recentlyClicked = time - clickTime < 40,
          recentlyDragged = time - dragEndTime < 40,
          alreadyDispatched = recentlyClicked && clickDispatch === clickTime,
          defaultPrevented = self.pointerEvent && self.pointerEvent.defaultPrevented,
          alreadyDispatchedTrusted = recentlyClicked && trustedClickDispatch === clickTime,
          trusted = e.isTrusted || e.isTrusted == null && recentlyClicked && alreadyDispatched; //note: Safari doesn't support isTrusted, and it won't properly execute native behavior (like toggling checkboxes) on the first synthetic "click" event - we must wait for the 2nd and treat it as trusted (but stop propagation at that point). Confusing, I know. Don't you love cross-browser compatibility challenges?


      if ((alreadyDispatched || recentlyDragged && self.vars.suppressClickOnDrag !== false) && e.stopImmediatePropagation) {
        e.stopImmediatePropagation();
      }

      if (recentlyClicked && !(self.pointerEvent && self.pointerEvent.defaultPrevented) && (!alreadyDispatched || trusted && !alreadyDispatchedTrusted)) {
        //let the first click pass through unhindered. Let the next one only if it's trusted, then no more (stop quick-succession ones)
        if (trusted && alreadyDispatched) {
          trustedClickDispatch = clickTime;
        }

        clickDispatch = clickTime;
        return;
      }

      if (self.isPressed || recentlyDragged || recentlyClicked) {
        if (!trusted || !e.detail || !recentlyClicked || defaultPrevented) {
          _preventDefault(e);
        }
      }

      if (!recentlyClicked && !recentlyDragged) {
        // for script-triggered event dispatches, like element.click()
        e && e.target && (self.pointerEvent = e);

        _dispatchEvent(self, "click", "onClick");
      }
    },
        localizePoint = function localizePoint(p) {
      return matrix ? {
        x: p.x * matrix.a + p.y * matrix.c + matrix.e,
        y: p.x * matrix.b + p.y * matrix.d + matrix.f
      } : {
        x: p.x,
        y: p.y
      };
    };

    old = Draggable.get(target);
    old && old.kill(); // avoids duplicates (an element can only be controlled by one Draggable)
    //give the user access to start/stop dragging...

    _this2.startDrag = function (event, align) {
      var r1, r2, p1, p2;
      onPress(event || self.pointerEvent, true); //if the pointer isn't on top of the element, adjust things accordingly

      if (align && !self.hitTest(event || self.pointerEvent)) {
        r1 = _parseRect(event || self.pointerEvent);
        r2 = _parseRect(target);
        p1 = localizePoint({
          x: r1.left + r1.width / 2,
          y: r1.top + r1.height / 2
        });
        p2 = localizePoint({
          x: r2.left + r2.width / 2,
          y: r2.top + r2.height / 2
        });
        startPointerX -= p1.x - p2.x;
        startPointerY -= p1.y - p2.y;
      }

      if (!self.isDragging) {
        self.isDragging = true;

        _dispatchEvent(self, "dragstart", "onDragStart");
      }
    };

    _this2.drag = onMove;

    _this2.endDrag = function (e) {
      return onRelease(e || self.pointerEvent, true);
    };

    _this2.timeSinceDrag = function () {
      return self.isDragging ? 0 : (_getTime() - dragEndTime) / 1000;
    };

    _this2.timeSinceClick = function () {
      return (_getTime() - clickTime) / 1000;
    };

    _this2.hitTest = function (target, threshold) {
      return Draggable.hitTest(self.target, target, threshold);
    };

    _this2.getDirection = function (from, diagonalThreshold) {
      //from can be "start" (default), "velocity", or an element
      var mode = from === "velocity" && InertiaPlugin ? from : _isObject(from) && !rotationMode ? "element" : "start",
          xChange,
          yChange,
          ratio,
          direction,
          r1,
          r2;

      if (mode === "element") {
        r1 = _parseRect(self.target);
        r2 = _parseRect(from);
      }

      xChange = mode === "start" ? self.x - startElementX : mode === "velocity" ? InertiaPlugin.getVelocity(target, xProp) : r1.left + r1.width / 2 - (r2.left + r2.width / 2);

      if (rotationMode) {
        return xChange < 0 ? "counter-clockwise" : "clockwise";
      } else {
        diagonalThreshold = diagonalThreshold || 2;
        yChange = mode === "start" ? self.y - startElementY : mode === "velocity" ? InertiaPlugin.getVelocity(target, yProp) : r1.top + r1.height / 2 - (r2.top + r2.height / 2);
        ratio = Math.abs(xChange / yChange);
        direction = ratio < 1 / diagonalThreshold ? "" : xChange < 0 ? "left" : "right";

        if (ratio < diagonalThreshold) {
          if (direction !== "") {
            direction += "-";
          }

          direction += yChange < 0 ? "up" : "down";
        }
      }

      return direction;
    };

    _this2.applyBounds = function (newBounds, sticky) {
      var x, y, forceZeroVelocity, e, parent, isRoot;

      if (newBounds && vars.bounds !== newBounds) {
        vars.bounds = newBounds;
        return self.update(true, sticky);
      }

      syncXY(true);
      calculateBounds();

      if (hasBounds && !isTweening()) {
        x = self.x;
        y = self.y;

        if (x > maxX) {
          x = maxX;
        } else if (x < minX) {
          x = minX;
        }

        if (y > maxY) {
          y = maxY;
        } else if (y < minY) {
          y = minY;
        }

        if (self.x !== x || self.y !== y) {
          forceZeroVelocity = true;
          self.x = self.endX = x;

          if (rotationMode) {
            self.endRotation = x;
          } else {
            self.y = self.endY = y;
          }

          dirty = true;
          render(true);

          if (self.autoScroll && !self.isDragging) {
            _recordMaxScrolls(target.parentNode);

            e = target;
            _windowProxy.scrollTop = _win$2.pageYOffset != null ? _win$2.pageYOffset : ownerDoc.documentElement.scrollTop != null ? ownerDoc.documentElement.scrollTop : ownerDoc.body.scrollTop;
            _windowProxy.scrollLeft = _win$2.pageXOffset != null ? _win$2.pageXOffset : ownerDoc.documentElement.scrollLeft != null ? ownerDoc.documentElement.scrollLeft : ownerDoc.body.scrollLeft;

            while (e && !isRoot) {
              //walk up the chain and sense wherever the scrollTop/scrollLeft exceeds the maximum.
              isRoot = _isRoot(e.parentNode);
              parent = isRoot ? _windowProxy : e.parentNode;

              if (allowY && parent.scrollTop > parent._gsMaxScrollY) {
                parent.scrollTop = parent._gsMaxScrollY;
              }

              if (allowX && parent.scrollLeft > parent._gsMaxScrollX) {
                parent.scrollLeft = parent._gsMaxScrollX;
              }

              e = parent;
            }
          }
        }

        if (self.isThrowing && (forceZeroVelocity || self.endX > maxX || self.endX < minX || self.endY > maxY || self.endY < minY)) {
          animate(vars.inertia || vars.throwProps, forceZeroVelocity);
        }
      }

      return self;
    };

    _this2.update = function (applyBounds, sticky, ignoreExternalChanges) {
      var x = self.x,
          y = self.y;
      updateMatrix(!sticky);

      if (applyBounds) {
        self.applyBounds();
      } else {
        dirty && ignoreExternalChanges && render(true);
        syncXY(true);
      }

      if (sticky) {
        setPointerPosition(self.pointerX, self.pointerY);
        dirty && render(true);
      }

      if (self.isPressed && !sticky && (allowX && Math.abs(x - self.x) > 0.01 || allowY && Math.abs(y - self.y) > 0.01 && !rotationMode)) {
        recordStartPositions();
      }

      if (self.autoScroll) {
        _recordMaxScrolls(target.parentNode, self.isDragging);

        checkAutoScrollBounds = self.isDragging;
        render(true); //in case reparenting occurred.

        _removeScrollListener(target, updateScroll);

        _addScrollListener(target, updateScroll);
      }

      return self;
    };

    _this2.enable = function (type) {
      var setVars = {
        lazy: true
      },
          id,
          i,
          trigger;

      if (!rotationMode && vars.cursor !== false) {
        setVars.cursor = vars.cursor || _defaultCursor;
      }

      if (gsap$6.utils.checkPrefix("touchCallout")) {
        setVars.touchCallout = "none";
      }

      if (type !== "soft") {
        _setTouchActionForAllDescendants(triggers, allowX === allowY ? "none" : vars.allowNativeTouchScrolling && target.scrollHeight === target.clientHeight === (target.scrollWidth === target.clientHeight) || vars.allowEventDefault ? "manipulation" : allowX ? "pan-y" : "pan-x"); // Some browsers like Internet Explorer will fire a pointercancel event when the user attempts to drag when touchAction is "manipulate" because it's perceived as a pan. If the element has scrollable content in only one direction, we should use pan-x or pan-y accordingly so that the pointercancel doesn't prevent dragging.


        i = triggers.length;

        while (--i > -1) {
          trigger = triggers[i];
          _supportsPointer || _addListener(trigger, "mousedown", onPress);

          _addListener(trigger, "touchstart", onPress);

          _addListener(trigger, "click", onClick, true); //note: used to pass true for capture but it prevented click-to-play-video functionality in Firefox.


          gsap$6.set(trigger, setVars);

          if (trigger.getBBox && trigger.ownerSVGElement) {
            // a bug in chrome doesn't respect touch-action on SVG elements - it only works if we set it on the parent SVG.
            gsap$6.set(trigger.ownerSVGElement, {
              touchAction: allowX === allowY ? "none" : vars.allowNativeTouchScrolling || vars.allowEventDefault ? "manipulation" : allowX ? "pan-y" : "pan-x"
            });
          }

          vars.allowContextMenu || _addListener(trigger, "contextmenu", onContextMenu);
        }

        _setSelectable(triggers, false);
      }

      _addScrollListener(target, updateScroll);

      enabled = true;

      if (InertiaPlugin && type !== "soft") {
        InertiaPlugin.track(scrollProxy || target, xyMode ? "x,y" : rotationMode ? "rotation" : "top,left");
      }

      target._gsDragID = id = "d" + _lookupCount++;
      _lookup[id] = self;

      if (scrollProxy) {
        scrollProxy.enable();
        scrollProxy.element._gsDragID = id;
      }

      (vars.bounds || rotationMode) && recordStartPositions();
      vars.bounds && self.applyBounds();
      return self;
    };

    _this2.disable = function (type) {
      var dragging = self.isDragging,
          i,
          trigger;

      if (!rotationMode) {
        i = triggers.length;

        while (--i > -1) {
          _setStyle(triggers[i], "cursor", null);
        }
      }

      if (type !== "soft") {
        _setTouchActionForAllDescendants(triggers, null);

        i = triggers.length;

        while (--i > -1) {
          trigger = triggers[i];

          _setStyle(trigger, "touchCallout", null);

          _removeListener(trigger, "mousedown", onPress);

          _removeListener(trigger, "touchstart", onPress);

          _removeListener(trigger, "click", onClick);

          _removeListener(trigger, "contextmenu", onContextMenu);
        }

        _setSelectable(triggers, true);

        if (touchEventTarget) {
          _removeListener(touchEventTarget, "touchcancel", onRelease);

          _removeListener(touchEventTarget, "touchend", onRelease);

          _removeListener(touchEventTarget, "touchmove", onMove);
        }

        _removeListener(ownerDoc, "mouseup", onRelease);

        _removeListener(ownerDoc, "mousemove", onMove);
      }

      _removeScrollListener(target, updateScroll);

      enabled = false;
      InertiaPlugin && type !== "soft" && InertiaPlugin.untrack(scrollProxy || target, xyMode ? "x,y" : rotationMode ? "rotation" : "top,left");
      scrollProxy && scrollProxy.disable();

      _removeFromRenderQueue(render);

      self.isDragging = self.isPressed = isClicking = false;
      dragging && _dispatchEvent(self, "dragend", "onDragEnd");
      return self;
    };

    _this2.enabled = function (value, type) {
      return arguments.length ? value ? self.enable(type) : self.disable(type) : enabled;
    };

    _this2.kill = function () {
      self.isThrowing = false;
      self.tween && self.tween.kill();
      self.disable();
      gsap$6.set(triggers, {
        clearProps: "userSelect"
      });
      delete _lookup[target._gsDragID];
      return self;
    };

    if (~type.indexOf("scroll")) {
      scrollProxy = _this2.scrollProxy = new ScrollProxy(target, _extend({
        onKill: function onKill() {
          //ScrollProxy's onKill() gets called if/when the ScrollProxy senses that the user interacted with the scroll position manually (like using the scrollbar). IE9 doesn't fire the "mouseup" properly when users drag the scrollbar of an element, so this works around that issue.
          self.isPressed && onRelease(null);
        }
      }, vars)); //a bug in many Android devices' stock browser causes scrollTop to get forced back to 0 after it is altered via JS, so we set overflow to "hidden" on mobile/touch devices (they hide the scroll bar anyway). That works around the bug. (This bug is discussed at https://code.google.com/p/android/issues/detail?id=19625)

      target.style.overflowY = allowY && !_isTouchDevice ? "auto" : "hidden";
      target.style.overflowX = allowX && !_isTouchDevice ? "auto" : "hidden";
      target = scrollProxy.content;
    }

    if (rotationMode) {
      killProps.rotation = 1;
    } else {
      if (allowX) {
        killProps[xProp] = 1;
      }

      if (allowY) {
        killProps[yProp] = 1;
      }
    }

    gsCache.force3D = "force3D" in vars ? vars.force3D : true; //otherwise, normal dragging would be in 2D and then as soon as it's released and there's an inertia tween, it'd jump to 3D which can create an initial jump due to the work the browser must to do layerize it.

    _this2.enable();

    return _this2;
  }

  Draggable.register = function register(core) {
    gsap$6 = core;

    _initCore$4();
  };

  Draggable.create = function create(targets, vars) {
    _coreInitted$2 || _initCore$4(true);
    return _toArray$1(targets).map(function (target) {
      return new Draggable(target, vars);
    });
  };

  Draggable.get = function get(target) {
    return _lookup[(_toArray$1(target)[0] || {})._gsDragID];
  };

  Draggable.timeSinceDrag = function timeSinceDrag() {
    return (_getTime() - _lastDragTime) / 1000;
  };

  Draggable.hitTest = function hitTest(obj1, obj2, threshold) {
    if (obj1 === obj2) {
      return false;
    }

    var r1 = _parseRect(obj1),
        r2 = _parseRect(obj2),
        top = r1.top,
        left = r1.left,
        right = r1.right,
        bottom = r1.bottom,
        width = r1.width,
        height = r1.height,
        isOutside = r2.left > right || r2.right < left || r2.top > bottom || r2.bottom < top,
        overlap,
        area,
        isRatio;

    if (isOutside || !threshold) {
      return !isOutside;
    }

    isRatio = (threshold + "").indexOf("%") !== -1;
    threshold = parseFloat(threshold) || 0;
    overlap = {
      left: Math.max(left, r2.left),
      top: Math.max(top, r2.top)
    };
    overlap.width = Math.min(right, r2.right) - overlap.left;
    overlap.height = Math.min(bottom, r2.bottom) - overlap.top;

    if (overlap.width < 0 || overlap.height < 0) {
      return false;
    }

    if (isRatio) {
      threshold *= 0.01;
      area = overlap.width * overlap.height;
      return area >= width * height * threshold || area >= r2.width * r2.height * threshold;
    }

    return overlap.width > threshold && overlap.height > threshold;
  };

  return Draggable;
}(EventDispatcher);

_setDefaults(Draggable.prototype, {
  pointerX: 0,
  pointerY: 0,
  startX: 0,
  startY: 0,
  deltaX: 0,
  deltaY: 0,
  isDragging: false,
  isPressed: false
});

Draggable.zIndex = 1000;
Draggable.version = "3.6.1";
_getGSAP$6() && gsap$6.registerPlugin(Draggable);

/*!
 * CSSRulePlugin 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var gsap$5,
    _coreInitted$1,
    _doc,
    CSSPlugin,
    _windowExists$2 = function _windowExists() {
  return typeof window !== "undefined";
},
    _getGSAP$5 = function _getGSAP() {
  return gsap$5 || _windowExists$2() && (gsap$5 = window.gsap) && gsap$5.registerPlugin && gsap$5;
},
    _checkRegister = function _checkRegister() {
  if (!_coreInitted$1) {
    _initCore$3();

    if (!CSSPlugin) {
      console.warn("Please gsap.registerPlugin(CSSPlugin, CSSRulePlugin)");
    }
  }

  return _coreInitted$1;
},
    _initCore$3 = function _initCore(core) {
  gsap$5 = core || _getGSAP$5();

  if (_windowExists$2()) {
    _doc = document;
  }

  if (gsap$5) {
    CSSPlugin = gsap$5.plugins.css;

    if (CSSPlugin) {
      _coreInitted$1 = 1;
    }
  }
};

var CSSRulePlugin = {
  version: "3.6.1",
  name: "cssRule",
  init: function init(target, value, tween, index, targets) {
    if (!_checkRegister() || typeof target.cssText === "undefined") {
      return false;
    }

    var div = target._gsProxy = target._gsProxy || _doc.createElement("div");

    this.ss = target;
    this.style = div.style;
    div.style.cssText = target.cssText;
    CSSPlugin.prototype.init.call(this, div, value, tween, index, targets); //we just offload all the work to the regular CSSPlugin and then copy the cssText back over to the rule in the render() method. This allows us to have all of the updates to CSSPlugin automatically flow through to CSSRulePlugin instead of having to maintain both
  },
  render: function render(ratio, data) {
    var pt = data._pt,
        style = data.style,
        ss = data.ss,
        i;

    while (pt) {
      pt.r(ratio, pt.d);
      pt = pt._next;
    }

    i = style.length;

    while (--i > -1) {
      ss[style[i]] = style[style[i]];
    }
  },
  getRule: function getRule(selector) {
    _checkRegister();

    var ruleProp = _doc.all ? "rules" : "cssRules",
        styleSheets = _doc.styleSheets,
        i = styleSheets.length,
        pseudo = selector.charAt(0) === ":",
        j,
        curSS,
        cs,
        a;
    selector = (pseudo ? "" : ",") + selector.split("::").join(":").toLowerCase() + ","; //note: old versions of IE report tag name selectors as upper case, so we just change everything to lowercase.

    if (pseudo) {
      a = [];
    }

    while (i--) {
      //Firefox may throw insecure operation errors when css is loaded from other domains, so try/catch.
      try {
        curSS = styleSheets[i][ruleProp];

        if (!curSS) {
          continue;
        }

        j = curSS.length;
      } catch (e) {
        console.warn(e);
        continue;
      }

      while (--j > -1) {
        cs = curSS[j];

        if (cs.selectorText && ("," + cs.selectorText.split("::").join(":").toLowerCase() + ",").indexOf(selector) !== -1) {
          //note: IE adds an extra ":" to pseudo selectors, so .myClass:after becomes .myClass::after, so we need to strip the extra one out.
          if (pseudo) {
            a.push(cs.style);
          } else {
            return cs.style;
          }
        }
      }
    }

    return a;
  },
  register: _initCore$3
};
_getGSAP$5() && gsap$5.registerPlugin(CSSRulePlugin);

/*!
 * EaselPlugin 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var gsap$4,
    _coreInitted,
    _win$1,
    _createJS,
    _ColorFilter,
    _ColorMatrixFilter,
    _colorProps$1 = "redMultiplier,greenMultiplier,blueMultiplier,alphaMultiplier,redOffset,greenOffset,blueOffset,alphaOffset".split(","),
    _windowExists$1 = function _windowExists() {
  return typeof window !== "undefined";
},
    _getGSAP$4 = function _getGSAP() {
  return gsap$4 || _windowExists$1() && (gsap$4 = window.gsap) && gsap$4.registerPlugin && gsap$4;
},
    _getCreateJS = function _getCreateJS() {
  return _createJS || _win$1 && _win$1.createjs || _win$1 || {};
},
    _warn$1 = function _warn(message) {
  return console.warn(message);
},
    _cache = function _cache(target) {
  var b = target.getBounds && target.getBounds();

  if (!b) {
    b = target.nominalBounds || {
      x: 0,
      y: 0,
      width: 100,
      height: 100
    };
    target.setBounds && target.setBounds(b.x, b.y, b.width, b.height);
  }

  target.cache && target.cache(b.x, b.y, b.width, b.height);

  _warn$1("EaselPlugin: for filters to display in EaselJS, you must call the object's cache() method first. GSAP attempted to use the target's getBounds() for the cache but that may not be completely accurate. " + target);
},
    _parseColorFilter = function _parseColorFilter(target, v, plugin) {
  if (!_ColorFilter) {
    _ColorFilter = _getCreateJS().ColorFilter;

    if (!_ColorFilter) {
      _warn$1("EaselPlugin error: The EaselJS ColorFilter JavaScript file wasn't loaded.");
    }
  }

  var filters = target.filters || [],
      i = filters.length,
      c,
      s,
      e,
      a,
      p,
      pt;

  while (i--) {
    if (filters[i] instanceof _ColorFilter) {
      s = filters[i];
      break;
    }
  }

  if (!s) {
    s = new _ColorFilter();
    filters.push(s);
    target.filters = filters;
  }

  e = s.clone();

  if (v.tint != null) {
    c = gsap$4.utils.splitColor(v.tint);
    a = v.tintAmount != null ? +v.tintAmount : 1;
    e.redOffset = +c[0] * a;
    e.greenOffset = +c[1] * a;
    e.blueOffset = +c[2] * a;
    e.redMultiplier = e.greenMultiplier = e.blueMultiplier = 1 - a;
  } else {
    for (p in v) {
      if (p !== "exposure") if (p !== "brightness") {
        e[p] = +v[p];
      }
    }
  }

  if (v.exposure != null) {
    e.redOffset = e.greenOffset = e.blueOffset = 255 * (+v.exposure - 1);
    e.redMultiplier = e.greenMultiplier = e.blueMultiplier = 1;
  } else if (v.brightness != null) {
    a = +v.brightness - 1;
    e.redOffset = e.greenOffset = e.blueOffset = a > 0 ? a * 255 : 0;
    e.redMultiplier = e.greenMultiplier = e.blueMultiplier = 1 - Math.abs(a);
  }

  i = 8;

  while (i--) {
    p = _colorProps$1[i];

    if (s[p] !== e[p]) {
      pt = plugin.add(s, p, s[p], e[p]);

      if (pt) {
        pt.op = "easel_colorFilter";
      }
    }
  }

  plugin._props.push("easel_colorFilter");

  if (!target.cacheID) {
    _cache(target);
  }
},
    _idMatrix$1 = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0],
    _lumR$1 = 0.212671,
    _lumG$1 = 0.715160,
    _lumB$1 = 0.072169,
    _applyMatrix$1 = function _applyMatrix(m, m2) {
  if (!(m instanceof Array) || !(m2 instanceof Array)) {
    return m2;
  }

  var temp = [],
      i = 0,
      z = 0,
      y,
      x;

  for (y = 0; y < 4; y++) {
    for (x = 0; x < 5; x++) {
      z = x === 4 ? m[i + 4] : 0;
      temp[i + x] = m[i] * m2[x] + m[i + 1] * m2[x + 5] + m[i + 2] * m2[x + 10] + m[i + 3] * m2[x + 15] + z;
    }

    i += 5;
  }

  return temp;
},
    _setSaturation$1 = function _setSaturation(m, n) {
  if (isNaN(n)) {
    return m;
  }

  var inv = 1 - n,
      r = inv * _lumR$1,
      g = inv * _lumG$1,
      b = inv * _lumB$1;
  return _applyMatrix$1([r + n, g, b, 0, 0, r, g + n, b, 0, 0, r, g, b + n, 0, 0, 0, 0, 0, 1, 0], m);
},
    _colorize$1 = function _colorize(m, color, amount) {
  if (isNaN(amount)) {
    amount = 1;
  }

  var c = gsap$4.utils.splitColor(color),
      r = c[0] / 255,
      g = c[1] / 255,
      b = c[2] / 255,
      inv = 1 - amount;
  return _applyMatrix$1([inv + amount * r * _lumR$1, amount * r * _lumG$1, amount * r * _lumB$1, 0, 0, amount * g * _lumR$1, inv + amount * g * _lumG$1, amount * g * _lumB$1, 0, 0, amount * b * _lumR$1, amount * b * _lumG$1, inv + amount * b * _lumB$1, 0, 0, 0, 0, 0, 1, 0], m);
},
    _setHue$1 = function _setHue(m, n) {
  if (isNaN(n)) {
    return m;
  }

  n *= Math.PI / 180;
  var c = Math.cos(n),
      s = Math.sin(n);
  return _applyMatrix$1([_lumR$1 + c * (1 - _lumR$1) + s * -_lumR$1, _lumG$1 + c * -_lumG$1 + s * -_lumG$1, _lumB$1 + c * -_lumB$1 + s * (1 - _lumB$1), 0, 0, _lumR$1 + c * -_lumR$1 + s * 0.143, _lumG$1 + c * (1 - _lumG$1) + s * 0.14, _lumB$1 + c * -_lumB$1 + s * -0.283, 0, 0, _lumR$1 + c * -_lumR$1 + s * -(1 - _lumR$1), _lumG$1 + c * -_lumG$1 + s * _lumG$1, _lumB$1 + c * (1 - _lumB$1) + s * _lumB$1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], m);
},
    _setContrast$1 = function _setContrast(m, n) {
  if (isNaN(n)) {
    return m;
  }

  n += 0.01;
  return _applyMatrix$1([n, 0, 0, 0, 128 * (1 - n), 0, n, 0, 0, 128 * (1 - n), 0, 0, n, 0, 128 * (1 - n), 0, 0, 0, 1, 0], m);
},
    _parseColorMatrixFilter$1 = function _parseColorMatrixFilter(target, v, plugin) {
  if (!_ColorMatrixFilter) {
    _ColorMatrixFilter = _getCreateJS().ColorMatrixFilter;

    if (!_ColorMatrixFilter) {
      _warn$1("EaselPlugin: The EaselJS ColorMatrixFilter JavaScript file wasn't loaded.");
    }
  }

  var filters = target.filters || [],
      i = filters.length,
      matrix,
      startMatrix,
      s,
      pg;

  while (--i > -1) {
    if (filters[i] instanceof _ColorMatrixFilter) {
      s = filters[i];
      break;
    }
  }

  if (!s) {
    s = new _ColorMatrixFilter(_idMatrix$1.slice());
    filters.push(s);
    target.filters = filters;
  }

  startMatrix = s.matrix;
  matrix = _idMatrix$1.slice();

  if (v.colorize != null) {
    matrix = _colorize$1(matrix, v.colorize, Number(v.colorizeAmount));
  }

  if (v.contrast != null) {
    matrix = _setContrast$1(matrix, Number(v.contrast));
  }

  if (v.hue != null) {
    matrix = _setHue$1(matrix, Number(v.hue));
  }

  if (v.saturation != null) {
    matrix = _setSaturation$1(matrix, Number(v.saturation));
  }

  i = matrix.length;

  while (--i > -1) {
    if (matrix[i] !== startMatrix[i]) {
      pg = plugin.add(startMatrix, i, startMatrix[i], matrix[i]);

      if (pg) {
        pg.op = "easel_colorMatrixFilter";
      }
    }
  }

  plugin._props.push("easel_colorMatrixFilter");

  if (!target.cacheID) {
    _cache();
  }

  plugin._matrix = startMatrix;
},
    _initCore$2 = function _initCore(core) {
  gsap$4 = core || _getGSAP$4();

  if (_windowExists$1()) {
    _win$1 = window;
  }

  if (gsap$4) {
    _coreInitted = 1;
  }
};

var EaselPlugin = {
  version: "3.6.1",
  name: "easel",
  init: function init(target, value, tween, index, targets) {
    if (!_coreInitted) {
      _initCore$2();

      if (!gsap$4) {
        _warn$1("Please gsap.registerPlugin(EaselPlugin)");
      }
    }

    this.target = target;
    var p, pt, tint, colorMatrix, end, labels, i;

    for (p in value) {
      end = value[p];

      if (p === "colorFilter" || p === "tint" || p === "tintAmount" || p === "exposure" || p === "brightness") {
        if (!tint) {
          _parseColorFilter(target, value.colorFilter || value, this);

          tint = true;
        }
      } else if (p === "saturation" || p === "contrast" || p === "hue" || p === "colorize" || p === "colorizeAmount") {
        if (!colorMatrix) {
          _parseColorMatrixFilter$1(target, value.colorMatrixFilter || value, this);

          colorMatrix = true;
        }
      } else if (p === "frame") {
        if (typeof end === "string" && end.charAt(1) !== "=" && (labels = target.labels)) {
          for (i = 0; i < labels.length; i++) {
            if (labels[i].label === end) {
              end = labels[i].position;
            }
          }
        }

        pt = this.add(target, "gotoAndStop", target.currentFrame, end, index, targets, Math.round);

        if (pt) {
          pt.op = p;
        }
      } else if (target[p] != null) {
        this.add(target, p, "get", end);
      }
    }
  },
  render: function render(ratio, data) {
    var pt = data._pt;

    while (pt) {
      pt.r(ratio, pt.d);
      pt = pt._next;
    }

    if (data.target.cacheID) {
      data.target.updateCache();
    }
  },
  register: _initCore$2
};

EaselPlugin.registerCreateJS = function (createjs) {
  _createJS = createjs;
};

_getGSAP$4() && gsap$4.registerPlugin(EaselPlugin);

/*!
 * EasePack 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var gsap$3,
    _registerEase,
    _getGSAP$3 = function _getGSAP() {
  return gsap$3 || typeof window !== "undefined" && (gsap$3 = window.gsap) && gsap$3.registerPlugin && gsap$3;
},
    _boolean = function _boolean(value, defaultValue) {
  return !!(typeof value === "undefined" ? defaultValue : value && !~(value + "").indexOf("false"));
},
    _initCore$1 = function _initCore(core) {
  gsap$3 = core || _getGSAP$3();

  if (gsap$3) {
    _registerEase = gsap$3.registerEase; //add weighted ease capabilities to standard eases so users can do "power2.inOut(0.8)" for example to push everything toward the "out", or (-0.8) to push it toward the "in" (0 is neutral)

    var eases = gsap$3.parseEase(),
        createConfig = function createConfig(ease) {
      return function (ratio) {
        var y = 0.5 + ratio / 2;

        ease.config = function (p) {
          return ease(2 * (1 - p) * p * y + p * p);
        };
      };
    },
        p;

    for (p in eases) {
      if (!eases[p].config) {
        createConfig(eases[p]);
      }
    }

    _registerEase("slow", SlowMo);

    _registerEase("expoScale", ExpoScaleEase);

    _registerEase("rough", RoughEase);

    for (p in EasePack) {
      p !== "version" && gsap$3.core.globals(p, EasePack[p]);
    }
  }
},
    _createSlowMo = function _createSlowMo(linearRatio, power, yoyoMode) {
  linearRatio = Math.min(1, linearRatio || 0.7);

  var pow = linearRatio < 1 ? power || power === 0 ? power : 0.7 : 0,
      p1 = (1 - linearRatio) / 2,
      p3 = p1 + linearRatio,
      calcEnd = _boolean(yoyoMode);

  return function (p) {
    var r = p + (0.5 - p) * pow;
    return p < p1 ? calcEnd ? 1 - (p = 1 - p / p1) * p : r - (p = 1 - p / p1) * p * p * p * r : p > p3 ? calcEnd ? p === 1 ? 0 : 1 - (p = (p - p3) / p1) * p : r + (p - r) * (p = (p - p3) / p1) * p * p * p : calcEnd ? 1 : r;
  };
},
    _createExpoScale = function _createExpoScale(start, end, ease) {
  var p1 = Math.log(end / start),
      p2 = end - start;
  ease && (ease = gsap$3.parseEase(ease));
  return function (p) {
    return (start * Math.exp(p1 * (ease ? ease(p) : p)) - start) / p2;
  };
},
    EasePoint = function EasePoint(time, value, next) {
  this.t = time;
  this.v = value;

  if (next) {
    this.next = next;
    next.prev = this;
    this.c = next.v - value;
    this.gap = next.t - time;
  }
},
    _createRoughEase = function _createRoughEase(vars) {
  if (typeof vars !== "object") {
    //users may pass in via a string, like "rough(30)"
    vars = {
      points: +vars || 20
    };
  }

  var taper = vars.taper || "none",
      a = [],
      cnt = 0,
      points = (+vars.points || 20) | 0,
      i = points,
      randomize = _boolean(vars.randomize, true),
      clamp = _boolean(vars.clamp),
      template = gsap$3 ? gsap$3.parseEase(vars.template) : 0,
      strength = (+vars.strength || 1) * 0.4,
      x,
      y,
      bump,
      invX,
      obj,
      pnt,
      recent;

  while (--i > -1) {
    x = randomize ? Math.random() : 1 / points * i;
    y = template ? template(x) : x;

    if (taper === "none") {
      bump = strength;
    } else if (taper === "out") {
      invX = 1 - x;
      bump = invX * invX * strength;
    } else if (taper === "in") {
      bump = x * x * strength;
    } else if (x < 0.5) {
      //"both" (start)
      invX = x * 2;
      bump = invX * invX * 0.5 * strength;
    } else {
      //"both" (end)
      invX = (1 - x) * 2;
      bump = invX * invX * 0.5 * strength;
    }

    if (randomize) {
      y += Math.random() * bump - bump * 0.5;
    } else if (i % 2) {
      y += bump * 0.5;
    } else {
      y -= bump * 0.5;
    }

    if (clamp) {
      if (y > 1) {
        y = 1;
      } else if (y < 0) {
        y = 0;
      }
    }

    a[cnt++] = {
      x: x,
      y: y
    };
  }

  a.sort(function (a, b) {
    return a.x - b.x;
  });
  pnt = new EasePoint(1, 1, null);
  i = points;

  while (i--) {
    obj = a[i];
    pnt = new EasePoint(obj.x, obj.y, pnt);
  }

  recent = new EasePoint(0, 0, pnt.t ? pnt : pnt.next);
  return function (p) {
    var pnt = recent;

    if (p > pnt.t) {
      while (pnt.next && p >= pnt.t) {
        pnt = pnt.next;
      }

      pnt = pnt.prev;
    } else {
      while (pnt.prev && p <= pnt.t) {
        pnt = pnt.prev;
      }
    }

    recent = pnt;
    return pnt.v + (p - pnt.t) / pnt.gap * pnt.c;
  };
};

var SlowMo = _createSlowMo(0.7);
SlowMo.ease = SlowMo; //for backward compatibility

SlowMo.config = _createSlowMo;
var ExpoScaleEase = _createExpoScale(1, 2);
ExpoScaleEase.config = _createExpoScale;
var RoughEase = _createRoughEase();
RoughEase.ease = RoughEase; //for backward compatibility

RoughEase.config = _createRoughEase;
var EasePack = {
  SlowMo: SlowMo,
  RoughEase: RoughEase,
  ExpoScaleEase: ExpoScaleEase
};

for (var p$1 in EasePack) {
  EasePack[p$1].register = _initCore$1;
  EasePack[p$1].version = "3.6.1";
}

_getGSAP$3() && gsap$3.registerPlugin(SlowMo);

/*!
 * paths 3.6.1
 * https://greensock.com
 *
 * Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var _svgPathExp = /[achlmqstvz]|(-?\d*\.?\d*(?:e[\-+]?\d+)?)[0-9]/ig,
    _numbersExp = /(?:(-)?\d*\.?\d*(?:e[\-+]?\d+)?)[0-9]/ig,
    _scientific = /[\+\-]?\d*\.?\d+e[\+\-]?\d+/ig,
    _selectorExp = /(^[#\.][a-z]|[a-y][a-z])/i,
    _DEG2RAD$2 = Math.PI / 180,
    _RAD2DEG = 180 / Math.PI,
    _sin = Math.sin,
    _cos = Math.cos,
    _abs = Math.abs,
    _sqrt = Math.sqrt,
    _atan2 = Math.atan2,
    _largeNum = 1e8,
    _isString$1 = function _isString(value) {
  return typeof value === "string";
},
    _isNumber = function _isNumber(value) {
  return typeof value === "number";
},
    _isUndefined = function _isUndefined(value) {
  return typeof value === "undefined";
},
    _temp = {},
    _temp2 = {},
    _roundingNum = 1e5,
    _wrapProgress = function _wrapProgress(progress) {
  return Math.round((progress + _largeNum) % 1 * _roundingNum) / _roundingNum || (progress < 0 ? 0 : 1);
},
    //if progress lands on 1, the % will make it 0 which is why we || 1, but not if it's negative because it makes more sense for motion to end at 0 in that case.
_round = function _round(value) {
  return Math.round(value * _roundingNum) / _roundingNum || 0;
},
    _roundPrecise = function _roundPrecise(value) {
  return Math.round(value * 1e10) / 1e10 || 0;
},
    _splitSegment = function _splitSegment(rawPath, segIndex, i, t) {
  var segment = rawPath[segIndex],
      shift = t === 1 ? 6 : subdivideSegment(segment, i, t);

  if (shift && shift + i + 2 < segment.length) {
    rawPath.splice(segIndex, 0, segment.slice(0, i + shift + 2));
    segment.splice(0, i + shift);
    return 1;
  }
},
    _reverseRawPath = function _reverseRawPath(rawPath, skipOuter) {
  var i = rawPath.length;
  skipOuter || rawPath.reverse();

  while (i--) {
    rawPath[i].reversed || reverseSegment(rawPath[i]);
  }
},
    _copyMetaData = function _copyMetaData(source, copy) {
  copy.totalLength = source.totalLength;

  if (source.samples) {
    //segment
    copy.samples = source.samples.slice(0);
    copy.lookup = source.lookup.slice(0);
    copy.minLength = source.minLength;
    copy.resolution = source.resolution;
  } else if (source.totalPoints) {
    //rawPath
    copy.totalPoints = source.totalPoints;
  }

  return copy;
},
    //pushes a new segment into a rawPath, but if its starting values match the ending values of the last segment, it'll merge it into that same segment (to reduce the number of segments)
_appendOrMerge = function _appendOrMerge(rawPath, segment) {
  var index = rawPath.length,
      prevSeg = rawPath[index - 1] || [],
      l = prevSeg.length;

  if (index && segment[0] === prevSeg[l - 2] && segment[1] === prevSeg[l - 1]) {
    segment = prevSeg.concat(segment.slice(2));
    index--;
  }

  rawPath[index] = segment;
};
/* TERMINOLOGY
 - RawPath - an array of arrays, one for each Segment. A single RawPath could have multiple "M" commands, defining Segments (paths aren't always connected).
 - Segment - an array containing a sequence of Cubic Bezier coordinates in alternating x, y, x, y format. Starting anchor, then control point 1, control point 2, and ending anchor, then the next control point 1, control point 2, anchor, etc. Uses less memory than an array with a bunch of {x, y} points.
 - Bezier - a single cubic Bezier with a starting anchor, two control points, and an ending anchor.
 - the variable "t" is typically the position along an individual Bezier path (time) and it's NOT linear, meaning it could accelerate/decelerate based on the control points whereas the "p" or "progress" value is linearly mapped to the whole path, so it shouldn't really accelerate/decelerate based on control points. So a progress of 0.2 would be almost exactly 20% along the path. "t" is ONLY in an individual Bezier piece.
 */
//accepts basic selector text, a path instance, a RawPath instance, or a Segment and returns a RawPath (makes it easy to homogenize things). If an element or selector text is passed in, it'll also cache the value so that if it's queried again, it'll just take the path data from there instead of parsing it all over again (as long as the path data itself hasn't changed - it'll check).


function getRawPath(value) {
  value = _isString$1(value) && _selectorExp.test(value) ? document.querySelector(value) || value : value;
  var e = value.getAttribute ? value : 0,
      rawPath;

  if (e && (value = value.getAttribute("d"))) {
    //implements caching
    if (!e._gsPath) {
      e._gsPath = {};
    }

    rawPath = e._gsPath[value];
    return rawPath && !rawPath._dirty ? rawPath : e._gsPath[value] = stringToRawPath(value);
  }

  return !value ? console.warn("Expecting a <path> element or an SVG path data string") : _isString$1(value) ? stringToRawPath(value) : _isNumber(value[0]) ? [value] : value;
} //copies a RawPath WITHOUT the length meta data (for speed)

function copyRawPath(rawPath) {
  var a = [],
      i = 0;

  for (; i < rawPath.length; i++) {
    a[i] = _copyMetaData(rawPath[i], rawPath[i].slice(0));
  }

  return _copyMetaData(rawPath, a);
}
function reverseSegment(segment) {
  var i = 0,
      y;
  segment.reverse(); //this will invert the order y, x, y, x so we must flip it back.

  for (; i < segment.length; i += 2) {
    y = segment[i];
    segment[i] = segment[i + 1];
    segment[i + 1] = y;
  }

  segment.reversed = !segment.reversed;
}

var _createPath = function _createPath(e, ignore) {
  var path = document.createElementNS("http://www.w3.org/2000/svg", "path"),
      attr = [].slice.call(e.attributes),
      i = attr.length,
      name;
  ignore = "," + ignore + ",";

  while (--i > -1) {
    name = attr[i].nodeName.toLowerCase(); //in Microsoft Edge, if you don't set the attribute with a lowercase name, it doesn't render correctly! Super weird.

    if (ignore.indexOf("," + name + ",") < 0) {
      path.setAttributeNS(null, name, attr[i].nodeValue);
    }
  }

  return path;
},
    _typeAttrs = {
  rect: "rx,ry,x,y,width,height",
  circle: "r,cx,cy",
  ellipse: "rx,ry,cx,cy",
  line: "x1,x2,y1,y2"
},
    _attrToObj = function _attrToObj(e, attrs) {
  var props = attrs ? attrs.split(",") : [],
      obj = {},
      i = props.length;

  while (--i > -1) {
    obj[props[i]] = +e.getAttribute(props[i]) || 0;
  }

  return obj;
}; //converts an SVG shape like <circle>, <rect>, <polygon>, <polyline>, <ellipse>, etc. to a <path>, swapping it in and copying the attributes to match.


function convertToPath(element, swap) {
  var type = element.tagName.toLowerCase(),
      circ = 0.552284749831,
      data,
      x,
      y,
      r,
      ry,
      path,
      rcirc,
      rycirc,
      points,
      w,
      h,
      x2,
      x3,
      x4,
      x5,
      x6,
      y2,
      y3,
      y4,
      y5,
      y6,
      attr;

  if (type === "path" || !element.getBBox) {
    return element;
  }

  path = _createPath(element, "x,y,width,height,cx,cy,rx,ry,r,x1,x2,y1,y2,points");
  attr = _attrToObj(element, _typeAttrs[type]);

  if (type === "rect") {
    r = attr.rx;
    ry = attr.ry || r;
    x = attr.x;
    y = attr.y;
    w = attr.width - r * 2;
    h = attr.height - ry * 2;

    if (r || ry) {
      //if there are rounded corners, render cubic beziers
      x2 = x + r * (1 - circ);
      x3 = x + r;
      x4 = x3 + w;
      x5 = x4 + r * circ;
      x6 = x4 + r;
      y2 = y + ry * (1 - circ);
      y3 = y + ry;
      y4 = y3 + h;
      y5 = y4 + ry * circ;
      y6 = y4 + ry;
      data = "M" + x6 + "," + y3 + " V" + y4 + " C" + [x6, y5, x5, y6, x4, y6, x4 - (x4 - x3) / 3, y6, x3 + (x4 - x3) / 3, y6, x3, y6, x2, y6, x, y5, x, y4, x, y4 - (y4 - y3) / 3, x, y3 + (y4 - y3) / 3, x, y3, x, y2, x2, y, x3, y, x3 + (x4 - x3) / 3, y, x4 - (x4 - x3) / 3, y, x4, y, x5, y, x6, y2, x6, y3].join(",") + "z";
    } else {
      data = "M" + (x + w) + "," + y + " v" + h + " h" + -w + " v" + -h + " h" + w + "z";
    }
  } else if (type === "circle" || type === "ellipse") {
    if (type === "circle") {
      r = ry = attr.r;
      rycirc = r * circ;
    } else {
      r = attr.rx;
      ry = attr.ry;
      rycirc = ry * circ;
    }

    x = attr.cx;
    y = attr.cy;
    rcirc = r * circ;
    data = "M" + (x + r) + "," + y + " C" + [x + r, y + rycirc, x + rcirc, y + ry, x, y + ry, x - rcirc, y + ry, x - r, y + rycirc, x - r, y, x - r, y - rycirc, x - rcirc, y - ry, x, y - ry, x + rcirc, y - ry, x + r, y - rycirc, x + r, y].join(",") + "z";
  } else if (type === "line") {
    data = "M" + attr.x1 + "," + attr.y1 + " L" + attr.x2 + "," + attr.y2; //previously, we just converted to "Mx,y Lx,y" but Safari has bugs that cause that not to render properly when using a stroke-dasharray that's not fully visible! Using a cubic bezier fixes that issue.
  } else if (type === "polyline" || type === "polygon") {
    points = (element.getAttribute("points") + "").match(_numbersExp) || [];
    x = points.shift();
    y = points.shift();
    data = "M" + x + "," + y + " L" + points.join(",");

    if (type === "polygon") {
      data += "," + x + "," + y + "z";
    }
  }

  path.setAttribute("d", rawPathToString(path._gsRawPath = stringToRawPath(data)));

  if (swap && element.parentNode) {
    element.parentNode.insertBefore(path, element);
    element.parentNode.removeChild(element);
  }

  return path;
} //returns the rotation (in degrees) at a particular progress on a rawPath (the slope of the tangent)

function getRotationAtBezierT(segment, i, t) {
  var a = segment[i],
      b = segment[i + 2],
      c = segment[i + 4],
      x;
  a += (b - a) * t;
  b += (c - b) * t;
  a += (b - a) * t;
  x = b + (c + (segment[i + 6] - c) * t - b) * t - a;
  a = segment[i + 1];
  b = segment[i + 3];
  c = segment[i + 5];
  a += (b - a) * t;
  b += (c - b) * t;
  a += (b - a) * t;
  return _round(_atan2(b + (c + (segment[i + 7] - c) * t - b) * t - a, x) * _RAD2DEG);
}

function sliceRawPath(rawPath, start, end) {
  end = _isUndefined(end) ? 1 : _roundPrecise(end) || 0; // we must round to avoid issues like 4.15 / 8 = 0.8300000000000001 instead of 0.83 or 2.8 / 5 = 0.5599999999999999 instead of 0.56 and if someone is doing a loop like start: 2.8 / 0.5, end: 2.8 / 0.5 + 1.

  start = _roundPrecise(start) || 0;
  var loops = Math.max(0, ~~(_abs(end - start) - 1e-8)),
      path = copyRawPath(rawPath);

  if (start > end) {
    start = 1 - start;
    end = 1 - end;

    _reverseRawPath(path);

    path.totalLength = 0;
  }

  if (start < 0 || end < 0) {
    var offset = Math.abs(~~Math.min(start, end)) + 1;
    start += offset;
    end += offset;
  }

  path.totalLength || cacheRawPathMeasurements(path);
  var wrap = end > 1,
      s = getProgressData(path, start, _temp, true),
      e = getProgressData(path, end, _temp2),
      eSeg = e.segment,
      sSeg = s.segment,
      eSegIndex = e.segIndex,
      sSegIndex = s.segIndex,
      ei = e.i,
      si = s.i,
      sameSegment = sSegIndex === eSegIndex,
      sameBezier = ei === si && sameSegment,
      wrapsBehind,
      sShift,
      eShift,
      i,
      copy,
      totalSegments,
      l,
      j;

  if (wrap || loops) {
    wrapsBehind = eSegIndex < sSegIndex || sameSegment && ei < si || sameBezier && e.t < s.t;

    if (_splitSegment(path, sSegIndex, si, s.t)) {
      sSegIndex++;

      if (!wrapsBehind) {
        eSegIndex++;

        if (sameBezier) {
          e.t = (e.t - s.t) / (1 - s.t);
          ei = 0;
        } else if (sameSegment) {
          ei -= si;
        }
      }
    }

    if (1 - (end - start) < 1e-5) {
      eSegIndex = sSegIndex - 1;
    } else if (!e.t && eSegIndex) {
      eSegIndex--;
    } else if (_splitSegment(path, eSegIndex, ei, e.t) && wrapsBehind) {
      sSegIndex++;
    }

    if (s.t === 1) {
      sSegIndex = (sSegIndex + 1) % path.length;
    }

    copy = [];
    totalSegments = path.length;
    l = 1 + totalSegments * loops;
    j = sSegIndex;
    l += (totalSegments - sSegIndex + eSegIndex) % totalSegments;

    for (i = 0; i < l; i++) {
      _appendOrMerge(copy, path[j++ % totalSegments]);
    }

    path = copy;
  } else {
    eShift = e.t === 1 ? 6 : subdivideSegment(eSeg, ei, e.t);

    if (start !== end) {
      sShift = subdivideSegment(sSeg, si, sameBezier ? s.t / e.t : s.t);
      sameSegment && (eShift += sShift);
      eSeg.splice(ei + eShift + 2);
      (sShift || si) && sSeg.splice(0, si + sShift);
      i = path.length;

      while (i--) {
        //chop off any extra segments
        (i < sSegIndex || i > eSegIndex) && path.splice(i, 1);
      }
    } else {
      eSeg.angle = getRotationAtBezierT(eSeg, ei + eShift, 0); //record the value before we chop because it'll be impossible to determine the angle after its length is 0!

      ei += eShift;
      s = eSeg[ei];
      e = eSeg[ei + 1];
      eSeg.length = eSeg.totalLength = 0;
      eSeg.totalPoints = path.totalPoints = 8;
      eSeg.push(s, e, s, e, s, e, s, e);
    }
  }

  path.totalLength = 0;
  return path;
} //measures a Segment according to its resolution (so if segment.resolution is 6, for example, it'll take 6 samples equally across each Bezier) and create/populate a "samples" Array that has the length up to each of those sample points (always increasing from the start) as well as a "lookup" array that's broken up according to the smallest distance between 2 samples. This gives us a very fast way of looking up a progress position rather than looping through all the points/Beziers. You can optionally have it only measure a subset, starting at startIndex and going for a specific number of beziers (remember, there are 3 x/y pairs each, for a total of 6 elements for each Bezier). It will also populate a "totalLength" property, but that's not generally super accurate because by default it'll only take 6 samples per Bezier. But for performance reasons, it's perfectly adequate for measuring progress values along the path. If you need a more accurate totalLength, either increase the resolution or use the more advanced bezierToPoints() method which keeps adding points until they don't deviate by more than a certain precision value.

function measureSegment(segment, startIndex, bezierQty) {
  startIndex = startIndex || 0;

  if (!segment.samples) {
    segment.samples = [];
    segment.lookup = [];
  }

  var resolution = ~~segment.resolution || 12,
      inc = 1 / resolution,
      endIndex = bezierQty ? startIndex + bezierQty * 6 + 1 : segment.length,
      x1 = segment[startIndex],
      y1 = segment[startIndex + 1],
      samplesIndex = startIndex ? startIndex / 6 * resolution : 0,
      samples = segment.samples,
      lookup = segment.lookup,
      min = (startIndex ? segment.minLength : _largeNum) || _largeNum,
      prevLength = samples[samplesIndex + bezierQty * resolution - 1],
      length = startIndex ? samples[samplesIndex - 1] : 0,
      i,
      j,
      x4,
      x3,
      x2,
      xd,
      xd1,
      y4,
      y3,
      y2,
      yd,
      yd1,
      inv,
      t,
      lengthIndex,
      l,
      segLength;
  samples.length = lookup.length = 0;

  for (j = startIndex + 2; j < endIndex; j += 6) {
    x4 = segment[j + 4] - x1;
    x3 = segment[j + 2] - x1;
    x2 = segment[j] - x1;
    y4 = segment[j + 5] - y1;
    y3 = segment[j + 3] - y1;
    y2 = segment[j + 1] - y1;
    xd = xd1 = yd = yd1 = 0;

    if (_abs(x4) < 1e-5 && _abs(y4) < 1e-5 && _abs(x2) + _abs(y2) < 1e-5) {
      //dump points that are sufficiently close (basically right on top of each other, making a bezier super tiny or 0 length)
      if (segment.length > 8) {
        segment.splice(j, 6);
        j -= 6;
        endIndex -= 6;
      }
    } else {
      for (i = 1; i <= resolution; i++) {
        t = inc * i;
        inv = 1 - t;
        xd = xd1 - (xd1 = (t * t * x4 + 3 * inv * (t * x3 + inv * x2)) * t);
        yd = yd1 - (yd1 = (t * t * y4 + 3 * inv * (t * y3 + inv * y2)) * t);
        l = _sqrt(yd * yd + xd * xd);

        if (l < min) {
          min = l;
        }

        length += l;
        samples[samplesIndex++] = length;
      }
    }

    x1 += x4;
    y1 += y4;
  }

  if (prevLength) {
    prevLength -= length;

    for (; samplesIndex < samples.length; samplesIndex++) {
      samples[samplesIndex] += prevLength;
    }
  }

  if (samples.length && min) {
    segment.totalLength = segLength = samples[samples.length - 1] || 0;
    segment.minLength = min;
    l = lengthIndex = 0;

    for (i = 0; i < segLength; i += min) {
      lookup[l++] = samples[lengthIndex] < i ? ++lengthIndex : lengthIndex;
    }
  } else {
    segment.totalLength = samples[0] = 0;
  }

  return startIndex ? length - samples[startIndex / 2 - 1] : length;
}

function cacheRawPathMeasurements(rawPath, resolution) {
  var pathLength, points, i;

  for (i = pathLength = points = 0; i < rawPath.length; i++) {
    rawPath[i].resolution = ~~resolution || 12; //steps per Bezier curve (anchor, 2 control points, to anchor)

    points += rawPath[i].length;
    pathLength += measureSegment(rawPath[i]);
  }

  rawPath.totalPoints = points;
  rawPath.totalLength = pathLength;
  return rawPath;
} //divide segment[i] at position t (value between 0 and 1, progress along that particular cubic bezier segment that starts at segment[i]). Returns how many elements were spliced into the segment array (either 0 or 6)

function subdivideSegment(segment, i, t) {
  if (t <= 0 || t >= 1) {
    return 0;
  }

  var ax = segment[i],
      ay = segment[i + 1],
      cp1x = segment[i + 2],
      cp1y = segment[i + 3],
      cp2x = segment[i + 4],
      cp2y = segment[i + 5],
      bx = segment[i + 6],
      by = segment[i + 7],
      x1a = ax + (cp1x - ax) * t,
      x2 = cp1x + (cp2x - cp1x) * t,
      y1a = ay + (cp1y - ay) * t,
      y2 = cp1y + (cp2y - cp1y) * t,
      x1 = x1a + (x2 - x1a) * t,
      y1 = y1a + (y2 - y1a) * t,
      x2a = cp2x + (bx - cp2x) * t,
      y2a = cp2y + (by - cp2y) * t;
  x2 += (x2a - x2) * t;
  y2 += (y2a - y2) * t;
  segment.splice(i + 2, 4, _round(x1a), //first control point
  _round(y1a), _round(x1), //second control point
  _round(y1), _round(x1 + (x2 - x1) * t), //new fabricated anchor on line
  _round(y1 + (y2 - y1) * t), _round(x2), //third control point
  _round(y2), _round(x2a), //fourth control point
  _round(y2a));
  segment.samples && segment.samples.splice(i / 6 * segment.resolution | 0, 0, 0, 0, 0, 0, 0, 0);
  return 6;
} // returns an object {path, segment, segIndex, i, t}

function getProgressData(rawPath, progress, decoratee, pushToNextIfAtEnd) {
  decoratee = decoratee || {};
  rawPath.totalLength || cacheRawPathMeasurements(rawPath);

  if (progress < 0 || progress > 1) {
    progress = _wrapProgress(progress);
  }

  var segIndex = 0,
      segment = rawPath[0],
      samples,
      resolution,
      length,
      min,
      max,
      i,
      t;

  if (!progress) {
    t = i = segIndex = 0;
    segment = rawPath[0];
  } else if (progress === 1) {
    t = 1;
    segIndex = rawPath.length - 1;
    segment = rawPath[segIndex];
    i = segment.length - 8;
  } else {
    if (rawPath.length > 1) {
      //speed optimization: most of the time, there's only one segment so skip the recursion.
      length = rawPath.totalLength * progress;
      max = i = 0;

      while ((max += rawPath[i++].totalLength) < length) {
        segIndex = i;
      }

      segment = rawPath[segIndex];
      min = max - segment.totalLength;
      progress = (length - min) / (max - min) || 0;
    }

    samples = segment.samples;
    resolution = segment.resolution; //how many samples per cubic bezier chunk

    length = segment.totalLength * progress;
    i = segment.lookup[~~(length / segment.minLength)] || 0;
    min = i ? samples[i - 1] : 0;
    max = samples[i];

    if (max < length) {
      min = max;
      max = samples[++i];
    }

    t = 1 / resolution * ((length - min) / (max - min) + i % resolution);
    i = ~~(i / resolution) * 6;

    if (pushToNextIfAtEnd && t === 1) {
      if (i + 6 < segment.length) {
        i += 6;
        t = 0;
      } else if (segIndex + 1 < rawPath.length) {
        i = t = 0;
        segment = rawPath[++segIndex];
      }
    }
  }

  decoratee.t = t;
  decoratee.i = i;
  decoratee.path = rawPath;
  decoratee.segment = segment;
  decoratee.segIndex = segIndex;
  return decoratee;
}

function getPositionOnPath(rawPath, progress, includeAngle, point) {
  var segment = rawPath[0],
      result = point || {},
      samples,
      resolution,
      length,
      min,
      max,
      i,
      t,
      a,
      inv;

  if (progress < 0 || progress > 1) {
    progress = _wrapProgress(progress);
  }

  if (rawPath.length > 1) {
    //speed optimization: most of the time, there's only one segment so skip the recursion.
    length = rawPath.totalLength * progress;
    max = i = 0;

    while ((max += rawPath[i++].totalLength) < length) {
      segment = rawPath[i];
    }

    min = max - segment.totalLength;
    progress = (length - min) / (max - min) || 0;
  }

  samples = segment.samples;
  resolution = segment.resolution;
  length = segment.totalLength * progress;
  i = segment.lookup[progress < 1 ? ~~(length / segment.minLength) : segment.lookup.length - 1] || 0;
  min = i ? samples[i - 1] : 0;
  max = samples[i];

  if (max < length) {
    min = max;
    max = samples[++i];
  }

  t = 1 / resolution * ((length - min) / (max - min) + i % resolution) || 0;
  inv = 1 - t;
  i = ~~(i / resolution) * 6;
  a = segment[i];
  result.x = _round((t * t * (segment[i + 6] - a) + 3 * inv * (t * (segment[i + 4] - a) + inv * (segment[i + 2] - a))) * t + a);
  result.y = _round((t * t * (segment[i + 7] - (a = segment[i + 1])) + 3 * inv * (t * (segment[i + 5] - a) + inv * (segment[i + 3] - a))) * t + a);

  if (includeAngle) {
    result.angle = segment.totalLength ? getRotationAtBezierT(segment, i, t >= 1 ? 1 - 1e-9 : t ? t : 1e-9) : segment.angle || 0;
  }

  return result;
} //applies a matrix transform to RawPath (or a segment in a RawPath) and returns whatever was passed in (it transforms the values in the array(s), not a copy).

function transformRawPath(rawPath, a, b, c, d, tx, ty) {
  var j = rawPath.length,
      segment,
      l,
      i,
      x,
      y;

  while (--j > -1) {
    segment = rawPath[j];
    l = segment.length;

    for (i = 0; i < l; i += 2) {
      x = segment[i];
      y = segment[i + 1];
      segment[i] = x * a + y * c + tx;
      segment[i + 1] = x * b + y * d + ty;
    }
  }

  rawPath._dirty = 1;
  return rawPath;
} // translates SVG arc data into a segment (cubic beziers). Angle is in degrees.

function arcToSegment(lastX, lastY, rx, ry, angle, largeArcFlag, sweepFlag, x, y) {
  if (lastX === x && lastY === y) {
    return;
  }

  rx = _abs(rx);
  ry = _abs(ry);

  var angleRad = angle % 360 * _DEG2RAD$2,
      cosAngle = _cos(angleRad),
      sinAngle = _sin(angleRad),
      PI = Math.PI,
      TWOPI = PI * 2,
      dx2 = (lastX - x) / 2,
      dy2 = (lastY - y) / 2,
      x1 = cosAngle * dx2 + sinAngle * dy2,
      y1 = -sinAngle * dx2 + cosAngle * dy2,
      x1_sq = x1 * x1,
      y1_sq = y1 * y1,
      radiiCheck = x1_sq / (rx * rx) + y1_sq / (ry * ry);

  if (radiiCheck > 1) {
    rx = _sqrt(radiiCheck) * rx;
    ry = _sqrt(radiiCheck) * ry;
  }

  var rx_sq = rx * rx,
      ry_sq = ry * ry,
      sq = (rx_sq * ry_sq - rx_sq * y1_sq - ry_sq * x1_sq) / (rx_sq * y1_sq + ry_sq * x1_sq);

  if (sq < 0) {
    sq = 0;
  }

  var coef = (largeArcFlag === sweepFlag ? -1 : 1) * _sqrt(sq),
      cx1 = coef * (rx * y1 / ry),
      cy1 = coef * -(ry * x1 / rx),
      sx2 = (lastX + x) / 2,
      sy2 = (lastY + y) / 2,
      cx = sx2 + (cosAngle * cx1 - sinAngle * cy1),
      cy = sy2 + (sinAngle * cx1 + cosAngle * cy1),
      ux = (x1 - cx1) / rx,
      uy = (y1 - cy1) / ry,
      vx = (-x1 - cx1) / rx,
      vy = (-y1 - cy1) / ry,
      temp = ux * ux + uy * uy,
      angleStart = (uy < 0 ? -1 : 1) * Math.acos(ux / _sqrt(temp)),
      angleExtent = (ux * vy - uy * vx < 0 ? -1 : 1) * Math.acos((ux * vx + uy * vy) / _sqrt(temp * (vx * vx + vy * vy)));

  isNaN(angleExtent) && (angleExtent = PI); //rare edge case. Math.cos(-1) is NaN.

  if (!sweepFlag && angleExtent > 0) {
    angleExtent -= TWOPI;
  } else if (sweepFlag && angleExtent < 0) {
    angleExtent += TWOPI;
  }

  angleStart %= TWOPI;
  angleExtent %= TWOPI;

  var segments = Math.ceil(_abs(angleExtent) / (TWOPI / 4)),
      rawPath = [],
      angleIncrement = angleExtent / segments,
      controlLength = 4 / 3 * _sin(angleIncrement / 2) / (1 + _cos(angleIncrement / 2)),
      ma = cosAngle * rx,
      mb = sinAngle * rx,
      mc = sinAngle * -ry,
      md = cosAngle * ry,
      i;

  for (i = 0; i < segments; i++) {
    angle = angleStart + i * angleIncrement;
    x1 = _cos(angle);
    y1 = _sin(angle);
    ux = _cos(angle += angleIncrement);
    uy = _sin(angle);
    rawPath.push(x1 - controlLength * y1, y1 + controlLength * x1, ux + controlLength * uy, uy - controlLength * ux, ux, uy);
  } //now transform according to the actual size of the ellipse/arc (the beziers were noramlized, between 0 and 1 on a circle).


  for (i = 0; i < rawPath.length; i += 2) {
    x1 = rawPath[i];
    y1 = rawPath[i + 1];
    rawPath[i] = x1 * ma + y1 * mc + cx;
    rawPath[i + 1] = x1 * mb + y1 * md + cy;
  }

  rawPath[i - 2] = x; //always set the end to exactly where it's supposed to be

  rawPath[i - 1] = y;
  return rawPath;
} //Spits back a RawPath with absolute coordinates. Each segment starts with a "moveTo" command (x coordinate, then y) and then 2 control points (x, y, x, y), then anchor. The goal is to minimize memory and maximize speed.


function stringToRawPath(d) {
  var a = (d + "").replace(_scientific, function (m) {
    var n = +m;
    return n < 0.0001 && n > -0.0001 ? 0 : n;
  }).match(_svgPathExp) || [],
      //some authoring programs spit out very small numbers in scientific notation like "1e-5", so make sure we round that down to 0 first.
  path = [],
      relativeX = 0,
      relativeY = 0,
      twoThirds = 2 / 3,
      elements = a.length,
      points = 0,
      errorMessage = "ERROR: malformed path: " + d,
      i,
      j,
      x,
      y,
      command,
      isRelative,
      segment,
      startX,
      startY,
      difX,
      difY,
      beziers,
      prevCommand,
      flag1,
      flag2,
      line = function line(sx, sy, ex, ey) {
    difX = (ex - sx) / 3;
    difY = (ey - sy) / 3;
    segment.push(sx + difX, sy + difY, ex - difX, ey - difY, ex, ey);
  };

  if (!d || !isNaN(a[0]) || isNaN(a[1])) {
    console.log(errorMessage);
    return path;
  }

  for (i = 0; i < elements; i++) {
    prevCommand = command;

    if (isNaN(a[i])) {
      command = a[i].toUpperCase();
      isRelative = command !== a[i]; //lower case means relative
    } else {
      //commands like "C" can be strung together without any new command characters between.
      i--;
    }

    x = +a[i + 1];
    y = +a[i + 2];

    if (isRelative) {
      x += relativeX;
      y += relativeY;
    }

    if (!i) {
      startX = x;
      startY = y;
    } // "M" (move)


    if (command === "M") {
      if (segment) {
        if (segment.length < 8) {
          //if the path data was funky and just had a M with no actual drawing anywhere, skip it.
          path.length -= 1;
        } else {
          points += segment.length;
        }
      }

      relativeX = startX = x;
      relativeY = startY = y;
      segment = [x, y];
      path.push(segment);
      i += 2;
      command = "L"; //an "M" with more than 2 values gets interpreted as "lineTo" commands ("L").
      // "C" (cubic bezier)
    } else if (command === "C") {
      if (!segment) {
        segment = [0, 0];
      }

      if (!isRelative) {
        relativeX = relativeY = 0;
      } //note: "*1" is just a fast/short way to cast the value as a Number. WAAAY faster in Chrome, slightly slower in Firefox.


      segment.push(x, y, relativeX + a[i + 3] * 1, relativeY + a[i + 4] * 1, relativeX += a[i + 5] * 1, relativeY += a[i + 6] * 1);
      i += 6; // "S" (continuation of cubic bezier)
    } else if (command === "S") {
      difX = relativeX;
      difY = relativeY;

      if (prevCommand === "C" || prevCommand === "S") {
        difX += relativeX - segment[segment.length - 4];
        difY += relativeY - segment[segment.length - 3];
      }

      if (!isRelative) {
        relativeX = relativeY = 0;
      }

      segment.push(difX, difY, x, y, relativeX += a[i + 3] * 1, relativeY += a[i + 4] * 1);
      i += 4; // "Q" (quadratic bezier)
    } else if (command === "Q") {
      difX = relativeX + (x - relativeX) * twoThirds;
      difY = relativeY + (y - relativeY) * twoThirds;

      if (!isRelative) {
        relativeX = relativeY = 0;
      }

      relativeX += a[i + 3] * 1;
      relativeY += a[i + 4] * 1;
      segment.push(difX, difY, relativeX + (x - relativeX) * twoThirds, relativeY + (y - relativeY) * twoThirds, relativeX, relativeY);
      i += 4; // "T" (continuation of quadratic bezier)
    } else if (command === "T") {
      difX = relativeX - segment[segment.length - 4];
      difY = relativeY - segment[segment.length - 3];
      segment.push(relativeX + difX, relativeY + difY, x + (relativeX + difX * 1.5 - x) * twoThirds, y + (relativeY + difY * 1.5 - y) * twoThirds, relativeX = x, relativeY = y);
      i += 2; // "H" (horizontal line)
    } else if (command === "H") {
      line(relativeX, relativeY, relativeX = x, relativeY);
      i += 1; // "V" (vertical line)
    } else if (command === "V") {
      //adjust values because the first (and only one) isn't x in this case, it's y.
      line(relativeX, relativeY, relativeX, relativeY = x + (isRelative ? relativeY - relativeX : 0));
      i += 1; // "L" (line) or "Z" (close)
    } else if (command === "L" || command === "Z") {
      if (command === "Z") {
        x = startX;
        y = startY;
        segment.closed = true;
      }

      if (command === "L" || _abs(relativeX - x) > 0.5 || _abs(relativeY - y) > 0.5) {
        line(relativeX, relativeY, x, y);

        if (command === "L") {
          i += 2;
        }
      }

      relativeX = x;
      relativeY = y; // "A" (arc)
    } else if (command === "A") {
      flag1 = a[i + 4];
      flag2 = a[i + 5];
      difX = a[i + 6];
      difY = a[i + 7];
      j = 7;

      if (flag1.length > 1) {
        // for cases when the flags are merged, like "a8 8 0 018 8" (the 0 and 1 flags are WITH the x value of 8, but it could also be "a8 8 0 01-8 8" so it may include x or not)
        if (flag1.length < 3) {
          difY = difX;
          difX = flag2;
          j--;
        } else {
          difY = flag2;
          difX = flag1.substr(2);
          j -= 2;
        }

        flag2 = flag1.charAt(1);
        flag1 = flag1.charAt(0);
      }

      beziers = arcToSegment(relativeX, relativeY, +a[i + 1], +a[i + 2], +a[i + 3], +flag1, +flag2, (isRelative ? relativeX : 0) + difX * 1, (isRelative ? relativeY : 0) + difY * 1);
      i += j;

      if (beziers) {
        for (j = 0; j < beziers.length; j++) {
          segment.push(beziers[j]);
        }
      }

      relativeX = segment[segment.length - 2];
      relativeY = segment[segment.length - 1];
    } else {
      console.log(errorMessage);
    }
  }

  i = segment.length;

  if (i < 6) {
    //in case there's odd SVG like a M0,0 command at the very end.
    path.pop();
    i = 0;
  } else if (segment[0] === segment[i - 2] && segment[1] === segment[i - 1]) {
    segment.closed = true;
  }

  path.totalPoints = points + i;
  return path;
} //populates the points array in alternating x/y values (like [x, y, x, y...] instead of individual point objects [{x, y}, {x, y}...] to conserve memory and stay in line with how we're handling segment arrays
/*
function getAngleBetweenPoints(x0, y0, x1, y1, x2, y2) { //angle between 3 points in radians
	var dx1 = x1 - x0,
		dy1 = y1 - y0,
		dx2 = x2 - x1,
		dy2 = y2 - y1,
		dx3 = x2 - x0,
		dy3 = y2 - y0,
		a = dx1 * dx1 + dy1 * dy1,
		b = dx2 * dx2 + dy2 * dy2,
		c = dx3 * dx3 + dy3 * dy3;
	return Math.acos( (a + b - c) / _sqrt(4 * a * b) );
},
*/
//pointsToSegment() doesn't handle flat coordinates (where y is always 0) the way we need (the resulting control points are always right on top of the anchors), so this function basically makes the control points go directly up and down, varying in length based on the curviness (more curvy, further control points)

function flatPointsToSegment(points, curviness) {
  if (curviness === void 0) {
    curviness = 1;
  }

  var x = points[0],
      y = 0,
      segment = [x, y],
      i = 2;

  for (; i < points.length; i += 2) {
    segment.push(x, y, points[i], y = (points[i] - x) * curviness / 2, x = points[i], -y);
  }

  return segment;
} //points is an array of x/y points, like [x, y, x, y, x, y]

function pointsToSegment(points, curviness, cornerThreshold) {
  //points = simplifyPoints(points, tolerance);
  _abs(points[0] - points[2]) < 1e-4 && _abs(points[1] - points[3]) < 1e-4 && (points = points.slice(2)); // if the first two points are super close, dump the first one.

  var l = points.length - 2,
      x = +points[0],
      y = +points[1],
      nextX = +points[2],
      nextY = +points[3],
      segment = [x, y, x, y],
      dx2 = nextX - x,
      dy2 = nextY - y,
      closed = Math.abs(points[l] - x) < 0.001 && Math.abs(points[l + 1] - y) < 0.001,
      prevX,
      prevY,
      angle,
      slope,
      i,
      dx1,
      dx3,
      dy1,
      dy3,
      d1,
      d2,
      a,
      b,
      c;

  if (isNaN(cornerThreshold)) {
    cornerThreshold = Math.PI / 10;
  }

  if (closed) {
    // if the start and end points are basically on top of each other, close the segment by adding the 2nd point to the end, and the 2nd-to-last point to the beginning (we'll remove them at the end, but this allows the curvature to look perfect)
    points.push(nextX, nextY);
    nextX = x;
    nextY = y;
    x = points[l - 2];
    y = points[l - 1];
    points.unshift(x, y);
    l += 4;
  }

  curviness = curviness || curviness === 0 ? +curviness : 1;

  for (i = 2; i < l; i += 2) {
    prevX = x;
    prevY = y;
    x = nextX;
    y = nextY;
    nextX = +points[i + 2];
    nextY = +points[i + 3];

    if (x === nextX && y === nextY) {
      continue;
    }

    dx1 = dx2;
    dy1 = dy2;
    dx2 = nextX - x;
    dy2 = nextY - y;
    dx3 = nextX - prevX;
    dy3 = nextY - prevY;
    a = dx1 * dx1 + dy1 * dy1;
    b = dx2 * dx2 + dy2 * dy2;
    c = dx3 * dx3 + dy3 * dy3;
    angle = Math.acos((a + b - c) / _sqrt(4 * a * b)); //angle between the 3 points

    d2 = angle / Math.PI * curviness; //temporary precalculation for speed (reusing d2 variable)

    d1 = _sqrt(a) * d2; //the tighter the angle, the shorter we make the handles in proportion.

    d2 *= _sqrt(b);

    if (x !== prevX || y !== prevY) {
      if (angle > cornerThreshold) {
        slope = _atan2(dy3, dx3);
        segment.push(_round(x - _cos(slope) * d1), //first control point
        _round(y - _sin(slope) * d1), _round(x), //anchor
        _round(y), _round(x + _cos(slope) * d2), //second control point
        _round(y + _sin(slope) * d2));
      } else {
        slope = _atan2(dy1, dx1);
        segment.push(_round(x - _cos(slope) * d1), //first control point
        _round(y - _sin(slope) * d1));
        slope = _atan2(dy2, dx2);
        segment.push(_round(x), //anchor
        _round(y), _round(x + _cos(slope) * d2), //second control point
        _round(y + _sin(slope) * d2));
      }
    }
  }

  x !== nextX || y !== nextY || segment.length < 4 ? segment.push(_round(nextX), _round(nextY), _round(nextX), _round(nextY)) : segment.length -= 2;

  if (closed) {
    segment.splice(0, 6);
    segment.length = segment.length - 6;
  }

  return segment;
} //returns the squared distance between an x/y coordinate and a segment between x1/y1 and x2/y2
/*
Takes any of the following and converts it to an all Cubic Bezier SVG data string:
- A <path> data string like "M0,0 L2,4 v20,15 H100"
- A RawPath, like [[x, y, x, y, x, y, x, y][[x, y, x, y, x, y, x, y]]
- A Segment, like [x, y, x, y, x, y, x, y]

Note: all numbers are rounded down to the closest 0.001 to minimize memory, maximize speed, and avoid odd numbers like 1e-13
*/

function rawPathToString(rawPath) {
  if (_isNumber(rawPath[0])) {
    //in case a segment is passed in instead
    rawPath = [rawPath];
  }

  var result = "",
      l = rawPath.length,
      sl,
      s,
      i,
      segment;

  for (s = 0; s < l; s++) {
    segment = rawPath[s];
    result += "M" + _round(segment[0]) + "," + _round(segment[1]) + " C";
    sl = segment.length;

    for (i = 2; i < sl; i++) {
      result += _round(segment[i++]) + "," + _round(segment[i++]) + " " + _round(segment[i++]) + "," + _round(segment[i++]) + " " + _round(segment[i++]) + "," + _round(segment[i]) + " ";
    }

    if (segment.closed) {
      result += "z";
    }
  }

  return result;
}
/*
// takes a segment with coordinates [x, y, x, y, ...] and converts the control points into angles and lengths [x, y, angle, length, angle, length, x, y, angle, length, ...] so that it animates more cleanly and avoids odd breaks/kinks. For example, if you animate from 1 o'clock to 6 o'clock, it'd just go directly/linearly rather than around. So the length would be very short in the middle of the tween.
export function cpCoordsToAngles(segment, copy) {
	var result = copy ? segment.slice(0) : segment,
		x, y, i;
	for (i = 0; i < segment.length; i+=6) {
		x = segment[i+2] - segment[i];
		y = segment[i+3] - segment[i+1];
		result[i+2] = Math.atan2(y, x);
		result[i+3] = Math.sqrt(x * x + y * y);
		x = segment[i+6] - segment[i+4];
		y = segment[i+7] - segment[i+5];
		result[i+4] = Math.atan2(y, x);
		result[i+5] = Math.sqrt(x * x + y * y);
	}
	return result;
}

// takes a segment that was converted with cpCoordsToAngles() to have angles and lengths instead of coordinates for the control points, and converts it BACK into coordinates.
export function cpAnglesToCoords(segment, copy) {
	var result = copy ? segment.slice(0) : segment,
		length = segment.length,
		rnd = 1000,
		angle, l, i, j;
	for (i = 0; i < length; i+=6) {
		angle = segment[i+2];
		l = segment[i+3]; //length
		result[i+2] = (((segment[i] + Math.cos(angle) * l) * rnd) | 0) / rnd;
		result[i+3] = (((segment[i+1] + Math.sin(angle) * l) * rnd) | 0) / rnd;
		angle = segment[i+4];
		l = segment[i+5]; //length
		result[i+4] = (((segment[i+6] - Math.cos(angle) * l) * rnd) | 0) / rnd;
		result[i+5] = (((segment[i+7] - Math.sin(angle) * l) * rnd) | 0) / rnd;
	}
	return result;
}

//adds an "isSmooth" array to each segment and populates it with a boolean value indicating whether or not it's smooth (the control points have basically the same slope). For any smooth control points, it converts the coordinates into angle (x, in radians) and length (y) and puts them into the same index value in a smoothData array.
export function populateSmoothData(rawPath) {
	let j = rawPath.length,
		smooth, segment, x, y, x2, y2, i, l, a, a2, isSmooth, smoothData;
	while (--j > -1) {
		segment = rawPath[j];
		isSmooth = segment.isSmooth = segment.isSmooth || [0, 0, 0, 0];
		smoothData = segment.smoothData = segment.smoothData || [0, 0, 0, 0];
		isSmooth.length = 4;
		l = segment.length - 2;
		for (i = 6; i < l; i += 6) {
			x = segment[i] - segment[i - 2];
			y = segment[i + 1] - segment[i - 1];
			x2 = segment[i + 2] - segment[i];
			y2 = segment[i + 3] - segment[i + 1];
			a = _atan2(y, x);
			a2 = _atan2(y2, x2);
			smooth = (Math.abs(a - a2) < 0.09);
			if (smooth) {
				smoothData[i - 2] = a;
				smoothData[i + 2] = a2;
				smoothData[i - 1] = _sqrt(x * x + y * y);
				smoothData[i + 3] = _sqrt(x2 * x2 + y2 * y2);
			}
			isSmooth.push(smooth, smooth, 0, 0, smooth, smooth);
		}
		//if the first and last points are identical, check to see if there's a smooth transition. We must handle this a bit differently due to their positions in the array.
		if (segment[l] === segment[0] && segment[l+1] === segment[1]) {
			x = segment[0] - segment[l-2];
			y = segment[1] - segment[l-1];
			x2 = segment[2] - segment[0];
			y2 = segment[3] - segment[1];
			a = _atan2(y, x);
			a2 = _atan2(y2, x2);
			if (Math.abs(a - a2) < 0.09) {
				smoothData[l-2] = a;
				smoothData[2] = a2;
				smoothData[l-1] = _sqrt(x * x + y * y);
				smoothData[3] = _sqrt(x2 * x2 + y2 * y2);
				isSmooth[l-2] = isSmooth[l-1] = true; //don't change indexes 2 and 3 because we'll trigger everything from the END, and this will optimize file size a bit.
			}
		}
	}
	return rawPath;
}
export function pointToScreen(svgElement, point) {
	if (arguments.length < 2) { //by default, take the first set of coordinates in the path as the point
		let rawPath = getRawPath(svgElement);
		point = svgElement.ownerSVGElement.createSVGPoint();
		point.x = rawPath[0][0];
		point.y = rawPath[0][1];
	}
	return point.matrixTransform(svgElement.getScreenCTM());
}

*/

/*!
 * MotionPathPlugin 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

var _xProps = "x,translateX,left,marginLeft,xPercent".split(","),
    _yProps = "y,translateY,top,marginTop,yPercent".split(","),
    _DEG2RAD$1 = Math.PI / 180,
    gsap$2,
    PropTween$1,
    _getUnit,
    _toArray,
    _getGSAP$2 = function _getGSAP() {
  return gsap$2 || typeof window !== "undefined" && (gsap$2 = window.gsap) && gsap$2.registerPlugin && gsap$2;
},
    _populateSegmentFromArray = function _populateSegmentFromArray(segment, values, property, mode) {
  //mode: 0 = x but don't fill y yet, 1 = y, 2 = x and fill y with 0.
  var l = values.length,
      si = mode === 2 ? 0 : mode,
      i = 0;

  for (; i < l; i++) {
    segment[si] = parseFloat(values[i][property]);
    mode === 2 && (segment[si + 1] = 0);
    si += 2;
  }

  return segment;
},
    _getPropNum = function _getPropNum(target, prop, unit) {
  return parseFloat(target._gsap.get(target, prop, unit || "px")) || 0;
},
    _relativize = function _relativize(segment) {
  var x = segment[0],
      y = segment[1],
      i;

  for (i = 2; i < segment.length; i += 2) {
    x = segment[i] += x;
    y = segment[i + 1] += y;
  }
},
    _segmentToRawPath = function _segmentToRawPath(plugin, segment, target, x, y, slicer, vars, unitX, unitY) {
  if (vars.type === "cubic") {
    segment = [segment];
  } else {
    segment.unshift(_getPropNum(target, x, unitX), y ? _getPropNum(target, y, unitY) : 0);
    vars.relative && _relativize(segment);
    var pointFunc = y ? pointsToSegment : flatPointsToSegment;
    segment = [pointFunc(segment, vars.curviness)];
  }

  segment = slicer(_align(segment, target, vars));

  _addDimensionalPropTween(plugin, target, x, segment, "x", unitX);

  y && _addDimensionalPropTween(plugin, target, y, segment, "y", unitY);
  return cacheRawPathMeasurements(segment, vars.resolution || (vars.curviness === 0 ? 20 : 12)); //when curviness is 0, it creates control points right on top of the anchors which makes it more sensitive to resolution, thus we change the default accordingly.
},
    _emptyFunc = function _emptyFunc(v) {
  return v;
},
    _numExp = /[-+\.]*\d+[\.e\-\+]*\d*[e\-\+]*\d*/g,
    _originToPoint = function _originToPoint(element, origin, parentMatrix) {
  // origin is an array of normalized values (0-1) in relation to the width/height, so [0.5, 0.5] would be the center. It can also be "auto" in which case it will be the top left unless it's a <path>, when it will start at the beginning of the path itself.
  var m = getGlobalMatrix(element),
      svg,
      x,
      y;

  if ((element.tagName + "").toLowerCase() === "svg") {
    svg = element.viewBox.baseVal;
    x = svg.x;
    y = svg.y;
    svg.width || (svg = {
      width: +element.getAttribute("width"),
      height: +element.getAttribute("height")
    });
  } else {
    svg = origin && element.getBBox && element.getBBox();
    x = y = 0;
  }

  if (origin && origin !== "auto") {
    x += origin.push ? origin[0] * (svg ? svg.width : element.offsetWidth || 0) : origin.x;
    y += origin.push ? origin[1] * (svg ? svg.height : element.offsetHeight || 0) : origin.y;
  }

  return parentMatrix.apply(x || y ? m.apply({
    x: x,
    y: y
  }) : {
    x: m.e,
    y: m.f
  });
},
    _getAlignMatrix = function _getAlignMatrix(fromElement, toElement, fromOrigin, toOrigin) {
  var parentMatrix = getGlobalMatrix(fromElement.parentNode, true, true),
      m = parentMatrix.clone().multiply(getGlobalMatrix(toElement)),
      fromPoint = _originToPoint(fromElement, fromOrigin, parentMatrix),
      _originToPoint2 = _originToPoint(toElement, toOrigin, parentMatrix),
      x = _originToPoint2.x,
      y = _originToPoint2.y,
      p;

  m.e = m.f = 0;

  if (toOrigin === "auto" && toElement.getTotalLength && toElement.tagName.toLowerCase() === "path") {
    p = toElement.getAttribute("d").match(_numExp) || [];
    p = m.apply({
      x: +p[0],
      y: +p[1]
    });
    x += p.x;
    y += p.y;
  }

  if (p || toElement.getBBox && fromElement.getBBox && toElement.ownerSVGElement === fromElement.ownerSVGElement) {
    p = m.apply(toElement.getBBox());
    x -= p.x;
    y -= p.y;
  }

  m.e = x - fromPoint.x;
  m.f = y - fromPoint.y;
  return m;
},
    _align = function _align(rawPath, target, _ref) {
  var align = _ref.align,
      matrix = _ref.matrix,
      offsetX = _ref.offsetX,
      offsetY = _ref.offsetY,
      alignOrigin = _ref.alignOrigin;

  var x = rawPath[0][0],
      y = rawPath[0][1],
      curX = _getPropNum(target, "x"),
      curY = _getPropNum(target, "y"),
      alignTarget,
      m,
      p;

  if (!rawPath || !rawPath.length) {
    return getRawPath("M0,0L0,0");
  }

  if (align) {
    if (align === "self" || (alignTarget = _toArray(align)[0] || target) === target) {
      transformRawPath(rawPath, 1, 0, 0, 1, curX - x, curY - y);
    } else {
      if (alignOrigin && alignOrigin[2] !== false) {
        gsap$2.set(target, {
          transformOrigin: alignOrigin[0] * 100 + "% " + alignOrigin[1] * 100 + "%"
        });
      } else {
        alignOrigin = [_getPropNum(target, "xPercent") / -100, _getPropNum(target, "yPercent") / -100];
      }

      m = _getAlignMatrix(target, alignTarget, alignOrigin, "auto");
      p = m.apply({
        x: x,
        y: y
      });
      transformRawPath(rawPath, m.a, m.b, m.c, m.d, curX + m.e - (p.x - m.e), curY + m.f - (p.y - m.f));
    }
  }

  if (matrix) {
    transformRawPath(rawPath, matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f);
  } else if (offsetX || offsetY) {
    transformRawPath(rawPath, 1, 0, 0, 1, offsetX || 0, offsetY || 0);
  }

  return rawPath;
},
    _addDimensionalPropTween = function _addDimensionalPropTween(plugin, target, property, rawPath, pathProperty, forceUnit) {
  var cache = target._gsap,
      harness = cache.harness,
      alias = harness && harness.aliases && harness.aliases[property],
      prop = alias && alias.indexOf(",") < 0 ? alias : property,
      pt = plugin._pt = new PropTween$1(plugin._pt, target, prop, 0, 0, _emptyFunc, 0, cache.set(target, prop, plugin));
  pt.u = _getUnit(cache.get(target, prop, forceUnit)) || 0;
  pt.path = rawPath;
  pt.pp = pathProperty;

  plugin._props.push(prop);
},
    _sliceModifier = function _sliceModifier(start, end) {
  return function (rawPath) {
    return start || end !== 1 ? sliceRawPath(rawPath, start, end) : rawPath;
  };
};

var MotionPathPlugin = {
  version: "3.6.1",
  name: "motionPath",
  register: function register(core, Plugin, propTween) {
    gsap$2 = core;
    _getUnit = gsap$2.utils.getUnit;
    _toArray = gsap$2.utils.toArray;
    PropTween$1 = propTween;
  },
  init: function init(target, vars) {
    if (!gsap$2) {
      console.warn("Please gsap.registerPlugin(MotionPathPlugin)");
      return false;
    }

    if (!(typeof vars === "object" && !vars.style) || !vars.path) {
      vars = {
        path: vars
      };
    }

    var rawPaths = [],
        _vars = vars,
        path = _vars.path,
        autoRotate = _vars.autoRotate,
        unitX = _vars.unitX,
        unitY = _vars.unitY,
        x = _vars.x,
        y = _vars.y,
        firstObj = path[0],
        slicer = _sliceModifier(vars.start, "end" in vars ? vars.end : 1),
        rawPath,
        p;

    this.rawPaths = rawPaths;
    this.target = target;

    if (this.rotate = autoRotate || autoRotate === 0) {
      //get the rotational data FIRST so that the setTransform() method is called in the correct order in the render() loop - rotation gets set last.
      this.rOffset = parseFloat(autoRotate) || 0;
      this.radians = !!vars.useRadians;
      this.rProp = vars.rotation || "rotation"; // rotation property

      this.rSet = target._gsap.set(target, this.rProp, this); // rotation setter

      this.ru = _getUnit(target._gsap.get(target, this.rProp)) || 0; // rotation units
    }

    if (Array.isArray(path) && !("closed" in path) && typeof firstObj !== "number") {
      for (p in firstObj) {
        if (!x && ~_xProps.indexOf(p)) {
          x = p;
        } else if (!y && ~_yProps.indexOf(p)) {
          y = p;
        }
      }

      if (x && y) {
        //correlated values
        rawPaths.push(_segmentToRawPath(this, _populateSegmentFromArray(_populateSegmentFromArray([], path, x, 0), path, y, 1), target, x, y, slicer, vars, unitX || _getUnit(path[0][x]), unitY || _getUnit(path[0][y])));
      } else {
        x = y = 0;
      }

      for (p in firstObj) {
        p !== x && p !== y && rawPaths.push(_segmentToRawPath(this, _populateSegmentFromArray([], path, p, 2), target, p, 0, slicer, vars, _getUnit(path[0][p])));
      }
    } else {
      rawPath = slicer(_align(getRawPath(vars.path), target, vars));
      cacheRawPathMeasurements(rawPath, vars.resolution);
      rawPaths.push(rawPath);

      _addDimensionalPropTween(this, target, vars.x || "x", rawPath, "x", vars.unitX || "px");

      _addDimensionalPropTween(this, target, vars.y || "y", rawPath, "y", vars.unitY || "px");
    }
  },
  render: function render(ratio, data) {
    var rawPaths = data.rawPaths,
        i = rawPaths.length,
        pt = data._pt;

    if (ratio > 1) {
      ratio = 1;
    } else if (ratio < 0) {
      ratio = 0;
    }

    while (i--) {
      getPositionOnPath(rawPaths[i], ratio, !i && data.rotate, rawPaths[i]);
    }

    while (pt) {
      pt.set(pt.t, pt.p, pt.path[pt.pp] + pt.u, pt.d, ratio);
      pt = pt._next;
    }

    data.rotate && data.rSet(data.target, data.rProp, rawPaths[0].angle * (data.radians ? _DEG2RAD$1 : 1) + data.rOffset + data.ru, data, ratio);
  },
  getLength: function getLength(path) {
    return cacheRawPathMeasurements(getRawPath(path)).totalLength;
  },
  sliceRawPath: sliceRawPath,
  getRawPath: getRawPath,
  pointsToSegment: pointsToSegment,
  stringToRawPath: stringToRawPath,
  rawPathToString: rawPathToString,
  transformRawPath: transformRawPath,
  getGlobalMatrix: getGlobalMatrix,
  getPositionOnPath: getPositionOnPath,
  cacheRawPathMeasurements: cacheRawPathMeasurements,
  convertToPath: function convertToPath$1(targets, swap) {
    return _toArray(targets).map(function (target) {
      return convertToPath(target, swap !== false);
    });
  },
  convertCoordinates: function convertCoordinates(fromElement, toElement, point) {
    var m = getGlobalMatrix(toElement, true, true).multiply(getGlobalMatrix(fromElement));
    return point ? m.apply(point) : m;
  },
  getAlignMatrix: _getAlignMatrix,
  getRelativePosition: function getRelativePosition(fromElement, toElement, fromOrigin, toOrigin) {
    var m = _getAlignMatrix(fromElement, toElement, fromOrigin, toOrigin);

    return {
      x: m.e,
      y: m.f
    };
  },
  arrayToRawPath: function arrayToRawPath(value, vars) {
    vars = vars || {};

    var segment = _populateSegmentFromArray(_populateSegmentFromArray([], value, vars.x || "x", 0), value, vars.y || "y", 1);

    vars.relative && _relativize(segment);
    return [vars.type === "cubic" ? segment : pointsToSegment(segment, vars.curviness)];
  }
};
_getGSAP$2() && gsap$2.registerPlugin(MotionPathPlugin);

/*!
 * PixiPlugin 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var gsap$1,
    _win,
    _splitColor,
    _PIXI,
    PropTween,
    _getSetter,
    _windowExists = function _windowExists() {
  return typeof window !== "undefined";
},
    _getGSAP$1 = function _getGSAP() {
  return gsap$1 || _windowExists() && (gsap$1 = window.gsap) && gsap$1.registerPlugin && gsap$1;
},
    _isFunction = function _isFunction(value) {
  return typeof value === "function";
},
    _warn = function _warn(message) {
  return console.warn(message);
},
    _idMatrix = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0],
    _lumR = 0.212671,
    _lumG = 0.715160,
    _lumB = 0.072169,
    _applyMatrix = function _applyMatrix(m, m2) {
  var temp = [],
      i = 0,
      z = 0,
      y,
      x;

  for (y = 0; y < 4; y++) {
    for (x = 0; x < 5; x++) {
      z = x === 4 ? m[i + 4] : 0;
      temp[i + x] = m[i] * m2[x] + m[i + 1] * m2[x + 5] + m[i + 2] * m2[x + 10] + m[i + 3] * m2[x + 15] + z;
    }

    i += 5;
  }

  return temp;
},
    _setSaturation = function _setSaturation(m, n) {
  var inv = 1 - n,
      r = inv * _lumR,
      g = inv * _lumG,
      b = inv * _lumB;
  return _applyMatrix([r + n, g, b, 0, 0, r, g + n, b, 0, 0, r, g, b + n, 0, 0, 0, 0, 0, 1, 0], m);
},
    _colorize = function _colorize(m, color, amount) {
  var c = _splitColor(color),
      r = c[0] / 255,
      g = c[1] / 255,
      b = c[2] / 255,
      inv = 1 - amount;

  return _applyMatrix([inv + amount * r * _lumR, amount * r * _lumG, amount * r * _lumB, 0, 0, amount * g * _lumR, inv + amount * g * _lumG, amount * g * _lumB, 0, 0, amount * b * _lumR, amount * b * _lumG, inv + amount * b * _lumB, 0, 0, 0, 0, 0, 1, 0], m);
},
    _setHue = function _setHue(m, n) {
  n *= Math.PI / 180;
  var c = Math.cos(n),
      s = Math.sin(n);
  return _applyMatrix([_lumR + c * (1 - _lumR) + s * -_lumR, _lumG + c * -_lumG + s * -_lumG, _lumB + c * -_lumB + s * (1 - _lumB), 0, 0, _lumR + c * -_lumR + s * 0.143, _lumG + c * (1 - _lumG) + s * 0.14, _lumB + c * -_lumB + s * -0.283, 0, 0, _lumR + c * -_lumR + s * -(1 - _lumR), _lumG + c * -_lumG + s * _lumG, _lumB + c * (1 - _lumB) + s * _lumB, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], m);
},
    _setContrast = function _setContrast(m, n) {
  return _applyMatrix([n, 0, 0, 0, 0.5 * (1 - n), 0, n, 0, 0, 0.5 * (1 - n), 0, 0, n, 0, 0.5 * (1 - n), 0, 0, 0, 1, 0], m);
},
    _getFilter = function _getFilter(target, type) {
  var filterClass = _PIXI.filters[type],
      filters = target.filters || [],
      i = filters.length,
      filter;

  if (!filterClass) {
    _warn(type + " not found. PixiPlugin.registerPIXI(PIXI)");
  }

  while (--i > -1) {
    if (filters[i] instanceof filterClass) {
      return filters[i];
    }
  }

  filter = new filterClass();

  if (type === "BlurFilter") {
    filter.blur = 0;
  }

  filters.push(filter);
  target.filters = filters;
  return filter;
},
    _addColorMatrixFilterCacheTween = function _addColorMatrixFilterCacheTween(p, plugin, cache, vars) {
  //we cache the ColorMatrixFilter components in a _gsColorMatrixFilter object attached to the target object so that it's easy to grab the current value at any time.
  plugin.add(cache, p, cache[p], vars[p]);

  plugin._props.push(p);
},
    _applyBrightnessToMatrix = function _applyBrightnessToMatrix(brightness, matrix) {
  var temp = new _PIXI.filters.ColorMatrixFilter();
  temp.matrix = matrix;
  temp.brightness(brightness, true);
  return temp.matrix;
},
    _copy = function _copy(obj) {
  var copy = {},
      p;

  for (p in obj) {
    copy[p] = obj[p];
  }

  return copy;
},
    _CMFdefaults = {
  contrast: 1,
  saturation: 1,
  colorizeAmount: 0,
  colorize: "rgb(255,255,255)",
  hue: 0,
  brightness: 1
},
    _parseColorMatrixFilter = function _parseColorMatrixFilter(target, v, pg) {
  var filter = _getFilter(target, "ColorMatrixFilter"),
      cache = target._gsColorMatrixFilter = target._gsColorMatrixFilter || _copy(_CMFdefaults),
      combine = v.combineCMF && !("colorMatrixFilter" in v && !v.colorMatrixFilter),
      i,
      matrix,
      startMatrix;

  startMatrix = filter.matrix;

  if (v.resolution) {
    filter.resolution = v.resolution;
  }

  if (v.matrix && v.matrix.length === startMatrix.length) {
    matrix = v.matrix;

    if (cache.contrast !== 1) {
      _addColorMatrixFilterCacheTween("contrast", pg, cache, _CMFdefaults);
    }

    if (cache.hue) {
      _addColorMatrixFilterCacheTween("hue", pg, cache, _CMFdefaults);
    }

    if (cache.brightness !== 1) {
      _addColorMatrixFilterCacheTween("brightness", pg, cache, _CMFdefaults);
    }

    if (cache.colorizeAmount) {
      _addColorMatrixFilterCacheTween("colorize", pg, cache, _CMFdefaults);

      _addColorMatrixFilterCacheTween("colorizeAmount", pg, cache, _CMFdefaults);
    }

    if (cache.saturation !== 1) {
      _addColorMatrixFilterCacheTween("saturation", pg, cache, _CMFdefaults);
    }
  } else {
    matrix = _idMatrix.slice();

    if (v.contrast != null) {
      matrix = _setContrast(matrix, +v.contrast);

      _addColorMatrixFilterCacheTween("contrast", pg, cache, v);
    } else if (cache.contrast !== 1) {
      if (combine) {
        matrix = _setContrast(matrix, cache.contrast);
      } else {
        _addColorMatrixFilterCacheTween("contrast", pg, cache, _CMFdefaults);
      }
    }

    if (v.hue != null) {
      matrix = _setHue(matrix, +v.hue);

      _addColorMatrixFilterCacheTween("hue", pg, cache, v);
    } else if (cache.hue) {
      if (combine) {
        matrix = _setHue(matrix, cache.hue);
      } else {
        _addColorMatrixFilterCacheTween("hue", pg, cache, _CMFdefaults);
      }
    }

    if (v.brightness != null) {
      matrix = _applyBrightnessToMatrix(+v.brightness, matrix);

      _addColorMatrixFilterCacheTween("brightness", pg, cache, v);
    } else if (cache.brightness !== 1) {
      if (combine) {
        matrix = _applyBrightnessToMatrix(cache.brightness, matrix);
      } else {
        _addColorMatrixFilterCacheTween("brightness", pg, cache, _CMFdefaults);
      }
    }

    if (v.colorize != null) {
      v.colorizeAmount = "colorizeAmount" in v ? +v.colorizeAmount : 1;
      matrix = _colorize(matrix, v.colorize, v.colorizeAmount);

      _addColorMatrixFilterCacheTween("colorize", pg, cache, v);

      _addColorMatrixFilterCacheTween("colorizeAmount", pg, cache, v);
    } else if (cache.colorizeAmount) {
      if (combine) {
        matrix = _colorize(matrix, cache.colorize, cache.colorizeAmount);
      } else {
        _addColorMatrixFilterCacheTween("colorize", pg, cache, _CMFdefaults);

        _addColorMatrixFilterCacheTween("colorizeAmount", pg, cache, _CMFdefaults);
      }
    }

    if (v.saturation != null) {
      matrix = _setSaturation(matrix, +v.saturation);

      _addColorMatrixFilterCacheTween("saturation", pg, cache, v);
    } else if (cache.saturation !== 1) {
      if (combine) {
        matrix = _setSaturation(matrix, cache.saturation);
      } else {
        _addColorMatrixFilterCacheTween("saturation", pg, cache, _CMFdefaults);
      }
    }
  }

  i = matrix.length;

  while (--i > -1) {
    if (matrix[i] !== startMatrix[i]) {
      pg.add(startMatrix, i, startMatrix[i], matrix[i], "colorMatrixFilter");
    }
  }

  pg._props.push("colorMatrixFilter");
},
    _renderColor = function _renderColor(ratio, _ref) {
  var t = _ref.t,
      p = _ref.p,
      color = _ref.color,
      set = _ref.set;
  set(t, p, color[0] << 16 | color[1] << 8 | color[2]);
},
    _renderDirtyCache = function _renderDirtyCache(ratio, _ref2) {
  var g = _ref2.g;

  if (g) {
    //in order for PixiJS to actually redraw GraphicsData, we've gotta increment the "dirty" and "clearDirty" values. If we don't do this, the values will be tween properly, but not rendered.
    g.dirty++;
    g.clearDirty++;
  }
},
    _renderAutoAlpha = function _renderAutoAlpha(ratio, data) {
  data.t.visible = !!data.t.alpha;
},
    _addColorTween = function _addColorTween(target, p, value, plugin) {
  var currentValue = target[p],
      startColor = _splitColor(_isFunction(currentValue) ? target[p.indexOf("set") || !_isFunction(target["get" + p.substr(3)]) ? p : "get" + p.substr(3)]() : currentValue),
      endColor = _splitColor(value);

  plugin._pt = new PropTween(plugin._pt, target, p, 0, 0, _renderColor, {
    t: target,
    p: p,
    color: startColor,
    set: _getSetter(target, p)
  });
  plugin.add(startColor, 0, startColor[0], endColor[0]);
  plugin.add(startColor, 1, startColor[1], endColor[1]);
  plugin.add(startColor, 2, startColor[2], endColor[2]);
},
    _colorProps = {
  tint: 1,
  lineColor: 1,
  fillColor: 1
},
    _xyContexts = "position,scale,skew,pivot,anchor,tilePosition,tileScale".split(","),
    _contexts = {
  x: "position",
  y: "position",
  tileX: "tilePosition",
  tileY: "tilePosition"
},
    _colorMatrixFilterProps = {
  colorMatrixFilter: 1,
  saturation: 1,
  contrast: 1,
  hue: 1,
  colorize: 1,
  colorizeAmount: 1,
  brightness: 1,
  combineCMF: 1
},
    _DEG2RAD = Math.PI / 180,
    _isString = function _isString(value) {
  return typeof value === "string";
},
    _degreesToRadians = function _degreesToRadians(value) {
  return _isString(value) && value.charAt(1) === "=" ? value.substr(0, 2) + parseFloat(value.substr(2)) * _DEG2RAD : value * _DEG2RAD;
},
    _renderPropWithEnd = function _renderPropWithEnd(ratio, data) {
  return data.set(data.t, data.p, ratio === 1 ? data.e : Math.round((data.s + data.c * ratio) * 100000) / 100000, data);
},
    _addRotationalPropTween = function _addRotationalPropTween(plugin, target, property, startNum, endValue, radians) {
  var cap = 360 * (radians ? _DEG2RAD : 1),
      isString = _isString(endValue),
      relative = isString && endValue.charAt(1) === "=" ? +(endValue.charAt(0) + "1") : 0,
      endNum = parseFloat(relative ? endValue.substr(2) : endValue) * (radians ? _DEG2RAD : 1),
      change = relative ? endNum * relative : endNum - startNum,
      finalValue = startNum + change,
      direction,
      pt;

  if (isString) {
    direction = endValue.split("_")[1];

    if (direction === "short") {
      change %= cap;

      if (change !== change % (cap / 2)) {
        change += change < 0 ? cap : -cap;
      }
    }

    if (direction === "cw" && change < 0) {
      change = (change + cap * 1e10) % cap - ~~(change / cap) * cap;
    } else if (direction === "ccw" && change > 0) {
      change = (change - cap * 1e10) % cap - ~~(change / cap) * cap;
    }
  }

  plugin._pt = pt = new PropTween(plugin._pt, target, property, startNum, change, _renderPropWithEnd);
  pt.e = finalValue;
  return pt;
},
    _initCore = function _initCore() {
  if (_windowExists()) {
    _win = window;
    gsap$1 = _getGSAP$1();
    _PIXI = _PIXI || _win.PIXI;

    _splitColor = function _splitColor(color) {
      return gsap$1.utils.splitColor((color + "").substr(0, 2) === "0x" ? "#" + color.substr(2) : color);
    }; // some colors in PIXI are reported as "0xFF4421" instead of "#FF4421".

  }
},
    i$1,
    p; //context setup...


for (i$1 = 0; i$1 < _xyContexts.length; i$1++) {
  p = _xyContexts[i$1];
  _contexts[p + "X"] = p;
  _contexts[p + "Y"] = p;
}

var PixiPlugin = {
  version: "3.6.1",
  name: "pixi",
  register: function register(core, Plugin, propTween) {
    gsap$1 = core;
    PropTween = propTween;
    _getSetter = Plugin.getSetter;

    _initCore();
  },
  registerPIXI: function registerPIXI(pixi) {
    _PIXI = pixi;
  },
  init: function init(target, values, tween, index, targets) {
    if (!_PIXI) {
      _initCore();
    }

    if (!target instanceof _PIXI.DisplayObject) {
      return false;
    }

    var isV4 = _PIXI.VERSION.charAt(0) === "4",
        context,
        axis,
        value,
        colorMatrix,
        filter,
        p,
        padding,
        i,
        data;

    for (p in values) {
      context = _contexts[p];
      value = values[p];

      if (context) {
        axis = ~p.charAt(p.length - 1).toLowerCase().indexOf("x") ? "x" : "y";
        this.add(target[context], axis, target[context][axis], context === "skew" ? _degreesToRadians(value) : value);
      } else if (p === "scale" || p === "anchor" || p === "pivot" || p === "tileScale") {
        this.add(target[p], "x", target[p].x, value);
        this.add(target[p], "y", target[p].y, value);
      } else if (p === "rotation" || p === "angle") {
        //PIXI expects rotation in radians, but as a convenience we let folks define it in degrees and we do the conversion.
        _addRotationalPropTween(this, target, p, target[p], value, p === "rotation");
      } else if (_colorMatrixFilterProps[p]) {
        if (!colorMatrix) {
          _parseColorMatrixFilter(target, values.colorMatrixFilter || values, this);

          colorMatrix = true;
        }
      } else if (p === "blur" || p === "blurX" || p === "blurY" || p === "blurPadding") {
        filter = _getFilter(target, "BlurFilter");
        this.add(filter, p, filter[p], value);

        if (values.blurPadding !== 0) {
          padding = values.blurPadding || Math.max(filter[p], value) * 2;
          i = target.filters.length;

          while (--i > -1) {
            target.filters[i].padding = Math.max(target.filters[i].padding, padding); //if we don't expand the padding on all the filters, it can look clipped.
          }
        }
      } else if (_colorProps[p]) {
        if ((p === "lineColor" || p === "fillColor") && target instanceof _PIXI.Graphics) {
          data = (target.geometry || target).graphicsData; //"geometry" was introduced in PIXI version 5

          this._pt = new PropTween(this._pt, target, p, 0, 0, _renderDirtyCache, {
            g: target.geometry || target
          });
          i = data.length;

          while (--i > -1) {
            _addColorTween(isV4 ? data[i] : data[i][p.substr(0, 4) + "Style"], isV4 ? p : "color", value, this);
          }
        } else {
          _addColorTween(target, p, value, this);
        }
      } else if (p === "autoAlpha") {
        this._pt = new PropTween(this._pt, target, "visible", 0, 0, _renderAutoAlpha);
        this.add(target, "alpha", target.alpha, value);

        this._props.push("alpha", "visible");
      } else if (p !== "resolution") {
        this.add(target, p, "get", value);
      }

      this._props.push(p);
    }
  }
};
_getGSAP$1() && gsap$1.registerPlugin(PixiPlugin);

/*!
 * strings: 3.6.1
 * https://greensock.com
 *
 * Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

/* eslint-disable */
var _trimExp = /(^\s+|\s+$)/g;
var emojiExp = /([\uD800-\uDBFF][\uDC00-\uDFFF](?:[\u200D\uFE0F][\uD800-\uDBFF][\uDC00-\uDFFF]){2,}|\uD83D\uDC69(?:\u200D(?:(?:\uD83D\uDC69\u200D)?\uD83D\uDC67|(?:\uD83D\uDC69\u200D)?\uD83D\uDC66)|\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC69\u200D(?:\uD83D\uDC69\u200D)?\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D(?:\uD83D\uDC69\u200D)?\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]\uFE0F|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC6F\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3C-\uDD3E\uDDD6-\uDDDF])\u200D[\u2640\u2642]\uFE0F|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF6\uD83C\uDDE6|\uD83C\uDDF4\uD83C\uDDF2|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uFE0F\u200D[\u2640\u2642]|(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642])\uFE0F|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2695\u2696\u2708]|\uD83D\uDC69\u200D[\u2695\u2696\u2708]|\uD83D\uDC68(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708]))\uFE0F|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83D\uDC69\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69]))|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67)\uDB40\uDC7F|\uD83D\uDC68(?:\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC66\u200D\uD83D\uDC66|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92])|(?:\uD83C[\uDFFB-\uDFFF])\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]))|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDD1-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|\uD83D\uDC68(?:\u200D(?:(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC67|(?:(?:\uD83D[\uDC68\uDC69])\u200D)?\uD83D\uDC66)|\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC69\uDC6E\uDC70-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD18-\uDD1C\uDD1E\uDD1F\uDD26\uDD30-\uDD39\uDD3D\uDD3E\uDDD1-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])?|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDEEB\uDEEC\uDEF4-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u2660\u2663\u2665\u2666\u2668\u267B\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEF8]|\uD83E[\uDD10-\uDD3A\uDD3C-\uDD3E\uDD40-\uDD45\uDD47-\uDD4C\uDD50-\uDD6B\uDD80-\uDD97\uDDC0\uDDD0-\uDDE6])\uFE0F)/;
function getText(e) {
  var type = e.nodeType,
      result = "";

  if (type === 1 || type === 9 || type === 11) {
    if (typeof e.textContent === "string") {
      return e.textContent;
    } else {
      for (e = e.firstChild; e; e = e.nextSibling) {
        result += getText(e);
      }
    }
  } else if (type === 3 || type === 4) {
    return e.nodeValue;
  }

  return result;
}
function splitInnerHTML(element, delimiter, trim) {
  var node = element.firstChild,
      result = [];

  while (node) {
    if (node.nodeType === 3) {
      result.push.apply(result, emojiSafeSplit((node.nodeValue + "").replace(/^\n+/g, "").replace(/\s+/g, " "), delimiter, trim));
    } else if ((node.nodeName + "").toLowerCase() === "br") {
      result[result.length - 1] += "<br>";
    } else {
      result.push(node.outerHTML);
    }

    node = node.nextSibling;
  }

  return result;
}
/*
//smaller kb version that only handles the simpler emoji's, which is often perfectly adequate.

let _emoji = "[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2694-\u2697]|\uD83E[\uDD10-\uDD5D]|[\uD800-\uDBFF][\uDC00-\uDFFF]",
	_emojiExp = new RegExp(_emoji),
	_emojiAndCharsExp = new RegExp(_emoji + "|.", "g"),
	_emojiSafeSplit = (text, delimiter, trim) => {
		if (trim) {
			text = text.replace(_trimExp, "");
		}
		return ((delimiter === "" || !delimiter) && _emojiExp.test(text)) ? text.match(_emojiAndCharsExp) : text.split(delimiter || "");
	};
 */

function emojiSafeSplit(text, delimiter, trim) {
  text += ""; // make sure it's cast as a string. Someone may pass in a number.

  if (trim) {
    text = text.replace(_trimExp, "");
  }

  if (delimiter && delimiter !== "") {
    return text.replace(/>/g, "&gt;").replace(/</g, "&lt;").split(delimiter);
  }

  var result = [],
      l = text.length,
      i = 0,
      j,
      character;

  for (; i < l; i++) {
    character = text.charAt(i);

    if (character.charCodeAt(0) >= 0xD800 && character.charCodeAt(0) <= 0xDBFF || text.charCodeAt(i + 1) >= 0xFE00 && text.charCodeAt(i + 1) <= 0xFE0F) {
      //special emoji characters use 2 or 4 unicode characters that we must keep together.
      j = ((text.substr(i, 12).split(emojiExp) || [])[1] || "").length || 2;
      character = text.substr(i, j);
      result.emoji = 1;
      i += j - 1;
    }

    result.push(character === ">" ? "&gt;" : character === "<" ? "&lt;" : character);
  }

  return result;
}

/*!
 * TextPlugin 3.6.1
 * https://greensock.com
 *
 * @license Copyright 2008-2021, GreenSock. All rights reserved.
 * Subject to the terms at https://greensock.com/standard-license or for
 * Club GreenSock members, the agreement issued with that membership.
 * @author: Jack Doyle, jack@greensock.com
*/

var gsap,
    _tempDiv,
    _getGSAP = function _getGSAP() {
  return gsap || typeof window !== "undefined" && (gsap = window.gsap) && gsap.registerPlugin && gsap;
};

var TextPlugin = {
  version: "3.6.1",
  name: "text",
  init: function init(target, value, tween) {
    var i = target.nodeName.toUpperCase(),
        data = this,
        _short,
        text,
        original,
        j,
        condensedText,
        condensedOriginal,
        aggregate,
        s;

    data.svg = target.getBBox && (i === "TEXT" || i === "TSPAN");

    if (!("innerHTML" in target) && !data.svg) {
      return false;
    }

    data.target = target;

    if (typeof value !== "object") {
      value = {
        value: value
      };
    }

    if (!("value" in value)) {
      data.text = data.original = [""];
      return;
    }

    data.delimiter = value.delimiter || "";
    original = splitInnerHTML(target, data.delimiter);

    if (!_tempDiv) {
      _tempDiv = document.createElement("div");
    }

    _tempDiv.innerHTML = value.value;
    text = splitInnerHTML(_tempDiv, data.delimiter);
    data.from = tween._from;

    if (data.from) {
      i = original;
      original = text;
      text = i;
    }

    data.hasClass = !!(value.newClass || value.oldClass);
    data.newClass = value.newClass;
    data.oldClass = value.oldClass;
    i = original.length - text.length;
    _short = i < 0 ? original : text;
    data.fillChar = value.fillChar || (value.padSpace ? "&nbsp;" : "");

    if (i < 0) {
      i = -i;
    }

    while (--i > -1) {
      _short.push(data.fillChar);
    }

    if (value.type === "diff") {
      j = 0;
      condensedText = [];
      condensedOriginal = [];
      aggregate = "";

      for (i = 0; i < text.length; i++) {
        s = text[i];

        if (s === original[i]) {
          aggregate += s;
        } else {
          condensedText[j] = aggregate + s;
          condensedOriginal[j++] = aggregate + original[i];
          aggregate = "";
        }
      }

      text = condensedText;
      original = condensedOriginal;

      if (aggregate) {
        text.push(aggregate);
        original.push(aggregate);
      }
    }

    if (value.speed) {
      tween.duration(Math.min(0.05 / value.speed * _short.length, value.maxDuration || 9999));
    }

    this.original = original;
    this.text = text;

    this._props.push("text");
  },
  render: function render(ratio, data) {
    if (ratio > 1) {
      ratio = 1;
    } else if (ratio < 0) {
      ratio = 0;
    }

    if (data.from) {
      ratio = 1 - ratio;
    }

    var text = data.text,
        hasClass = data.hasClass,
        newClass = data.newClass,
        oldClass = data.oldClass,
        delimiter = data.delimiter,
        target = data.target,
        fillChar = data.fillChar,
        original = data.original,
        l = text.length,
        i = ratio * l + 0.5 | 0,
        applyNew,
        applyOld,
        str;

    if (hasClass) {
      applyNew = newClass && i;
      applyOld = oldClass && i !== l;
      str = (applyNew ? "<span class='" + newClass + "'>" : "") + text.slice(0, i).join(delimiter) + (applyNew ? "</span>" : "") + (applyOld ? "<span class='" + oldClass + "'>" : "") + delimiter + original.slice(i).join(delimiter) + (applyOld ? "</span>" : "");
    } else {
      str = text.slice(0, i).join(delimiter) + delimiter + original.slice(i).join(delimiter);
    }

    if (data.svg) {
      //SVG text elements don't have an "innerHTML" in Microsoft browsers.
      target.textContent = str;
    } else {
      target.innerHTML = fillChar === "&nbsp;" && ~str.indexOf("  ") ? str.split("  ").join("&nbsp;&nbsp;") : str;
    }
  }
};
TextPlugin.splitInnerHTML = splitInnerHTML;
TextPlugin.emojiSafeSplit = emojiSafeSplit;
TextPlugin.getText = getText;
_getGSAP() && gsap.registerPlugin(TextPlugin);

var gsapWithCSS = gsap$9.registerPlugin(CSSPlugin$1) || gsap$9; // to protect from tree shaking
	gsapWithCSS.core.Tween;

//BONUS EXPORTS
//export * from "./CustomEase.js";
//export * from "./DrawSVGPlugin.js";
//export * from "./Physics2DPlugin.js";
//export * from "./PhysicsPropsPlugin.js";
//export * from "./ScrambleTextPlugin.js";
//export * from "./CustomBounce.js";
//export * from "./CustomWiggle.js";
//export * from "./Flip.js";
//export * from "./GSDevTools.js";
//export * from "./InertiaPlugin.js";
//export * from "./MorphSVGPlugin.js";
//export * from "./MotionPathHelper.js";
//export * from "./SplitText.js";

// payload options:

const Overlay = (options) => {

    const els = {
        body: null,
        targetDiv: null,
        content: null,
        title: null,
        paragraph: null,
        image: null,
        href: null,
        closeBtn: null,
    };


    const init = () => {

        els.body = document.querySelector("body");
        els.targetDiv = document.querySelector(".overlay");
        els.content = els.targetDiv.querySelector(".overlay-inner");
        els.closeBtn = els.content.querySelector(".cta-button--overlay-close");

        els.title = els.content.querySelector("h2");
        els.paragraph = els.content.querySelector(".paragraphs > p");
        els.image = els.content.querySelector(".image-wrapper__popup > img");
        els.href = els.content.querySelector(".button-group > a.cta-button");

      options.signal.on(config.eventNames.TRIGGER_OVERLAY, (payload) => {
          //if (!utils.eventFilter(options, payload)) return;
          generateOverlay(payload);
          // actionRunner(payload);
        });
    };


    const generateOverlay = (payload) =>{


        fadeInOverlay();

        els.title.textContent  = payload.title;
        els.paragraph.textContent  = payload.paragraph;
        els.image.src = payload.image;
        els.href.setAttribute("href", payload.href);

        els.closeBtn.addEventListener("click",(e)=>{
            e.preventDefault();
            cancelOverlay();
        });
    };

    const fadeInOverlay = () =>{
        els.targetDiv.style = "top: 0%";
        Tween.to(els.targetDiv, config.timings.animation.short(), {opacity: 1, ease: Power1.easeIn});
        Tween.to(els.content, config.timings.animation.short(), {top: "50%", ease: Power1.easeIn});
        els.body.classList.add('stop-scroll');

    };

    const hideProductView = () => {
        Tween.to(els.targetDiv, 0, {top: "100%"});
    };

    const cancelOverlay = () => {
        Tween.to(els.content, config.timings.animation.short(), {top: "200%", ease: Power1.easeOut});
        Tween.to(els.targetDiv, config.timings.animation.short(), {opacity: 0, ease: Power1.easeOut, onComplete: hideProductView});
        els.body.classList.remove('stop-scroll');
    };

    return {
        init,
      }
    };

/**
 * adds clipboard and notification triggering - no dom manipulation or injection
 * satisfies requirement for hero share button, but extensible for other use cases
 * see ref impl in src/components/SMIT-1730-HeroBanners
 * update if data-share-url left blank defaults to storing page href
 *
 * @param options
 * @return {{init: init}}
 * @constructor
 */
 const Clipboard = (options) => {
  
  const props = {
    actionAttrs: {
      buttonAction: 'data-button-action',
      assetShareUrl: 'data-share-url'
    },
  };

  let copyTarget, clipboardContent;

  const init = () => {
    
    //console.log('/Clipboard/ -init');
    // *** setting up for multiple targets for future extension
    const targets = [...options.els.el.querySelectorAll(`[${props.actionAttrs.buttonAction}]`)];
    //console.log('clipboard targets', targets);
    targets.map((item) => {
      const attr = item.getAttribute(props.actionAttrs.buttonAction);
      examine(item, attr);

    });
  };

  async function copyPageUrl(clipboardContent) {
    try {
      await navigator.clipboard.writeText(clipboardContent);
      triggerNotification();
    } catch (err) {
      console.error('Failed to copy: ', err);
    }
  }
  const triggerNotification = () => {
      // *** fire notification trigger event

      //console.log('/Clipboard/ --click', clipboardContent);

      options.signal.emit(options.globalConfig.eventNames.TRIGGER_NOTIFICATION, {
        dismissable: options.config.notificationDismissable || true,
        duration: options.config.notificationDuration || 3,
        notificationType: options.config.notificationType || 'success',
        text: options.config.notificationText || clipboardContent,
      });
  };
  const examine = (item, attr) => {
    switch (attr) {
      case 'to-clipboard-and-notify':

        item.addEventListener('click', (e)=>{
          copyTarget = item.getAttribute(props.actionAttrs.assetShareUrl);

          if (!copyTarget) {
            clipboardContent = window.location.href;
          } else {
            clipboardContent = copyTarget;
          }

          copyPageUrl(clipboardContent);
        });
        break;
        
      case 'to-clipboard-and-notify-coveo':
        item.addEventListener('click', (e)=>{
          let checkURL =item.querySelector('.CoveoFieldValue.pageUrlHidden.data-share-url span');
          if (checkURL) {
            copyTarget = checkURL.innerText;
          } else {
            copyTarget = item.closest("a.CoveoResultLink").href;
          }
          clipboardContent = !copyTarget ? window.location.href : copyTarget; // if no assetShareUrl set uses current page url
          copyPageUrl(clipboardContent);
        });
    }
  };
  
  return {
    init,
  }
};

/**
 *
 * @param options
 * @return {{init: init}}
 * @constructor
 */
 const Quickview = (options) => {
  
  const props = {
    actionAttrs: {
      buttonAction: 'data-button-action',
    },
  };
  
  const init = () => {
    // *** setting up for multiple targets for future extension
    const targets = [...options.els.el.querySelectorAll(`[${props.actionAttrs.buttonAction}]`)];
    
    targets.map((item) => {
      const attr = item.getAttribute(props.actionAttrs.buttonAction);
      if (attr) {
        examine(item, attr);
      }
    });
  };
  
  const examine = (item, attr) => {
    switch (attr) {
      case 'quickview':
        
        item.addEventListener('click', (e)=>{

          // *** fire notification trigger event
          
          //console.log('/Quickview/ --click', options);
          
          options.signal.emit(options.globalConfig.eventNames.TRIGGER_OVERLAY, {

              title: options.jsonContent.overlayTitle,
              paragraph: options.jsonContent.overlayParagraph,
              image: options.jsonContent.overlayImage,
              href: options.jsonContent.overlayHref,

          });
        });
        break;
    }
  };
  
  return {
    init,
  }
};

class SingleModeClass {
  constructor(options) {
    this.options = options;
    this.state = {
      currSelection: null,
      onlyUpdateOnSelectionChanged: !options.els.submitBtn, // *** when no submitBtn present, cause update dispatch on selection change only
    };
    // this.state.onlyUpdateOnSelectionChanged = !options.els.submitBtn;
  }

  init() {
    this.updateUi();

    this.options.els.selects.addEventListener('change', (e) => {
      this.fireSelectChangeHandler(e, true);
    });

    if (!this.state.onlyUpdateOnSelectionChanged) {
      // *** only listen to button if it is present
      this.options.els.submitBtn.addEventListener('click', () => {
        this.fireButtonClickHandler(true);
      });
    }
  }

  // @ja pulled this out, so we can call it independantly if needed
  // shouldscroll tells us if we should scroll to element
  fireSelectChangeHandler(e, shouldScroll) {
    let el = e.target ? e.target : e;
    let scrollToElement = shouldScroll ? shouldScroll : false;
    this.onSelectionChanged(el);
    if (this.state.onlyUpdateOnSelectionChanged) {
      this.options.onSubmitFn(
        this.state.currSelection,
        this.options.uid,
        scrollToElement
      ); // *** DISPATCH on selection change
    }
  }

  // @ja pulled this out, so we can call it independantly if needed
  fireButtonClickHandler(shouldScroll) {
    let scrollToElement = shouldScroll ? shouldScroll : false;
    this.options.onSubmitFn(
      this.state.currSelection,
      this.options.uid,
      scrollToElement
    ); // *** DISPATCH on submitBtn clicked
  }

  onSelectionChanged(target) {
    const id = target.value;
    if (id.length > 0) {
      this.state.currSelection = {
        ...target.options[target.selectedIndex].dataset,
        id,
      };
    } else {
      this.state.currSelection = null;
    }
    this.updateUi();
  }

  updateUi() {
    if (this.state.onlyUpdateOnSelectionChanged) return; // *** don't try to update non existent submitBtn ui
    if (!this.state.currSelection) {
      this.options.els.submitBtn.classList.add('cta-button--is-disabled');
    } else {
      this.options.els.submitBtn.classList.remove('cta-button--is-disabled');
    }
  }
}

/*
double dropdown mode for HeroSelectBoxExtender - not to be used in isolation
*/
const doubleMode = {
  options: {},

  state: {
    select1Selection: null,
    select2Selection: null,
  },

  els: {
    select1: null,
    select2: null,
    submitBtn: null,
  },

  init: (options) => {
    doubleMode.options = options;

    // console.log('/doubleMode/ -init', doubleMode.options);
    
    doubleMode.els.select1 = options.els.selects[0];
    doubleMode.els.select2 = options.els.selects[1];
    doubleMode.els.submitBtn = options.els.submitBtn;

    doubleMode.updateButtonState(false);
    doubleMode.updateSelect2State(false);

    doubleMode.els.select1.addEventListener('change', (e) => {
      doubleMode.onSelectionChanged('select1');
    });

    doubleMode.els.select2.addEventListener('change', (e) => {
      doubleMode.onSelectionChanged('select2');
    });

    // *** DISPATCH
    options.els.submitBtn.addEventListener('click', () => {
      localStorage.setItem(
        'heroDouble',
        doubleMode.state.select2Selection.title
      );
      doubleMode.options.onSubmitFn(doubleMode.state.select2Selection, true);
    });
  },

  onSelectionChanged: (target) => {
    const field = doubleMode.els[target];
    const hasValue = !!field.value.length;
    // console.log('/doubleMode/ -onSelectionChanged', target, field, hasValue);

    if (target === 'select1') {
      doubleMode.destroyOptions(); // *** remove select2 options. TODO cache previous results
      if (!hasValue) {
        doubleMode.updateButtonState(false); // *** disable button
        doubleMode.updateSelect2State(false);
        doubleMode.state.select1Selection = null;
        doubleMode.state.select2Selection = null;
        return;
      }
      doubleMode.state.select1Selection = {
        ...field.options[field.selectedIndex].dataset,
      };
      doubleMode.updateButtonState(false); // *** toggle button
      doubleMode.fetchData(field.value);
    }

    if (target === 'select2') {
      doubleMode.state.select2Selection = null;
      if (hasValue) {
        doubleMode.state.select2Selection = {
          id: field.value,
          ...field.options[field.selectedIndex].dataset,
        };
        localStorage.setItem('isCachedPage', false);
      }
      doubleMode.updateButtonState(hasValue); // *** toggle button
    }
  },

  fetchData: (id) => {
    doubleMode.updateSelect2State(false);

    const config = doubleMode.options.config; // *** shorthand

    // console.log('/doubleMode/ -fetchData', doubleMode.options);

    const fetchOptions = {
      datasource: `${config.datasource}`,
      signal: doubleMode.options.signal,
    };

    const requestParams = requestModel.format({
      Key: config.key,
      Values: id,
	  OtherKey: config.otherkey || '',
      Paginate: config.paginate || false,
      PageSize: config.pageSize || '', //FIXIT
      PageNumber: 1,
    });

    const select2Data = async () => {
    const data = await apiService.call(fetchOptions, requestParams);
    // on data...
    doubleMode.populateDynamicField(data.payload.Result);
    doubleMode.updateSelect2State(true);
    };

    select2Data(); // *** invoke fetch
  },

  // *** remove all options from select in preparation for new children
  destroyOptions: (selectEl = doubleMode.els.select2) => {
    while (selectEl.firstChild) {
      selectEl.removeChild(selectEl.firstChild);
    }
  },

  populateDynamicField: (data) => {
    // *** destroy existing options
    doubleMode.destroyOptions();

    const template = (value, label, title, description) => {
      // console.log('/doubleMode/ -template', value);
      const option = document.createElement('option');
      option.setAttribute('value', value);
      option.setAttribute('data-title', title);
      option.setAttribute('data-description', description);
      option.innerText = label;
      return option;
    };
    // console.log('/doubleMode/ -populateDynamicField', doubleMode.options.config);
    // *** add default option
    doubleMode.els.select2.appendChild(
      template(
        '',
        doubleMode.options.config.secondDropdownDefaultOption || '',
        null
      )
    );

    // *** add dynamic options
    data.map((item) => {
      // TODO refactor for dynamic keys
      // console.log('\n/doubleMode/ -???', doubleMode.options.config.map);
      // console.log('/doubleMode/ ->>>', item[doubleMode.options.config.map[0][1]]);
      // console.log('/doubleMode/ -CCC', item);
      doubleMode.els.select2.appendChild(
        template(
          item[doubleMode.options.config.map[0][1]], // *** value
          item[doubleMode.options.config.map[1][1]], // *** label
          item[doubleMode.options.config.map[2][1]] || '',
          item[doubleMode.options.config.map[3][1]] || ''
        )
      );
    });
    // we do a quick check here to see if we are on a cached page accessed from back button
    // if so we need to select the previously stored value for 2nd select
    doubleMode.checkLocalStorage();
  },

  checkLocalStorage: () => {
    // TODO: If we have a value stored for the 2nd select, use it to select now...
    const storedHeroSelection = localStorage.getItem('heroDouble');
    const isCachedPage = localStorage.getItem('isCachedPage');
    if (storedHeroSelection && isCachedPage === 'true') {
      // set the 2nd select to this value..
      doubleMode.els.select2.selectedIndex = [
        ...doubleMode.els.select2.options,
      ].findIndex((option) => option.text === storedHeroSelection);
      if(doubleMode.els.select2.selectedIndex >= 0) {
        // now we need to mock the change event
        doubleMode.onSelectionChanged('select2');
        // and finally mock clicking the button..
        // with scroll value of false
        doubleMode.options.onSubmitFn(doubleMode.state.select2Selection, false);
      } else {
        doubleMode.onSelectionChanged('select1');
      }
      localStorage.setItem('isCachedPage', false);
    }
  },

  updateSelect2State: (bool) => {
    // console.log('/doubleMode/ -updateSelect2State', bool);
    if (!bool) {
      doubleMode.els.select2.classList.add('select-dropdown--is-disabled');
    } else {
      doubleMode.els.select2.classList.remove('select-dropdown--is-disabled');
    }
  },

  updateButtonState: (bool) => {
    if (!bool) {
      doubleMode.els.submitBtn.classList.add('cta-button--is-disabled');
    } else {
      doubleMode.els.submitBtn.classList.remove('cta-button--is-disabled');
    }
  },
};

/*
double dropdown mode for HeroSelectBoxExtender - not to be used in isolation
*/
const trippleMode = {
  options: {},

  state: {
    select1Selection: null,
    select2Selection: null,
    select3Selection: null,
    select1Value: null,
    woundManagement: null,
    isAWMEnabled: false,
  },

  els: {
    select1: null,
    select2: null,
    select3: null,
    submitBtn: null,
  },

  init: (options) => {
    trippleMode.options = options;

    //  console.log('/trippleMode/ -init', trippleMode.options);
    trippleMode.state.woundManagement = document.getElementById('woundmanagement');
    trippleMode.els.select1 = options.els.selects[0];
    trippleMode.els.select2 = options.els.selects[1];
    trippleMode.els.select3 = options.els.selects[2];
    trippleMode.els.submitBtn = options.els.submitBtn;

    trippleMode.updateButtonState(false);
    trippleMode.updateSelectState(false, 'select2');
    trippleMode.updateSelectState(false, 'select3');

    trippleMode.els.select1.addEventListener('change', (e) => {
      trippleMode.onSelectionChanged('select1');
    });

    trippleMode.els.select2.addEventListener('change', (e) => {
      trippleMode.onSelectionChanged('select2');
    });

    trippleMode.els.select3.addEventListener('change', (e) => {
        trippleMode.onSelectionChanged('select3');
    });

    // *** DISPATCH
    options.els.submitBtn.addEventListener('click', () => {
      if(trippleMode.state.isAWMEnabled) {
        localStorage.setItem(
          'trippleDouble',
          trippleMode.state.select3Selection.title
        );
        let payloadData = {
          id:trippleMode.state.select2Selection.id,
          title: trippleMode.state.select3Selection.title,
          areaOfInterestId: trippleMode.state.select3Selection.id
        };
        trippleMode.options.onSubmitFn(payloadData, true);
      } else {
        localStorage.setItem(
          'heroDouble',
          trippleMode.state.select2Selection.title
        );
        trippleMode.options.onSubmitFn(trippleMode.state.select2Selection, true);
      }
    });
  },

  onSelectionChanged: (target) => {
    const field = trippleMode.els[target];
    const hasValue = !!field.value.length;
    // console.log('/trippleMode/ -onSelectionChanged', target, field, hasValue);

    if (target === 'select1') {
      trippleMode.destroyOptions(); // *** remove select2 options. TODO cache previous results
      trippleMode.destroyOptions(trippleMode.els.select3);
      if (!hasValue) {
        trippleMode.updateButtonState(false); // *** disable button
        trippleMode.updateSelectState(false, 'select2');
        trippleMode.state.select1Selection = null;
        trippleMode.state.select2Selection = null;
        trippleMode.state.select3Selection = null;
        return;
      }
      trippleMode.state.select1Selection = {
        ...field.options[field.selectedIndex].dataset,
      };
      
      if(trippleMode.els.select3.closest(".dropdown--full-width.dropdown--split-5.d-none") && trippleMode.state.woundManagement && field.value === trippleMode.state.woundManagement.value ) {
        trippleMode.state.isAWMEnabled = true;
        trippleMode.els.select3.closest(".dropdown--full-width.dropdown--split-5.d-none").classList.remove('d-none');
      }
      
      if(trippleMode.els.select3.closest(".dropdown--full-width.dropdown--split-5") && trippleMode.state.woundManagement && field.value !== trippleMode.state.woundManagement.value ) {
        trippleMode.state.isAWMEnabled = false;
        trippleMode.els.select3.closest(".dropdown--full-width.dropdown--split-5").classList.add('d-none');
      }

      trippleMode.updateButtonState(false); // *** toggle button
      trippleMode.state.select1Value = field.value;
      trippleMode.fetchData(field.value, 'select2');
    }

    if (target === 'select2') {
      trippleMode.state.select2Selection = null;
      trippleMode.state.select3Selection = null;
      if (hasValue) {
        trippleMode.state.select2Selection = {
          id: field.value,
          ...field.options[field.selectedIndex].dataset,
        };
        localStorage.setItem('isCachedPage', false);
      }
      
      if(trippleMode.els.select3.closest(".dropdown--full-width.dropdown--split-5") && trippleMode.state.woundManagement && trippleMode.state.select1Value === trippleMode.state.woundManagement.value ) {
        trippleMode.fetchData(field.value, 'select3');
        trippleMode.updateButtonState(false); // *** toggle button
      } else {
        trippleMode.updateButtonState(hasValue); // *** toggle button
      }
    }
    if (target === 'select3') {
        trippleMode.state.select3Selection = null;
        if (hasValue) {
          trippleMode.state.select3Selection = {
            id: field.value,
            ...field.options[field.selectedIndex].dataset,
          };
          localStorage.setItem('isCachedPage', false);
        }
  
        trippleMode.updateButtonState(hasValue); // *** toggle button
      }
  },

  fetchData: (id, target) => {
    trippleMode.updateSelectState(false, target);

    const config = trippleMode.options.config; // *** shorthand

    // console.log('/trippleMode/ -fetchData', trippleMode.options);

    const fetchOptions = {
      datasource: `${config.datasource}`,
      signal: trippleMode.options.signal,
    };

    const requestParams = requestModel.format({
      Key: config.key,
      Values: id,
	    OtherKey: config.otherkey || '',
      Paginate: config.paginate || false,
      PageSize: config.pageSize || '', //FIXIT
      PageNumber: 1,
    });

    const select2Data = async () => {
    const data = await apiService.call(fetchOptions, requestParams);
    // on data...
    trippleMode.populateDynamicField(data.payload.Result);
    trippleMode.updateSelectState(true, target);
    };

    const select3Data = async () => {
      const fetchData = {
        datasource: `/en/api/products/specialitiesGetAwmDDL?itemID=${id}`,
        signal: trippleMode.options.signal,
      };
      const data = await apiService.call(fetchData, {});
      // on data...
      trippleMode.populateAWMTagsField(data.payload.Result);
      trippleMode.updateSelectState(true, target);
    };
    if(target === 'select2') {
      select2Data(); // *** invoke fetch
    }

    if(target === 'select3') {
      select3Data(); // *** invoke fetch
    }
    
  },

  // *** remove all options from select in preparation for new children
  destroyOptions: (selectEl = trippleMode.els.select2) => {
    while (selectEl.firstChild) {
      selectEl.removeChild(selectEl.firstChild);
    }
  },

  populateAWMTagsField: (data) => {
    // *** destroy existing options
    trippleMode.destroyOptions(trippleMode.els.select3);
    const template = (value, label) => {
      // console.log('/trippleMode/ -template', value);
      const option = document.createElement('option');
      option.setAttribute('value', value);
      option.setAttribute('data-title', label);
      option.innerText = label;
      return option;
    };
    trippleMode.els.select3.appendChild(
      template(
        '',
        'Select Tag'
      )
    );
    // *** add dynamic options
    if(data.length) {
      data.map((item) => {
        trippleMode.els.select3.appendChild(
          template(
            item['ItemId'], // *** value
            item['DropDownValue'], // *** label
          )
        );
      });
    }
  },

  populateDynamicField: (data) => {
    // *** destroy existing options
    trippleMode.destroyOptions();

    const template = (value, label, title, description) => {
      // console.log('/trippleMode/ -template', value);
      const option = document.createElement('option');
      option.setAttribute('value', value);
      option.setAttribute('data-title', title);
      option.setAttribute('data-description', description);
      option.innerText = label;
      return option;
    };
    // console.log('/trippleMode/ -populateDynamicField', trippleMode.options.config);
    // *** add default option
    trippleMode.els.select2.appendChild(
      template(
        '',
        trippleMode.options.config.secondDropdownDefaultOption || '',
        null
      )
    );

    // *** add dynamic options
    data.map((item) => {
      // TODO refactor for dynamic keys
      // console.log('\n/trippleMode/ -???', trippleMode.options.config.map);
      // console.log('/trippleMode/ ->>>', item[trippleMode.options.config.map[0][1]]);
      // console.log('/trippleMode/ -CCC', item);
      trippleMode.els.select2.appendChild(
        template(
          item[trippleMode.options.config.map[0][1]], // *** value
          item[trippleMode.options.config.map[1][1]], // *** label
          item[trippleMode.options.config.map[2][1]] || '',
          item[trippleMode.options.config.map[3][1]] || ''
        )
      );
    });
    // we do a quick check here to see if we are on a cached page accessed from back button
    // if so we need to select the previously stored value for 2nd select
    trippleMode.checkLocalStorage();
  },

  checkLocalStorage: () => {
    // TODO: If we have a value stored for the 2nd select, use it to select now...
    const storedHeroSelection = localStorage.getItem('heroDouble');
    const isCachedPage = localStorage.getItem('isCachedPage');
    if (storedHeroSelection && isCachedPage === 'true') {
      // set the 2nd select to this value..
      trippleMode.els.select2.selectedIndex = [
        ...trippleMode.els.select2.options,
      ].findIndex((option) => option.text === storedHeroSelection);
      if(trippleMode.els.select2.selectedIndex >= 0) {
        // now we need to mock the change event
        trippleMode.onSelectionChanged('select2');
        // and finally mock clicking the button..
        // with scroll value of false
        trippleMode.options.onSubmitFn(trippleMode.state.select2Selection, false);
      } else {
        trippleMode.onSelectionChanged('select1');
      }
      localStorage.setItem('isCachedPage', false);
    }
  },

  updateSelectState: (bool, target) => {
    //  console.log('/trippleMode/ -updateSelectState', bool, target);
    if (!bool) {
      trippleMode.els[[target]].classList.add('select-dropdown--is-disabled');
    } else {
      trippleMode.els[[target]].classList.remove('select-dropdown--is-disabled');
    }
  },

  updateButtonState: (bool) => {
    if (!bool) {
      trippleMode.els.submitBtn.classList.add('cta-button--is-disabled');
    } else {
      trippleMode.els.submitBtn.classList.remove('cta-button--is-disabled');
    }
  },
};

// *** @as for the contacts stuff, 'singleMode' with a few changes seems to suffice - maybe park this one for that use case
const singleModeNoCTA = {
  options: {},

  state: {
    currSelection: null,
  },

  init: (options) => {
    //console.log('/singleModeNoCTA/ -init XXX', options);
    // return;

    singleModeNoCTA.options = options;
    singleModeNoCTA.setDefaultSelection();
    // singleModeNoCTA.fetchData(singleModeNoCTA.state.currSelection);
    singleModeNoCTA.options.onSubmitFn(
      singleModeNoCTA.state.currSelection,
      false
    );

    options.els.selects[0].addEventListener('change', (e) => {
      singleModeNoCTA.fireSelectChangeHandler(e, true);
    });
  },

  // @ja pulled this out, so we can call it independantly if needed
  fireSelectChangeHandler: (e, shouldScroll) => {
    let el = e.target ? e.target : e;
    let scrollToElement = shouldScroll ? shouldScroll : false;
    singleModeNoCTA.onSelectionChanged(el);
    singleModeNoCTA.fetchData(singleModeNoCTA.state.currSelection); // update
    singleModeNoCTA.options.onSubmitFn(
      singleModeNoCTA.state.currSelection,
      scrollToElement
    );
  },

  onSelectionChanged: (target) => {
    const id = target.value;
    if (id.length > 0) {
      singleModeNoCTA.state.currSelection = {
        ...target.options[target.selectedIndex].dataset,
        id,
      };
    } else {
      singleModeNoCTA.state.currSelection = null;
    }
  },
  setDefaultSelection: () => {
    singleModeNoCTA.state.currSelection = 0;
  },
  fetchData: (id) => {
    const config = singleModeNoCTA.options.config; // *** shorthand

    //console.log('/singleModeNoCTA/ -fetchData', singleModeNoCTA.options);

    const fetchOptions = {
      datasource: `${config.datasource}`,
      signal: singleModeNoCTA.options.signal,
    };

    const requestParams = requestModel.format({
      Key: config.key,
      Values: id,
      Paginate: config.paginate || false,
      PageSize: config.pageSize || '', //FIXIT
      PageNumber: 1,
    });

    const getData = async () => {
      const data = await apiService.call(fetchOptions, requestParams);
      // on data...
      singleModeNoCTA.updateContactFooterDetails(data.payload.Result[0]);
    };

    getData(); // *** invoke fetch
  },

  // this needs sorting out - using a template?
  updateContactFooterDetails: (payload) => {
    let module = document.querySelector('.contact-module');

    let detailsAreaLeft = module.querySelector(
      '.contact-module--bottom--details--left'
    );
    detailsAreaLeft.querySelector('h4').innerText = payload.Title;
    detailsAreaLeft.querySelector('p').innerText = payload.Introduction;

    let detailsAreaRight = module.querySelector(
      '.contact-module--bottom--details--right'
    );
    detailsAreaRight.querySelector('p').innerHTML = payload.Address;
    detailsAreaRight.querySelector('a').innerText = payload.PhoneNumber;
    detailsAreaRight.querySelector('a').href = payload.PhoneNumber;
  },

  updateUi: () => {
    console.log(
      '/index/ -updateUi --has selection:',
      !!singleModeNoCTA.state.currSelection
    );
  },
};

/*
patient specialty filter dropdown mode for HeroSelectBoxExtender - not to be used in isolation
*/
const patientSpecialtyFilter = {
  options: {},

  state: {
    select1Selection: null,
    select2Selection: null,
  },

  els: {
    select1: null,
    select2: null,
    submitBtn: null,
  },
  productsList: null,
  filteredProductsList: null,

  init: (options) => {
    patientSpecialtyFilter.options = options;

    // console.log('/patientSpecialtyFilter/ -init', patientSpecialtyFilter.options);
    
    patientSpecialtyFilter.els.select1 = options.els.selects[0];
    patientSpecialtyFilter.els.select2 = options.els.selects[1];
    patientSpecialtyFilter.els.submitBtn = options.els.submitBtn;

    patientSpecialtyFilter.updateButtonState(false);
    patientSpecialtyFilter.updateSelect2State(false);

    patientSpecialtyFilter.els.select1.addEventListener('change', (e) => {
      patientSpecialtyFilter.onSelectionChanged('select1');
    });

    patientSpecialtyFilter.els.select2.addEventListener('change', (e) => {
      patientSpecialtyFilter.onSelectionChanged('select2');
    });

    // *** DISPATCH
    options.els.submitBtn.addEventListener('click', () => {
      localStorage.setItem(
        'heroDouble',
        patientSpecialtyFilter.state.select2Selection.title
      );
      const getFilteredProducts = (item) => {
        if(item.Specialities.length) {
          for (let x = 0; x < item.Specialities.length; x++) {
            if (item.Specialities[x].SpecialityId === patientSpecialtyFilter.state.select2Selection.id) {
              return true;
            }
          }
        } 
        return false;
      };
      patientSpecialtyFilter.filteredProductsList = patientSpecialtyFilter.productsList.filter(getFilteredProducts);
      let parentId = null;
      const closestAlchemyComponent = options.els.el.closest('.alchemy');
      if (closestAlchemyComponent) {
        parentId = closestAlchemyComponent.dataset.uid
          ? closestAlchemyComponent.dataset.uid
          : null;
      }
      options.signal.emit(
        options.globalConfig.eventNames.PATIENT_SPECIALTY_DROPDOWN_SELECTION,
          {
            parentId,
            caller: uid,
            selection: patientSpecialtyFilter.state.select2Selection,
            scroll: true,
            filteredProductsList: patientSpecialtyFilter.filteredProductsList,
          }
        );
    });
  },

  onSelectionChanged: (target) => {
    const field = patientSpecialtyFilter.els[target];
    const hasValue = !!field.value.length;
    // console.log('/patientSpecialtyFilter/ -onSelectionChanged', target, field, hasValue);

    if (target === 'select1') {
      patientSpecialtyFilter.destroyOptions(); // *** remove select2 options. TODO cache previous results
      if (!hasValue) {
        patientSpecialtyFilter.updateButtonState(false); // *** disable button
        patientSpecialtyFilter.updateSelect2State(false);
        patientSpecialtyFilter.state.select1Selection = null;
        patientSpecialtyFilter.state.select2Selection = null;
        return;
      }
      patientSpecialtyFilter.state.select1Selection = {
        ...field.options[field.selectedIndex].dataset,
      };
      patientSpecialtyFilter.updateButtonState(false); // *** toggle button
      patientSpecialtyFilter.fetchData(field.value);
    }

    if (target === 'select2') {
      patientSpecialtyFilter.state.select2Selection = null;
      if (hasValue) {
        patientSpecialtyFilter.state.select2Selection = {
          id: field.value,
          ...field.options[field.selectedIndex].dataset,
        };
        localStorage.setItem('isCachedPage', false);
      }
      patientSpecialtyFilter.updateButtonState(hasValue); // *** toggle button
    }
  },

  fetchData: (id) => {
    patientSpecialtyFilter.updateSelect2State(false);

    const config = patientSpecialtyFilter.options.config; // *** shorthand

    // console.log('/patientSpecialtyFilter/ -fetchData', patientSpecialtyFilter.options);

    const fetchOptions = {
      datasource: `${config.datasource}`,
      signal: patientSpecialtyFilter.options.signal,
    };

    const requestParams = requestModel.format({
      Key: config.key,
      Values: id,
	    OtherKey: config.otherkey || '',
      Paginate: config.paginate || false,
      PageSize: config.pageSize || '', //FIXIT
      PageNumber: 1,
    });

    const select2Data = async () => {
      const data = await apiService.call(fetchOptions, requestParams);
      // on data....
      let specialities = [];
      data.payload.Result.forEach((item) => {
        specialities = [...specialities, ...item.Specialities];
      });
      const jsonObject = specialities.map(JSON.stringify);
      const uniqueSpecialitiesSet = new Set(jsonObject);
      const uniqueSpecialitiesArray = Array.from(uniqueSpecialitiesSet).map(JSON.parse);
      patientSpecialtyFilter.productsList = data.payload.Result;
      patientSpecialtyFilter.populateDynamicField(uniqueSpecialitiesArray);
      patientSpecialtyFilter.updateSelect2State(true);
    };

    select2Data(); // *** invoke fetch
  },

  // *** remove all options from select in preparation for new children
  destroyOptions: (selectEl = patientSpecialtyFilter.els.select2) => {
    while (selectEl.firstChild) {
      selectEl.removeChild(selectEl.firstChild);
    }
  },

  populateDynamicField: (data) => {
    // *** destroy existing options
    patientSpecialtyFilter.destroyOptions();

    const template = (value, label, title, description) => {
      // console.log('/patientSpecialtyFilter/ -template', value);
      const option = document.createElement('option');
      option.setAttribute('value', value);
      option.setAttribute('data-title', title);
      option.setAttribute('data-description', description);
      option.innerText = label;
      return option;
    };
    // console.log('/patientSpecialtyFilter/ -populateDynamicField', patientSpecialtyFilter.options.config);
    // *** add default option
    patientSpecialtyFilter.els.select2.appendChild(
      template(
        '',
        patientSpecialtyFilter.options.config.secondDropdownDefaultOption || '',
        null
      )
    );

    // *** add dynamic options
    data.map((item) => {
      // TODO refactor for dynamic keys
      // console.log('\n/patientSpecialtyFilter/ -???', patientSpecialtyFilter.options.config.map);
      // console.log('/patientSpecialtyFilter/ ->>>', item[patientSpecialtyFilter.options.config.map[0][1]]);
      // console.log('/patientSpecialtyFilter/ -CCC', item);
      if(Object.keys(item).length > 0) {
        patientSpecialtyFilter.els.select2.appendChild(
          template(
            item[patientSpecialtyFilter.options.config.map[0][1]], // *** value
            item[patientSpecialtyFilter.options.config.map[1][1]], // *** label
            item[patientSpecialtyFilter.options.config.map[2][1]] || '',
            item[patientSpecialtyFilter.options.config.map[3][1]] || ''
          )
        );
      }
    });
    // we do a quick check here to see if we are on a cached page accessed from back button
    // if so we need to select the previously stored value for 2nd select
    patientSpecialtyFilter.checkLocalStorage();
  },

  checkLocalStorage: () => {
    // TODO: If we have a value stored for the 2nd select, use it to select now...
    const storedHeroSelection = localStorage.getItem('heroDouble');
    const isCachedPage = localStorage.getItem('isCachedPage');
    if (storedHeroSelection && isCachedPage === 'true') {
      // set the 2nd select to this value..
      patientSpecialtyFilter.els.select2.selectedIndex = [
        ...patientSpecialtyFilter.els.select2.options,
      ].findIndex((option) => option.text === storedHeroSelection);
      if(patientSpecialtyFilter.els.select2.selectedIndex >= 0) {
        // now we need to mock the change event
        patientSpecialtyFilter.onSelectionChanged('select2');
        // and finally mock clicking the button..
        // with scroll value of false
        patientSpecialtyFilter.options.onSubmitFn(patientSpecialtyFilter.state.select2Selection, false);
      } else {
        patientSpecialtyFilter.onSelectionChanged('select1');
      }
      localStorage.setItem('isCachedPage', false);
    }
  },

  updateSelect2State: (bool) => {
    // console.log('/patientSpecialtyFilter/ -updateSelect2State', bool);
    if (!bool) {
      patientSpecialtyFilter.els.select2.classList.add('select-dropdown--is-disabled');
    } else {
      patientSpecialtyFilter.els.select2.classList.remove('select-dropdown--is-disabled');
    }
  },

  updateButtonState: (bool) => {
    if (!bool) {
      patientSpecialtyFilter.els.submitBtn.classList.add('cta-button--is-disabled');
    } else {
      patientSpecialtyFilter.els.submitBtn.classList.remove('cta-button--is-disabled');
    }
  },
};

/*
@as
wraps hero select box from dom and adds functionality that can't be included as hero component script is not permitted
*/

const HeroSelectBoxExtender = (options) => {
  let els = {
    main: null, // *** i.e. 'dropdown__button' - the main element
    selects: null,
    submitBtn: null,
  };

  const state = {
    singleModeInstance: null,
  };

  const init = () => {
    els = options.els;
    els.main = els.el.firstElementChild;
    if (!els.main) {
      console.warn(
        '/HeroSelectBoxExtender/ -init --no child dropdown found, quitting'
      );
      return;
    }

    els.selects = [...els.main.querySelectorAll('select')];
    els.submitBtn = els.main.querySelector('button');

    if (options.config.mode === 'single') {
      els.selects = els.selects[0];
      // singleMode.init({
      //   els,
      //   uid: options.config.uid, // *** required for multiple instances on single page, state gets overwritten otherwise. extend to others if required
      //   onSubmitFn: (payload, uid, scroll) => {
      //     dispatch(payload, uid, scroll);
      //   },
      // });

      // @ja redo this as a class so you can have separate instances if multiple items on page
      state.singleModeInstance = new SingleModeClass({
        els,
        uid: options.config.uid, // *** required for multiple instances on single page, state gets overwritten otherwise. extend to others if required
        onSubmitFn: (payload, uid, scroll) => {
          dispatch(payload, uid, scroll);
        },
      });
      state.singleModeInstance.init();
    }
    
    if (options.config.mode === 'double') {
      doubleMode.init({
        els,
        config: options.config,
        signal: options.signal,
        onSubmitFn: (payload, scroll) => {
          dispatch(payload, scroll);
        },
      });
    }

    if (options.config.mode === 'tripple') {
      trippleMode.init({
        els,
        config: options.config,
        signal: options.signal,
        onSubmitFn: (payload, scroll) => {
          dispatch(payload, scroll);
        },
      });
    }

    if (options.config.mode === 'singleModeNoCTA') {
      els.selects = els.selects[0];

      singleModeNoCTA.init({
        els,
        config: options.config,
        signal: options.signal,
        onSubmitFn: (payload, scroll) => {
          dispatch(payload, scroll);
        },
      });
    }

    if (options.config.mode === 'patientSpecialtyFilter') {
      patientSpecialtyFilter.init({
        els,
        config: options.config,
        signal: options.signal,
        globalConfig: options.globalConfig,
      });
    }
    watchPageNav();
  };

  const watchPageNav = () => {
    window.addEventListener('pageshow', (event) => {
      // check if we are on cached page, therefore we have come here from back button...
      var historyPage =
        event.persisted ||
        (typeof window.performance != 'undefined' &&
          window.performance.navigation.type === 2);
      // if so, we bail as dont want to execute the rest...
      if (!historyPage) return;
      // if we are on a 'back button' page then we need to fire these events artificially
      if (options.config.mode === 'double' && historyPage) {
        if (els.selects[0].value !== '') {
          localStorage.setItem('isCachedPage', true);
          doubleMode.onSelectionChanged('select1');
        }
      }
      // not sure we will need this as items will always be pre-selected anyways
      if (options.config.mode === 'single' && historyPage) {
        if (els.selects.value !== '') {
          state.singleModeInstance.fireSelectChangeHandler(els.selects, false);
          state.singleModeInstance.fireButtonClickHandler(false);
        }
      }
    });
  };

  /**
   *
   * @param data
   * @param uid {string} - when using multiple instances on a single page, retrieve value from the select child
   * @param scroll {bool} - whether to scroll to the element when updating ui
   */
  const dispatch = (data, uid = null, scroll) => {
    // console.log('dispatch');
    // console.log(options.els.el);
    // set a parent-id so we know which select is being calle and can filter calls
    let parentId = null;
    const closestAlchemyComponent = options.els.el.closest('.alchemy');
    if (closestAlchemyComponent) {
      parentId = closestAlchemyComponent.dataset.uid
        ? closestAlchemyComponent.dataset.uid
        : null;
    }
    // console.log(closestAlchemyComponent);
    // console.log(parentId);
    // console.log('/HeroSelectBoxExtender/ -dispatch', options);
    options.signal.emit(
      options.globalConfig.eventNames.HERO_DROPDOWN_SELECTION,
      {
        parentId,
        caller: uid,
        selection: data,
        scroll,
      }
    );
  };

  const getOptions = () => {
    return options;
  };

  return {
    init,
    getOptions,
  };
};

const Paginator = (options) => {
  let els = {};
  let leftArrowHTML =
      "<li><a class='leftArrow sn-icon-navigation-left'>  </a></li>",
    rightArrowHTML =
      "<li><a class='rightArrow sn-icon-navigation-right'>  </a></li>",
    leftArrow = '.leftArrow',
    rightArrow = '.rightArrow';

  const state = {
    intentionIndex: null, // *** clicked on, but not updated by new update() call
    currentApiId: null, // *** if apiId changes, will trigger rebuilding numbers
    data: {
      pageNumber: null,
      totalPages: null,
    },
    numbers: [], // *** keeps generated number els to allow highlighting
  };

  const init = () => {
    els = options.els;
  };

  // *** updates value from other controls
  const update = (options, apiId) => {
    
    //console.log('/Paginator/ -update', options, apiId);
    
    if (!options.IsPaginated) return; // *** do nothing

    state.data = {
      pageNumber: options.PageNumber,
      totalPages: options.TotalPages,
    };
    // *** if first call or apiid has changed, rebuild
    // if (!state.currentApiId || apiId !== state.currentApiId) {
    // rerender each time
    state.currentApiId = apiId;
    destroy();
    build();
  };

  const destroy = () => {
    if (els.inner) els.inner.remove();
    state.numbers = [];
  };

  const build = () => {
    //console.log('/Paginator/ -build WILL DEFEAT?', state.data.totalPages <= 1);
    if (state.data.totalPages <= 1) return; // *** defeat build when no pages / 1 page
    //console.log('/Paginator/ -build ====');

    // console.log('current page ' + state.data.pageNumber);
    // console.log('total pages ' + state.data.totalPages);
    els.inner = document.createElement('div');
    els.inner.classList.add('pagination');
    options.els.el.append(els.inner);
    // *** intentionally sparse dom - replace with proper els and classes

    var pagHtml = '';
    if (state.data.totalPages <= 5) {
      for (i = 1; i <= state.data.totalPages; i++) {
        //currentPage = i;
        pagHtml += addNav(i, state.data.pageNumber);
      }
    } else {
      pagHtml += addNav('1', state.data.pageNumber);
      if (state.data.pageNumber > 3) {
        pagHtml += '<li>...</li>';
      }
      if (state.data.pageNumber == state.data.totalPages) {
        pagHtml += addNav(state.data.pageNumber - 2, state.data.pageNumber);
      }
      if (state.data.pageNumber > 2) {
        pagHtml += addNav(state.data.pageNumber - 1, state.data.pageNumber);
      }
      if (
        state.data.pageNumber != 1 &&
        state.data.pageNumber != state.data.totalPages
      ) {
        pagHtml += addNav(state.data.pageNumber, state.data.pageNumber);
      }
      if (state.data.pageNumber < state.data.totalPages - 1) {
        pagHtml += addNav(state.data.pageNumber + 1, state.data.pageNumber);
      }
      if (state.data.pageNumber == 1) {
        pagHtml += addNav(state.data.pageNumber + 2, state.data.pageNumber);
      }
      if (state.data.pageNumber < state.data.totalPages - 2) {
        pagHtml += '<li>...</li>';
      }
      if (state.data.totalPages > 1) {
        pagHtml += addNav(state.data.totalPages, state.data.pageNumber);
      }
    }

    els.inner.insertAdjacentHTML(
      'beforeend',
      "<ul class='pagiNav'>" +
        leftArrowHTML +
        pagHtml +
        rightArrowHTML +
        '</ul>'
    );
    updateArrows(state.data.pageNumber, state.data.totalPages);

    //update links
    links = els.inner.querySelectorAll('a[data-page-number]');
    for (link of links) {
      link.addEventListener('click', getParam);
    }
  };

  const addNav = (number, currentPage) => {
    var btnHtml = '<li ';

    if (number == currentPage) {
      btnHtml += " class='current' ";
    }

    var numberText = number;
    if (number < 10) {
      numberText = '0' + number;
    } else {
      numberText = number;
    }

    btnHtml += `><a data-page-number=${number}>` + numberText + `</a>`;
    btnHtml += '</li>';

    return btnHtml;
  };

  const getParam = (evt) => {
    index = evt.target.getAttribute('data-page-number');

    state.intentionIndex = index; // *** could use this to set a visual state on click while waiting for api response on parent
    dispatch({
      pageNumber: index,
    });
  };

  const updateArrows = (currentPage, totalPages) => {
    //console.log('arrows update currentPage = ' + currentPage);
    //console.log('arrows update totalPages = ' + totalPages);

    els.inner
      .querySelector(leftArrow)
      .setAttribute('data-page-number', currentPage);
    els.inner
      .querySelector(rightArrow)
      .setAttribute('data-page-number', currentPage + 1); //take into consideration zero based index

    if (currentPage == 1) {
      //disabled left arrow
      els.inner.querySelector(leftArrow).removeAttribute('data-page-number');
      els.inner.querySelector(leftArrow).classList.add('disabled');
    }
    if (currentPage > 1) {
      els.inner
        .querySelector(leftArrow)
        .setAttribute('data-page-number', currentPage - 1);
    }
    if (currentPage == totalPages || currentPage > totalPages) {
      //disabled right arrow
      els.inner.querySelector(rightArrow).classList.add('disabled');
      els.inner.querySelector(rightArrow).removeAttribute('data-page-number');
    }
  };

  const dispatch = (data) => {
    utils.dispatcher(options, {
      type: 'PAGINATOR_INTENTION',
      data,
    });
  };

  const getOptions = () => {
    return options;
  };

  return {
    init,
    update,
    getOptions,
  };
};

// generic accordion behaviour

const Accordion = (props) => {
  const els = {
    el: null,
    accordions: null,
    accordion: null,
    title: null,
    panel: null,
    panelTitle: null,
    toggle: null,
  };

  // *** mandatory function - called by `AbstractComponent` on instantiation
  const init = () => {
    //console.log('/generic-accordion/ -init ', props);
    els.accordion = props.els.el;
    els.panel = els.accordion.querySelectorAll('.panel');
    els.panel.forEach(function (accordionItem) {
      els.panelTitle = accordionItem.querySelector('.panel-title');
      els.panelTitle.addEventListener('click', togglePanel);
    });
  };

  const togglePanel = (event) => {
    const panelTitle = event.target;
    const panel = panelTitle.parentNode;
    const innerPanel = panel.querySelector('.panel-inner');
    //const toggle = panel.querySelector('.toggle');
    let panelStatus;

    //check panel is open
    if (!panel.classList.contains('open')) {
      panel.classList.add('open');
      //toggle.classList.add('open');
      panelStatus = 'open';
    } else {
      panel.classList.remove('open');
      //toggle.classList.remove('open');
      panelStatus = 'closed';
    }

    if (innerPanel.style.maxHeight) {
      innerPanel.style.maxHeight = null;
    } else {
      innerPanel.style.maxHeight = innerPanel.scrollHeight + 'px';
    }

    dispatch({ panelStatus: panelStatus });
  };

  const dispatch = (data) => {
    utils.dispatcher(props, {
      type: config.eventNames.ACCORDION_TOGGLE,
      data,
    });
  };

  return {
    init,
    togglePanel,
  };
};

const Contactcard = (props, options) => {

  const init = () => {
    // *** NOTE props.els.el is now the actual generated card el, previously it was cardstack

    // console.log('/contactcard/ -init ', props);

    let contactCard = props.els.el;
    let ctaList = contactCard.querySelectorAll('[class^="sn-icon-"]');
    ctaList.forEach(function (ctaItem) {
      ctaItem.addEventListener('click', toggleInfo);
    });
  };

  const toggleInfo = (event) => {
    let clicked = event.target,
      ctaType = clicked.getAttribute('data-attribute-ctatype'),
      ctaData = clicked.getAttribute(`data-attribute-${ctaType}`),
      ctaTypeHTML = '',
      parent = event.target.closest('aside'),
      ctaTarget = parent.querySelector('.contact-content a'),
      allIcons = parent.querySelectorAll('[class^="sn-icon-"]');

    allIcons.forEach(function (ctaItem) {
      ctaItem.classList.remove('selected');
    });

    clicked.classList.add('selected');

    switch (ctaType) {
      case 'email':
        ctaTypeHTML = 'mailto:';
        ctaTarget.removeAttribute('target');
        break;
      case 'fax':
        ctaTypeHTML = 'tel:';
        ctaTarget.removeAttribute('target');
        break;
      case 'phone':
        ctaTypeHTML = 'tel:';
        ctaTarget.removeAttribute('target');
        break;
      case 'url':
        ctaTypeHTML = '//';
        ctaTarget.setAttribute('target', '_blank');
        break;
      default:
        ctaTypeHTML = '';
    }

    ctaTarget.innerText = ctaData;
    ctaTarget.setAttribute('href', ctaTypeHTML + ctaData);
  };

  return {
    init,
    toggleInfo,
  };
};

const updatedTableList = [];
const tableIdList = [];
const tablesList = [];
const updatedTableIdList = [];
const Tablescroller = (options) =>{
    const els = {
        table:null,
        tableWidth: null,
        tableWrapper: null,
        tableColumn: null,
        tableColumnWidth: null,
        viewPortWidth: null,
        addScroller: null,
        scrollNav: null,
        scrollFader: null,
        tableId: null,
    };

    const init = () => {
        els.viewPortWidth = options.els.parentEl.parentElement.offsetWidth;
        els.tableId = options.config.uid;
        //console.log('/els.viewPortWidth/ -init ', els.viewPortWidth);
        els.tableWrapper = options.els.el.querySelector(".table-scroller--wrapper");
        els.scrollFader = document.querySelector(".feature--scroll-fader");
        if(els.tableWrapper) {
            els.table = els.tableWrapper.querySelector('table');
            els.tableWidth = els.table.offsetWidth;
        }
        if(els.table) {
            if(tableIdList.indexOf(els.tableId) == -1) {
                tableIdList.push(els.tableId);
                tablesList.push({id: els.tableId, options: options});
            }
            els.table.setAttribute('id', els.tableId);
            if (els.tableWidth > els.viewPortWidth) {
                addNav();
            } else {
                removeNav();
            }
            if(els.tableWidth === 0) {
                if(updatedTableIdList.indexOf(els.tableId) == -1) {
                    updatedTableIdList.push(els.tableId);
                    updatedTableList.push({id: els.tableId, options: options});
                }
            }
        }

    options.signal.on(config.eventNames.APP_RESIZE_END, () => {
        update();
    });
    };
    const update = (id = null) => {
        if(id) {
            tablesList.map((table) => {
                if(table.id == id) {
                    Tablescroller(table.options).init();
                }
            });
        } else {
            setTimeout(() => {
                init();
            }, 100);
        }
        
    };




    const addNav = () => {
        // remove existing
        removeNav();

        els.addScroller = true;
        els.tableColumn = els.tableWrapper.querySelector("table tr td");
        els.tableColumnWidth = els.tableColumn.offsetWidth;
        leftAmount = els.tableColumnWidth,
        rightAmount = els.tableColumnWidth;
        addArrows(els.tableWrapper);
    };
    const addArrows = (tableWrapper) => {
        const arrowsTop = '<div class="scrollNav scrollNav--top"><div class="scrollNav__left"><span data-direction="left" class="sn-icon-navigation-left"></span></div><div class="scrollNav__right active"><span data-direction="right"  class="sn-icon-navigation-right"></span></div></div>';
        const arrowsBottom = `<div id="table-scrollnav-bottom-${els.tableId}" class="scrollNav scrollNav--bottom"><div class="scrollNav__left"><span data-direction="left"  class="sn-icon-navigation-left"></span></div><div class="scrollNav__right active"><span data-direction="right"  class="sn-icon-navigation-right"></span></div></div>`;
        tableWrapper.insertAdjacentHTML('afterbegin',arrowsTop);
        tableWrapper.insertAdjacentHTML('beforeend', arrowsBottom);
        els.scrollNav = tableWrapper.querySelectorAll('.scrollNav span');

        els.scrollNav.forEach((nav) => {
            nav.addEventListener('click', scrollTable);
        });
    };
    const scrollTable = (e) => {
        if(e.target.parentElement.classList.contains('active')) {
            e.target.parentElement.classList.remove('active');
            e.target.dataset.direction == "left" ? scrollLeft() : scrollRight();
            if(els.scrollFader) {
                const scroolNavBottom =  document.getElementById(`table-scrollnav-bottom-${els.tableId}`);                scroolNavBottom.style.opacity = 0;
            }
        }
    };
    const scrollLeft = (e) => {
        if (els.tableWrapper.scrollLeft >=0) {
            els.tableWrapper.scrollLeft = els.tableWrapper.scrollLeft - els.tableColumnWidth;
            setTimeout(() => {   updateNavStatus();  }, 500);
        }
       };
    const scrollRight = (e) => {
        if (els.tableWrapper.scrollLeft <= els.tableWidth-els.tableColumnWidth) {
            els.tableWrapper.scrollLeft = els.tableWrapper.scrollLeft + els.tableColumnWidth;
            setTimeout(() => {   updateNavStatus();  }, 500);
        }

    };

    const removeNav = () => {
        els.addScroller = false;
        const nav = els.tableWrapper.querySelectorAll('.scrollNav');
        nav.forEach((nav) => {
            nav.remove();
        }  );

    };
    const updateNavStatus = () => {

        const left = els.tableWrapper.querySelectorAll('.scrollNav__left');
        const right = els.tableWrapper.querySelectorAll('.scrollNav__right');
        const scroolNavBottom =  document.getElementById(`table-scrollnav-bottom-${els.tableId}`);        if(scroolNavBottom && els.scrollFader) {
            scroolNavBottom.style.opacity = 1;
            scroolNavBottom.style.left = `${els.tableWrapper.scrollLeft}px`;
        }

        if (els.tableWrapper.scrollLeft === 0) {
            left.forEach((nav) => {
                nav.classList.remove('active');
            });
        } else {
            left.forEach((nav) => {
                nav.classList.add('active');
            });
        }
        if (els.viewPortWidth + els.tableWrapper.scrollLeft  >= els.tableWidth ) {
            right.forEach((nav) => {
                nav.classList.remove('active');
            });
        } else {

            right.forEach((nav) => {
                nav.classList.add('active');
            });
        }
    };
    return {
        init,
        update
      }
    };

    window.addEventListener('load', () => {
        updatedTableList.map((table) => {
            Tablescroller(table.options).init();
        });
    });

const StaticProductCard = (props) => {
  const init = () => {
    const el = props.els.el.querySelector(`[data-button-action]`);
    if (!el) return; // *** stop if not required

    const action = general.attributeParser(
      el.getAttribute('data-button-action'),
      'rendered template',
      'json'
    );

    if (action) {
      el.addEventListener('click', () => {
        const key = Object.keys(action)[0]; // *** e.g. 'LAUNCH_POPOVER',
        if (!config.eventNames[key]) return; // *** defeat if not a defined event name...
        props.signal.emit(Object.keys(action)[0], {
          // *** ...otherwise fire event
          caller: 'StaticProductCard',
          ...action,
        });
      });
    }
  };

  return {
    init,
  };
};

const Breadcrumbs = (props) => {
  let els = {};

  const state = {
    items: [], // *** persist original item els
    numItems: 0,

    maskChars: '...',
    separatorChars: '>',
    mobileSeparatorChars: '<',

    removedItems: [], // *** stores items removed and masked on each update pass
  };

  const classes = {
    inner: 'breadcrumbs__inner',
    item: 'breadcrumbs__item',
    itemTestWidth: 'breadcrumbs__item--test-width', // *** mocks size with separator
    tooltip: 'breadcrumbs__tooltip',
    tooltipIsActive: 'breadcrumbs__tooltip--is-active',
    separator: 'breadcrumbs__separator',

    hasInitialised: 'breadcrumbs--has-initialised',
    hasUpdated: 'breadcrumbs--has-updated',

    header: '[class^="header"]',
    hasBreadcrumbs: 'header--has-breadcrumbs',
  };

  const init = () => {
    els = { ...props.els };

    state.items = [...els.el.querySelectorAll('a')];
    state.numItems = state.items.length;

    if (state.numItems === 0) {
      //console.log('/Breadcrumbs/ -init DEFEAT');
      return;
    }

    const headerRef = document.body.querySelector(classes.header);
    headerRef.classList.add(classes.hasBreadcrumbs);

    els.inner = document.createElement('div');
    els.inner.classList.add(classes.inner);
    els.el.append(els.inner);

    // *** keep ui tidy on change
    props.signal.on(config.eventNames.APP_RESIZE_START, () => {
      els.el.classList.remove(classes.hasUpdated);
    });

    // *** update
    props.signal.on(config.eventNames.APP_BREAKPOINT_READY, (payload) => {
      if (payload.isArtificialOrigin) return;
      update(payload.breakpoint);
    });
  };

  const update = (breakpoint) => {
    // *** destroy
    state.removedItems = [];

    while (els.inner.lastChild) {
      els.inner.removeChild(els.inner.lastChild);
    }

    // *** create
    state.items.map((item, index) => {
      els.inner.append(item);
      item.classList.add(classes.item);
      item.classList.add(classes.itemTestWidth);
    });

    if (breakpoint !== 'mobile') {
      shift(); // *** more complex fork
      return;
    }

    // *** mobile paint - show only last item
    const children = [...els.inner.children];
    children.map((item, index) => {
      if (index < state.numItems - 1) {
        item.remove();
      }
    });

    // *** add 'separator'
    const separator = document.createElement('div');
    separator.classList.add(classes.separator);
    separator.innerText = state.mobileSeparatorChars;
    els.inner.prepend(separator);

    els.el.classList.add(classes.hasInitialised);
    els.el.classList.add(classes.hasUpdated);
  };

  const paint = () => {
    // *** mask items
    if (state.removedItems.length > 0) {
      const blank = document.createElement('div');
      blank.innerText = state.maskChars;
      els.inner.insertBefore(blank, els.inner.children[1]);
    }

    // *** add separators
    const children = [...els.inner.children];
    children.map((item, index) => {
      item.classList.remove(classes.itemTestWidth); // *** remove test width
      if (index < children.length - 1) {
        const separator = document.createElement('div');
        separator.classList.add(classes.separator);
        separator.innerText = state.separatorChars;
        els.inner.insertBefore(separator, item.nextSibling);
      }
    });

    els.el.classList.add(classes.hasInitialised);
    els.el.classList.add(classes.hasUpdated);
    addLastItemHoverState();
  };

  const addLastItemHoverState = () => {
    const lastItemEl = state.items[state.numItems - 1];
    if (lastItemEl.offsetWidth >= lastItemEl.scrollWidth) return;

    if (!els.tooltip) {
      els.tooltip = document.createElement('div');
      els.tooltip.classList.add(classes.tooltip);
      els.el.append(els.tooltip);

      lastItemEl.addEventListener('mouseover', (e) => {
        els.tooltip.classList.add(classes.tooltipIsActive);
      });

      lastItemEl.addEventListener('mouseout', (e) => {
        els.tooltip.classList.remove(classes.tooltipIsActive);
      });

      lastItemEl.addEventListener('mousemove', (e) => {
        const x = e.clientX - els.tooltip.offsetWidth / 2;
        els.tooltip.style.left = `${x}px`;
      });
    }

    els.tooltip.innerText = lastItemEl.innerText;
  };

  // *** adjust layout by removing items until last item is visible enough
  const shift = () => {
    const lastItemEl = state.items[state.numItems - 1];

    const remove = () => {
      observer.disconnect();

      // *** @as 4.3.22 : not properly tested! hopefully fixes sporadic error that may cause cascade
      try {
        const removedItem = [...els.inner.children][1];
        state.removedItems.push(removedItem); // *** store item
        removedItem.remove(); // *** remove item from dom
      } catch (error) {
        // ***
      }

      /*const removedItem = [...els.inner.children][1];
      state.removedItems.push(removedItem); // *** store item
      removedItem.remove(); // *** remove item from dom*/

      setTimeout(() => {
        observer.observe(lastItemEl); // *** need to retrigger
      }, 0);
    };

    const observer = new IntersectionObserver(
      (entries) => {
        if (!entries[0].isIntersecting) {
          remove();
        } else {
          //console.log('/Breadcrumbs/ -done shiftin');
          observer.disconnect();
          paint();
        }
      },
      {
        root: els.inner,
        threshold: 1.0,
        rootMargin: '0px',
      }
    );

    observer.observe(lastItemEl);
  };

  return {
    init,
  };
};

const PageShare = (options) => {
  let shareData = {
    title: document.title,
    text: null,
    url: window.location,
  };

  const init = () => {
    //console.log('/page-share/ -init');
    if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {


        // get page data

        if (document.querySelector("meta[property='og:description']") !== null) {
          shareData.text = document.querySelector("meta[property='og:description']").getAttribute("content");
        } 

      [...document.body.querySelectorAll(`[data-button-action]`)].map(
        (item) => {
          if (
            item.getAttribute('data-button-action') ==
              '{"LAUNCH_POPOVER": "page-share-popover"}' 
          ) {
            item.removeAttribute('data-button-action');
            item.addEventListener('click', () => {
              triggerNativeShare();
            });
          }
        }
      );
    }
  };
  const triggerNativeShare = async () => {
    if (navigator.share) {
      try {
        await navigator.share(shareData);
      } catch (err) {
        // this will catch the second share attempt on ios14
        console.log(err.message);
      }
    }
  };
  return {
    init,
  };
};

/**
 * prepares 'behaviour' components, adding events and parsed attributes including binding directives
 */
const behaviours = {
  types: {
    ['carousel']: Carousel,
    ['carousel-controls']: CarouselControls,
    ['carousel-paginator']: CarouselPaginator,
    ['carousel-dots']: CarouselDots,
    ['tabstack']: TabStack,
    ['tab-switcher-control']: TabSwitcherControl,
    ['shapes']: Shapes,
    ['sticky-nav']: StickyNav,
    ['select-control']: SelectControl,
    ['video-player']: VideoPlayer,
    ['notification']: Notification,
    ['overlay']: Overlay,
    ['clipboard']: Clipboard,
    ['quickview']: Quickview,
    ['hero-select-box-extender']: HeroSelectBoxExtender,
    ['paginator']: Paginator,
    ['accordion']: Accordion,
    ['contactcard']: Contactcard,
    ['tablescroller']: Tablescroller,
    ['static-product-card']: StaticProductCard,
    ['breadcrumbs']: Breadcrumbs,
    ['page-share']: PageShare,
  },

  registry: [],

  examine: (el, signal) => {
    const type = el.getAttribute('data-behaviour');
    const Behaviour = behaviours.types[type];

    if (!Behaviour) {
      console.warn(
        '/behaviours/ -examine',
        `'${type}'`,
        'does not have a handler'
      );
      return;
    }

    const parentEl = el.parentNode; // TODO find a safe way to get this if more deeply nested

    let config$1 = general.attributeParser(
      el.getAttribute('data-behaviour-config'),
      type
    );
    const binding = general.attributeParser(
      el.getAttribute('data-behaviour-binding'),
      type
    );
    const jsonContent = general.attributeParser(
      el.getAttribute('data-behaviour-json-content'),
      type,
      'json'
    );

    config$1 = config$1 ? config$1 : {};
    config$1.uid = el.getAttribute('data-behaviour-uid');

    const duplicateUid = behaviours.registry.find((item) => {
      return item.uid === config$1.uid;
    });

    if (duplicateUid) {
      /*console.warn(
        '*** /behaviours/ -examine --ERROR duplicate uid ***',
        `\n'${config.uid}'`,
        'already exists!',
        el,
        '\nclashes with:',
        duplicateUid.options.els
      );*/
      config$1.uid = `${type}_${general.uid()}`; // *** generate unique uid
      /*console.warn(
        '*** /behaviours/ -examine --replacing with generated uid:',
        config.uid
      );*/
    }

    // console.log('/index/ -examine', options.config.async);

    const options = {
      globalConfig: config,
      config: config$1,
      binding,
      jsonContent,
      signal, // *** allow pub/sub
      getBehaviourByUid: behaviours.getBehaviourByUid, // *** fixes circular dep issue - when instantiating a behaviour inside a behaviour use this method
      registerBehaviour: behaviours.registerBehaviour, // *** JA adds a means to manually register a behaviour, eg when new dom els are inserted
      state: null, // *** exposed slot to operate directly on instance's state object. requires behaviour to set options.state = state;
      els: {
        el,
        parentEl,
      },
    };

    if (!options.config.async) {
      Behaviour(options).init();
    }

    behaviours.registry.push({
      uid: config$1.uid,
      type,
      Behaviour,
      options,
    });

    // console.log('/index/ -examine ADDED', config.uid);
    signal.emit(config.eventNames.BEHAVIOUR_ADDED, {
      type,
      uid: config$1.uid,
    });
  },

  // *** allow behaviour to be publicly retrievable, i.e. by AbstractComponent
  getBehaviourByUid: (uid) => {
    return behaviours.registry.find((item) => {
      if (uid === item.uid) {
        // console.log('============= /index/ -getBehaviourByUid', uid, item);
        return item;
      }
    });
  },

  // *** allows to register a new injected dom element as a behaviour i.e. by AbstractComponent
  registerBehaviour: (el, signal) => {
    const type = el.getAttribute('data-behaviour');
    let config = general.attributeParser(
      el.getAttribute('data-behaviour-config'),
      type
    );
    config = config ? config : {};
    config.uid = el.getAttribute('data-behaviour-uid');

    const duplicateUid = behaviours.registry.find((item) => {
      return item.uid === config.uid;
    });
    if (duplicateUid) {
      // remove from the registry as we are effectively replacing it with itself..
      const index = behaviours.registry.indexOf(duplicateUid);
      behaviours.registry.splice(index, 1);
    }
    // const registeredItem = behaviours.examine(el, signal);
    // console.log(registeredItem);
    return behaviours.examine(el, signal);
  },
};

/* parses dataset */
const passthru = {
  parse: (config, data) => {
    // console.log('/passthru/ -parse', data);
  
    // *** this is just keeping the data structure intact versus 'chunked' or any other topology
    let result = {
      topology: 'flat',
      data: {
        uid: null,
        label: null,
        items: data
      }
    };
    return result;
  }
};

/* parses dataset */

const productGroupsBySpeciality = {
  parse: (config, data) => {
    const key1 = config.key1; // *** e.g. 'Treatments'
    const key2 = config.key2; // *** e.g. 'TreatmentsName'
    const key3 = config.key3; // *** e.g. 'TreatmentsId'

    if (!key1 || !key2 || !key3) {
      console.warn(
        '/ProductGroupsBySpeciality/ -parse --keys missing, quitting'
      );
      return;
    }

    let criteria = []; // *** should be uids for matching process
    let labels = []; // *** text strings for output
    let multiItems = []; // *** defer items that belong to multiple chunks, and append later

    if (!data) data = [];

    data.map((item) => {
      item[key1].map((targetItem) => {
        criteria.push(targetItem[key3]); // *** get all identifiers
        labels.push(targetItem[key2]);
      });
    });
    criteria = [...new Set(criteria)]; // *** unique values only
    labels = [...new Set(labels)]; // *** unique values only

    // console.log('/ProductGroupsBySpeciality/ -parse', criteria, labels);

    // *** chunk data by criteria
    const chunks = criteria.map((value) => {
      return data.filter((item) => {
        // *** all data
        const container = item[key1]; // *** bit of data inside item
        if (container.length === 1) {
          // *** has only one entry
          return container[0][key3] === value; // *** belongs to chunk
        } else {
          // *** has multiple entries

          const checkExists = multiItems.find((existingItem) => {
            return item.ProductId === existingItem.ProductId;
          });
          if (!checkExists) {
            multiItems.push(item); // *** store more complex items to process later
          }

          return false;
        }
      });
    });

    // *** we have structure defined
    let result = {
      topology: 'chunked',
      data: chunks.map((item, index) => {
        return {
          uid: criteria[index],
          label: labels[index],
          items: chunks[index],
        };
      }),
    };

    // *** now go the other way around and put the deferred multi items into correct chunk
    multiItems.map((item) => {
      item[key1].map((target) => {
        const uid = target[key3];
        result.data.find((chunk) => {
          if (uid === chunk.uid) {
            const clone = Object.assign({}, item);
            chunk.items.push(clone);
          }
        });
      });
    });

    // console.log('/ProductGroupsBySpeciality/ -parse RESULT:', result);

    return result;
  },
};

/* parses dataset */
const Contacts = {
  parse: (config, data) => {
    let target = null;
    let items = [];

    //console.log(config.key1);

    if (!config.key1) {
      console.warn(
        '/Contacts/ -parse --parser requires "key1" in  @data-parser'
      );
    }

    if (data[0] && config.key1) {
      target = data[0];
      items = target[config.key1];
    }

    let result = {
      topology: 'flat',
      data: {
        uid: null,
        label: null,
        raw: target,
        items,
      },
    };
    //console.log('/Contacts/ -parse', result);
    return result;
  },
};

/* parses dataset */
const ContactsLocation = {
  parse: (config, data) => {
    
    //console.log("config",config);
    //console.log("data",data);

    let target = null;
    let items = [];
    
    if (!config.key1) {
      console.warn('/ContactsLocation/ -parse --parser requires "key1" in  @data-parser');
    }
    
    if (data[0] && config.key1) {
      target = data[0];
      items = target[config.key1];
    }
  
    let result = {
      topology: 'flat',
      data: {
        uid: null,
        label: null,
        raw: target,
        items,
      }
    };
    //console.log('/ContactsLocation/ -parse', result);
    return result;
  }
};

const dataParsers = {
  types: {
    ['passthru']: passthru,
    ['productgroupsbyspeciality']: productGroupsBySpeciality,
    ['contacts']: Contacts,
    ['contactslocation']: ContactsLocation,
  },
  
  examine: (options) => {
    if (!options) {
      //console.log('/dataParsers/ -examine --using default parser');
      return passthru;
    }
    //console.log('/dataParsers/ -examine', options.type);
    const parser = dataParsers.types[options.type.toLowerCase()];
    if (!parser) {
      console.warn('/dataParsers/ -examine', options.type, 'not found');
      return passthru;
    }
    return parser;
  }
};

const AbstractComponent = (options) => {
  let props = {};
  let component = null;

  const init = () => {
    props = { ...options };
    props.hasScript = !!options.script;
    props.apiService = apiService; // *** added api service
    props.requestModel = requestModel; // *** for new API
    props.utils = general; // *** added utils
    
    props.utils.mediaQueries = mediaQueries; // *** allows the exposition of props.utils.mediaQueries.getValue()
    
    props.attributes = general.attributeParser(
      props.el.getAttribute('data-component-props'),
      'AbstractComponent'
    );
    props.fetchData = fetchData; // *** make public
    props.renderDynamicContent = uiFactory.examine; // *** make public
    props.getBehaviour = getBehaviourByUid; // *** make public
    props.getBehaviourRaw = getBehaviourRaw; // *** make public
    props.registerBehaviour = registerBehaviour; // *** make public
    props.simpleTweens = simpleTweens; // *** make public
    props.TweenMax = gsapWithCSS$1;
    props.dataParsers = dataParsers;

    if (props.hasScript) {
      component = props.script(props);
      component.init();
    }

    getBehaviours();

    canOverrideThis({ someData: ['foo', 'bar', 'baz'] });
  };

  const canOverrideThis = (params) => {
    if (call('canOverrideThis', params)) return;
  };

  const fetchData = async (options, params) => {
    // console.log('/AbstractComponent/ -fetchData', options, params);
    options.signal = props.events; // *** mix in signal for call (using name 'events' as more obvious in component script)
    return await apiService.call(options, params);
  };

  // *** give access to behaviours registered by this instance for use in child script
  const getBehaviours = () => {
    props.behaviours = {}; // *** accessible by child script
    [...props.el.querySelectorAll('[data-behaviour]')].map((item) => {
      // *** find behaviours in html
      const uid = item.getAttribute('data-behaviour-uid'); // *** extract uid
      // console.log('/AbstractComponent/ -', uid);
      if (uid) {
        // console.log('/AbstractComponent/ -ADDED?', uid);
        const behaviour = behaviours.getBehaviourByUid(uid); // *** retrieve actual behaviour
        props.behaviours[behaviour.uid] = behaviour; // *** add as object with uid as key
        // console.log('/AbstractComponent/ -REG', props.behaviours);
      }
    });
  };

  // *** method exposed for child script
  const getBehaviourByUid = (el) => {
    if (!el) {
      console.warn(
        '/AbstractComponent/ -getBehaviourByUid --item does not exist!'
      );
      return;
    }
    const uid = el.getAttribute('data-behaviour-uid'); // *** extract uid to use as key
    const instance = props.behaviours[uid]; // *** get behaviour by uid
    // console.log('===== /AbstractComponent/ -getBehaviourByUid', instance);
    return instance.Behaviour(instance.options); // *** reinstantiate behaviour with original options object
  };
  
  // *** 'getBehaviourByUid' should be deprecated in favour of this method which returns the entire behaviour entry, not just the 'class'
  const getBehaviourRaw = (el) => {
    if (!el) {
      console.warn(
          '/AbstractComponent/ -getBehaviourRaw --item does not exist!'
      );
      return;
    }
    const uid = el.getAttribute('data-behaviour-uid'); // *** extract uid to use as key
    return props.behaviours[uid]; // *** get behaviour by uid
  };

  // *** method exposed for child script
  const registerBehaviour = (el) => {
    if (!el) {
      console.warn(
        '/AbstractComponent/ -registerBehaviour --item does not exist!'
      );
      return;
    }
    const uid = el.getAttribute('data-behaviour-uid'); // *** extract uid to use as key
    props.behaviours[uid]; // *** get behaviour by uid
    return behaviours.registerBehaviour(el, props.events);
  };

  /**
   * utility to call a function in the child component
   * if the function does not exist on the child this object can invoke it
   * @param {String} fn - function name
   * @return {boolean}
   */
  const call = (fn, params) => {
    if (!props.hasScript) return false;
    if (component[fn]) {
      component[fn](params);
      return true;
    }
    return false;
  };

  return {
    init,
  };
};

/**
 * rough and ready scroll velocity on get()
 */
const scrollVelocity = {
  
  state: {
    prevTime: 0,
    prevY: 0,
    velocity: 0.01, // *** stops ios initial scroll passing resizeNotifier check
  },
  
  props: {
    idleResetTimeMs: 99,
    timer: null,
    minVelocity: 0,
  },
  
  init: () => {
  
    /**
     * @as fixes SMIT-2986
     * didn't want to do sniffing, and really this should be a utility
     */
    const isIos = () => {
      return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
    };
    
    if (isIos()) {
      scrollVelocity.props.minVelocity = 0.01;
      console.log('/scrollVelocity/ -init IOS');
      window.debugWindow.update('/scrollVelocity/ -init IOS');
    }
    
    window.addEventListener('scroll', (e) => {
      scrollVelocity.startTimer();
      scrollVelocity.update();
    });
  },
  
  startTimer: () => {
    clearTimeout(scrollVelocity.props.timer);
  
    scrollVelocity.props.timer = setTimeout(() => {
      scrollVelocity.state.velocity = scrollVelocity.props.minVelocity;
      // console.log('/scrollVelocity/ -STOPPED?', scrollVelocity.state.velocity);
    }, scrollVelocity.props.idleResetTimeMs);
    
  },
  
  // *** simple calc returns approx 0.1 - 3.9 (higher is faster)
  update: () => {
    const y = window.scrollY;
    scrollVelocity.state.velocity = Math.abs(y - scrollVelocity.state.prevY) / 100;
    // console.log('/scrollVelocity/ -update', scrollVelocity.state.velocity);
  
    // window.debugWindow.update(`>>> /scrollVelocity/ UPDATE ${scrollVelocity.state.velocity}`);
  
  
    scrollVelocity.state.prevY = y;
  },
  
  get: () => {
    return {
      velocity: scrollVelocity.state.velocity,
      // label
    }
  }
};

const resizeNotifier = {
  
  state: {
    block: false,
    prevTime: 0,
  },
  
  init: (signal) => {

    let willResize = true;
    
    window.addEventListener('resize', () => {
  
      window.debugWindow.update(`=== /resizeNotifier/ EVENT ${scrollVelocity.get().velocity}`);
      
      if (scrollVelocity.get().velocity > 0 && document.documentElement.scrollTop !== 0) return;  // *** prevent dispatch during scrolling, fixes mobile issue
  
      if (willResize) {
        signal.emit(config.eventNames.APP_RESIZE_START, null);
        onResize();
        willResize = false;
      }
    });
    
    // *** NOTE slowly dragging the viewport will cause multiple retriggers but is probably an edge case
    const onResize = general.debounce(() => {
      signal.emit(config.eventNames.APP_RESIZE_END, null);
      willResize = true;
    }, config.timings.resizeDebounceDelayTime);
  }
};

const networkListener = {
  
  props: {
    signal: null,
  },
  
  
  
  init: (signal) => {
    networkListener.props.signal = signal;
    networkListener.props.signal.on(config.eventNames.APP_NETWORK_ACTIVITY, (payload) => {
      networkListener.actionRunner(payload);
    });
  },
  
  // *** TODO handle request action, e.g. show/hide spinners, error messages etc
  actionRunner: (payload) => {
    
    switch (payload.state) {
          }
  }
};

const renderFeaturedProduct = {
  update: (options) => {
    const { props, parsedData, els, state } = options; // *** reference args from parent
    const time = props.config.timings.animation.short(); // *** for animation

    if (!state.hasFeaturedProduct) return;
    if (!parsedData.data.items) return;
    
    const heroData = parsedData.data.items.find((item) => {
      return item.IsHero;
    });
	
	if(heroData) {
      const procedureFeaturedProductListText = document.getElementById('procedureFeaturedProductList');
      if(procedureFeaturedProductListText) {
        procedureFeaturedProductListText.style.display = 'none';
      }
	}

    if (!heroData) {
      console.warn(
        '/renderFeaturedProduct/ -update --no featured product data available'
      );
      [...els.featuredProduct.children].map((item) => {
        item.remove();
      });
      return;
    }
    
    const jsonMap = props.utils.attributeParser(
      els.featuredProduct.getAttribute('data-json-mapping')
    );

    if(els && els.featuredProduct) {
      const ctaButtonLabel = els.featuredProduct.getAttribute('data-cta-button-label');
      if(ctaButtonLabel) {
        jsonMap.CTAButtonLabel = `%${ctaButtonLabel}`;
      }
    }
  
    // console.log('============ /renderFeaturedProduct/ -update', jsonMap, heroData.ProductDescriptor);
    // console.log('============ /renderFeaturedProduct/ -update', els);
  
  
    if (!props.attributes.featuredTemplate) {
      console.warn('/renderFeaturedProduct/ -update --featuredTemplate is not defined, quitting');
    }

    console.log('/renderFeaturedProduct/ -update --using @data-component-props: featuredTemplate =', props.attributes.featuredTemplate);

    const node = window.alchemyTemplates.getTemplate(
      {
        type: props.attributes.featuredTemplate || '',
        map: jsonMap,
        parent: props.el,
      },
      heroData
    );

    // *** fade out container
    props.TweenMax.to(els.featuredProduct, time, {
      opacity: 0,
      onComplete: () => {
        // *** destroy
        [...els.featuredProduct.children].map((item) => {
          item.remove();
        });
        // *** create
        els.featuredProduct.append(node);
      },
    });

    // *** fade in container
    props.TweenMax.to(els.featuredProduct, time, {
      opacity: 1,
      delay: time,
    });
  },
};

const renderContactHead = {
  
  update: (options) => {
    
    const {props, parsedData, els, state} = options; // *** reference args from parent
    const time = props.config.timings.animation.short(); // *** for animation
    
    if (!state.hasContactHead) return;
    
    const jsonMap = props.utils.attributeParser(
        els.contactHead.getAttribute('data-json-mapping')
    );
    
    console.log('/renderContactHead/ -update --using @data-component-props: featuredTemplate =', props.attributes.featuredTemplate);
  // return;
    const node = window.alchemyTemplates.getTemplate(
        {
          type: props.attributes.featuredTemplate || '',
          map: jsonMap,
          parent: props.el,
        },
        parsedData.data.raw
    );
    
    // *** fade out container
    props.TweenMax.to(els.contactHead, time, {
      opacity: 0,
      onComplete: () => {
        // *** destroy
        [...els.contactHead.children].map((item) => {
          item.remove();
        });
        // *** create
        els.contactHead.append(node);
      }
    });
  
    // *** fade in container
    props.TweenMax.to(els.contactHead, time, {
      opacity: 1,
      delay: time
    });
  }
  
};

/* flat data fork */

const renderFlat = {
  update: (options) => {
    // *** optional subrenders handle their own activity
    renderFeaturedProduct.update(options);
    renderContactHead.update(options);
    // ...others
    const { props, jsonMap, parsedData, template, els, state } = options; // *** reference args from parent
    if (!state.hasCardstack) {
      console.warn(
        '/renderFlat/ -- cardstack element is required! quitting...'
      );
      return;
    }

    // If no data, make an empty array to avoid errors..
    if (!parsedData.data.items) {
      parsedData.data.items = [];
    }

    const nodes = parsedData.data.items.map((item) => {
      return window.alchemyTemplates.getTemplate(
        {
          type: template || '',
          map: jsonMap,
          parent: props.el,
        },
        item
      );
    });

    // *** inspect rendered item for extra functionality
    nodes.map((item) => {
      // renderFlat.inspectForButtonAction(item, props);
      renderFlat.addListenersToNode(item, props);
      renderFlat.inspectForBehaviour(item, props);
    });
    // @ja add scroll param to advise if UI should scroll to element once data is fetched
    options.scroll;
    renderFlat.updateUi({ els, nodes, props, scroll: options.scroll });
  },

  // *** adds and removes children with simple fade effect
  updateUi: (options) => {
    const { els, nodes, props, scroll } = options;
    const time = props.config.timings.animation.short(); // *** for animation
    // *** fade out container
    props.TweenMax.to(els.cardstack, time, {
      opacity: 0,
      onComplete: () => {
        // *** destroy
        [...els.cardstack.children].map((item) => {
          item.remove();
        });
        // *** create
        nodes.map((item) => {
          els.cardstack.append(item);
        });

        // hide this first so we dont dupe
        renderFlat.hideNoResults(els);

        if (nodes.length === 0) {
          if(!props.attributes.isNewsGrid) {
            if(options.els.el && options.els.el.dataset && options.els.el.dataset.anchor) {
              const tabButtonsGroup = document.querySelector('.tab-button-group');
              const tabButtons = [...tabButtonsGroup.querySelectorAll('.tab-button')] || [];
              const acchorSection = options.els.el.querySelector('.offset-anchor');
              if(tabButtons.length) {
                tabButtons.forEach((btn) => {
                  if(btn.dataset && btn.dataset.buttonAction) {
                    if(acchorSection.classList.contains(`offset-anchor__${btn.dataset.buttonAction.split(':')[1].trim()}`)) {
                      btn.style.display = 'none';
                    }
                  }
                });
              }
              options.els.el.style.display = 'none';
            }
          }
          renderFlat.showNoResults(props, els);
        }
      },
    });

    // *** fade in container
    props.TweenMax.to(els.cardstack, time, {
      opacity: 1,
      delay: time,
      onStart: () => {
        // @ja:: only scroll if we have instruction to..
        if (scroll) {
          // first we check if there is a stickynav on the page
          // we can do this by checking for dynamic class added to element by stickynav component
          const hasStickyNav = props.el.classList.contains(
            'alchemy--has-offset-anchor'
          );
          if (hasStickyNav) {
            // if so we need to get the id of the anchor on this element
            const anchor = props.el.querySelector('.offset-anchor');
            const anchorId = anchor ? anchor.id : null;
            // and then fire an event so the stickynav can scroll to the anchor
            if (anchorId) {
              props.events.emit('SCROLL_ANCHOR', {
                id: anchorId,
              });
            }
          } else {
            // if no stickynav we just scroll to the element
            props.el.scrollIntoView();
          }
        }
      },
    });
  },

  showNoResults: (props, els) => {
    
    // *** @as when @data-component-props -> showNoResult: false, don't display noresults message
    if (props.attributes.showNoResult && props.attributes.showNoResult === 'false') return;
    
    // if nothing comes back from api lets append a message to tell user
    const noResultsDiv = document.createElement('div');
    const resultsText = props.attributes.noresult
      ? props.attributes.noresult
      : '';
    noResultsDiv.classList.add('cardstack__no-results');
    noResultsDiv.innerText = resultsText;
    els.cardstack.append(noResultsDiv);

    // quick fix hide entire component if results are empty AND text is set to hide
    if (resultsText=="hide") {
      els.el.style.display = 'none';

    }
  },

  hideNoResults: (els) => {
    // if we have results and a NO results div, then remove it
    const noResultsDiv = els.el.querySelector('.cardstack__no-results');
    if (noResultsDiv) {
      noResultsDiv.parentElement.removeChild(noResultsDiv);
    }
  },

  inspectForBehaviour: (item, props) => {
    const type = item.getAttribute('data-behaviour');
    if (!type) return;
    behaviours.registerBehaviour(item, signal);
  },

  /* ***
    inspect rendered item for (first and only - add loop if necessary) @data-button-action, and adds click listener
    test from somewhere with this or similar:
    props.events.on(props.config.eventNames.TRIGGER_PRODUCT_QUICKVIEW, (payload) => {
      console.log('/RenderListFromApiOnEvent2/ -EVENT',payload);
    })*/
  addListenersToNode: (node, props) => {
    const el = node.querySelector(`[data-button-action]`);
    if (!el) return; // *** stop if not required

    const action = props.utils.attributeParser(
      el.getAttribute('data-button-action'),
      'rendered template',
      'json'
    );

    if (action) {
      el.addEventListener('click', () => {
        const key = Object.keys(action)[0]; // *** e.g. 'TRIGGER_PRODUCT_QUICKVIEW',
        if (!props.config.eventNames[key]) return; // *** defeat if not a defined event name...
        props.events.emit(Object.keys(action)[0], {
          // *** ...otherwise fire event
          caller: 'rendered template',
          ...action,
        });
      });
    }
  },
};

/* chunked data fork
 *  ref imp via: src/components/SMIT-668_patient-products--BODY/index.html
 *  FIXIT prob not best idea having specific ui functionality in this class, should refactor into a behaviour
 * */

const renderChunked = {
  subProps: {
    tabSwitcher: null,
    select: null,
  },

  // TODO split into separate functions
  update: (options) => {
    const { props, jsonMap, parsedData, els, state, isPatientFilter } = options; // *** reference args from parent
    const labels = isPatientFilter ? [] : [{ label: 'All' }]; // *** for tabswitcher/select TODO localise!!!
    const hasNoResults = parsedData.data.length === 0;

    if (!state.hasRenderset) {
      console.warn(
        '/renderFlat/ -- renderset element is required! quitting...'
      );
      return;
    }

    // console.log('/renderChunked/ -update PARSEDDATA', parsedData);

    // *** destroy
    [...els.renderset.children].map((item) => {
      item.remove();
    });

    // *** inspect rendered item for (first and only - add loop if necessary) @data-button-action, and adds click listener
    // *** TODO centralise this to keep DRY
    const addListenersToNode = (node) => {
      const el = node.querySelector(`[data-button-action]`);
      if (!el) return; // *** stop if not required

      const action = props.utils.attributeParser(
        el.getAttribute('data-button-action'),
        'rendered template',
        'json'
      );

      if (action) {
        el.addEventListener('click', () => {
          const key = Object.keys(action)[0]; // *** e.g. 'TRIGGER_PRODUCT_QUICKVIEW',
          if (!props.config.eventNames[key]) return; // *** defeat if not a defined event name...
          props.events.emit(Object.keys(action)[0], {
            // *** ...otherwise fire event
            caller: 'rendered template',
            ...action,
          });
        });
      }
    };

    // *** construct wrapper div and inject child nodes generated by alchemy-templates
    const chunks = parsedData.data.map((chunk, index) => {
      labels.push({ label: chunk.label });

      const wrapper = document.createElement('div');
      wrapper.setAttribute('data-renderset-uid', chunk.uid);
      wrapper.setAttribute('data-renderset-label', chunk.label);
      wrapper.setAttribute('data-renderset-index', index + 1);
      wrapper.classList.add('renderset-item');

      const inner = document.createElement('div');
      inner.classList.add('renderset-item__inner');
      wrapper.append(inner);

      chunk.items.map((item) => {
        const node = window.alchemyTemplates.getTemplate(
          {
            type: props.attributes.template || '',
            map: jsonMap,
            parent: props.el,
          },
          item
        );

        addListenersToNode(node);
        inner.append(node);
      });

      return wrapper;
    });

    // *** create renderset elements
    chunks.map((chunk) => {
      const inner = chunk.querySelector('.renderset-item__inner');
      const title = document.createElement('h4');
      inner.classList.add('cardstack');
      title.innerText = `${chunk.getAttribute('data-renderset-label')}`;
      title.classList.add('renderset__title');
      chunk.prepend(title);
      els.renderset.append(chunk);
    });

    if (!renderChunked.subProps.tabSwitcher) {
      renderChunked.subProps.tabSwitcher = props.getBehaviour(els.tabSwitcher);
    }
    if (!renderChunked.subProps.select) {
      renderChunked.subProps.select = props.getBehaviour(els.select);
    }
    // console.log(
    //   '/renderChunked/ -update SELECT',
    //   renderChunked.subProps.select
    // );

    const showAll = () => {
      // console.log('/renderChunked/ -showAll');
      chunks.map((chunk) => {
        chunk.classList.add('renderset-item--is-expanded');
      });
    };

    const showPatientFilterByIndex = (reqIndex) => {
      // console.log('/renderChunked/ -showPatientFilterByIndex', reqIndex);
      chunks.map((chunk, index) => {
        if (reqIndex === index) {
          chunk.classList.add('renderset-item--is-expanded');
        } else {
          chunk.classList.remove('renderset-item--is-expanded');
        }
      });
    };

    props.events.on('BEHAVIOUR_BINDING_SIGNAL', (payload) => {
      const tabSwitcherUid =
        renderChunked.subProps.tabSwitcher.getOptions().config.uid;
      const selectUid = renderChunked.subProps.select.getOptions().config.uid;
      let pass =
        payload.sender === tabSwitcherUid || payload.sender === selectUid;

      if (!pass) return;
      if(isPatientFilter) {
        showPatientFilterByIndex(payload.data.index);
      } else {
        if (payload.data.index === 0) {
          showAll();
        } else {
          showByIndex(payload.data.index);
        }
      }
      
      // *** force update controls as we are not in a behaviour context
      renderChunked.subProps.tabSwitcher.update(payload.data.index);
      renderChunked.subProps.select.update(payload.data.index);
    });

    // *** add items
    renderChunked.subProps.tabSwitcher.inject(labels, 0);
    renderChunked.subProps.select.inject(labels, 0);

    const showByIndex = (reqIndex) => {
      // console.log('/renderChunked/ -showByIndex', reqIndex);
      chunks.map((chunk, index) => {
        if (reqIndex === index + 1) {
          chunk.classList.add('renderset-item--is-expanded');
        } else {
          chunk.classList.remove('renderset-item--is-expanded');
        }
      });
    };

    // hide this first so we dont dupe
    renderChunked.hideNoResults(els);

    if (hasNoResults) {
      renderChunked.showNoResults(props, els);
    }

    // TEST ONLY
    /*props.events.on(props.config.eventNames.TRIGGER_PRODUCT_QUICKVIEW, (payload) => {
      console.log('/RenderListFromApiOnEvent2/ -EVENT',payload);
    })*/
  },

  showNoResults: (props, els) => {
  
    // *** @as when @data-component-props -> showNoResult: false, don't display noresults message
    if (props.attributes.showNoResult && props.attributes.showNoResult === 'false') return;
    
    // if nothing comes back from api lets append a message to tell user
    const noResultsDiv = document.createElement('div');
    const resultsText = props.attributes.noresult
      ? props.attributes.noresult
      : '';
    noResultsDiv.classList.add('cardstack__no-results');
    noResultsDiv.innerText = resultsText;
    els.el.append(noResultsDiv);
    els.el.classList.add('tabstack--noresults');
        // quick fix hide entire component if results are empty AND text is set to hide
        if (resultsText=="hide") {
          els.el.style.display = 'none';
    
        }
  },

  hideNoResults: (els) => {
    els.el.classList.remove('tabstack--noresults');
    const noResultsDiv = els.el.querySelector('.cardstack__no-results');
    if (noResultsDiv) {
      noResultsDiv.parentElement.removeChild(noResultsDiv);
    }
  },
};

//// custom analytics trackers
//// please note form related stuff is baked in with forms
//// video stuff is baked in with videos
//// search bar header related stuff is in SMIT-2260-meganav-header / index.js and searchhead.js
//// with hero select components that may render results into a cardstack / renderset etc
//// code here will re-examine after async load of new results
//// if there is a cardstack it will also look for a pagination component and add interaction listener to that to check for updates

const analytics = {

    tracking : {},

    observer : null,
    observeDom: false,

    props: {
        signal: null,
        tracking: 'data-analytics',
       },

    init: (signal) => {
        analytics.props.signal = signal;

        if (!window.dataLayer) {
            console.log("no datalayer configured - aborting");
            return;
        }
      },

    examine: () => {
        setTimeout(() => {
            [...document.body.querySelectorAll(`[${analytics.props.tracking}]`)].map(
                (item, index) => {

                // console.log('examine',item);
                const attr = item.getAttribute(analytics.props.tracking);

                var trackingJSON = JSON.parse(attr);

                    analytics.handleTrackers(item,trackingJSON);

                }
            );
            analytics.trackCTAButtons();
            analytics.trackDropdowns();
            analytics.trakForm();
            analytics.trackTabs();
            analytics.trackProducts();
            analytics.trackAddToCalander();
       }, 5000);

    },

    trackProductList: (listOfProducts) => {
        if('Products' in listOfProducts) {
            const productListFromDom = [...document.body.querySelectorAll('aside.card.card--product')];
            productListFromDom.map((listItem, index) => {
                const btnSection = listItem.querySelector('.button-group');
                const viewBtn = btnSection.querySelector('a');
                const preViewBtn = btnSection.querySelector('button');
                const cardTitle = listItem.querySelector('.card__title');
                const cardTitleText = analytics.getInnerText(cardTitle);
                const product = listOfProducts['Products'][index];
                const productName = product.ProductName;
                const div = document.createElement("div");
                div.innerHTML = productName;
                const productNameText = analytics.getInnerText(div);
                if(productNameText.replace(/\S+/g, '').trim() == cardTitleText.replace(/\S+/g, '').trim()) {
                    viewBtn.addEventListener('click', () => {
                        analytics.trackViewListProducts('view_item', {Products: [product]});
                    });
                    preViewBtn.addEventListener('click', () => {
                        analytics.trackProductPreviewDetails('preview_item', product);
                    });
                }
            });
        }
    },

    getInnerText: (htmlNode) => {
        let text = "";
        for (let i = 0; i < htmlNode.childNodes.length; i++) {
            let node = htmlNode.childNodes[i];
            if (node.nodeType  === Node.TEXT_NODE) {
                text += node.data;
            }
        }
        return text;
    },

    getFormatedItemName: (name) => {
        let itemName = '';
        const div = document.createElement("div");
        div.innerHTML = name;
        itemName = analytics.getInnerText(div);
        return itemName;
    },

    trackProducts: () => {
        let listOfProducts = {};
        const productItems = [...document.body.querySelectorAll('[ga-product-data]')];
        productItems.map(
            (item, index) => {
                const productType = item.getAttribute('ga-product-filter');
                if(productType in listOfProducts) {
                    listOfProducts[productType].push(JSON.parse(item.getAttribute('ga-product-data')));
                } else {
                    listOfProducts[productType] = [JSON.parse(item.getAttribute('ga-product-data'))];
                }
                
                if(productItems.length == listOfProducts[productType].length) {
                    analytics.trackViewListProducts('view_item_list', listOfProducts);
                }
                const btnSection = item.querySelector('.button-group');
                if(btnSection) {
                    const viewBtn = btnSection.querySelector('a');
                    const preViewBtn = btnSection.querySelector('button');
                    if(preViewBtn) {
                        viewBtn.addEventListener('click', () => {
                            analytics.trackViewListProducts('view_item', {[productType]: [JSON.parse(item.getAttribute('ga-product-data'))]});
                        });
                        preViewBtn.addEventListener('click', () => {
                            analytics.trackProductPreviewDetails('preview_item', JSON.parse(item.getAttribute('ga-product-data')));
                        });
                    } else {
                        if(viewBtn){
                            viewBtn.addEventListener('click', () => {
                                analytics.trackViewListProducts('select_item', {[productType]: [JSON.parse(item.getAttribute('ga-product-data'))]});
                            });
                        }
                    }
                } else {
                    const exploreBtn = item.querySelector('a');
                    if(exploreBtn) {
                        exploreBtn.addEventListener('click', () => {
                            analytics.trackViewListProducts('select_item', {[productType]: [JSON.parse(item.getAttribute('ga-product-data'))]});
                        });
                    }
                }
            }
        );
    },

    trackViewListProducts: (eventName, listOfProducts, isTrackProductList = false) => {
        let listEl = null;
        if(isTrackProductList) {
            setTimeout(() => {
                analytics.trackProductList(listOfProducts);
            }, 3000);
        }
        let trackObj = {
            event: eventName,
            ecommerce: {},
        };
        for(let item in listOfProducts) {
            if(eventName === 'view_item_list' && !('item_list_id' in trackObj.ecommerce)) {
                trackObj.ecommerce['item_list_id'] = item.toLowerCase().replace(' ', '_');
                trackObj.ecommerce['item_list_name'] = item;
            }
            const productsItems = listOfProducts[item].map((product, index) => {
                const itemName = product.Title || product.ProductName || '';
                let productObj = {
                    item_name: analytics.getFormatedItemName(itemName),
                    item_id: '',
                    item_category: product.DataLayer ? product.DataLayer.productCategory : product.DisciplineName ? product.DisciplineName : '',
                    item_category2: product.DataLayer ? product.DataLayer.areaOfConcern || '' : '',
                };
                if(eventName === 'view_item_list') {
                    productObj = {...productObj, index: index, item_list_id: item.toLowerCase().replace(' ', '_'), item_list_name: item};
                }
               return productObj;
            });
            trackObj.ecommerce['items'] = productsItems.length ? productsItems : [];
            if(Object.keys(listOfProducts).length && eventName === 'view_item_list') {
                const listType = Object.keys(listOfProducts)[0];
                if(listEl === null) {
                    listEl = document.querySelector(`[ga-product-filter="${listType}"]`);
                    let productsListObserver = new IntersectionObserver(function (entries, observer) {
                        entries.forEach(function (entry) {
                            if (entry.isIntersecting) {
                                let productEl = entry.target;
                                analytics.sendIt(trackObj);
                                productsListObserver.unobserve(productEl);
                            }
                        });
                    });
                    if(listEl) {
                        productsListObserver.observe(listEl);
                    }
                }
            } else {
                analytics.sendIt(trackObj);
            }
        }
    },

    trackProductPreviewDetails: (eventName, product) => {
        const itemName = product.Title || product.ProductName || '';
        const trackObj = {
            event: eventName,
            item_name: analytics.getFormatedItemName(itemName),
            item_id: '',
            item_category:  product.DataLayer ? product.DataLayer.productCategory : product.DisciplineName ? product.DisciplineName : '',
            item_category2: product.DataLayer ? product.DataLayer.areaOfConcern : '',
            };
        analytics.sendIt(trackObj);
    },

    trackTabs: () => {
        [...document.body.querySelectorAll(`.tab-switcher-control`)].map(
            (tabItem) => {
                [...tabItem.querySelectorAll('.tab-button')].map(
                    (btnItem) => {
                            btnItem.addEventListener('click', () => {
                            const trackingObj = {
                                event:"tab_interaction",
                                item_selected: btnItem.innerText,
                            };
                            analytics.sendIt(trackingObj);
                        });
                    });
                });
        },
    
    trackDropdowns: () => {
        [...document.body.querySelectorAll(`select.select-dropdown`)].map(
            (item, index) => {
            const dropdownCategory = item.name ? item.name : item.title ? item.title : '';
            const articleSection = item.closest('article');
            const hasHeroBannerClass = articleSection.classList.contains(
                'heroBanner--select'
            );
            const formcls = articleSection.classList.contains(
                'form'
            );
            if(!hasHeroBannerClass && !formcls) {
                item.addEventListener('change', () => {
                    const trackingObj = {
                        event:"dropdown",
                        value: item.options[item.selectedIndex].text,
                        dropdown_category: dropdownCategory,
                    };
                    analytics.sendIt(trackingObj);
                });
            }
        });
    },

    trackCTAButtons: () => {
        [...document.body.querySelectorAll(`.tracking-cta-button`)].map((item, index) => {
            item.addEventListener('click', () => {
                let trackingObj=null;
                if(item.attributes.ctalink && (item.attributes.ctalink.value.toLowerCase().includes('stylelabs') || item.attributes.ctalink.value.toLowerCase().includes('shareable assets'))) {
                    trackingObj = {
                        event:"downloads",
                        file_name:"",
                        file_url: item.attributes.ctalink.value,
                        file_type:"",
                    };
                } else {
                    const origin = window.location.origin;
                    trackingObj = {
                        event:"clicks",
                        click_element_type:"link",
                        click_text: item.attributes.ctaText.value,
                        link_url:item.attributes.ctalink.value,
                    };
                    if(!(trackingObj.link_url.includes(origin))) {
                        trackingObj = {...trackingObj, link_url: `${origin}${trackingObj.link_url}`};
                    }
                }
                analytics.sendIt(trackingObj);
            });
        });
    },
    trackAddToCalander: () => {
        const addToCalanderButton=document.body.querySelector(`#AddToCalander`);
        if(addToCalanderButton){
            addToCalanderButton.addEventListener('click', () => {
                let Difference_In_Days = null;
                if(addToCalanderButton.dataset.eventDate)
                {
                    const eventDate = new Date(addToCalanderButton.dataset.eventDate); 
                    const today = new Date(); 
                    const Difference_In_Time = eventDate.getTime() - today.getTime() ; 
                    Difference_In_Days = Difference_In_Time / (1000 * 3600 * 24);
                }
                
                let trackingObj = {
                    event: "add_to_calendar",
                    calendar_event_name: addToCalanderButton.dataset.eventName || "",
                    days_to_event: Difference_In_Days != null ? Difference_In_Days.toFixed() : "",
                    link_url: addToCalanderButton.dataset.downloadUrl || "",
                };
                analytics.sendIt(trackingObj);
            });
        }
    },

    trakForm: () => {
        var eventAction;
        [...document.querySelectorAll('.gaFormTracking')].map((btn) => {
            btn.addEventListener('click', () => {
                eventAction = false;
                const formSection = btn.closest('article.form');
                setTimeout(() => {
                    const hasValidationErrorClass = [...formSection.querySelectorAll('.field-validation-error')];
                    const hasValidationSummaryClass = [...formSection.querySelectorAll('.validation-summary-errors')];
                    const formName = formSection.querySelector('h1');
                    const professioncyEl = formSection.querySelector('[data-sc-field-name="I am a"]');
                    const enquiryEl = formSection.querySelector('[data-sc-field-name="I am enquiring about"]');
                    const intrestEl = formSection.querySelector('[data-sc-field-name="Franchise"]');
                    let enquiryArea = '';
                    if (enquiryEl && enquiryEl.value) {
                        enquiryArea = enquiryEl.value;
                    }
                    else if (intrestEl && intrestEl.value) {
                        enquiryArea = intrestEl.value;
                    }
                    if (hasValidationErrorClass && !hasValidationErrorClass.length) {
                        if (hasValidationSummaryClass && hasValidationSummaryClass.length) {
                            const trackingObj = {
                                event: "enquiry_error",
                                form_name: formName ? formName.innerText : '',
                                error_message: hasValidationSummaryClass[0].innerText || '',
                            };
                            analytics.sendIt(trackingObj);
                        }
                        else {
                            const trackingObj = {
                                event: "submit_enquiry",
                                form_name: formName ? formName.innerText : '',
                                enquiry_person: professioncyEl ? professioncyEl.value : '',
                                enquiry_area: enquiryArea,
                            };
                            analytics.sendIt(trackingObj);
                        }
                    }
    
                }, 100);
    
            });
        });
    
        var formSelector = 'form';
        let formName = '';
        window.addEventListener('beforeunload', function (e) {
            if (eventAction) {
                const trackingObj = {
                    event: "enquiry_abandonment",
                    form_name: formName ? formName.innerText : '',
                    enquiry_abandonment: 1,
                };
                analytics.sendIt(trackingObj);
            }
        });
        const formSelectorEl = document.querySelector(formSelector);
        if (formSelectorEl) {
            document.querySelector(formSelector).addEventListener('change', function (e) {
                const formSection = e.target.closest('article.form');
                if(formSection){
                    formName = formSection.querySelector('h1');
                    eventAction = 'True';
                }
            });
        }
    },

    handleTrackers: (item,trackingJSON) => {
        switch (trackingJSON['type']) {
            // custom analytics handlers
            case 'hero-select-double':
                analytics.handleHeroSelectDouble(item, trackingJSON);
                break;
            
            case 'hero-select-single':
                analytics.handleHeroSelectSingle(item, trackingJSON);
                break;

            case 'dropdown-filter':
                analytics.handleSingleDropdownFilter(item, trackingJSON);
                break;

            case 'contact':
                analytics.handleContact(item, trackingJSON);
                break;

            case 'tabClick':
                 analytics.handleTabClick(item, trackingJSON);
                 break;    

            case 'search-coveo':
                analytics.handleCoveoSearch(item, trackingJSON);
                break;

            case 'search':
                analytics.handleSearch(item, trackingJSON);
                break;

            default: 
                analytics.handleGenericClick(item, trackingJSON); //cta-button, linkClick, navigationClick,  viewClicks, all values from attributes only  etc
                break;
        }
    },
    examineSpecific: (element) => {
        //console.log("element to examine", element);

        [...element.querySelectorAll(`[${analytics.props.tracking}]`)].map(  
            (item, index) => {
                //console.log("item", item);
                const attr = item.getAttribute(analytics.props.tracking);
                var trackingJSON = JSON.parse(attr);
                analytics.handleTrackers(item,trackingJSON);
            }
        );
        observer.disconnect();
        analytics.observeDom = false;
    },

    handleSearch: (item, trackingJSON) => {
        let query, button;
        query = item.querySelector('.search-input');
        button = item.querySelector('.search-button');
        button.addEventListener("click", () => {

            var tracking = {};

            Object.keys(trackingJSON).forEach(key => { 
                tracking[`${[key]}`] = `${json[key]}`;
            });

            tracking.searchInput = query.value;
            analytics.sendIt(tracking);
            
        });
    },
    handleTabClick: (item, trackingJSON) => {
        let target, eventType;
        
        if (mediaQueries.getValue().breakpoint === 'desktop') {
            target = item.querySelector('.tab-switcher-control__inner');
            eventType = 'click';
        } else {
            target = item.querySelector('.select-dropdown');
            eventType = 'change';
        }
        if(target) {
            target.addEventListener(eventType, (e) => {  
                //console.log("tab click", e.target.value);
                analytics.sendIt(analytics.generateNuGenericDataLayer(trackingJSON));
                if (!analytics.observeDom) {
                    //console.log("added mutations listener for tabswitcher");
                    analytics.domUpdate(item);
                }
            });
        }
        
    },
    handleContact: (item, trackingJSON) => {
        //console.log("handleContact",trackingJSON, item);

        let clickArea = item.querySelector('.contact-options');
        let areaOfContact = trackingJSON['areaOfContact'];

        // if no areaOfContact is set try and use nearest h2 (e.g in context of using dynamic contact cards)
        if (areaOfContact == null) {
            if (item.querySelector('h2')) {
                areaOfContact = item.closest('h2').innerText;
            } else if (item.querySelector('h4')) {
                areaOfContact = item.closest('h4').innerText;
            } else {
                areaOfContact = 'Contact Module';
            }
        }
        clickArea.addEventListener('click', (e) => {
            let contactType = e.target.getAttribute('data-attribute-ctatype');
            let tracking = {"event": "contact", "clickType":"contact", "contactType": contactType, "areaOfContact": areaOfContact};
            analytics.sendIt(tracking);
        });
    },
    handleCoveoSearch: (item, trackingJSON) => {

        let searchField = item.querySelector('.CoveoSearchbox input[type="text"]');
        searchField.addEventListener('focus', (e) => {
            if(searchField === document.activeElement) {
                analytics.sendIt({"event": 'focusSearch',"searchLocation": "searchInSearch"});
            }
        });
        searchField.addEventListener('keydown', analytics.analyticsKeyPressed);

        let searchBTN = item.querySelector('.CoveoSearchbox .CoveoSearchButton');
        searchBTN.addEventListener('click', (e) => {
            analytics.sendIt({"event": "search_submitted","search_term": searchField.value});
        });
		
		searchBTN.addEventListener('keypress', (e) => {
			if (e.key === 'Enter') {
				analytics.sendIt({"event": "search_submitted","search_term": searchField.value});
			}
        });
    },
    handleHeroSelectDouble: (item, json) => {
        var submitBTN = item.querySelector('[data-button-action="submit"]');

        submitBTN.addEventListener('click', (e) => {
            // get values
            var select = item.querySelectorAll('.select-dropdown');
            var dropdownOne = (select[0].options[select[0].selectedIndex].text);
            var dropdownTwo = (select[1].options[select[1].selectedIndex].text);

            var tracking = {};

            Object.keys(json).forEach(key => { 
                tracking[`${[key]}`] = `${json[key]}`;
            });

            tracking.discipline = dropdownOne;
            tracking.areaOfConcern = dropdownTwo;

            analytics.sendIt(tracking);

            analytics.checkForCardstack();
        });

    },
    handleHeroSelectSingle: (item, json) => {

        var submitBTN = item.querySelector('[data-button-action="submit"]');

        submitBTN.addEventListener('click', (e) => {

            var select = item.querySelectorAll('.select-dropdown');
            var dropdownOne = (select[0].options[select[0].selectedIndex].text);
            var tracking = {};

            Object.keys(json).forEach(key => { 
                tracking[`${[key]}`] = `${json[key]}`;
            });

			if (tracking["areaOfConcern"])
			{
				tracking["areaOfConcern"] = dropdownOne;
			}
			else {
				tracking.discipline = dropdownOne;
			}

            
            analytics.sendIt(tracking);

            analytics.checkForCardstack();

        });
    },
    handleSingleDropdownFilter: (item, json) => {
        //console.log("handleSingleDropdownFilter",json, item);
        var selectDD = item.querySelector('select.select-dropdown');

        selectDD.addEventListener('change', (e) => {
            // listens for results change
            if (!analytics.observeDom) {
                analytics.domUpdate(item);
            }
            var dropdownOne = (selectDD.options[selectDD.selectedIndex].text);
            var tracking = {};

            Object.keys(json).forEach(key => { 
                tracking[`${[key]}`] = `${json[key]}`;
            });

            tracking.dropDownSelection = dropdownOne;
            analytics.sendIt(tracking);
        });
    },
    handleGenericClick:(item, json)=> {
        item.addEventListener('click', (e) => {  
            const origin = window.location.origin;
            let trackObj = {};
            if(json && json.link_url && (json.link_url.toLowerCase().includes('stylelabs') || json.link_url.toLowerCase().includes('shareable assets'))) {
                trackObj = {event:'downloads', file_url: `${json.link_url}`, file_name: "", file_type: ""};  
            } else {
                if(json && json.link_url && !(json.link_url.includes(origin))) {
                 trackObj = {...json, link_url: `${origin}${json.link_url}`,event:'clicks'};
                }
            }
            if(Object.keys(trackObj).length === 0) {
                trackObj = json;
            }
            analytics.sendIt(analytics.generateNuGenericDataLayer(trackObj));
        });
    },
    generateNuGenericDataLayer: (json) => {
        var tracking = {};
        Object.keys(json).forEach(key => { 
            tracking[`${[key]}`] = `${json[key]}`;
        });

        return tracking;
    },
    sendIt: (tracking) => {
        delete tracking.type;
        if (window.dataLayer) {
        window.dataLayer.push(tracking);
        } else {
            console.log("please configure window.dataLayer");
        }
    },
    analyticsKeyPressed: (e) => {
        analytics.sendIt({event: 'searchTyping',searchLocation: 'searchInSearch'});
        //searchField.removeEventListener('keydown', analytics.analyticsKeyPressed);
    },
    checkForCardstack: () => {
        let cardstackparent = document.querySelector('[data-component="use: RenderListFromApiOnEvent"]');
        if (cardstackparent) {
            //console.log("found api results parent");

            let cardstack = cardstackparent.querySelector('.cardstack');
            if (!cardstack) {
                 cardstack = cardstackparent.querySelector('.renderset');
            } else {
                analytics.checkForPagination(cardstackparent,cardstack);
            }
            analytics.elementUpdate(cardstack);

             
        }
    },
    checkForPagination: (cardstackparent,cardstack) => {
        let pagination = cardstackparent.querySelector('.paginator');
        if (pagination) {
            pagination.addEventListener('click', (e) => {   
                analytics.elementUpdate(cardstack);
            });
        }   
    },
    domUpdate: (elementToExamine) => {
            observer = new MutationObserver(function(mutations) {
                if (mutations[0].type === 'childList' && mutations[0].addedNodes.length > 0) {
                    analytics.examineSpecific(elementToExamine);
                }
            });

            observer.observe(document.body, {childList: true, subtree: true});
            analytics.observeDom = true;

    },
    elementUpdate: (elementToWatch) => {
            observer = new MutationObserver(function(mutations) {
                if (!!mutations) {
                    //console.log("mutations", mutations);
                    analytics.examineSpecific(elementToWatch, );
                }
            });

            observer.observe(elementToWatch, {childList: true, subtree: true});
            analytics.observeDom = true;

    },
};


// legacy may need to reintroduce

    // handleCTAButton:(item, json)=> {
    //     //console.log("analytics",json, item);
    //     item.addEventListener('click', (e) => {
    //        //e.preventDefault(); //uncomment for rtesting
    //         var tracking = analytics.generateGenericDataLayer(json);
    //         analytics.sendIt(tracking);
    //     });
    // },
    // generic stuff
    // generateGenericDataLayer: (json) => {
    //     var tracking = {};
    //     for (var i = 1; i < Object.keys(json).length; i++) { // skip first key as that is the 'type'
    //         tracking[`${analytics.parseAttributes(json[`key${i}`])['key']}`] = `${analytics.parseAttributes(json[`key${i}`])['value']}`;
    //     }

    //     return tracking;
    // },
    // generic stuff
    // parseAttributes: (item) => {
    //     var fields = item.split("=");
    //     var key = fields[0], value = fields[1];
    //     return {key,value};
    // },

/* Shared 'script'
developed for treatments list components (SMIT-691 & 700) but resuse/fork/extend for similar use cases
on receipt of HERO_DROPDOWN_SELECTION event payload calls api with args defined on parent alchemy block
renders cards via alchemy-templates and populates cardstack element (defined on parent html)
optionally populates a text-block and updates an optional paginator behaviour
handles some light spinner duties via network listener
* */

const RenderListFromApiOnEvent = (props) => {
  const els = {
    el: null,
    textBlock: null,
    cardstack: null,
    renderset: null,
    paginator: null,
    spinner: null,

    featuredProduct: null,
    contactHead: null,
  };

  /*
  TODO
  add flag to container dom elements to scoop them up and dependencies can
  figure out what to do with them, instead of doing it here and relying on state and els
   */
  const state = {
    uid: null,
    currentApiId: null,
    hasTextBlock: false,
    hasCardstack: false, // *** for flat type
    hasRenderset: false, // *** for chunked type
    hasPaginator: false,
    hasTabSwitcher: false,
    hasSelect: false,
    hasFeaturedProduct: false,
    hasContactHead: false,

    localHeroSelectBoxUid: null, // *** if a local hero-select-box-extender is present in this scope, only listen to its events
    areaOfInterestId: null,
  };

  const subProps = {
    paginator: null, // *** paginator not ready on init, add later
    parser: null,
    renderer: null,
  };

  const init = () => {
    props.name = `${props.name}__${props.utils.uid()}`; // *** create unique name
    //console.log('/RenderListFromApiOnEvent/ -init', props, props.name);

    els.el = props.el;
    revealUI(false);

    els.textBlock = els.el.querySelector('.text-block');
    els.cardstack = els.el.querySelector('.cardstack');
    els.renderset = els.el.querySelector('.renderset');
    els.paginator = props.el.querySelector('.paginator');
    els.tabSwitcher = props.el.querySelector('.tab-switcher-control');
    els.select = props.el.querySelector('.select-control');
    els.featuredProduct = props.el.querySelector('.dynamic-featured-product');
    els.contactHead = props.el.querySelector('.dynamic-contact-head');

    els.localHeroSelectBox = props.el.querySelector(
      `[data-behaviour="hero-select-box-extender"]`
    );

    // TODO refactor out these things
    state.hasTextBlock = !!els.textBlock;
    state.hasCardstack = !!els.cardstack;
    state.hasRenderset = !!els.renderset;
    state.hasPaginator = !!els.paginator;
    state.hasTabSwitcher = !!els.tabSwitcher;
    state.hasSelect = !!els.select;
    state.hasFeaturedProduct = !!els.featuredProduct;
    state.hasContactHead = !!els.contactHead;
    state.uid = els.el.dataset.uid;

    els.spinner = document.createElement('div');
    els.spinner.classList.add('spinner');
    els.spinner.classList.add('ellipsis-spinner');

    els.el.insertBefore(els.spinner, els.cardstack);

    if (els.localHeroSelectBox) {
      state.localHeroSelectBoxUid =
        els.localHeroSelectBox.getAttribute('data-behaviour-uid');
    }

		if(props.el.getAttribute("data-is-pitpage"))
		{
		
			const treatmentId =  document.getElementById('Item_TreatmentId');
			document.getElementById('Item_SpecialityName');
			document.getElementById('Item_SpecialityDescription');
			
			if (typeof(treatmentId) != 'undefined' && treatmentId != null)
			{
			  getData({ id: treatmentId.value, pageNumber: 1, scroll: false });
        revealUI(true);
			}
		}
    
    // *** from hero dropdown
    props.events.on(
      props.config.eventNames.HERO_DROPDOWN_SELECTION,
      (payload) => {
        // lets bail out here if not from this specific component,
        // else it will fire twice with catastrophic consequences
        if (payload.parentId && payload.parentId !== state.uid) return;

        if (state.localHeroSelectBoxUid) {
          // *** if a local heroselectbox is in use, ensure it is in the correct scope
          if (payload.caller !== state.localHeroSelectBoxUid) {
            return;
          }
        }
        const scroll = payload.scroll ? payload.scroll : false;
        state.currentApiId = payload.selection.id;
        state.areaOfInterestId = payload.selection.areaOfInterestId;
        getData({ id: state.currentApiId, pageNumber: 1, scroll, areaOfInterestId: state.areaOfInterestId });
        updateTextBlock(payload.selection);
        revealUI(true);
      }
    );

    // *** from patient specialty dropdown
    props.events.on(
      props.config.eventNames.PATIENT_SPECIALTY_DROPDOWN_SELECTION,
      (payload) => {
        // lets bail out here if not from this specific component,
        // else it will fire twice with catastrophic consequences
        if (payload.parentId && payload.parentId !== state.uid) return;

        if (state.localHeroSelectBoxUid) {
          // *** if a local heroselectbox is in use, ensure it is in the correct scope
          if (payload.caller !== state.localHeroSelectBoxUid) {
            return;
          }
        }
        const scroll = payload.scroll ? payload.scroll : false;
        updateList(payload.filteredProductsList, props.attributes.template, { scroll });
        updateTextBlock(payload.selection);
        revealUI(true);
      }
    );

    // from paginator
    props.events.on('BEHAVIOUR_BINDING_SIGNAL', (payload) => {
      // make sure we are only checking the correct behaviour signal
      if (payload.type !== props.config.eventNames.PAGINATOR_INTENTION) return;
      if (
        !state.hasPaginator ||
        payload.sender !== subProps.paginator.getOptions().config.uid
      )
        return;
      //console.log('/RenderListFromApiOnEvent/ -PAGINATOR?');
      getData({
        id: state.currentApiId,
        pageNumber: payload.data.pageNumber,
        scroll: true,
        areaOfInterestId: state.areaOfInterestId
      });
    });

    // *** for spinner
    props.events.on(props.config.eventNames.APP_NETWORK_ACTIVITY, (payload) => {
      if (payload.caller !== props.name) return;
      switch (payload.state) {
        case 'start':
          els.spinner.classList.add('spinner--is-active');
          break;
        case 'end':
          els.spinner.classList.remove('spinner--is-active');
          break;
      }
    });

    // *** when 'autoFetchDefaultValue' present fire fetch immediately
    // @ja Put scroll:false here to prevent scrolling to UI element
    if (props.attributes.autoFetchDefaultValue) {
      getData({
        id: props.attributes.autoFetchDefaultValue,
        scroll: false,
      });
      state.currentApiId = props.attributes.autoFetchDefaultValue; // *** fixes SMIT-3087?
      revealUI(true);
    }
  };

  // *** TODO add class instead of using style
  const revealUI = (bool) => {
    if (bool) {
      els.el.style.display = 'block';
    } else {
      els.el.style.display = 'none';
    }
  };

  // *** called on event change
  const getData = (options) => {
    //console.log('/RenderListFromApiOnEvent/ -getData', options.id);
    const scroll = options.scroll ? options.scroll : false;
    const fetchOptions = {
      datasource: `${props.attributes.datasource}`,
      caller: props.name,
      // *** optionally add -> method: 'post' etc
    };

    const request = {
      Key: props.attributes.key,
      Values: options.id || [],
      Paginate: props.attributes.paginate,
      PageSize: props.attributes.pageSize,
      PageNumber: options.pageNumber,
      BypassCache: props.attributes.bypassCache || false,
    };

    // *** add persistent parameter to be delivered on every request
    if (props.attributes.appendPersistentParam) {
      //console.log('/RenderListFromApiOnEvent/ -getData --appendPersistentParam:', props.attributes.appendPersistentParam);
      if (request.Values.includes('{')) {
        // *** looks like json... maybe need to be a little more thorough

        const valuesAsJson = JSON.parse(request.Values); // *** convert to actual object
        const newData = props.utils.attributeParser(
          props.attributes.appendPersistentParam
        ); // *** expecting a key/value
        const key = Object.keys(newData)[0]; // *** only first key - extend as necessary

        if (key) {
          let value = newData[key];
          value = value.replace(/'/g, ''); // *** cleanup
          valuesAsJson[key] = value;
          request.Values = JSON.stringify(valuesAsJson); // *** back to string
        }
        //
      } else {
        request.Values = `${request.Values} & ${props.attributes.appendPersistentParam}`;
      }
    }

    // *** base request for new API schema, update 'PageNumber' dynamically
    let requestParams = props.requestModel.format(request);
    // for AWM we are adding area of interest tag
    if(options.areaOfInterestId) {
      requestParams = {...requestParams, AreaOfIntrestIds: options.areaOfInterestId.replace(/[}{]/g, '')};
    }
    // for news we are adding newsStoryTypeTags
    if(props.attributes.NewsStoryTypeTags) {
      requestParams = {...requestParams, NewsStoryTypeTags: props.attributes.NewsStoryTypeTags.replace(/[}{]/g, '')};
    }
    // *** make api call, and generate content via frontend/alchemy-templates.js
    const fetchData = async () => {
      const data = await props.fetchData(fetchOptions, requestParams);
      if (!data.payload.Result) data.payload.Result = [];
      if(data.payload.Result && props.attributes.datasource == '/en/api/products/products') {
        const listOfProducts = {
          Products: data.payload.Result
        };
        // isTrackProductList is required to inform to analytics component to store api result data for future tracking like button click.
        const isTrackProductList = true;
        analytics.trackViewListProducts('view_item_list', listOfProducts, isTrackProductList);
      }
      updateList(data.payload.Result, props.attributes.template, { scroll });
      updatePaginator(data.payload);
    };
    if (props.attributes.datasource) {
      fetchData();
    }
  };

  const updatePaginator = (data) => {
    if (!state.hasPaginator) return;
    if (!subProps.paginator) {
      subProps.paginator = props.getBehaviour(els.paginator);
    }
    subProps.paginator.update(data, state.currentApiId);
  };

  const updateTextBlock = (data) => {
    // console.log('/index/ -updateTextBlock', data);
    if (!state.hasTextBlock) return;
    const time = props.config.timings.animation.short();
    // *** fade out and update content
    props.TweenMax.to(els.textBlock, time, {
      opacity: 0,
      onComplete: () => {
        els.textBlock.innerHTML = `
          <h2>${data.title || ''}</h2>
          <div class="paragraphs">
            <p>${data.description || ''}</p>
          </div>
      `;
      },
    });

    // *** fade back in
    props.TweenMax.to(els.textBlock, time, {
      opacity: 1,
      delay: time,
      onStart: () => {
        // TODO: COnsider using scrolling functionality from renderflat if any stickynav issues crop up
        props.el.scrollIntoView();
      },
    });
  };

  const updateList = (data, template, scroll) => {
    const shouldScroll = scroll.scroll ? scroll.scroll : false;
    // console.log('/index/ -update', data);
    const parserConfig = props.utils.attributeParser(
      props.el.getAttribute('data-parser')
    );

    if (!subProps.parser) {
      subProps.parser = props.dataParsers.examine(parserConfig);
      // console.log('/RenderListFromApiOnEvent2/ -updateList PARSER', subProps.parser);
    }

    // const parser = props.dataParsers.examine(parserConfig);
    const parsedData = subProps.parser.parse(parserConfig, data);
    // console.log('/RenderListFromApiOnEvent/ -updateList PARSED', data);

    const jsonMap = props.utils.attributeParser(
      props.el.getAttribute('data-json-mapping')
    );
    
    if(els && els.featuredProduct) {
      const ctaButtonLabel = els.featuredProduct.getAttribute('data-cta-button-label');
      if(ctaButtonLabel) {
        jsonMap.CTAButtonLabel = `%${ctaButtonLabel}`;
      }
    }
    
    let rendererOptions = {
      props,
      jsonMap,
      parsedData,
      template,
      els,
      state,
      scroll: shouldScroll,
    };

    if(parserConfig && parserConfig.key1 === "PatientFilter") {
      rendererOptions = {...rendererOptions, isPatientFilter: true};
    }

    // *** fork here - attempt to separate behaviour ==============================
    if (parsedData.topology === 'flat') {
      if (!subProps.renderer) {
        subProps.renderer = renderFlat;
      }
    }

    if (parsedData.topology === 'chunked') {
      if (!subProps.renderer) {
        subProps.renderer = renderChunked;
      }
    }

    subProps.renderer.update(rendererOptions);

    // ...others

    // now broadcast that list is updated, can use if inside tabstack for eg to call setHeight()
    props.events.emit(props.config.eventNames.APILIST_DOM_UPDATED, {
      el: props.el,
    });
  };

  return {
    init,
  };
};

const FormController = (props) => {
  const classes = {
    hidden: 'hidden',
    success: 'form--success',
    problem: 'form--problem',
  };
  const els = {
    el: null,
    formElements: null,
    formSteps: null,
    formErrorDiv: null,
    submitButton: null,
    spinner: null,
    submitShowElement: null,
    submitHideElement: null,
  };

  const state = {
    uid: null,
    datasource: null,
    formType: null,
    formData: null,
    formSubmitted: false,
    submitFollowLink: null,
    submitNonHCPFollowLink: null,
    hasUploadedFile: false, // is there a file uploaded with this form
    isFormWizard: false, // if form has steps that are shown/hidden as you move through them
    isFormPartial: false, // if this form is sitting in a popup outside of the main form
    formPartialData: null, // means of passing data between relatde form fragments
    targetPartial: null, // for above, to filter the target this data is meant for
    hasRecaptcha: false,
    recaptchaSiteKey: null,
    requiredGroups: null, // groups (eg checkboxes) where you need to pick 1 or more items
    isRequiredGroupsValid: false, // for above, a global flag
    isAjaxForm: true, // send JSON payload with Ajax (default) or just POST form normally if false
    trackingObject: {}, // send payload object for page amalytics
    errors: {
      default: '',
    },
    yearYYYYFormatRegex: /^\d{4}$/
  };

  const init = () => {
    els.el = props.el;
    state.uid = els.el.dataset.formUid;
    state.formType = els.el.dataset.formType;
    state.formData = new Map();
    // Submit button
    els.submitButton = els.el.querySelector('button[type=submit]');
    // Get all our form elements
    els.formElements = [...els.el.querySelectorAll('.form__element')];
    // do we need a step-through wizard-like form ?
    // check to see if any form steps have been added
    els.formSteps = [...els.el.querySelectorAll('.form__step')];
    if (els.formSteps && els.formSteps.length > 1) {
      state.isFormWizard = true;
      if (state.isFormWizard) changeFormStep(0);
    }
    // loop thru and attach useScripts to all of the items
    els.formElements.map((item) => {
      if (item.classList.contains('form__element--split')) {
        const fields = [...item.querySelectorAll('input, select, textarea')];
        if (fields) {
          fields.map((el, index) => {
            el.dataset.component = 'use: FormElement';
            // el.dataset.element = el.tagName;
            el.dataset.elementId = el.tagName + index;
          });
        }
      } else {
        item.dataset.component = 'use: FormElement';
      }
    });
    // add any hidden elements for contact forms
    // this is for sitecore formbuilder submit...
    if (state.formType === 'contact') {
      const hiddenItems = [...els.el.querySelectorAll('input[type=hidden]')];
      // loop thru and attach useScripts to all of the items
      hiddenItems.map((item) => {
        item.dataset.component = 'use: FormElement';
      });
    }
    els.formErrorDiv = els.el.querySelector('.form__problem');
    if (els.formErrorDiv) {
      state.errors.default = els.formErrorDiv.innerHTML;
    }

    // Now inform the Mothership that it needs to attach scripts to our new Elements
    // We dispatch a custom window Event to the main app for this..
    const updateEvent = new CustomEvent('refreshScripts', {
      detail: { node: els.el },
    });
    window.dispatchEvent(updateEvent);

    /* Loading spinner */
    els.spinner = document.createElement('div');
    els.spinner.classList.add('wrap-spinner');
    const spinner = document.createElement('div');
    spinner.classList.add('spinner');
    spinner.classList.add('ellipsis-spinner');
    els.spinner.appendChild(spinner);
    els.el.appendChild(els.spinner);
    // apply datasource if exists
    if (props.attributes && props.attributes.datasource) {
      state.datasource = props.attributes.datasource;
    } else {
      // if we have NO datasource then it is not an ajax form, we will submit form manually
      state.isAjaxForm = false;
    }
    // is this a form partial, ie popup
    if (props.attributes && props.attributes.partial) {
      state.isFormPartial = true;
      // listen for any comms broadcasted between form fragments
      listenForPartialSuccess();
    }
    // is this a form partial, ie popup AND has a target popup to go to next
    // typically it will need to pass data to that target
    if (props.attributes && props.attributes.targetPartial) {
      state.targetPartial = props.attributes.targetPartial;
    }
    // do we need a recaptcha for this form ??
    if (props.attributes && props.attributes.recaptcha === 'True') {
      initGoogleRecaptcha();
    }
    // Do we have any checkbox groups etc where we require at least 1 item checked
    if (els.el.dataset.requiredGroups) {
      state.requiredGroups = [];
      const items = els.el.dataset.requiredGroups.split(',');
      items.map((i) => {
        state.requiredGroups.push(i);
      });
    }

    // set what to do when form is submitted
    if (props.attributes && props.attributes.actionLink)
      state.submitFollowLink = props.attributes.actionLink;
    if (props.attributes && props.attributes.nonhcpactionLink)
      state.submitNonHCPFollowLink = props.attributes.nonhcpactionLink;
    if (props.attributes && props.attributes.actionShow)
      els.submitShowElement = els.el.querySelector(
        `#${props.attributes.actionShow}`
      );
    if (props.attributes && props.attributes.actionHide)
      els.submitHideElement = els.el.querySelector(
        `#${props.attributes.actionHide}`
      );

    // User clicked the submit button
    els.submitButton.addEventListener('click', (e) => {
      e.preventDefault();
      // if we are stepping through a form, move to the next group of fields...
      // or else if not form wizard, just submit the form
      if (state.isFormWizard) {
        changeFormStep();
      } else {
        submitForm();
      }
    });
    listenEvents();
    alignColumns();
  };

  const alignColumns = () => {
    if (state.formType === 'profile') return;
    const groups = els.el.querySelectorAll('.form__group');
    [...groups].map((item) => {
      // if its a checkbox grouping, bail out..
      if (item.classList.contains('form__group--checkbox') || item.classList.contains('form__group--organisation-fields')) return;
      const elementLabels = item.querySelectorAll('.form__element label');
      let height = 0;
      [...elementLabels].map((elemLabel) => {
        if (elemLabel.clientHeight > height) height = elemLabel.clientHeight;
      });
      [...elementLabels].map((elemLabel) => {
        elemLabel.style.height = height + 'px';
      });
    });
  };

  const initGoogleRecaptcha = () => {
    const environment = props.utils.environment();
    state.hasRecaptcha = true;
    state.recaptchaSiteKey =
      environment === 'production'
        ? props.config.recaptchaLiveSiteKey
        : props.config.recaptchaDevSiteKey;
    // attach google recaptcha script to head of page if not already there..
    addAPIScript(
      `https://www.google.com/recaptcha/api.js?render=${state.recaptchaSiteKey}`
    );
    // if this is a manually posted form we need to add the recaptcha field input
    if (!state.isAjaxForm) {
      const tokenField = document.createElement('input');
      tokenField.setAttribute('type', 'hidden');
      tokenField.setAttribute('name', 'token');
      tokenField.setAttribute('id', 'token');
      els.el.appendChild(tokenField);
    }
  };

  const addAPIScript = (script) => {
    // first check this isnt already in the page
    let alreadyPresent = false;
    const scriptUrl = script;
    const scripts = document.getElementsByTagName('script');
    for (let i = scripts.length; i--; ) {
      if (scripts[i].src == scriptUrl) alreadyPresent = true;
    }
    // if not, append to head..
    if (!alreadyPresent) {
      const tag = document.createElement('script');
      tag.src = scriptUrl;
      const firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    }
  };

  const changeFormStep = (num) => {
    if (!state.isFormWizard) return;
    const isFormStepValid = checkFormStepValidity();
    // if we are valid, proceed..
    if (isFormStepValid) {
      const index = typeof num !== 'undefined' ? num : state.formWizardStep + 1;
      // this just checks if we are on the last step or not
      const canProceed = checkFormStep(index);
      if (canProceed) {
        // If we are a form partial here, we need to tell the partial script that the step is ready to be posted
        // We will make the call from the partial script, and when complete we can call the below function to reveal
        // next step ...
        revealFormStep(index);
      } else {
        props.events.emit(props.config.eventNames.FORM_STEPS_COMPLETED, {
          uid: state.uid,
        });
        // TODO:: maybe call submitForm() for non-partial forms, ie not change-email form ??
      }
    } else {
      console.log('STEP NOT VALID');
    }
  };

  const revealFormStep = (index) => {
    state.formWizardStep = index;
    // hide all
    els.formSteps.map((item) => {
      item.classList.add(classes.hidden);
    });
    // show relevant step
    els.formSteps[state.formWizardStep].classList.remove(classes.hidden);
  };

  const checkFormStep = (num) => {
    return els.formSteps[num] ? true : false;
  };

  const submitForm = () => {
    const isFormValid = checkFormValidity();
    // const isFormValid = true;
    // tell the elements..
    props.events.emit(props.config.eventNames.FORM_SUBMIT, {
      uid: state.uid,
    });
    // if we are valid, proceed..
    if (isFormValid) {
      if (state.hasRecaptcha) {
        // we wrap all this in a recaptcha action
        grecaptcha.ready(function () {
          grecaptcha
            .execute(state.recaptchaSiteKey, {
              action: 'submit',
            })
            .then(function (token) {
              resetClassesAndErrors();
              submitActions(token);
            });
        });
      } else {
        resetClassesAndErrors();
        submitActions();
      }
    } else {
      // switch this on now if needed
      if (state.requiredGroups && !state.isRequiredGroupsValid) {
        const nameOfField = state.requiredGroups[0];
        toggleGroupRequiredError('on', nameOfField);
      }
      // scroll to topmost error (unless we are a popup)
      if (!state.isFormPartial) scrollToTopError();
    }
  };

  const resetClassesAndErrors = () => {
    // reset the classes first
    els.el.classList.remove(classes.success);
    els.el.classList.remove(classes.problem);
    // first set the default error msg
    if (els.formErrorDiv) {
      els.formErrorDiv.innerHTML = state.errors.default;
    }
  };

  const submitActions = (token = null) => {
    // is this ajax form post or normal form submit...
    if (state.isAjaxForm) {
      // is an ajax form, so build the payload..
      // remembering to send through the recaptcha token
      buildFormObject(token);
    } else {
      // if this is a manually submitted form lets add the token to the form field
      if (token && state.hasRecaptcha)
        els.el.querySelector('#token').value = token;
      els.el.submit();
    }
  };

  const scrollToTopError = () => {
    const topError = document.querySelectorAll('.form__element--error')[0];
    topError.scrollIntoView({ block: 'center', behavior: 'smooth' });
  };

  const buildFormObject = (mytoken = null) => {
    const form = {};
    state.formData.forEach(function (value, key) {
      if (value.type === 'checkbox') {
        // 2 cases for checkboxes - if standalone, we need value true|false,
        // but if part of a group, we need the actual string value
        if (form[value.name] && value.checked === true) {
          form[value.name] += `|${value.value}`;
        } else {
          if (value.name !== value.value) {
            if (value.checked === true) {
              form[value.name] = value.value;
            }
          } else {
            form[value.name] = value.checked ? 'true' : 'false';
          }
        }
        // ALSO, if the item has a data-group-* attribute and is checked, we need to push that parent group value into form obj as well
        if (value.checked === true && value.groupName && value.groupValue) {
          if (form[value.groupName]) {
            form[value.groupName] += `|${value.groupValue}`;
          } else {
            form[value.groupName] = value.groupValue;
          }
        }
      } else {
        if (value.exclude) return;
        // here , for the contact form, we need to see if we have the same name then we need to create an array
        // out of the hidden input element (ie Fields.Index type=hidden fields for formbuilder)
        if (
          state.formType === 'contact' &&
          value.type === 'hidden' &&
          value.name &&
          value.name.indexOf('Fields.Index') > -1
        ) {
          if (form[value.name]) {
            form[value.name].push(value.value);
          } else {
            form[value.name] = [];
            form[value.name].push(value.value);
          }
        } else {
          // otherwise just continue and allocate key/val pair as normal...
          if (value.name) form[value.name] = value.value.trim();
        }
      }
    });
    const cleanedForm = stripDuplicatePipeValues(form);
    if (state.hasRecaptcha) cleanedForm.token = mytoken;
    // Now lets POST this data off to the API (if we have a datasource)
    if (state.datasource) getData(cleanedForm);
  };

  const stripDuplicatePipeValues = (form) => {
    for (let item in form) {
      // NB:: only change strings...
      // any objects, leave them as is,  eg file uploads..
      if (typeof form[item] === 'string') {
        const val = form[item].split('|');
        if (val.length > 0) {
          let unique = [...new Set(val)];
          form[item] = unique.join('|');
        }
      }
    }
    return form;
  };

  // *** called on event change
  const getData = async (formObject) => {
    showSpinner();
    let fetchOptions = {
      datasource: `${props.attributes.datasource}`,
      caller: props.name,
      method: 'post',
    };

    let finalFormObject = processFormObject(formObject);

    if (state.hasUploadedFile) {
      finalFormObject = makeFormData(finalFormObject);
    }

    if (state.formType === 'contact') {
      finalFormObject = makeContactFormData(finalFormObject);
    }

    if(props.attributes.callFrom === 'ahpraPopup' && finalFormObject.ahpranumber) {
      fetchOptions = {
        ...fetchOptions,
        datasource: `${fetchOptions.datasource}?registrationNumber=${finalFormObject.ahpranumber}`
      };
      finalFormObject = {};
    }
    if(props.attributes.callFrom === 'marketingemail' && finalFormObject.marketingemail) {
      fetchOptions = {
        ...fetchOptions,
        datasource: `${fetchOptions.datasource}`
      };
      
      finalFormObject = {
        EmailId : finalFormObject.marketingemail,
        PageUrl : window.location.href,
        MarketingConsentInfo : finalFormObject.marketingEmailInfo,
        Franchising: props.attributes.parentName,
        Language: props.attributes.language,
        Country: props.attributes.country
      };
      
      const today = new Date();
      let expire = new Date();
      expire.setTime(today.getTime() + 3600000 * 24 * 365 * 100);
      cookieManager.set('marketingEmail', true, expire.toGMTString());
    }  
    // need to pass profile state field as empty for NZ country  
    if(finalFormObject && finalFormObject.profile && finalFormObject.profile.country === 'NZ') {
      finalFormObject.profile.state = '';
    }

    if(finalFormObject && finalFormObject.profile && finalFormObject.profile.facilityText && finalFormObject.profile.organization === 'others' && finalFormObject.profile.facility === '') {
      finalFormObject.profile.facility = finalFormObject.profile.facilityText;
    }
    if(finalFormObject && finalFormObject.profile && finalFormObject.profile.facilityText) {
      delete finalFormObject.profile['facilityText'];
    }

    // *** make api call
    const data = await props.fetchData(fetchOptions, finalFormObject);
    // console.log(data.payload);
    // tracking analytics for registration
    if(window.location.pathname.includes('/my-account/register')) {
      doTracking(data.payload, finalFormObject.profile);
    }
    if ((data.payload && data.payload.Success && !data.payload.isAhpraRequest) || (data.payload.isAhpraRequest && data.payload.Result)) {
      // storing user information in local storage for japan to send pardot iframe
      if(window.location.pathname.includes('/ja-jp/my-account/register')) {
        let pardotData = JSON.parse(JSON.stringify(finalFormObject.profile));
        delete pardotData['token']; 
        const pardotSpecialtyFormatedString = getPardotSpecialtyList(finalFormObject.profile.specialtySpecifics);
        pardotData['pardotSpecialtyFormatedString'] = pardotSpecialtyFormatedString;
        const encodedObject = encodeURIComponent(JSON.stringify(pardotData));
        localStorage.setItem('userinfo', encodedObject);
      }
      
      formSuccessActions(data.payload);
      hideSpinner();
    } else {
      
      formErrorActions(data.payload);
      hideSpinner();
    }
  };

  const getPardotSpecialtyList = (specialtySpecifics = '') => {
    let pardotSpecialtyFormatedString = '';
    const specialtySpecificsArr = specialtySpecifics.split('|');
    const pardotSpeciltyCodesMapping = {
      audiology: 'AREA_ENT_Audiology',
      general: 'AREA_ENT_General',
      laryngology: 'AREA_ENT_Laryngology',
      rhinology: 'AREA_ENT_Rhinology', 
      jr_foot_and_ankle: 'AREA_FootAndAnkle',
      jr_hand_and_wrist: 'AREA_HandAndWrist', 
      jr_hip: 'AREA_Hip',
      jr_knee: 'AREA_Knee',
      jr_shoulder: 'AREA_Shoulder',
      acute_wounds: 'AREA_W_Acute', 
      burns: 'AREA_W_Burns',
      chronic_challenging_wounds: 'AREA_W_ChronicChallenge',
      dermatology: 'AREA_W_Dermatology', 
      plastic_surgery: 'AREA_W_PlasticSurgery',
      podiatry: 'AREA_W_Podiatry', 
      surgical_wounds: 'AREA_W_Surgical',
      sm_foot_and_ankle: 'AREA_SPM_FootAndAnkle',
      sm_hand_and_wrist: 'AREA_SPM_HandAndWrist',
      sm_hip: 'AREA_SPM_Hip', 
      sm_knee: 'AREA_SPM_Knee',
      sm_shoulder: 'AREA_SPM_Shoulder',
      abdominal: 'AREA_Trauma_Abdominal',
      lower_limb: 'AREA_Trauma_LowerLimb',
      upper_limb: 'AREA_Trauma_UpperLimb',
    };
    for(let key in pardotSpeciltyCodesMapping) {
      const isSpecialtySelected = specialtySpecificsArr.indexOf(key) !== -1 ? 'true' : 'false';
      if(pardotSpecialtyFormatedString !== '') {
        pardotSpecialtyFormatedString += `&${pardotSpeciltyCodesMapping[key]}=${isSpecialtySelected}`;
      } else {
        pardotSpecialtyFormatedString = `${pardotSpeciltyCodesMapping[key]}=${isSpecialtySelected}`;
      }
    }
    return pardotSpecialtyFormatedString;
  };

  const doTracking = (payload, profileDetails) => {
    state.trackingObject['event'] = payload.Success ? 'registration_successful' : 'registration_failed';
    state.trackingObject['marketing_consent'] = profileDetails.receiveMarketingInfo === "true" ? "yes" : "No";
    state.trackingObject['hcpProfession'] = profileDetails.profession ? profileDetails.profession : '';
    state.trackingObject['hcpSiteOfWork'] = profileDetails.siteofwork ? profileDetails.siteofwork : '';
    if(payload.Result) {
      state.trackingObject['hcpProfessionCategory'] = payload.Result.HCPProfessionCategory ? payload.Result.HCPProfessionCategory : '';
      state.trackingObject['areasOfInterest'] = payload.Result.AreaOfIntrest ? payload.Result.AreaOfIntrest : '';
    }
    if(!payload.Success) {
      state.trackingObject['error_message'] = payload.StatusMessage ? payload.StatusMessage : '';
    }
    window.dataLayer = window.dataLayer || [];
    window.dataLayer.push(state.trackingObject);
    state.trackingObject = {};
  };

  const processFormObject = (formObject) => {
    // based on form type we may need to wrap the values in an object field, eg 'profile' for register form
    // defaults to just a flat json structure
    let finalFormObject = {};
    switch (state.formType) {
      case 'registration':
      case 'profile':
        finalFormObject['profile'] = formObject;
        break;
      case 'change-email':
        finalFormObject = {
          Id: formObject.Id,
          profile: {
            SecondEmail: formObject.Email,
          },
        };
        break;
      case 'change-email-code':
        finalFormObject = {
          Id: state.formPartialData['data.Result.Id'],
          FactorId: state.formPartialData['data.Result.FactorId'],
          PassCode: formObject.PassCode,
          profile: {
            Email: state.formPartialData['data.Result.Profile.Email'],
            SecondEmail:
              state.formPartialData['data.Result.Profile.SecondEmail'],
          },
        };
        break;
      default:
        finalFormObject = formObject;
        break;
    }
    return finalFormObject;
  };

  // TODO *** THis currently only works with a single file upload field named 'upload'
  // May need to accommodate multiple files/fields in future ?
  const makeFormData = (formObject) => {
    let finalFormObject = {};
    let fileObject = {};
    // split into JSON and File objects first..
    for (let item in formObject) {
      // NB:: only change strings...
      // any objects, leave them as is,  eg file uploads..
      if (typeof formObject[item] === 'string') {
        finalFormObject[item] = formObject[item];
      } else {
        fileObject[item] = formObject[item];
      }
    }
    // now build the FOrmData object..
    const json = JSON.stringify(finalFormObject);
    const blob = new Blob([json], {
      type: 'application/json',
    });
    const data = new FormData();
    data.append('document', blob);
    data.append('file', fileObject['upload']);
    return data;
  };

  const makeContactFormData = (formObject) => {
    const data = new FormData();
    for (let item in formObject) {
      // console.log(typeof item);
      // arrays of hidden inputs for Field.Index items for formbuilder...
      if (typeof formObject[item] === 'object') {
        // loop thru and split into individual keys with same name
        formObject[item].map((val) => {
          data.append(item, val);
        });
      } else {
        data.append(item, formObject[item]);
      }
    }
    data.append('X-Requested-With', 'XMLHttpRequest');
    return data;
  };

  const checkFormValidity = () => {
    let isValid = true;
    state.isRequiredGroupsValid = false;
    // check the form elements first
    state.formData.forEach(function (value, key) {
      // if we have global required groups and this name is included in that array
      // check if its checked property is true and switch flag if so
      if (
        state.requiredGroups &&
        state.requiredGroups.includes(value.name) &&
        value.checked === true
      )
        state.isRequiredGroupsValid = true;
        // now check for experience field
      if (value.name === 'experienceLength' && value.value) {
        const today = new Date();
          const currentYear = today.getUTCFullYear();
          if(!value.value.match(state.yearYYYYFormatRegex) || +value.value < currentYear - 100) {
            isValid = false;
          }
      }
      // otherwise just a normal check against required and valid fields
      if (value.required && !value.valid) isValid = false;
    });
    // now check for any global required groups
    if (state.requiredGroups && !state.isRequiredGroupsValid) {
      isValid = false;
    }
    return isValid;
  };

  const toggleGroupRequiredError = (state, name) => {
    // here we need to switch on the error for the first item in the name group
    const errorField = els.el.querySelector(`[data-error-for=${name}]`);
    const parentEl = errorField.closest('.form__text, .form__element');
    if (state === 'on') {
      parentEl.classList.add('form__element--error');
    } else {
      parentEl.classList.remove('form__element--error');
    }
  };

  const checkFormStepValidity = () => {
    let isValid = true;
    if (!state.formData) return true;
    state.formData.forEach(function (value, key) {
      if (value.required && !value.valid && value.step === state.formWizardStep)
        isValid = false;
    });
    return isValid;
  };

  const getElementStep = (payload) => {
    if (!state.isFormWizard) return;
    const uid = payload.uid;
    const el = els.el.querySelector(`[data-element-uid="${uid}"]`);
    const parentEl = el.closest('.form__step');
    if (parentEl) {
      let elIndex = 0;
      els.formSteps.map((item, index) => {
        if (item === parentEl) elIndex = index;
      });
      return elIndex;
    }
    // default to 0
    return 0;
  };

  const listenEvents = () => {
    // An element has atatched itself to this form
    props.events.on(
      props.config.eventNames.FORM_REGISTER_ELEMENT,
      (payload) => {
        if (payload.form !== state.uid) return;
        // lets add the formWizardStep if we have one (else will just be default 0)
        // this will allow us to validate required fields step by step later if necessary
        if (state.isFormWizard) payload.data.step = getElementStep(payload);
        // set to our Data object
        state.formData.set(payload.uid, payload.data);
      }
    );
    // An element has been updated by user
    props.events.on(props.config.eventNames.FORM_ELEMENT_CHANGE, (payload) => {
      if (payload.form !== state.uid) return;
      // update values to our Data object
      let item = state.formData.get(payload.uid);
      item.value = payload.data.value;
      item.valid = payload.data.valid;
      item.required = payload.data.required;
      if (typeof item.checked !== 'undefined')
        item.checked = payload.data.checked;
      state.formData.set(payload.uid, item);
      // we need to check here if there is a group requirement error that can possibly be turned off
      if (
        state.requiredGroups &&
        state.requiredGroups.includes(payload.data.name)
      ) {
        toggleGroupRequiredError('off', payload.data.name);
      }
      // check if field is type of file upload input, if it is
      // AND it has a value then set a global flag for use later
      // TODO: build in support later for multiple files ??
      if (payload.data.file) {
        if (typeof payload.data.value === 'object') {
          state.hasUploadedFile = true;
        } else {
          state.hasUploadedFile = false;
        }
      }
    });
  };

  const formSuccessActions = (data) => {
    if(data.isAhpraRequest && data.Result) {
      onCompleteAhpraVerification();
    }
    // if we are a popup announce that we are done and exit
    if (state.isFormPartial) broadcastPartialSuccess(data);
    // if we are a normal body form the rest will run...
    els.el.classList.add(classes.success);
    // if we have a link, follow it...
    if (state.submitFollowLink) document.location.href = (state.submitNonHCPFollowLink && data.isNonHCPUser) ? state.submitNonHCPFollowLink : state.submitFollowLink;
    // if we have something to show, show it
    if (props.attributes.actionShow && els.submitShowElement)
      els.submitShowElement.classList.remove(classes.hidden);
    // if we have something to hide, hide it
    if (props.attributes.actionHide && els.submitHideElement)
      els.submitHideElement.classList.add(classes.hidden);
      if(data.isMarketingPopupUser && data.Success) {
        setTimeout(() => {
          props.events.emit(props.config.eventNames.CLOSE_POPOVER, {
            args: {
              CLOSE_POPOVER: 'closed on fetch success',
            }
          });
        }, 5000);
      }
  };

  const onCompleteAhpraVerification = async () => {
    const fetchOptions = {
      datasource: `/en/api/hcp/ahpradeclare`,
      caller: props.name,
    };
    const requestParams = props.requestModel.format({
      Key: '', Values: '', // *** simple request doesn't need anything
    });
    const data = await props.fetchData(fetchOptions, requestParams);
    if (data.payload['Success']) {
      props.events.emit(props.config.eventNames.CLOSE_POPOVER, {
        args: {
          CLOSE_POPOVER: 'closed on fetch success',
        }
      });
      // we have to check wether marketing popup is open or not
      // calling one api in init function entire logic will preset in popover script init function
      props.events.emit('LAUNCH_POPOVER', {
        type: 'marketing-email-popover',
        popoverType: 'marketing-email-popover',
        popoverDelay: 0.5,
      });
    }
  };
  
  const formErrorActions = (data) => {
    els.el.classList.add(classes.problem);
    // set the server error msg if it exists
    if (data && els.formErrorDiv && data.StatusMessage && !data.isAhpraRequest) {
      els.formErrorDiv.innerHTML = data.StatusMessage;
    }
  };

  const broadcastPartialSuccess = (data) => {
    props.events.emit(props.config.eventNames.FORM_PARTIAL_SUCCESS, {
      uid: state.uid,
      type: state.formType,
      target: state.targetPartial ? state.targetPartial : '',
      data,
    });
  };

  const listenForPartialSuccess = (data) => {
    props.events.on(props.config.eventNames.FORM_PARTIAL_SUCCESS, (payload) => {
      if (payload.target && payload.target !== state.formType) return;
      // lets flatten this down so we can easily iterate through later
      state.formPartialData = props.utils.toFlatPropertyMap(payload);
    });
  };

  const showSpinner = () => {
    els.spinner.classList.add('spinner--show');
  };

  const hideSpinner = () => {
    els.spinner.classList.remove('spinner--show');
  };

  return {
    init,
  };
};

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// LICENSE
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

// Copyright by Code Boxx

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Please visit https://code-boxx.com/ for more!
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

const datePicker = {
  instances: [],
  locale: 'en',
  getLocale: () => {
    // get locale of current user
    const languageLocale = document.querySelector('html').getAttribute('lang');
    return languageLocale ? languageLocale : 'en';
  },
  attach: (opt) => {
    opt.target = opt.target;
    opt.target.readOnly = true;
    opt.container = opt.container ? opt.container : null;
    opt.startmon = opt.startmon ? true : false;
    opt.yrange = opt.yrange ? opt.yrange : 10;
    // set the users locale
    datePicker.locale = datePicker.getLocale();
    const id = datePicker.instances.length;
    datePicker.instances.push(opt);

    let inst = datePicker.instances[id];
    // get localised daynames array first, then build to our needs
    let months = datePicker.getMonthNames(datePicker.locale, 'short');
    let temp,
      today = new Date(),
      thisMonth = today.getUTCMonth(), // JAN IS 0
      thisYear = today.getUTCFullYear();

    inst.hPick = document.createElement('div');
    inst.hPick.classList.add('picker');

    inst.hMonth = document.createElement('select');
    inst.hMonth.classList.add('picker-m');
    for (let m in months) {
      temp = document.createElement('option');
      temp.value = +m + 1;
      temp.text = months[m];
      inst.hMonth.appendChild(temp);
    }
    inst.hMonth.selectedIndex = thisMonth;
    inst.hMonth.onchange = () => {
      datePicker.draw(id);
    };
    inst.hPick.appendChild(inst.hMonth);

    inst.hYear = document.createElement('select');
    inst.hYear.classList.add('picker-y');
    for (let y = thisYear - inst.yrange; y < thisYear + 1; y++) {
      temp = document.createElement('option');
      temp.value = y;
      temp.text = y;
      inst.hYear.appendChild(temp);
    }
    inst.hYear.selectedIndex = inst.yrange;
    inst.hYear.onchange = () => {
      datePicker.draw(id);
    };
    inst.hPick.appendChild(inst.hYear);

    // (A3-4) HTML DAYS
    inst.hDays = document.createElement('div');
    inst.hDays.classList.add('picker-d');
    inst.hPick.appendChild(inst.hDays);
    datePicker.draw(id);

    if (inst.container) {
      inst.container.appendChild(inst.hPick);
    } else {
      inst.hWrap = document.createElement('div');
      inst.hWrap.classList.add('picker-wrap');
      inst.hWrap.appendChild(inst.hPick);

      inst.target.onfocus = () => {
        inst.hWrap.classList.add('show');
      };
      inst.hWrap.onclick = (evt) => {
        if (evt.target == inst.hWrap) {
          inst.hWrap.classList.remove('show');
        }
      };
      document.body.appendChild(inst.hWrap);
    }
  },

  draw: (id) => {
    let inst = datePicker.instances[id],
      month = inst.hMonth.value,
      year = inst.hYear.value;
    let daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate(),
      startDay = new Date(Date.UTC(year, month - 1, 1)).getUTCDay(), // SUN IS 0
      endDay = new Date(Date.UTC(year, month - 1, daysInMonth)).getUTCDay();
    (startDay = startDay == 0 ? 7 : startDay),
      (endDay = endDay == 0 ? 7 : endDay);
    let today = new Date(),
      todayDate = null;
    if (today.getUTCMonth() + 1 == month && today.getUTCFullYear() == year) {
      todayDate = today.getUTCDate();
    }
    // get localised daynames array first, then build to our needs
    const locale = datePicker.locale ? datePicker.locale : 'en';
    let rawNames = datePicker.getDayNames(locale, 'short');
    let daynames = [
      rawNames[1],
      rawNames[2],
      rawNames[3],
      rawNames[4],
      rawNames[5],
      rawNames[6],
    ];
    if (inst.startmon) {
      daynames.push(rawNames[0]);
    } else {
      daynames.unshift(rawNames[0]);
    }
    let table,
      row,
      cell,
      squares = [];
    if (inst.startmon && startDay != 1) {
      for (let i = 1; i < startDay; i++) {
        squares.push('B');
      }
    }
    if (!inst.startmon && startDay != 7) {
      for (let i = 0; i < startDay; i++) {
        squares.push('B');
      }
    }

    if (inst.disableday) {
      let thisDay = startDay;
      for (let i = 1; i <= daysInMonth; i++) {
        squares.push([i, inst.disableday.includes(thisDay)]);
        thisDay++;
        if (thisDay == 8) {
          thisDay = 1;
        }
      }
    } else {
      for (let i = 1; i <= daysInMonth; i++) {
        squares.push([i, false]);
      }
    }

    if (inst.startmon && endDay != 7) {
      for (let i = endDay; i < 7; i++) {
        squares.push('B');
      }
    }
    if (!inst.startmon && endDay != 6) {
      for (let i = endDay; i < (endDay == 7 ? 13 : 6); i++) {
        squares.push('B');
      }
    }

    table = document.createElement('table');
    row = table.insertRow();
    row.classList.add('picker-d-h');
    for (let d of daynames) {
      cell = row.insertCell();
      cell.innerHTML = d;
    }

    row = table.insertRow();
    for (let i = 0; i < squares.length; i++) {
      if (i != squares.length && i % 7 == 0) {
        row = table.insertRow();
      }
      cell = row.insertCell();
      if (squares[i] == 'B') {
        cell.classList.add('picker-d-b');
      } else {
        cell.innerHTML = squares[i][0];
        if (squares[i][1]) {
          cell.classList.add('picker-d-dd');
        } else {
          if (squares[i][0] == todayDate) {
            cell.classList.add('picker-d-td');
          }
          cell.classList.add('picker-d-d');
          cell.onclick = () => {
            datePicker.pick(id, squares[i][0]);
          };
        }
      }
    }
    inst.hDays.innerHTML = '';
    inst.hDays.appendChild(table);
  },

  // choose date
  pick: (id, day) => {
    let inst = datePicker.instances[id],
      month = inst.hMonth.value,
      year = inst.hYear.value;
    // format and set day as yyyy-mm--dd
    if (+month < 10) {
      month = '0' + month;
    }
    if (+day < 10) {
      day = '0' + day;
    }
    inst.target.value = `${year}-${month}-${day}`;
    // close
    if (inst.container === undefined) {
      inst.hWrap.classList.remove('show');
    }
    // call func
    if (inst.onpick) {
      inst.onpick();
    }
  },

  getDayNames: (locale = 'en', format = 'short') => {
    const formatter = new Intl.DateTimeFormat(locale, {
      weekday: format,
      timeZone: 'UTC',
    });
    const days = [1, 2, 3, 4, 5, 6, 7].map((day) => {
      const dd = day < 10 ? `0${day}` : day;
      return new Date(`2017-01-${dd}T00:00:00+00:00`);
    });
    return days.map((date) => formatter.format(date));
  },

  getMonthNames: (locale = 'en', format = 'short') => {
    const formatter = new Intl.DateTimeFormat(locale, {
      month: format,
      timeZone: 'UTC',
    });
    const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].map((month) => {
      const mm = month < 10 ? `0${month}` : month;
      return new Date(`2017-${mm}-01T00:00:00+00:00`);
    });
    return months.map((date) => formatter.format(date));
  },
};

var strictUriEncode = str => encodeURIComponent(str).replace(/[!'()*]/g, x => `%${x.charCodeAt(0).toString(16).toUpperCase()}`);

var token = '%[a-f0-9]{2}';
var singleMatcher = new RegExp('(' + token + ')|([^%]+?)', 'gi');
var multiMatcher = new RegExp('(' + token + ')+', 'gi');

function decodeComponents(components, split) {
	try {
		// Try to decode the entire string first
		return [decodeURIComponent(components.join(''))];
	} catch (err) {
		// Do nothing
	}

	if (components.length === 1) {
		return components;
	}

	split = split || 1;

	// Split the array in 2 parts
	var left = components.slice(0, split);
	var right = components.slice(split);

	return Array.prototype.concat.call([], decodeComponents(left), decodeComponents(right));
}

function decode(input) {
	try {
		return decodeURIComponent(input);
	} catch (err) {
		var tokens = input.match(singleMatcher) || [];

		for (var i = 1; i < tokens.length; i++) {
			input = decodeComponents(tokens, i).join('');

			tokens = input.match(singleMatcher) || [];
		}

		return input;
	}
}

function customDecodeURIComponent(input) {
	// Keep track of all the replacements and prefill the map with the `BOM`
	var replaceMap = {
		'%FE%FF': '\uFFFD\uFFFD',
		'%FF%FE': '\uFFFD\uFFFD'
	};

	var match = multiMatcher.exec(input);
	while (match) {
		try {
			// Decode as big chunks as possible
			replaceMap[match[0]] = decodeURIComponent(match[0]);
		} catch (err) {
			var result = decode(match[0]);

			if (result !== match[0]) {
				replaceMap[match[0]] = result;
			}
		}

		match = multiMatcher.exec(input);
	}

	// Add `%C2` at the end of the map to make sure it does not replace the combinator before everything else
	replaceMap['%C2'] = '\uFFFD';

	var entries = Object.keys(replaceMap);

	for (var i = 0; i < entries.length; i++) {
		// Replace all decoded components
		var key = entries[i];
		input = input.replace(new RegExp(key, 'g'), replaceMap[key]);
	}

	return input;
}

var decodeUriComponent = function (encodedURI) {
	if (typeof encodedURI !== 'string') {
		throw new TypeError('Expected `encodedURI` to be of type `string`, got `' + typeof encodedURI + '`');
	}

	try {
		encodedURI = encodedURI.replace(/\+/g, ' ');

		// Try the built in decoder first
		return decodeURIComponent(encodedURI);
	} catch (err) {
		// Fallback to a more advanced decoder
		return customDecodeURIComponent(encodedURI);
	}
};

var splitOnFirst = (string, separator) => {
	if (!(typeof string === 'string' && typeof separator === 'string')) {
		throw new TypeError('Expected the arguments to be of type `string`');
	}

	if (separator === '') {
		return [string];
	}

	const separatorIndex = string.indexOf(separator);

	if (separatorIndex === -1) {
		return [string];
	}

	return [
		string.slice(0, separatorIndex),
		string.slice(separatorIndex + separator.length)
	];
};

var filterObj = function (obj, predicate) {
	var ret = {};
	var keys = Object.keys(obj);
	var isArr = Array.isArray(predicate);

	for (var i = 0; i < keys.length; i++) {
		var key = keys[i];
		var val = obj[key];

		if (isArr ? predicate.indexOf(key) !== -1 : predicate(key, val, obj)) {
			ret[key] = val;
		}
	}

	return ret;
};

var queryString = createCommonjsModule(function (module, exports) {





const isNullOrUndefined = value => value === null || value === undefined;

const encodeFragmentIdentifier = Symbol('encodeFragmentIdentifier');

function encoderForArrayFormat(options) {
	switch (options.arrayFormat) {
		case 'index':
			return key => (result, value) => {
				const index = result.length;

				if (
					value === undefined ||
					(options.skipNull && value === null) ||
					(options.skipEmptyString && value === '')
				) {
					return result;
				}

				if (value === null) {
					return [...result, [encode(key, options), '[', index, ']'].join('')];
				}

				return [
					...result,
					[encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join('')
				];
			};

		case 'bracket':
			return key => (result, value) => {
				if (
					value === undefined ||
					(options.skipNull && value === null) ||
					(options.skipEmptyString && value === '')
				) {
					return result;
				}

				if (value === null) {
					return [...result, [encode(key, options), '[]'].join('')];
				}

				return [...result, [encode(key, options), '[]=', encode(value, options)].join('')];
			};

		case 'colon-list-separator':
			return key => (result, value) => {
				if (
					value === undefined ||
					(options.skipNull && value === null) ||
					(options.skipEmptyString && value === '')
				) {
					return result;
				}

				if (value === null) {
					return [...result, [encode(key, options), ':list='].join('')];
				}

				return [...result, [encode(key, options), ':list=', encode(value, options)].join('')];
			};

		case 'comma':
		case 'separator':
		case 'bracket-separator': {
			const keyValueSep = options.arrayFormat === 'bracket-separator' ?
				'[]=' :
				'=';

			return key => (result, value) => {
				if (
					value === undefined ||
					(options.skipNull && value === null) ||
					(options.skipEmptyString && value === '')
				) {
					return result;
				}

				// Translate null to an empty string so that it doesn't serialize as 'null'
				value = value === null ? '' : value;

				if (result.length === 0) {
					return [[encode(key, options), keyValueSep, encode(value, options)].join('')];
				}

				return [[result, encode(value, options)].join(options.arrayFormatSeparator)];
			};
		}

		default:
			return key => (result, value) => {
				if (
					value === undefined ||
					(options.skipNull && value === null) ||
					(options.skipEmptyString && value === '')
				) {
					return result;
				}

				if (value === null) {
					return [...result, encode(key, options)];
				}

				return [...result, [encode(key, options), '=', encode(value, options)].join('')];
			};
	}
}

function parserForArrayFormat(options) {
	let result;

	switch (options.arrayFormat) {
		case 'index':
			return (key, value, accumulator) => {
				result = /\[(\d*)\]$/.exec(key);

				key = key.replace(/\[\d*\]$/, '');

				if (!result) {
					accumulator[key] = value;
					return;
				}

				if (accumulator[key] === undefined) {
					accumulator[key] = {};
				}

				accumulator[key][result[1]] = value;
			};

		case 'bracket':
			return (key, value, accumulator) => {
				result = /(\[\])$/.exec(key);
				key = key.replace(/\[\]$/, '');

				if (!result) {
					accumulator[key] = value;
					return;
				}

				if (accumulator[key] === undefined) {
					accumulator[key] = [value];
					return;
				}

				accumulator[key] = [].concat(accumulator[key], value);
			};

		case 'colon-list-separator':
			return (key, value, accumulator) => {
				result = /(:list)$/.exec(key);
				key = key.replace(/:list$/, '');

				if (!result) {
					accumulator[key] = value;
					return;
				}

				if (accumulator[key] === undefined) {
					accumulator[key] = [value];
					return;
				}

				accumulator[key] = [].concat(accumulator[key], value);
			};

		case 'comma':
		case 'separator':
			return (key, value, accumulator) => {
				const isArray = typeof value === 'string' && value.includes(options.arrayFormatSeparator);
				const isEncodedArray = (typeof value === 'string' && !isArray && decode(value, options).includes(options.arrayFormatSeparator));
				value = isEncodedArray ? decode(value, options) : value;
				const newValue = isArray || isEncodedArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : value === null ? value : decode(value, options);
				accumulator[key] = newValue;
			};

		case 'bracket-separator':
			return (key, value, accumulator) => {
				const isArray = /(\[\])$/.test(key);
				key = key.replace(/\[\]$/, '');

				if (!isArray) {
					accumulator[key] = value ? decode(value, options) : value;
					return;
				}

				const arrayValue = value === null ?
					[] :
					value.split(options.arrayFormatSeparator).map(item => decode(item, options));

				if (accumulator[key] === undefined) {
					accumulator[key] = arrayValue;
					return;
				}

				accumulator[key] = [].concat(accumulator[key], arrayValue);
			};

		default:
			return (key, value, accumulator) => {
				if (accumulator[key] === undefined) {
					accumulator[key] = value;
					return;
				}

				accumulator[key] = [].concat(accumulator[key], value);
			};
	}
}

function validateArrayFormatSeparator(value) {
	if (typeof value !== 'string' || value.length !== 1) {
		throw new TypeError('arrayFormatSeparator must be single character string');
	}
}

function encode(value, options) {
	if (options.encode) {
		return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
	}

	return value;
}

function decode(value, options) {
	if (options.decode) {
		return decodeUriComponent(value);
	}

	return value;
}

function keysSorter(input) {
	if (Array.isArray(input)) {
		return input.sort();
	}

	if (typeof input === 'object') {
		return keysSorter(Object.keys(input))
			.sort((a, b) => Number(a) - Number(b))
			.map(key => input[key]);
	}

	return input;
}

function removeHash(input) {
	const hashStart = input.indexOf('#');
	if (hashStart !== -1) {
		input = input.slice(0, hashStart);
	}

	return input;
}

function getHash(url) {
	let hash = '';
	const hashStart = url.indexOf('#');
	if (hashStart !== -1) {
		hash = url.slice(hashStart);
	}

	return hash;
}

function extract(input) {
	input = removeHash(input);
	const queryStart = input.indexOf('?');
	if (queryStart === -1) {
		return '';
	}

	return input.slice(queryStart + 1);
}

function parseValue(value, options) {
	if (options.parseNumbers && !Number.isNaN(Number(value)) && (typeof value === 'string' && value.trim() !== '')) {
		value = Number(value);
	} else if (options.parseBooleans && value !== null && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) {
		value = value.toLowerCase() === 'true';
	}

	return value;
}

function parse(query, options) {
	options = Object.assign({
		decode: true,
		sort: true,
		arrayFormat: 'none',
		arrayFormatSeparator: ',',
		parseNumbers: false,
		parseBooleans: false
	}, options);

	validateArrayFormatSeparator(options.arrayFormatSeparator);

	const formatter = parserForArrayFormat(options);

	// Create an object with no prototype
	const ret = Object.create(null);

	if (typeof query !== 'string') {
		return ret;
	}

	query = query.trim().replace(/^[?#&]/, '');

	if (!query) {
		return ret;
	}

	for (const param of query.split('&')) {
		if (param === '') {
			continue;
		}

		let [key, value] = splitOnFirst(options.decode ? param.replace(/\+/g, ' ') : param, '=');

		// Missing `=` should be `null`:
		// http://w3.org/TR/2012/WD-url-20120524/#collect-url-parameters
		value = value === undefined ? null : ['comma', 'separator', 'bracket-separator'].includes(options.arrayFormat) ? value : decode(value, options);
		formatter(decode(key, options), value, ret);
	}

	for (const key of Object.keys(ret)) {
		const value = ret[key];
		if (typeof value === 'object' && value !== null) {
			for (const k of Object.keys(value)) {
				value[k] = parseValue(value[k], options);
			}
		} else {
			ret[key] = parseValue(value, options);
		}
	}

	if (options.sort === false) {
		return ret;
	}

	return (options.sort === true ? Object.keys(ret).sort() : Object.keys(ret).sort(options.sort)).reduce((result, key) => {
		const value = ret[key];
		if (Boolean(value) && typeof value === 'object' && !Array.isArray(value)) {
			// Sort object keys, not values
			result[key] = keysSorter(value);
		} else {
			result[key] = value;
		}

		return result;
	}, Object.create(null));
}

exports.extract = extract;
exports.parse = parse;

exports.stringify = (object, options) => {
	if (!object) {
		return '';
	}

	options = Object.assign({
		encode: true,
		strict: true,
		arrayFormat: 'none',
		arrayFormatSeparator: ','
	}, options);

	validateArrayFormatSeparator(options.arrayFormatSeparator);

	const shouldFilter = key => (
		(options.skipNull && isNullOrUndefined(object[key])) ||
		(options.skipEmptyString && object[key] === '')
	);

	const formatter = encoderForArrayFormat(options);

	const objectCopy = {};

	for (const key of Object.keys(object)) {
		if (!shouldFilter(key)) {
			objectCopy[key] = object[key];
		}
	}

	const keys = Object.keys(objectCopy);

	if (options.sort !== false) {
		keys.sort(options.sort);
	}

	return keys.map(key => {
		const value = object[key];

		if (value === undefined) {
			return '';
		}

		if (value === null) {
			return encode(key, options);
		}

		if (Array.isArray(value)) {
			if (value.length === 0 && options.arrayFormat === 'bracket-separator') {
				return encode(key, options) + '[]';
			}

			return value
				.reduce(formatter(key), [])
				.join('&');
		}

		return encode(key, options) + '=' + encode(value, options);
	}).filter(x => x.length > 0).join('&');
};

exports.parseUrl = (url, options) => {
	options = Object.assign({
		decode: true
	}, options);

	const [url_, hash] = splitOnFirst(url, '#');

	return Object.assign(
		{
			url: url_.split('?')[0] || '',
			query: parse(extract(url), options)
		},
		options && options.parseFragmentIdentifier && hash ? {fragmentIdentifier: decode(hash, options)} : {}
	);
};

exports.stringifyUrl = (object, options) => {
	options = Object.assign({
		encode: true,
		strict: true,
		[encodeFragmentIdentifier]: true
	}, options);

	const url = removeHash(object.url).split('?')[0] || '';
	const queryFromUrl = exports.extract(object.url);
	const parsedQueryFromUrl = exports.parse(queryFromUrl, {sort: false});

	const query = Object.assign(parsedQueryFromUrl, object.query);
	let queryString = exports.stringify(query, options);
	if (queryString) {
		queryString = `?${queryString}`;
	}

	let hash = getHash(object.url);
	if (object.fragmentIdentifier) {
		hash = `#${options[encodeFragmentIdentifier] ? encode(object.fragmentIdentifier, options) : object.fragmentIdentifier}`;
	}

	return `${url}${queryString}${hash}`;
};

exports.pick = (input, filter, options) => {
	options = Object.assign({
		parseFragmentIdentifier: true,
		[encodeFragmentIdentifier]: false
	}, options);

	const {url, query, fragmentIdentifier} = exports.parseUrl(input, options);
	return exports.stringifyUrl({
		url,
		query: filterObj(query, filter),
		fragmentIdentifier
	}, options);
};

exports.exclude = (input, filter, options) => {
	const exclusionFilter = Array.isArray(filter) ? key => !filter.includes(key) : (key, value) => !filter(key, value);

	return exports.pick(input, exclusionFilter, options);
};
});

const anzSelectOptions = {    
getANZSelectFieldsOptions: async (data) => {
        const { targetElement, elementName, paramsData, fetchData, isFromProfile = false } = data;
        let dataSource = `../api/account/getdatafilter?${queryString.stringify(paramsData)}`;
        if(window.location.pathname.toLowerCase().includes('my-account/my-profile')) {
            dataSource = `${dataSource}&isProfilePage=true`;
        }
        const fetchOptions = {
            datasource: dataSource,
            caller: 'anz_select_options',
        };
 
        const optionsData = await fetchData(fetchOptions);
 
        if (optionsData.payload && optionsData.payload.Result) {
            anzSelectOptions.populateDynamicField(optionsData.payload.Result, targetElement, elementName, isFromProfile);
        }
    },

    populateDynamicField: (data, targetElement, elementName, isFromProfile) => {
        // *** destroy existing options
        anzSelectOptions.destroyOptions(targetElement);

        const template = (value, label) => {
            const option = document.createElement('option');
            option.setAttribute('value', value);
            option.innerText = label;
            return option;
        };

        // *** add dynamic options
        let addedOptions = [];
        data[elementName].map((item) => {
            if (addedOptions.indexOf(item.Value) === -1) {
                addedOptions.push(item.Value);
                targetElement.appendChild(
                    template(
                        item.Value, // *** value
                        item.Text, // *** label
                    )
                );
            }
        });
        targetElement.removeAttribute('disabled');
        //set back the values after getting the options
        if(isFromProfile) {
            anzSelectOptions.setANZSelectedOption(targetElement);
        }
    },

    // *** remove all options from select in preparation for new children
    destroyOptions: (selectEl) => {
        for(let i = selectEl.options.length - 1; i > 0 ; i--) {
            selectEl.options[i] = null;
        }
    },

    // set selected option for my profile page
    setANZSelectedOption: (targetElement) => {
        for(let i = targetElement.options.length - 1; i > 0 ; i--) {
            if(targetElement.options[i].value === targetElement.dataset.apiParamValue) {
                targetElement.options[i].setAttribute('selected', 'selected');
                break;
            }
        }
    }
};

const FormElement = (props) => {
  const els = {
    el: null,
    form: null,
    field: null,
    label: null,
    fileClear: null,
    datePickerContainer: null,
    datePickerClearButton: null,
  };

  const classes = {
    hidden: 'hidden',
    error: 'form__element--error',
  };

  const state = {
    uid: null, // uid of this element
    formUID: null, // uid of parent form
    type: null, // type of element
    name: null,
    error: false,
    dirty: false,
    isHidden: false,
    isRequired: false,
    isExcluded: false, // if this is set the item will be excluded from the final data object sent to server (eg email/password on profile page)
    isConditionalRequired: false, // this elements required value is controlled by another element/selection
    controlsRequiredElement: false, // the element that this element controls the required field of
    conditionalRequiredLitArray: [], // keep a running tally of what is selected (eg Country & Profession dropdowns controlling Healthcare Identifier field required status)
    profileFormPrepopulating: true, // simple timer used on Profile form only to indicate form elements are being prepopulated at start
    isValid: false,
    isCheckboxEnabler: false, // checking this element enables another one/group
    checkboxIsEnabledBy: null, // array of elements that enable this checkbox
    checkboxEnablerLitArray: [], // keep a running tally of what is checked
    selectController: null, // this selectbox is controlled by this item (ie I am secondary select)
    selectSubject: null, // this selectbox controls this item (ie I am primary select)
    selectFilterOptionSelected: false, // used on Profile form to indicate selected option has already been accounted for once
    splitElementId: null, // used on for eg phone field, where we have multiple field elements within one form__element
    fileNameText: null,
    datePickerOpen: false,
    validator: {
      validationType: null,
      validationConfirmType: null,
      validationConfirmNotType: null,
      patternRegex: null,
      phoneRegex: /^[0-9\+\-\s()]{5,30}$/, // numeric, plus, minus, space, parentheses, 5-30 chars
      textRegex: /^[\p{L}'\s-]{2,60}$/iu, // alpha, apostrophe, dash, space, 2-60 chars
      alphanumericRegex: /^[0-9\p{L}'\s-]{2,60}$/iu, // alpha, apostrophe, dash, number, space, 2-60 chars
      alphanumericSlashDotDashRegex: /^[0-9\p{L}\s.\-\/]{2,60}$/iu, // number, alpha, space, dot, slash, dash, 2-60 chars
      mailRegex:
        /^[\p{L}0-9.!#$%&'*+/=?^_`{|}~-]+@[\p{L}0-9-]+(?:.[\p{L}0-9-]+)*$/iu,
      passwordRegex:
        /^(?=.*?[\p{Lu}])(?=.*?[\p{Ll}])(?=.*?[0-9])(?=.*?[£#?!@$%^&*-]).{8,}$/iu, // min 1 digit, min 1 upper, min 1 lower, min 1 special char(£#?!@$%^&*-), 8 char minimum..
      api: null,
      yearYYYYFormatRegex: /^\d{4}$/,
      // apiValidated: false, ** TODO: maybe use this later to avoid another trip to server ??
    },
    errors: {
      default: '',
    },
  };

  const init = () => {
    state.isHidden = props.el.getAttribute('type') === 'hidden' ? true : false;
    els.el = setParentElement();
    els.field = setFieldElement();
    els.label = els.el.querySelector('label');
    els.errorDiv = els.el.querySelector('.form__error');
    if (els.errorDiv) {
      state.errors.default = els.errorDiv.innerHTML;
    }
    state.type = getElementType();
    state.name = els.field.getAttribute('name');
    state.validator.api = els.field.getAttribute('data-validation-api');
    state.validator.validationType = els.field.getAttribute('data-validation');
    state.validator.validationConfirmType = els.field.getAttribute(
      'data-validation-confirm'
    );
    state.validator.validationConfirmNotType = els.field.getAttribute(
      'data-validation-confirm-not'
    );
    state.validator.patternRegex = els.field.getAttribute(
      'data-validation-pattern'
    );
    state.isRequired = els.field.hasAttribute('required');
    // state.isDisabled = els.field.hasAttribute('disabled');

    // Optional required controlling stuff
    state.isConditionalRequired = els.field.getAttribute(
      'data-controlled-require'
    );
    state.controlsRequiredElement = els.field.getAttribute(
      'data-control-require'
    );
    state.isExcluded = els.field.getAttribute('data-exclude');
    // selectbox filtering functionality
    state.selectController = els.field.getAttribute('data-filter-controller');
    state.selectSubject = els.field.getAttribute('data-filter-controls');

    // checkbox enable/disable functionality
    if (state.type === 'checkbox') {
      state.isCheckboxEnabler = els.field.hasAttribute('data-enabler');
      state.checkboxIsEnabledBy = getCheckboxEnabledBy();
      if (state.checkboxIsEnabledBy) {
        els.field.disabled = true;
      }
    }
    if (state.type === 'file') {
      els.fileClear = els.el.querySelector('.fileclear');
    }
    state.formUID = getFormUID();
    state.uid = window.uid();
    // attach uid to element so we can find in dom later
    els.el.dataset.elementUid = state.uid;
    setupEventsAndListeners();
    prepopulatingCounter();
    // slight delay to fix bug..
    setTimeout(() => {
      registerElement();
    }, 200);
    setEmailForLMSPage();
    // year picker for registration/profile page
    if(state.name === 'experienceLength') {
        
      const yearPicker = $(document).ready(function() {
        const today = new Date();
        const currentYear = today.getUTCFullYear();
        
        $("#experienceLength").datepicker({
          format: "yyyy",
          viewMode: "years", 
          updateViewDate: true,
          minViewMode: "years",
          autoclose:true,
          endDate: `${currentYear}`
        });   
      });
      yearPicker.on('changeYear', function (e) { 
          setTimeout(() => {
            updateAndBroadcast();
          });
     });
    }
  };

  const setEmailForLMSPage = () => {
    if(state.type === 'email') {
      const queryParams = queryString.parse(window.location.search);
      if(Object.keys(queryParams).length > 0 && queryParams.origin && queryParams.origin.toLowerCase() == 'lms' && queryParams.email) {
        els.field.value = queryParams.email;
        els.field.setAttribute('readonly', true);
      }
    }
  };


  const setParentElement = () => {
    // now we need to check if it is a split element, if so we need to
    // shift els.el up to the correct div (.form__element)
    if (props.el.tagName !== 'DIV') {
      // if this is a hidden input just declare it, and return it as parent
      if (state.isHidden) {
        return props.el;
      }
      const el = props.el.closest('.form__element');
      if (el) {
        // set this to use as a reference to this particulr field later
        state.splitElementId = props.el.dataset.elementId;
        return el;
      }
    }
    return props.el;
  };

  const setFieldElement = () => {
    // we need to check if it is a split element, if so we need to find the correct field element
    // to do this we check data-element-id attrs which should have been stored in state.splitElementId
    if (state.splitElementId) {
      const field = els.el.querySelector(
        `[data-element-id="${state.splitElementId}"]`
      );
      if (field) {
        return field;
      }
    }
    if (state.isHidden) {
      return els.el;
    }
    return els.el.querySelector('input, select, textarea');
  };

  const registerElement = () => {
    const elementObj = {
      uid: state.uid,
      form: state.formUID,
      data: {
        name: state.name,
        type: state.type,
        value: els.field.value,
        required: state.isRequired,
        valid: state.isValid,
      },
    };
    if (state.type === 'checkbox') {
      elementObj.data.checked = els.field.checked;
      // also check if there are any data- attributes for grouping
      const hasGroupingAttributes = getCheckboxDataAttributes();
      if (hasGroupingAttributes) {
        elementObj.data.groupName = hasGroupingAttributes.name;
        elementObj.data.groupValue = hasGroupingAttributes.value;
      }
    }
    if (state.isExcluded) {
      elementObj.data.exclude = true;
    }
    props.events.emit(
      props.config.eventNames.FORM_REGISTER_ELEMENT,
      elementObj
    );
    // give it some time to register itself first..
    setTimeout(() => {
      checkPrePopulatedValues();
    }, 200);
  };

  const getCheckboxDataAttributes = () => {
    const attributes = els.field.dataset;
    for (let a in attributes) {
      if (a.indexOf('group') !== -1) {
        const groupName = a.replace('group', '');
        return {
          name: groupName.toLowerCase(),
          value: attributes[a],
        };
      }
    }
    return null;
  };

  const checkPrePopulatedValues = () => {
    if (state.type === 'checkbox' && els.field.checked === false) return;
    if (els.field.value === '') return;
    // if we are still here, it has a value and we should broadcast that
    updateAndBroadcast();
    // and also do any filtering/enabling stuff as well..
    // selectboxes
    if (state.selectSubject || state.selectController)
      broadcastSelectFilterChange();
    // conditional require
    if (state.controlsRequiredElement) broadcastConditionalRequiredChange();
    // checkboxes
    if (state.isCheckboxEnabler) broadcastCheckboxEnableChange();
  };

  // rough little timer to get around bugs on updating certain elements
  // that are conditionally required on Profile form
  const prepopulatingCounter = () => {
    state.profileFormPrepopulating = true;
    setTimeout(() => {
      state.profileFormPrepopulating = false;
    }, 2000);
  };

  const setupEventsAndListeners = () => {
    switch (state.type) {
      case 'select':
        initSelectEvents();
        listenForSelectEvents();
        listenForRequiredEvents();
        checkForSelectGroupings();
        break;
      case 'text':
      case 'email':
      case 'tel':
      case 'password':
      case 'textarea':
        initTextEvents();
        listenForRequiredEvents();
        break;
      case 'checkbox':
        initCheckboxEvents();
        listenForCheckboxEvents();
        break;
      case 'file':
        initFileEvents();
        break;
      case 'date':
        // listenForRequiredEvents();
        initDatePicker();
        break;
    }
    // has someone clicked the submit button ??
    listenForFormSubmit();
    // has a form partial popup closed and we need to update a field ??
    listenForPopupFormClose();
    if (state.splitElementId) {
      // need to listen out for validation events
      listenSplitElementValidChange();
    }
  };

  const getCheckboxEnabledBy = () => {
    const fieldLevelEnabled = els.field.getAttribute('data-enabled-by');
    const parentLevelEnabledElement = els.el.closest('[data-enabled-by]');
    const parentLevelEnabled = parentLevelEnabledElement
      ? parentLevelEnabledElement.dataset.enabledBy.split('|')
      : null;
    if (fieldLevelEnabled) {
      return els.field.dataset.enabledBy.split('|');
    } else {
      return parentLevelEnabled;
    }
  };

  const getElementType = () => {
    const tag = els.field.tagName;
    switch (tag) {
      case 'SELECT':
        return 'select';
      case 'TEXTAREA':
        return 'textarea';
      case 'INPUT':
        return els.field.getAttribute('type');
    }
  };

  const getFormUID = () => {
    const form = els.el.closest('[data-form-uid]');
    if (form) {
      els.form = form;
      return form.dataset.formUid;
    }
  };

  const listenForFormSubmit = () => {
    /* If someone clicks submit before interacting with form
       we must show all errors */
    props.events.on(props.config.eventNames.FORM_SUBMIT, (payload) => {
      if (payload.uid !== state.formUID) return;
      // sending param as 'submit' to catch the events to update state where it is coming from whethre submit button or from field
      updateState('submit');
      updateErrorStatus();
    });
  };

  // *** TODO @as this is throwing an error on closing any *other* popover, needs some defense
  const listenForPopupFormClose = () => {
    props.events.on(props.config.eventNames.CLOSE_POPOVER, (payload) => {
      if (!payload || !payload.data) return; // regular popup close with button
      if (payload.data.formId !== state.formUID) return;
      if (payload.data.elementId !== state.name) return;
      if (payload.data.elementValue) {
        els.field.value = payload.data.elementValue;
        updateAndBroadcast();
      }
      if (payload.data.Success && payload.data.StatusMessage !== '') {
        showConfirmationMessageNotification(payload.data.StatusMessage);
      }
    });
  };

  const showConfirmationMessageNotification = (msg) => {
    props.events.emit(props.config.eventNames.TRIGGER_NOTIFICATION, {
      dismissable: true,
      duration: 3,
      notificationType: 'success',
      text: msg,
    });
  };

  const initSelectEvents = () => {
    if (state.type !== 'select') return;
    els.field.addEventListener('change', () => {
      updateAndBroadcast();
      if (state.selectSubject || state.selectController) broadcastSelectFilterChange();
      toggleANZAdditionalFields();
      nonHCPProfessionChange();
      if (state.controlsRequiredElement) broadcastConditionalRequiredChange();
    });
    if(window.location.pathname.toLowerCase().includes('my-account/my-profile')) {
      toggleANZSection();
    }
  };

  const toggleANZSection = () => {
    const selectedCountry = document.getElementById('country');
    const siteofWork = document.getElementById('siteofwork');
    const countryOrganizationOptionsEl = document.getElementById('OrganizationEnabledCountries');
    const anzCountryFilteredOptions = (countryOrganizationOptionsEl && countryOrganizationOptionsEl.value) ? countryOrganizationOptionsEl.value.split('|') : [];
    const siteOfWorkOrganizationOptionsEl = document.getElementById('OrganizationApplicableSiteofWork');
    const anzSiteOfWorkFilteredOptions = (siteOfWorkOrganizationOptionsEl && siteOfWorkOrganizationOptionsEl.value) ? siteOfWorkOrganizationOptionsEl.value.split('|') : [];
    const organisationFieldsEl = document.getElementById('organisationFields');
    const mainWorkPlaceLocation = document.getElementById('MainWorkPlaceLocation');
    const stateField = document.getElementById('stateField');
    const locationEl = document.getElementById('location');
    const organisationEl = document.getElementById('organization');
    const facilityEl = document.getElementById('facility');
    const professionEl = document.getElementById('profession');
    const organizationNonApplicableHealthcareProfessionEl = document.getElementById('OrganizationNonApplicableHealthcareProfession');
    const anzOrganizationNonApplicableHealthcareProfessionOptions = (organizationNonApplicableHealthcareProfessionEl && organizationNonApplicableHealthcareProfessionEl.value) ? organizationNonApplicableHealthcareProfessionEl.value.split('|') : [];
    const stateEl = document.getElementById('state');
    if(selectedCountry && selectedCountry.value && anzCountryFilteredOptions.indexOf(selectedCountry.value) !== -1) {
      const selectedState = stateEl.value ? stateEl.value : selectedCountry.value === 'NZ' ? 'state_of_newzealand' : '';
      if(selectedState && els.field.name === 'state') {
        // get organizations data
        if(organisationEl.options.length === 1) {
          const orgParamsData = {'state': selectedState, 'siteofwork': siteofWork.value};
          anzSelectOptions.getANZSelectFieldsOptions({targetElement: organisationEl, elementName: 'Organizations', paramsData: orgParamsData, fetchData: props.fetchData, isFromProfile: true});
        }

        // get location data
        if(locationEl.options.length === 1) {
          const locParamsData = {'state': selectedState, 'siteofwork': siteofWork.value, 'organization': organisationEl.dataset.apiParamValue};
          anzSelectOptions.getANZSelectFieldsOptions({targetElement: locationEl, elementName: 'Locations', paramsData: locParamsData, fetchData: props.fetchData, isFromProfile: true});
        }

        // get facility data
        if(facilityEl.options.length === 1) {
          const facilityParamsData = {'state': selectedState, 'siteofwork': siteofWork.value, 'organization': organisationEl.dataset.apiParamValue, 'location': locationEl.dataset.apiParamValue};
          anzSelectOptions.getANZSelectFieldsOptions({targetElement: facilityEl, elementName: 'Facilities', paramsData: facilityParamsData, fetchData: props.fetchData, isFromProfile: true});
        }
      }

      if(selectedCountry && selectedCountry.value === 'AU' && stateEl.value){
        stateField.classList.remove('d-none');
      }
      if(selectedCountry && selectedCountry.value === 'NZ') {
        stateField.classList.add('d-none');
      }
      if(organisationEl.value) {
        const locationFacilityFields = document.getElementById('locationFacilityFields');
        
        if(organisationEl.value === 'others' && locationFacilityFields) {
          const facilityTextField = document.getElementById('facilityTextField');
          
          locationEl.value = '';
          facilityEl.value = '';
          
          locationFacilityFields.classList.add('d-none');
          facilityTextField.classList.remove('d-none');
          mainWorkPlaceLocation.classList.remove('d-none');
        }
        locationEl.removeAttribute('disabled');
        organisationEl.removeAttribute('disabled');
        facilityEl.removeAttribute('disabled');
      }
      if(anzOrganizationNonApplicableHealthcareProfessionOptions.indexOf(professionEl.value) === -1) {
        if(siteofWork && siteofWork.value && anzSiteOfWorkFilteredOptions.indexOf(siteofWork.value) != -1) {
          organisationFieldsEl.classList.remove('d-none');
          if(organisationEl.value !== 'others') {
            mainWorkPlaceLocation.classList.add('d-none');
          }
        } 
      }
    } 
  };

  const toggleANZAdditionalFields = () => {
    if(['region', 'siteofwork', 'country', 'state', 'location', 'organization', 'facility', 'profession'].indexOf(state.name) !== -1) {
      const selectedCountry = document.getElementById('country');
      const locationEl = document.getElementById('location');
      const organisationEl = document.getElementById('organization');
      const facilityEl = document.getElementById('facility');
      const siteofWork = document.getElementById('siteofwork');
      const organisationFieldsEl = document.getElementById('organisationFields');
      const mainWorkPlaceLocation = document.getElementById('MainWorkPlaceLocation');
      const countryOrganizationOptionsEl = document.getElementById('OrganizationEnabledCountries');
      const anzCountryFilteredOptions = (countryOrganizationOptionsEl && countryOrganizationOptionsEl.value) ? countryOrganizationOptionsEl.value.split('|') : [];
      const siteOfWorkOrganizationOptionsEl = document.getElementById('OrganizationApplicableSiteofWork');
      const anzSiteOfWorkFilteredOptions = (siteOfWorkOrganizationOptionsEl && siteOfWorkOrganizationOptionsEl.value) ? siteOfWorkOrganizationOptionsEl.value.split('|') : [];
      const stateField = document.getElementById('stateField');
      const professionEl = document.getElementById('profession');
      const stateEl = document.getElementById('state');
      const organizationNonApplicableHealthcareProfessionEl = document.getElementById('OrganizationNonApplicableHealthcareProfession');
      const anzOrganizationNonApplicableHealthcareProfessionOptions = (organizationNonApplicableHealthcareProfessionEl && organizationNonApplicableHealthcareProfessionEl.value) ? organizationNonApplicableHealthcareProfessionEl.value.split('|') : [];
      const facilityTextField = document.getElementById('facilityTextField');
      const facilityTextEl = document.getElementById('facilityText');
      if((state.name === 'region' || state.name === 'country') && selectedCountry && selectedCountry.value) {
        if(selectedCountry && selectedCountry.value === 'AU') {
          stateEl.value = '';
          handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'state', required: 'true'});
          stateField.classList.remove('d-none');
          state.controlsRequiredElement = 'state';
        } else {
          handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'state', required: false});
          stateField.classList.add('d-none');
          state.controlsRequiredElement = 'state';
        }
        if(anzCountryFilteredOptions.indexOf(selectedCountry.value) !== -1) {
          if(siteofWork && siteofWork.value && anzSiteOfWorkFilteredOptions.indexOf(siteofWork.value) == -1) {
            handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'mainWorkLocation', required: 'true'});
            handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: 'mainWorkLocation', required: 'true'});
          }
          if(anzOrganizationNonApplicableHealthcareProfessionOptions.indexOf(professionEl.value) === -1) {
            if(siteofWork && siteofWork.value && anzSiteOfWorkFilteredOptions.indexOf(siteofWork.value) != -1) {
              organisationFieldsEl.classList.remove('d-none');
              mainWorkPlaceLocation.classList.add('d-none');
              const controlEl = selectedCountry.value === 'AU' ? 'state' : 'organization';
              handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: controlEl, required: 'true'});
              state.controlsRequiredElement = controlEl;
              handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'companyRole', required: 'true'});
            }
            if(selectedCountry && selectedCountry.value === 'NZ') {
              // const options = [...organisationEl.querySelectorAll('option')];
              // const newZealandFilter = organisationEl.dataset.filterNewZealand ? organisationEl.dataset.filterNewZealand: 'state_of_newzealand';
              // const siteOfWorkFilter = siteofWork && siteofWork.value ? siteofWork.value : '';
              // const filterValue = siteOfWorkFilter ? siteOfWorkFilter : newZealandFilter; 
              // filterSelectOptions(filterValue, options);
              // organisationEl.removeAttribute('disabled');
              handleConditionalRequiredEvent({name: 'country', value: 'NZ', element: 'organization', required: 'true'});
              state.controlsRequiredElement = 'organization';
              stateField.classList.add('d-none');
            }
          }
        } else {
          handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'mainWorkLocation', required: false});
          handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: 'mainWorkLocation', required: false});
          organisationFieldsEl.classList.add('d-none');
          facilityTextField.classList.add('d-none');
          mainWorkPlaceLocation.classList.remove('d-none');
          handleOrganisationFieldRemoveRequired({name: 'country', value: selectedCountry.value, required: false});
        }
      }
      if(state.name === 'siteofwork') {
        if(siteofWork && siteofWork.value && anzSiteOfWorkFilteredOptions.indexOf(siteofWork.value) != -1) {
          if(selectedCountry && selectedCountry.value && anzCountryFilteredOptions.indexOf(selectedCountry.value) != -1) {
            if(professionEl && professionEl.value && anzOrganizationNonApplicableHealthcareProfessionOptions.indexOf(professionEl.value) === -1) {
              // anz select options data
              const selectedState = stateEl.value ? stateEl.value : selectedCountry.value === 'NZ' ? 'state_of_newzealand' : '';
              if(selectedState) {
                const paramsData = {'state': selectedState, 'siteofwork': siteofWork.value};
                anzSelectOptions.getANZSelectFieldsOptions({targetElement: organisationEl, elementName: 'Organizations', paramsData, fetchData: props.fetchData});
              }
              organisationFieldsEl.classList.remove('d-none');
              mainWorkPlaceLocation.classList.add('d-none');
              // organisationEl.removeAttribute('disabled');
              organisationEl.value = '';
              handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'organization', required: 'true'});
              handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: 'organization', required: 'true'});
              handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'companyRole', required: 'true'});
              handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: 'companyRole', required: 'true'});
            } else {
              organisationFieldsEl.classList.add('d-none');
              facilityTextField.classList.add('d-none');
              mainWorkPlaceLocation.classList.remove('d-none');
              handleOrganisationFieldRemoveRequired({name: 'siteofwork', value: siteofWork.value, required: false});
            }
          } else {
            organisationFieldsEl.classList.add('d-none');
            facilityTextField.classList.add('d-none');
            mainWorkPlaceLocation.classList.remove('d-none');
            handleOrganisationFieldRemoveRequired({name: 'siteofwork', value: siteofWork.value, required: false});
          }
        } else {
          organisationFieldsEl.classList.add('d-none');
          facilityTextField.classList.add('d-none');
          mainWorkPlaceLocation.classList.remove('d-none');
          handleOrganisationFieldRemoveRequired({name: 'siteofwork', value: siteofWork.value, required: false});
          if(selectedCountry && selectedCountry.value && anzCountryFilteredOptions.indexOf(selectedCountry.value) != -1) {
            handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'mainWorkLocation', required: 'true'});
            handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: 'mainWorkLocation', required: 'true'});
          }
        }
      }
      if(state.name === 'profession') {
        if(professionEl && professionEl.value && anzOrganizationNonApplicableHealthcareProfessionOptions.indexOf(professionEl.value) === -1) {
          if(selectedCountry && selectedCountry.value && anzCountryFilteredOptions.indexOf(selectedCountry.value) != -1) {
            if(siteofWork && siteofWork.value && anzSiteOfWorkFilteredOptions.indexOf(siteofWork.value) != -1) {
              organisationFieldsEl.classList.remove('d-none');
              mainWorkPlaceLocation.classList.add('d-none');
              // const options = [...organisationEl.querySelectorAll('option')];
              // filterSelectOptions(siteofWork.value, options);
              // organisationEl.removeAttribute('disabled');
              handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'organization', required: 'true'});
              handleConditionalRequiredEvent({name: 'profession', value: professionEl.value, element: 'organization', required: 'true'});
              handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'companyRole', required: 'true'});
              handleConditionalRequiredEvent({name: 'profession', value: professionEl.value, element: 'companyRole', required: 'true'});
            }
          } else {
            organisationFieldsEl.classList.add('d-none');
            facilityTextField.classList.add('d-none');
            mainWorkPlaceLocation.classList.remove('d-none');
            handleOrganisationFieldRemoveRequired({name: 'profession', value: professionEl.value, required: false});
            handleOrganisationFieldRemoveRequired({name: 'country', value: selectedCountry.value, required: false});
          }
        } else {
          organisationFieldsEl.classList.add('d-none');
          facilityTextField.classList.add('d-none');
          mainWorkPlaceLocation.classList.remove('d-none');
          handleOrganisationFieldRemoveRequired({name: 'profession', value: professionEl.value, required: false});
          handleOrganisationFieldRemoveRequired({name: 'country', value: selectedCountry.value, required: false});
        }
      }
      if(state.name === 'state') {
        if(selectedCountry && selectedCountry.value && anzCountryFilteredOptions.indexOf(selectedCountry.value) != -1 && 
          anzOrganizationNonApplicableHealthcareProfessionOptions.indexOf(professionEl.value) === -1 && 
          siteofWork && siteofWork.value && anzSiteOfWorkFilteredOptions.indexOf(siteofWork.value) != -1) {
            // anz select options data
            if(siteofWork && siteofWork.value) {
              const selectedState = stateEl.value ? stateEl.value : selectedCountry.value === 'NZ' ? 'state_of_newzealand' : '';
              const paramsData = {'state': selectedState, 'siteofwork': siteofWork.value};
              anzSelectOptions.getANZSelectFieldsOptions({targetElement: organisationEl, elementName: 'Organizations', paramsData, fetchData: props.fetchData});
            }
            // const options = [...organisationEl.querySelectorAll('option')];
            // filterSelectOptions(siteofWork.value, options);
            handleOrganisationFieldRequired(organisationEl, 'organization', selectedCountry, siteofWork);
        } 
      }
      if(state.name === 'organization') {
        const selectedState = stateEl.value ? stateEl.value : selectedCountry.value === 'NZ' ? 'state_of_newzealand' : '';
        const paramsData = {'state': selectedState, 'siteofwork': siteofWork.value, 'organization': organisationEl.value};
        anzSelectOptions.getANZSelectFieldsOptions({targetElement: locationEl, elementName: 'Locations', paramsData, fetchData: props.fetchData});
        const locationFacilityFields = document.getElementById('locationFacilityFields');
        if(organisationEl.value === 'others' && locationFacilityFields) {
          locationEl.value = '';
          facilityEl.value = '';
          facilityTextEl.value = '';
          locationFacilityFields.classList.add('d-none');
          facilityTextField.classList.remove('d-none');
          mainWorkPlaceLocation.classList.remove('d-none');
          handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'mainWorkLocation', required: 'true'});
          handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: 'mainWorkLocation', required: 'true'});
          handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'location', required: false});
          handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'facility', required: false});
        } else {
          if(locationFacilityFields) {
            locationFacilityFields.classList.remove('d-none');
            facilityTextField.classList.add('d-none');
            mainWorkPlaceLocation.classList.add('d-none');
          }
          handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: 'mainWorkLocation', required: false});
          handleOrganisationFieldRequired(locationEl, 'location', selectedCountry, siteofWork);
        }
      }
      if(state.name === 'location') {
        const selectedState = stateEl.value ? stateEl.value : selectedCountry.value === 'NZ' ? 'state_of_newzealand' : '';
        const paramsData = {'state': selectedState, 'siteofwork': siteofWork.value, 'organization': organisationEl.value, 'location': locationEl.value};
        anzSelectOptions.getANZSelectFieldsOptions({targetElement: facilityEl, elementName: 'Facilities', paramsData, fetchData: props.fetchData});
        handleOrganisationFieldRequired(facilityEl, 'facility', selectedCountry, siteofWork);
      }
    } 
  };

  const handleOrganisationFieldRequired = (elementNode, elementName, countryEl, siteofworkEl) => {        
    elementNode.value = '';   
    // elementNode.removeAttribute('disabled');
    handleConditionalRequiredEvent({name: 'country', value: countryEl.value, element: elementName, required: 'true'});
    handleConditionalRequiredEvent({name: 'siteofwork', value: siteofworkEl.value, element: elementName, required: 'true'});
    state.controlsRequiredElement = elementName;
  };

  const handleOrganisationFieldRemoveRequired = (controlObj) => {
    const organisationDataFields = ['organization', 'location', 'facility', 'companyRole'];
    organisationDataFields.forEach((field) => {
      const fieldEl = document.getElementById(field);
      if(field === 'organization') {
        handleConditionalRequiredEvent({...controlObj, element: 'mainWorkLocation'});
      }
      fieldEl.value = '';
      handleConditionalRequiredEvent({...controlObj, element: field});
      state.controlsRequiredElement = field;
    });
  };

  const nonHCPProfessionChange = () => {
    const selectedCountry = document.getElementById('country');
    const emailEl = document.getElementById('email');
    const profession = document.getElementById('profession');
    const reasonToRequest = document.getElementById('reasonForRequest');
    const reasonToRequestField = document.getElementById('reasonToRequestField');
    const healthcareIdentifierField = document.getElementById('healthcareIdentifierField');
    let groupHeaderTitle = '';
    let controlledElement = state.controlsRequiredElement;
    if(profession.selectedIndex > 0) {
      groupHeaderTitle = profession.options[profession.selectedIndex].dataset.headerValue || null;
    }
    if((['AU', 'FR', 'BE'].indexOf(selectedCountry.value) !== -1 || (reasonToRequest && reasonToRequest.dataset.languageMatches === "True"))) {
      if(groupHeaderTitle && ['Non Clinical', 'Non Health Care Professionals'].indexOf(groupHeaderTitle) !== -1 && ['pharmacist'].indexOf(profession.value) === -1 && !emailEl.value.includes('@smith-nephew.com')) {
        controlledElement = 'reasonForRequest';
        if(reasonToRequestField){
          reasonToRequestField.style.display = 'block';
          healthcareIdentifierField.style.display = 'none';
        }
        handleConditionalRequiredEvent({name: 'profession', value: profession.value, element: 'healthcareidentifier', required: false});
      } else {
        controlledElement = 'healthcareidentifier';
        if(reasonToRequestField){
          reasonToRequestField.style.display = 'none';
          reasonToRequest.value = '';
          healthcareIdentifierField.style.display = 'block';
        }
       }
    } else {
      controlledElement = 'healthcareidentifier';
      if(reasonToRequestField){
        reasonToRequestField.style.display = 'none';
        reasonToRequest.value = '';
        healthcareIdentifierField.style.display = 'block';
      }
      handleConditionalRequiredEvent({name: 'profession', value: profession.value, element: 'reasonForRequest', required: false});
    }
    if(['country', 'profession'].indexOf(state.name) !== -1) {
      if(selectedCountry.value && state.name !== 'country' && state.conditionalRequiredLitArray.indexOf('country') === -1 && (selectedCountry.options[selectedCountry.selectedIndex].dataset.required === 'true' || (reasonToRequest && reasonToRequest.dataset.languageMatches === "True"))) {
        handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: controlledElement, required: 'true'});
      }
      if(profession.value && state.name !== 'profession' && state.conditionalRequiredLitArray.indexOf('profession') === -1 && profession.options[profession.selectedIndex].dataset.required === 'true') {
        handleConditionalRequiredEvent({name: 'profession', value: profession.value, element: controlledElement, required: 'true'});
      }
    }
    state.controlsRequiredElement = controlledElement;
  };

  const initTextEvents = () => {
    els.field.addEventListener('blur', (event) => {
      if(els.field.name === 'experienceLength' && !els.field.value) {
        els.el.classList.remove(classes.error);
        return;
      }
      // mainwork place location make mandatory
      if(window.location.pathname.toLowerCase().includes('my-account/my-profile') && els.field.name === 'mainWorkLocation' && !els.field.value) {
        const selectedCountry = document.getElementById('country');
        const siteofWork = document.getElementById('siteofwork');
        const countryOrganizationOptionsEl = document.getElementById('OrganizationEnabledCountries');
        const anzCountryFilteredOptions = (countryOrganizationOptionsEl && countryOrganizationOptionsEl.value) ? countryOrganizationOptionsEl.value.split('|') : [];
        const siteOfWorkOrganizationOptionsEl = document.getElementById('OrganizationApplicableSiteofWork');
        const anzSiteOfWorkFilteredOptions = (siteOfWorkOrganizationOptionsEl && siteOfWorkOrganizationOptionsEl.value) ? siteOfWorkOrganizationOptionsEl.value.split('|') : [];
        if(anzCountryFilteredOptions.indexOf(selectedCountry.value) !== -1) {
          if(siteofWork && siteofWork.value && anzSiteOfWorkFilteredOptions.indexOf(siteofWork.value) == -1) {
            handleConditionalRequiredEvent({name: 'country', value: selectedCountry.value, element: 'mainWorkLocation', required: 'true'});
            handleConditionalRequiredEvent({name: 'siteofwork', value: siteofWork.value, element: 'mainWorkLocation', required: 'true'});
          }
        }
      }
      updateAndBroadcast();
    });
  };

  const initDatePicker = () => {
    if (state.type !== 'date') return;
    // do some dom manipulation first..
    addDatePickerContainer();
    addDateInputWrapper();
    // now reference these..
    els.datePickerContainer = els.el.querySelector('.datepicker-wrapper');
    els.datePickerClearButton = els.el.querySelector('.clear-date');
    // hide clear button initially
    toggleDateClearButton();
    // build the datepicker object
    const options = {
      target: els.field,
      container: els.datePickerContainer,
      onpick: () => {
        closeDatePicker();
        updateAndBroadcast();
        toggleDateClearButton();
      },
    };
    // and attach the datepicker instance
    datePicker.attach(options);
    // and setup listeners
    els.datePickerClearButton.addEventListener('click', (event) => {
      event.preventDefault();
      els.field.value = '';
      closeDatePicker();
      updateAndBroadcast();
      toggleDateClearButton();
    });
    els.field.addEventListener('click', (event) => {
      if (state.datePickerOpen === false) {
        openDatePicker();
      } else {
        closeDatePicker();
      }
    });
  };

  const openDatePicker = () => {
    // nb: remove error msg if showing
    els.el.classList.remove(classes.error);
    state.datePickerOpen = true;
    els.datePickerContainer.classList.remove('hidden');
  };

  const closeDatePicker = () => {
    state.datePickerOpen = false;
    els.datePickerContainer.classList.add('hidden');
    // nb: check/show if we have any validation errors
    updateAndBroadcast();
  };

  const toggleDateClearButton = () => {
    if (els.field.value === '') {
      els.datePickerClearButton.classList.add('hidden');
    } else {
      els.datePickerClearButton.classList.remove('hidden');
    }
  };

  const addDatePickerContainer = () => {
    // append date-picker wrapper/container div to date input div
    const wrapper = document.createElement('div');
    wrapper.classList.add('datepicker-wrapper');
    wrapper.classList.add('hidden');
    els.el.appendChild(wrapper);
  };

  const addDateInputWrapper = () => {
    // create wrapper container to align/add a date clear button
    const wrapper = document.createElement('div');
    const button = document.createElement('button');
    wrapper.classList.add('wrap-date');
    button.classList.add('clear-date');
    button.innerText = 'x';
    els.field.parentNode.insertBefore(wrapper, els.field);
    wrapper.appendChild(els.field);
    wrapper.appendChild(button);
  };

  const initFileEvents = () => {
    const uploadedFileName = els.el.querySelector('.filenameText');
    state.fileNameText = uploadedFileName.textContent;
    els.field.addEventListener('change', function () {
      uploadedFileName.textContent = this.files[0].name;
      els.fileClear.classList.remove('hidden');
      updateAndBroadcast();
    });
    if (els.fileClear) {
      els.fileClear.addEventListener('click', function (e) {
        e.preventDefault();
        els.field.value = '';
        els.fileClear.classList.add('hidden');
        uploadedFileName.textContent = state.fileNameText;
        updateAndBroadcast();
      });
    }
  };

  const initCheckboxEvents = () => {
    if (state.type !== 'checkbox') return;
    els.field.addEventListener('click', (event) => {
      updateAndBroadcast();
      if (state.isCheckboxEnabler) broadcastCheckboxEnableChange();
    });
  };

  const listenForSelectEvents = () => {
    // ** TODO, merge these together  ??
    if (state.type !== 'select') return;
    // if this is a selectbox and being controlled/filtered by another selectbox
    // If I am the Primary selectbox eg Region
    if (state.selectSubject) {
      props.events.on(props.config.eventNames.FORM_SELECT_FILTER, (payload) => {
        if (payload.form !== state.formUID) return;
        if (payload.name !== state.selectSubject) return;
        if(payload.name === 'organization') {
          if(payload.value === 'others') return;
        }
        if (payload.filtervalue) els.field.value = payload.filtervalue;
        updateAndBroadcast();
      });
    }
    // If I am the secondary selectbox eg Country
    if (state.selectController) {
      props.events.on(props.config.eventNames.FORM_SELECT_FILTER, (payload) => {
        if (payload.form !== state.formUID) return;
        if (payload.name !== state.selectController) return;
        if(window.location.pathname.toLowerCase().includes('my-account/my-profile') && ['organization', 'location', 'facility'].indexOf(state.name) !== -1) {
          const organisationFieldsEl = document.getElementById('organisationFields');
          if(organisationFieldsEl.classList.contains('d-none')) {
            return;
          }
        }
        filterSelectOptions(payload.value);
        updateAndBroadcast();
      });
    }
  };

  const checkForSelectGroupings = () => {
    let hasHeaders = false;
    [...els.field.options].map((item) => {
      if (item.dataset.header) hasHeaders = true;
    });
    if (hasHeaders) groupSelectHeadings();
  };

  const groupSelectHeadings = () => {
    const headerObj = {};
    let selectedIndex = 0;
    // const htmlString = '';
    const fragment = document.createDocumentFragment();
    [...els.field.options].map((item, index) => {
      // NB: firstly, keep tabs on if an option is selected in case we are pre-populating profile form
      if (item.hasAttribute('selected')) selectedIndex = index;
      // now check if it has header, etc
      if (item.dataset.header) {
        if (!headerObj[item.dataset.header])
          headerObj[item.dataset.header] = [];
        headerObj[item.dataset.header].push(item);
      } else {
        let label = 'other';
        // nb: dont do if no value , ie we are on first item , like 'select an option', etc
        // in this instance we push into its own 'empty' so we can place first
        if (item.value === '') label = 'empty';
        if (!headerObj[label]) headerObj[label] = [];
        headerObj[label].push(item);
      }
    });
    // now loop through and build up html string
    // Add fragment to a list:
    if (headerObj['empty']) fragment.appendChild(headerObj['empty'][0]);
    for (const key in headerObj) {
      if (key !== 'empty' && key !== 'other') {
        const header = document.createElement('optgroup');
        header.label = key;
        headerObj[key].map((item) => {
          header.appendChild(item);
        });
        fragment.appendChild(header);
      }
    }
    // if any with no data-header just put them at end..
    if (headerObj['other']) {
      headerObj['other'].map((item) => {
        fragment.appendChild(item);
      });
    }
    // clear existing fields from selct
    els.field.length = 0;
    // and re-append new doc fragment..
    els.field.appendChild(fragment);
    // for registration form will just stay with default of 0
    els.field.selectedIndex = selectedIndex;
  };

  const listenForCheckboxEvents = () => {
    if (state.type !== 'checkbox') return;
    // if this is a checkbox and being enabled/disabled by another checkbox
    if (state.checkboxIsEnabledBy) {
      props.events.on(
        props.config.eventNames.FORM_CHECKBOX_ENABLE,
        (payload) => {
          if (payload.form !== state.formUID) return;
          toggleCheckboxEnabled(payload);
          updateAndBroadcast();
        }
      );
    }
  };

  const listenForRequiredEvents = () => {
    // only these three for now
    if (state.type !== 'text' && state.type !== 'textarea' && state.type !== 'date' && state.type !== 'select') return;
    const emailEl = document.getElementById('email');
    // if this is a textbox and require status is being enabled/disabled by another 1 (or more) elements
    if (state.isConditionalRequired) {
      props.events.on(
        props.config.eventNames.FORM_CONDITIONAL_REQUIRE,
        (payload) => {
          if (payload.form !== state.formUID) return;
          if (payload.element !== state.name) return;
          // if user enters smith-nephew email healthcare identifier is non mandatory for all cases(country and profession)
          if(payload.element === 'healthcareidentifier' && emailEl.value.includes('@smith-nephew.com') && state.conditionalRequiredLitArray.length !== 2) return;
          if(payload.element === 'reasonForRequest') {
            const selectedCountry = document.getElementById('country');
            const profession = document.getElementById('profession');
            if(selectedCountry.value && state.conditionalRequiredLitArray.indexOf('country') === -1 && selectedCountry.options[selectedCountry.selectedIndex].dataset.required === 'true') {
              state.conditionalRequiredLitArray.push('country');
            }
            if(profession.value && state.conditionalRequiredLitArray.indexOf('profession') === -1 && profession.options[profession.selectedIndex].dataset.required === 'true') {
              state.conditionalRequiredLitArray.push('profession');
            }
          }
          makeRequired(payload);
          updateAndBroadcast();
        }
      );
    }
  };

  const handleConditionalRequiredEvent = (payload) => {
    if(['country', 'profession', 'siteofwork'].indexOf(payload.name) !== -1) {
      const postData = {
        form: state.formUID,
        name: payload.name,
        value: payload.value,
        element: payload.element,
        required: payload.required,
      };
      props.events.emit(
        props.config.eventNames.FORM_CONDITIONAL_REQUIRE,
        postData
      );
    }
  };

  const makeRequired = (payload) => {
    // console.log('/FormElement/ -makeRequired', payload);
    const labelText = els.label.innerHTML.replace('*', '');
    if (payload.required === 'true') {
      if (state.conditionalRequiredLitArray.indexOf(payload.name) === -1) {
        state.conditionalRequiredLitArray.push(payload.name);
      }
    } else {
      state.conditionalRequiredLitArray =
        state.conditionalRequiredLitArray.filter((val) => {
          return val !== payload.name;
        });
    }
    if(payload.required === 'true' && payload.element === 'state') {
      state.isRequired = true;
    } else {
      state.isRequired = checkForRequired(state.conditionalRequiredLitArray);
    }
    els.label.innerHTML = labelText + ' *';
    // if switched off, then clear field
    // UNLESS We are on prepopulated Profile form in which case we dont clear the value to begin with !!
    if (!state.isRequired && !state.profileFormPrepopulating) {
      els.field.value = '';
      els.label.innerHTML = labelText.replace('*', '');
    }
  };

  const checkForRequired = (requiredListArr) => {
    if(requiredListArr.length == 2) {
      let isRequired = false;
      requiredListArr.forEach(element => {
        if(document.getElementById(element) && document.getElementById(element).value !== '') {
          isRequired = true;
        }
      });
      return isRequired;
    }
    return false;
  };

  /* General notification to FormController that select has changed */
  const broadcastElementChange = () => {
    const elementObj = {
      uid: state.uid,
      form: state.formUID,
      data: {
        value: els.field.value,
        valid: state.isValid,
        required: state.isRequired,
      },
    };
    // some extra info for checkboxes
    if (state.type === 'checkbox') {
      elementObj.data.checked = els.field.checked;
      elementObj.data.name = state.name;
    }
    // are we a file upload ?
    if (state.type === 'file') {
      elementObj.data.value = els.field.files[0];
      elementObj.data.file = true;
    }
    props.events.emit(props.config.eventNames.FORM_ELEMENT_CHANGE, elementObj);
  };

  /* For checkbox enable/disable filtering interactions */
  const broadcastCheckboxEnableChange = () => {
    if (state.type !== 'checkbox') return;
    const checkObj = {
      form: state.formUID,
      name: state.name,
      value: els.field.value,
      checked: els.field.checked,
    };
    props.events.emit(props.config.eventNames.FORM_CHECKBOX_ENABLE, checkObj);
  };

  /* For selectController/selectSubject filtering interactions */
  const broadcastSelectFilterChange = () => {
    if (state.type !== 'select') return;
    const selectObj = {
      form: state.formUID,
      name: state.name,
      value: els.field.value,
    };
    if (
      state.selectController &&
      els.field.options[els.field.selectedIndex].dataset.filter
    ) {
      els.field.options[els.field.selectedIndex];
      if(window.location.pathname.toLowerCase().includes('my-account/my-profile') && ['organization', 'location'].indexOf(els.field.name) !== -1) {
        const selectControllerEl = document.getElementById(state.selectController);
        selectObj.filtervalue = selectControllerEl.value;
      } else {
        selectObj.filtervalue =
        els.field.options[els.field.selectedIndex].dataset.filter;
      }
    }
      
    props.events.emit(props.config.eventNames.FORM_SELECT_FILTER, selectObj);
  };

  /* For conditional required field interactions */
  const broadcastConditionalRequiredChange = () => {
    if (state.type !== 'select' || !state.controlsRequiredElement) return;
    const targetField =
      els.field.options[els.field.selectedIndex].dataset.required || false;
      // no point in broadcasting if no target
    if (targetField === '') return;

    // Handle reason to request field for AU, BE and FR countries and health profession as non-clinical and non-healthcare-professional
    // emiting the FORM_CONDITIONAL_REQUIRE event two times one is for healthcareidentifier and another is reasontorequest 
    if(state.controlsRequiredElement === 'reasonForRequest') {
      const healthcareIdentifierField = document.getElementById('healthcareIdentifierField');
      if(healthcareIdentifierField && healthcareIdentifierField.classList.contains(classes.error)) {
        handleConditionalRequiredEvent({name: state.name, value: els.field.value, element: 'healthcareidentifier', required: false});
      }
    }
    if(state.controlsRequiredElement === 'healthcareidentifier') {
      const reasonToRequestField = document.getElementById('reasonToRequestField');
      if(reasonToRequestField && reasonToRequestField.classList.contains(classes.error)) {
        handleConditionalRequiredEvent({name: state.name, value: els.field.value, element: 'reasonForRequest', required: false});
      }
    }
    // second one
    handleConditionalRequiredEvent({name: state.name, value: els.field.value, element: state.controlsRequiredElement, required: targetField});
  };

  /* For validating split element field interactions */
  const broadcastSplitElementValidChange = () => {
    // no point in broadcasting if not needed
    if (!state.splitElementId) return;
    const elementObj = {
      form: state.formUID,
      elementUID: state.uid,
      splitElementId: state.splitElementId,
    };
    props.events.emit(props.config.eventNames.FORM_SPLIT_VALIDATE, elementObj);
  };

  /* For validating split element field interactions */
  const listenSplitElementValidChange = () => {
    if (!state.splitElementId || !state.isRequired) return;
    props.events.on(props.config.eventNames.FORM_SPLIT_VALIDATE, (payload) => {
      if (payload.form !== state.formUID) return;
      if (payload.elementUID === state.uid) return;
      if (payload.splitElementId === state.splitElementId) return;
      if (state.isValid || !state.isRequired) return;
      state.dirty = true;
      updateErrorStatus();
    });
  };

  const toggleCheckboxEnabled = (payload) => {
    if (state.checkboxIsEnabledBy.includes(payload.value)) {
      if (payload.checked === true) {
        if (state.checkboxEnablerLitArray.indexOf(payload.value) === -1)
          state.checkboxEnablerLitArray.push(payload.value);
      } else {
        state.checkboxEnablerLitArray = state.checkboxEnablerLitArray.filter(
          (val) => {
            return val !== payload.value;
          }
        );
      }
      els.field.disabled =
        state.checkboxEnablerLitArray.length > 0 ? false : true;
      // if we were checked and controller gets unchecked then reset the value...
      if (els.field.disabled) els.field.checked = false;
    }
  };

  /* Only show relevant filtered select options */
  const filterSelectOptions = (filter, optionsObj = null) => {
    // console.log('check for test', filter, optionsObj)
    let match = false;
    let originalSelectedIndex = null;
    if (state.type !== 'select') return;
    const options = optionsObj ? optionsObj : [...els.field.querySelectorAll('option')];
    // nb: reverse to always have the first visible item selected in list
    options.reverse().map((item, index) => {
      // NB: FIrst see if we had a selected attribute on any option
      // Remember we only need this the first time on a pre-populated profile form
      // So we set a flag the first time we run it..
      if (item.hasAttribute('selected') && !state.selectFilterOptionSelected) {
        originalSelectedIndex = index;
        state.selectFilterOptionSelected = true;
      }
      // now check for filter values
      if(item.dataset.stateFilter) {
        const selectedState = document.getElementById('state');
        let stateValue = selectedState && selectedState.value ? selectedState.value : '';
        if(!stateValue) {
          const selectedCountry = document.getElementById('country');
          if(selectedCountry && selectedCountry.value === 'NZ') {
            stateValue = 'state_of_newzealand';
          }
        }
        if (item.dataset.isOrganizationField) {
          if((stateValue && stateValue === item.dataset.stateFilter) || item.value === 'others') {
            if(item.dataset.filter === filter || filter === '' || item.value === 'others') {
              item.classList.remove(classes.hidden);
              if (originalSelectedIndex === null) item.selected = true;
              match = true;
            } else {
              item.classList.add(classes.hidden);
            }
          } else {
            item.classList.add(classes.hidden);
          }
        } else if(item.dataset.isLocationField && stateValue && stateValue === item.dataset.stateFilter) {
          if(item.dataset.filter === filter || filter === '') {
            item.classList.remove(classes.hidden);
            if (!originalSelectedIndex) item.selected = true;
            match = true;
          } else {
            item.classList.add(classes.hidden);
          }
        } else {
          item.classList.add(classes.hidden);
        }
      } else if(item.dataset.organizationFilter) {
          const selectedOrganization = document.getElementById('organization');
          if(selectedOrganization && selectedOrganization.value === item.dataset.organizationFilter) {
            if (item.dataset.filter === filter || filter === '') {
              item.classList.remove(classes.hidden);
              if (!originalSelectedIndex) item.selected = true;
              match = true;
            } else {
              item.classList.add(classes.hidden);
            }
          } else {
            item.classList.add(classes.hidden);
          }
      } else {
        if (item.dataset.filter === filter || filter === '') {
          item.classList.remove(classes.hidden);
          if (!originalSelectedIndex) item.selected = true;
          match = true;
        } else {
          item.classList.add(classes.hidden);
        }
      }
    });
    // If we had a pre-populated option value (eg on Profile form) before we need to select it now..
    if (originalSelectedIndex) options[originalSelectedIndex].selected = true;
    // if there were no matches for this filter, just show everything
    if (!match) {
      options.map((item, index) => {
        item.classList.remove(classes.hidden);
        item.selected = true;
      });
    }
    // we need to do this here as well, in case the region has been changed, which then changes the country
    if (state.controlsRequiredElement) broadcastConditionalRequiredChange();
  };

  const updateState = (callFrom = '') => {
    state.dirty = true;
    const isElementValueValid = checkElement();
    // ** API VALIDATION (if data-validation-api="path/to/some/api")
    // eg Email check if exists with email endpoint
    if (isElementValueValid && state.validator.api) {
      const apiCheckIsValid = getData();
      apiCheckIsValid.then((result) => {
        if (result && callFrom !== 'submit') {
          const professionEl = document.getElementById('profession');
          const siteofworkEl = document.getElementById('siteofwork');
          if(professionEl && siteofworkEl) {
            const professionOptions = professionEl.options;
            const siteofworkOptions = siteofworkEl.options;
            const professionParentDiv = professionEl.closest('.form__element');
            const siteofworkParentDiv = siteofworkEl.closest('.form__element');
            if (els.field.value.includes('@smith-nephew.com')) {
              autoPopulateSelectedValue(professionOptions, 'smith_and_nephew_employee', professionParentDiv.dataset.elementUid);
              autoPopulateSelectedValue(siteofworkOptions, 'smith_and_nephew', siteofworkParentDiv.dataset.elementUid);
              // remove all error fields for smith and nephew employe
              const healthcareIdentifierField = document.getElementById('healthcareIdentifierField');
              const reasonToRequestField = document.getElementById('reasonToRequestField');
              professionParentDiv.classList.remove(classes.error);
              siteofworkParentDiv.classList.remove(classes.error);
              healthcareIdentifierField.classList.remove(classes.error);
              if(reasonToRequestField){
                reasonToRequestField.classList.remove(classes.error);
                reasonToRequestField.style.display = 'none';
              }
              if(professionEl.value) {
                handleConditionalRequiredEvent({name: 'profession', value: professionEl.value, element: 'reasonForRequest', required: false});
              }
            } else {
              const countryEl = document.getElementById('country');
              if(countryEl.value) {
                const required = countryEl.options[countryEl.selectedIndex].dataset.required === 'true' ? 'true' : false;
                handleConditionalRequiredEvent({name: 'country', value: countryEl.value, element: 'healthcareidentifier', required: required});
              }
              if(professionEl.value) {
                nonHCPProfessionChange();
                const groupHeaderTitle = professionEl.options[professionEl.selectedIndex].dataset.headerValue || null;
                const required = professionEl.options[professionEl.selectedIndex].dataset.required === 'true' ? 'true' : false;
                const targetElement = ['Non Clinical', 'Non Health Care Professionals'].indexOf(groupHeaderTitle) !== -1 && professionEl.value !== 'pharmacist' ? 'reasonForRequest' : 'healthcareidentifier';
                handleConditionalRequiredEvent({name: 'profession', value: professionEl.value, element: targetElement, required: required});
              }
            }
          }
        }

        state.isValid = result;
        updateErrorStatus();
        // checkSplitElementStatus(isElementValueValid);
      });
    }
    state.isValid = isElementValueValid;
    checkSplitElementStatus(isElementValueValid);
  };

  const autoPopulateSelectedValue = (options, value, uid) => {
    for (var i = 0; i < options.length; i++) {
      if (options[i].value == value) {
        options[i].selected = true;
        break;
      }
    }
    const elementObj = {
      uid: uid,
      form: state.formUID,
      data: {
        value: value,
        valid: value ? true : false,
        required: true,
      },
    };
    props.events.emit(props.config.eventNames.FORM_ELEMENT_CHANGE, elementObj);
  };

  const checkSplitElementStatus = (isElementValueValid) => {
    // if we are valid and 1 part of a split element then we
    // need to check the other part/element is valid as well
    if (isElementValueValid && state.splitElementId) {
      // set some delay here to beat race condition..
      // first element needs to update its form__element first and not 2nd
      setTimeout(() => {
        broadcastSplitElementValidChange();
      }, 500);
    }
  };

  const updateErrorStatus = () => {
    if (state.dirty && !state.isValid) {
      els.el.classList.add(classes.error);
    } else {
      els.el.classList.remove(classes.error);
    }
  };

  const checkElement = () => {
    // first set the correct error msg
    if (els.errorDiv) {
      els.errorDiv.innerHTML = state.errors.default;
    }
    const fieldValue = els.field.value.trim();
    if (state.isRequired) {
      // ** SELECTBOXES && EMPTY FIELDS (if required attribute is present)
      if (fieldValue === '') return false;

      // ** PATTERN REGEX  (eg data-validation-pattern="^[\p{L}]{2,4}$") this takes precedence over others !
      if (state.validator.patternRegex !== null) {
        const regex = new RegExp(state.validator.patternRegex, 'iu');
        return regex.test(fieldValue);
      }

      //  ** PASSWORD INPUTS (if type="password")
      if (
        state.type === 'password' &&
        !fieldValue.match(state.validator.passwordRegex)
      )
        return false;

      //  ** EMAIL INPUTS (if type="email")
      if (
        state.type === 'email' &&
        !fieldValue.match(state.validator.mailRegex)
      )
        return false;

      //  ** TEL INPUTS (if type="tel")
      if (state.type === 'tel' && !fieldValue.match(state.validator.phoneRegex))
        return false;

      // ** TEXT (if [data-validation="text"]
      if (
        state.validator.validationType &&
        state.validator.validationType === 'text' &&
        !fieldValue.match(state.validator.textRegex)
      )
        return false;

      // ** ALPHANUMERIC (if [data-validation="alphanumeric"]
      if (
        state.validator.validationType &&
        state.validator.validationType === 'alphanumeric' &&
        !fieldValue.match(state.validator.alphanumericRegex)
      )
        return false;
  
      // *** @as ALPHANUMERIC-SLASH-DOT-DASH ./-
      if (state.validator.validationType &&
          state.validator.validationType === 'alphanumeric_slash_dot_dash' &&
          !fieldValue.match(state.validator.alphanumericSlashDotDashRegex)
          ) {
        return false;
      }

      // ** CONFIRM FIELDS (eg Confirm email [data-validation-confirm="email"]
      if (state.validator.validationConfirmType) return isConfirmValid(true);
      // ** CONFIRM NOT FIELDS (eg Old/New password [data-validation-confirm-not="OldPassword"]
      if (state.validator.validationConfirmNotType)
        return isConfirmValid(false);

      // ** CHECKBOXES (if type="checkbox")
      if (state.type === 'checkbox' && els.field.checked === false)
        return false;
    }

    // *** @as YEAR YYYY FORMAT ./-
    if (state.validator.validationType &&
      state.validator.validationType === 'year_yyyy' && fieldValue) {
        const today = new Date();
        const currentYear = today.getUTCFullYear();
        if(!fieldValue.match(state.validator.yearYYYYFormatRegex) || +fieldValue < currentYear - 100) {
          return false;
        }
    }
    return true;
  };

  const isConfirmValid = (which) => {
    const targetElement =
      which === true
        ? els.form.querySelector(
            `[name=${state.validator.validationConfirmType}]`
          )
        : els.form.querySelector(
            `[name=${state.validator.validationConfirmNotType}]`
          );
    if (targetElement) {
      const targetElementValue = targetElement.value;
      if (which === true && els.field.value === targetElementValue) return true;
      if (which === false && els.field.value !== targetElementValue)
        return true;
    }
    return false;
  };

  const updateAndBroadcast = () => {
    updateState();
    updateErrorStatus();
    broadcastElementChange();
  };

  const getData = async () => {
    const fieldName = state.name;
    const fieldValue = els.field.value.trim();
    const fetchOptions = {
      datasource: `${state.validator.api}`,
    };
    let requestParams = {};
    requestParams[fieldName] = fieldValue;
    const data = await props.fetchData(fetchOptions, requestParams);
    if (data.payload.Success && data.payload.Success === true) {
      return true;
    } else {
      // set the server error msg if it exists
      if (els.errorDiv && data.payload.StatusMessage) {
        els.errorDiv.innerHTML = data.payload.StatusMessage;
      }
      return false;
    }
  };

  return {
    init,
  };
};

const sharedComponentScripts = {
  types: {
    ['RenderListFromApiOnEvent']: RenderListFromApiOnEvent,
    ['FormController']: FormController,
    ['FormElement']: FormElement,
  },

  examine: (scriptName) => {
    const script = sharedComponentScripts.types[scriptName];
    if (!script) {
      console.warn(
        '/sharedComponentScripts/ -examine',
        scriptName,
        'not found'
      );
      return;
    }
    return script;
  },
};

const DebugWindow = (signal) => {
  const els = {};

  const classes = {
    main: 'debug-window',
    trigger: 'alchemy-footer',
  };

  const init = (openByDefault) => {
    
    console.warn('/DebugWindow/ -init', openByDefault);
    
    els.trigger = document.querySelector(`.${classes.trigger}`);
    build();

    const mc = new Hammer.Manager(els.trigger);
    mc.add(new Hammer.Tap());
    mc.add(new Hammer.Tap({ event: 'quadrupletap', taps: 4 }));
    mc.get('quadrupletap').recognizeWith('tap');

    mc.on('tap quadrupletap', (e) => {
      if (e.type === 'quadrupletap') {
        // build();
        els.el.style.display = 'block';
      }
    });
    
    if (openByDefault) els.el.style.display = 'block';
    // build();
  };

  const build = () => {
    els.el = document.createElement('textarea');
    els.el.classList.add(classes.main);
    els.el.style.display = 'none';
    
    document.body.append(els.el);

    update('>hello alchemist!');

    // *** add anything you want to log here
    addListeners(config.eventNames.APP_RESIZE_START);
    addListeners(config.eventNames.APP_BREAKPOINT_READY);
    // addListeners(config.eventNames.BEHAVIOUR_BINDING_SIGNAL);
  };

  const addListeners = (eventName) => {
    signal.on(eventName, (payload) => {
      const r = `${eventName} : ${JSON.stringify(payload)}`;
      update(r);
    });
  };

  const update = (msg) => {
    els.el.value = els.el.value + '\r\n' + msg;
    els.el.scrollTop = els.el.scrollHeight;
  };
  
  return {
    init,
    update,
  };
};

/**
 * config scroll fades
 */
const fadeConfig = {
  
  // ***  for intersection observers - lower value is ~ more sensitive
/*  thresholds: {
    'default': 0.4,
    'cardstack': 0.1,
    'carousel': 0.8,
    'timeline': 0.2,
  },*/
  
  ignoreTypes: [
    'offset-anchor',
    'hero-carousel',
    'heroBanner',
    // 'carousel-control-strip',
    'spinner',
    'homepage-tabs',
  ],
  
  animation: {
    baseTime: config.timings.animation.medium() / 1.3,
    baseEase: 'Expo.inout',
    
    useYOffset: false, // *** not very performant
    useScaleIn: true,
    useCardStagger: true,
    
    yOffset: () => {
      return 0;
    },
    scaleFrom: () => {
      return 0.95 ;
    },
    cardStaggerDivisor: () => {
      return 24 ; // *** high value invisible
    },
    carouselStaggerDivisor: () => {
      return 8 ; // *** high value invisible
    },
  }
  
};

const DefaultFade2 = (options) => {
  const classes = {
    cardstack: 'cardstack',
    carousel: 'carousel',
    carouselPage: 'carousel__page',
    carouselItem: 'carousel__item',

    tabstack: 'tabstack',
  };

  const attributes = {
    index: 'data-animation-index',
    transformFlag: 'data-animation-transform',
  };

  // *** checks against @data-component for known dynamic component names
  const asyncAttributes = ['RenderListFromApiOnEvent'];

  const state = {
    isAsync: false,
    useRerun: false, // *** keeps active, i.e. hides and reshows
    items: [],
    cardstackItems: null,
    carouselItems: null,
  };

  const init = () => {
    // console.log('/DefaultFade2/ -init >>>', options);

    checkAsync();

    if (state.isAsync) return;
    build();
  };

  const build = () => {
    state.items.push(preflight()); // *** prep incoming stuff

    if (state.cardstackItems) state.items.push(state.cardstackItems);
    if (state.carouselItems) state.items.push(state.carouselItems);

    state.items = state.items.flat();

    state.items.map((item) => {
      setItem(item);
      addObserver(item);
    });
  };

  /**
   * determine if this is likely an async component, and if so use a mutation observer to wait until items are present
   */
  const checkAsync = () => {
    if (!options.parent || !options.children) return;
    const attr = options.parent.getAttribute('data-component');
    if (!attr) return;

    const isAsync = asyncAttributes.find((item) => {
      return attr.includes(item);
    });

    if (!isAsync) return;
    state.isAsync = isAsync;

    //console.log('/DefaultFade2/ -checkAsync', !!isAsync);

    // *** mutation observer
    const observer = new MutationObserver((entries) => {
      if (!!entries) {
        //console.log('/DefaultFade2/ -ASYNC READY');
        // a check for contact card icons which change the dom
        if (
          entries.length === 1 &&
          entries[0].target.parentElement.className === 'contact-content'
        )
          return;
        build();
      }
    });
    observer.observe(options.parent, { subtree: true, childList: true });
  };

  const processCardstack = (item) => {
    const wrappedItems = [];
    const items = [...item.children];
    const numItems = items.length;
    let stagger = fadeConfig.animation.cardStaggerDivisor();
    if (numItems < 6) {
      stagger = fadeConfig.animation.carouselStaggerDivisor();
    }
    
    const rowLength = calcRowLength(item, items[0]);

    items.map((el, index) => {
      el.setAttribute(attributes.index, index);
      el.setAttribute(attributes.transformFlag, 'none');

      wrappedItems.push({
        el,
        type: 'cardstack',
        stagger,
        fastStagger: stagger / 1.5, // *** when fast scrolling
        rowLength,
      });
    });

    if (items.length > 0) {
      if (!state.cardstackItems) state.cardstackItems = [];
      state.cardstackItems.push(wrappedItems);
      state.cardstackItems = state.cardstackItems.flat();
    }
    // console.log('/DefaultFade2/ -processCardstack', state.cardstackItems);
  };
  
  // *** calc num items per row
  const calcRowLength = (parent, sample) => {
    if (!parent || !sample) return null;
    const w1 = parent.getBoundingClientRect().width;
    const w2 = sample.getBoundingClientRect().width;
    return Math.floor(w1 / w2);
  };

  const processCarousel = (item) => {
    const wrappedItems = [];

    const pages = [...item.querySelectorAll(`.${classes.carouselPage}`)];
    let items = null;

    // *** only animate visible carousel items, i.e. first 'page'
    if (pages && pages[0]) {
      items = [...pages[0].querySelectorAll(`.${classes.carouselItem}`)];
    }

    if (!items) return;

    items.map((el, index) => {
      el.setAttribute(attributes.index, index);
      el.setAttribute(attributes.transformFlag, 'none');
      wrappedItems.push({
        el,
        type: 'carousel',
        stagger: fadeConfig.animation.carouselStaggerDivisor(),
        fastStagger: fadeConfig.animation.carouselStaggerDivisor(), // *** when fast scrolling
      });
    });

    if (items.length > 0) {
      if (!state.carouselItems) state.carouselItems = [];
      state.carouselItems.push(wrappedItems);
      state.carouselItems = state.carouselItems.flat();
    }
  };

  const preflight = () => {
    const plainItems = [];

    options.children.filter((item) => {
      let passThru = true;

      const carousel = item.querySelector(`.${classes.carousel}`);
      const cardstacks = [...item.querySelectorAll(`.${classes.cardstack}`)];

      if (item.classList.contains(classes.tabstack)) {
        item.setAttribute(attributes.transformFlag, 'none');
      }

      if (item.classList.contains(classes.carousel)) {
        processCarousel(item);
        passThru = false;
      }

      if (carousel) {
        processCarousel(carousel);
        passThru = false;
      }

      if (item.classList.contains(classes.cardstack)) {
        processCardstack(item);
        passThru = false;
      }

      if (cardstacks.length > 0) {
        cardstacks.map((cardstack) => {
          processCardstack(cardstack);
        });
        passThru = false;
      }

      if (passThru) {
        plainItems.push({
          el: item,
          type: 'plain',
          jitter: true,
        });
        return item;
      }
    });

    return plainItems;
  };

  const setItem = (item) => {
    const el = item.el;

    // console.log('/DefaultFade2/ -setItem XXX', item);

    const transformFlag = el.getAttribute(attributes.transformFlag);
    if (!transformFlag) {
      el.setAttribute(attributes.transformFlag, 'unset');
    }

    if (transformFlag === 'none') {
      gsapWithCSS$1.set(el, {
        opacity: 0,
      });
      return;
    }

    gsapWithCSS$1.set(el, {
      opacity: 0,
      y: fadeConfig.animation.yOffset(),
      scale: fadeConfig.animation.scaleFrom(),
    });
  };
  

  const showItem = (item) => {
    const el = item.el;

    const transformFlag = el.getAttribute(attributes.transformFlag);

    const velocity = options.getVelocity().velocity;
    const baseDelay = 1 / (velocity + 1) / 2;
    let time = fadeConfig.animation.baseTime; // - (velocity / 2);
    let delay = baseDelay;
    let predelay = 0;

    // *** stagger if required
    let index = el.getAttribute(attributes.index);
    if (index) {
    
      const mod = item.rowLength || 4; // TODO this effectively relates to cardstack rows, add breakpoint awareness to guess at row length (not really critical)
      predelay = (index % mod) / (item.stagger || 1);
    }

    if (item.jitter) {
      const jitter = general.randomIntegerRange(0, 3) / 16;
      // console.log('/DefaultFade2/ -showItem  ==========', jitter);

      time = time + jitter;
      predelay = predelay + jitter;
    }

    if (velocity > 1) {
      // *** fast scrolling, make it snappier
      delay = 0;
      time = time / 1.5;
      if (item.fastStagger) predelay = predelay / (item.fastStagger || 1); // *** reduce stagger
    }

    if (options.isMobile) {
      // *** simplify for mobile
      delay = baseDelay / 4;
      predelay = 0;
    }

    // console.log('/DefaultFade2/ -showItem', item);

    if (transformFlag === 'none') {
      gsapWithCSS$1.to(el, time, {
        opacity: 1,
        delay: predelay + delay,
        onComplete: () => {
          // *** clear anim attributes?
        },
      });
      return;
    }

    gsapWithCSS$1.to(el, time, {
      opacity: 1,
      y: 0,
      scale: 1,
      delay: predelay + delay,
      onComplete: () => {
        // *** clear anim attributes?
      },
    });
  };

  const addObserver = (item) => {
    let threshold = 0.1;

    const height = item.el.getBoundingClientRect().height;
    if (height > window.innerHeight) {
      threshold = 0.01; // *** tall items need lower threshold
    }

    const observer = new IntersectionObserver(
      (entries) => {
        const entry = entries[entries.length - 1];
        if (!entry.isIntersecting) ; else {
          showItem(item);
          observer.disconnect(); // *** forget about this item
        }
      },
      {
        root: null,
        threshold,
      }
    );

    observer.observe(item.el);
  };

  return {
    init,
  };
};

/**
 * entry point for scroll fading
 * @return {{init: init}}
 * @constructor
 */
const ScrollFader = () => {
  const els = {
    inner: null,
  };

  const classes = {
    mobileBreakpoint: 'break--mobile',
  
    alchemy: 'alchemy',
    viewport: 'viewport__inner',
    isInitialising: 'viewport--is-initialising',
    // isUsingScrollFader: 'feature--scroll-fader',
  };
  
  const attributes = {
    marked: 'data-animation-marked',
  };
  
  const state = {
    items: [],
    isMobile: document.body.classList.contains(classes.mobileBreakpoint), // *** not much point using events for this, just set it on load
  };
  

  const init = () => {
    //console.log('/ScrollFader/ -init', state);

    els.inner = document.body.querySelector(`.${classes.viewport}`);
    state.items = [...els.inner.querySelectorAll(`.${classes.alchemy}`)];
    
    // *** mark alchemy items as added...
    state.items.map((item, index) => {
      item.setAttribute(attributes.marked, `alchemy:${index}`);
    });
    
    // ***  and inspect homepage tab items (that haven't already been marked)
    const tabContentItems = [...els.inner.querySelectorAll('.tab-content--panel')];
    let extraItems = tabContentItems.map((item) => {
      return [...item.children].filter((child, index) => {
        if (!child.hasAttribute(attributes.marked)) {
          child.setAttribute(attributes.marked, `tab-content:${index}`);
          return child;
        }
      })
    });
  
    // *** and then munge it all together - should capture more items for animation
    extraItems = extraItems.flat();
    state.items = [...state.items, ...extraItems];
    
    // *** kick off dom examination
    state.items.map((item, index) => {
      examine(item);
    });

    scrollVelocity.init();
  };

  const examine = (item) => {
    if (checkType(item).ignoreType) {
      // console.log('/ScrollFader/ -examine SKIP:', item);
      return;
    }
    
    let take = {
      // *** output object
      parent: item,
      children: null,
      level: null,
      getVelocity: scrollVelocity.get,
      isMobile: state.isMobile,
    };

    const exec = (el, level = 0) => {
      let tick = level;
      const maxDepth = 5;
      const children = [...el.children];
      
      // console.log('/ScrollFader/ -exec', tick);
      
      if (tick > 2) {
        console.warn('/ScrollFader/ -exec --found overly deeply nested component', el, 'depth:', tick);
      }

      // *** only accept good children
      const cleanChildren = children.filter((child) => {
        // console.log('/ScrollFader/ -CHILD', child);
        if (!checkType(child).ignoreType) {
          return child;
        }
      });

      // *** accept group if it looks like there are enough to use
      if (cleanChildren.length > 1) {
        take.children = cleanChildren;
        take.level = tick;
        return;
      }

      // *** looks like the children are hiding in a box
      if (cleanChildren.length === 1) {
        const deepChildren = [...cleanChildren[0].children];

        // *** when enough to use
        if (deepChildren.length > 1) {
          tick += 1;
          take.children = deepChildren;
          take.level = tick;
          return;
        }

        // *** another box! have a look inside
        if (deepChildren.length === 1) {
          tick += 2;
          if (tick > maxDepth) return; // *** stop process
          exec(deepChildren[0], tick);
        }
      }
    };

    exec(item);

    if (!take.children) {
      console.warn(
        '/ScrollFader/ -examine --skipping malformed component',
        take
      );
      return;
    }

    const fade = DefaultFade2(take);
    fade.init();
  };

  /**
   * type checker
   * @param item
   * @return {{ignoreType: null, specialType: null}}
   */
  const checkType = (item) => {
    const vo = {
      ignoreType: null,
      specialType: null,
    };

    // *** check for ignore match
    vo.ignoreType = fadeConfig.ignoreTypes.find((type) => {
      return item.classList.contains(type);
    });
    if (vo.ignoreType) return vo; // *** quit
    
    return vo;
  };

  return {
    init,
  };
};

const HeroSingle = (props) => {
  const state = {
    type: null, // TODO e.g. heroBanner--large
    
    contentItems: [],
    hasImageLoaded: false,
    
    // *** counters to determine animation done
    numShowTweens: null,
    numHideTweens: null,
    numShowTweensComplete: null,
    numHideTweensComplete: null,
    
    showAnimationBaseTime: config.timings.animation.longest(),
    hideAnimationBaseTime: config.timings.animation.medium(),
    contentStaggerDivisor: 16,
    
    //mobileTimeDivisor: 2, // TODO - maybe speed up on mobile?
  };
  
  const els = {
    el: null,
    shapes: null,
    image: null,
    headings: [],
    paragraphs: [],
    buttons: [],
    icons: [],
    selectBox: null,
  };
  
  const classes = {
    shapes: 'shapes__group',
    enableAnimationFlag: 'feature--hero-animation',
    image: 'image-wrapper',
    button: 'cta-button',
    icon: 'icon',
    selectBox: `[data-behaviour="hero-select-box-extender"`,
    selectBoxAlt: `dropdown__button`,
  };
  
  const init = () => {
    // *** check enabled
    if (!document.body.classList.contains(classes.enableAnimationFlag)) return;
    
    els.el = props.el;
    els.shapes = els.el.querySelector(`.${classes.shapes}`);
    els.image = els.el.querySelector(`.${classes.image}`);
    
    // *** find headings h1-6
    for (let i = 1; i < 7; i++) {
      const item = els.el.querySelector(`h${i}`);
      if (item) els.headings.push(item);
    }
    els.headings.reverse();
    
    // *** other content elements
    els.paragraphs = [...els.el.querySelectorAll('p')];
    els.buttons = [...els.el.querySelectorAll(`.${classes.button}`)];
    els.icons = [...els.el.querySelectorAll(`.${classes.icon}`)];
    els.selectBox = props.el.querySelector(classes.selectBox);
    if (!els.selectBox) { // *** SMIT-2688, catch 'find a surgeon' alternative impl.
      els.selectBox = props.el.querySelector(`.${classes.selectBoxAlt}`);
    }
    
    
    // nb filter out any buttons that are disabled as we shouldnt set opacity on those..
    els.buttons = els.buttons.filter(
        (item) => !item.classList.contains('cta-button--is-disabled')
    );

    // *** munge all into flat array for ease
    state.contentItems = [
      els.headings,
      els.paragraphs,
      els.icons,
      els.buttons,
      els.selectBox,
    ].flat();
    
    // *** overwrite defaults (from hero carousel)
    state.showAnimationBaseTime = props.showAnimationBaseTime
        ? props.showAnimationBaseTime
        : state.showAnimationBaseTime;
    state.hideAnimationBaseTime = props.hideAnimationBaseTime
        ? props.hideAnimationBaseTime
        : state.hideAnimationBaseTime;
    state.contentStaggerDivisor = props.contentStaggerDivisor
        ? props.contentStaggerDivisor
        : state.contentStaggerDivisor;
    
    if (props.auto) triggerAnimation(); // *** auto true for single heroes, false for hero carousel items which will invoke show/hide
    
    // *** on breakpoint change
    props.signal.on(config.eventNames.APP_BREAKPOINT_READY, () => {
      clearInlineStyles();
    });
  };
  
  // *** inline styles set by animation break responsiveness, clear on resize
  const clearInlineStyles = () => {
    state.contentItems.map((item) => {
      if (item) item.style.removeProperty('transform');
    });
    if (els.image) els.image.style.removeProperty('transform');
    if (els.shapes) els.shapes.style.removeProperty('transform');
    if (els.shapes && els.shapes.children) {
      [...els.shapes.children].map((item) => {
        if (item) item.style.removeProperty('transform');
      });
    }    
  };
  
  // *** for heroCarousel control
  const show = () => {
    triggerAnimation();
  };
  
  // *** for heroCarousel control
  const hide = () => {
    const time = state.hideAnimationBaseTime;
    
    gsapWithCSS$1.to(els.el, time * 2, {
      scale: 0.9,
      opacity: 0,
      transformOrigin: 'top',
      ease: 'power2.out',
      delay: 0,
      onStart: () => (state.numHideTweens += 1),
      onComplete: () => {
        registerHideTweenComplete();
      },
    });
    
    // *** @as prepare items to hide, defence against SMIT-3486
    const getItems = () => {
      const keys = ['image', 'shapes', 'buttons', 'paragraphs'];
      let result = [];
      
      keys.map((key) => {
        const item = els[key];
  
        if (item) {
          const isArray = Array.isArray(item);
          if (!isArray) result.push(item);
          if (isArray && item.length > 0) result.push(item);
        }
        return false;
      });
      result = result.flat();
      //console.log('/HeroSingle/ -hide --getItems:', keys, result);
      return result;
    };
    
    gsapWithCSS$1.to(
        getItems(),
        time / 1.5,
        {
          opacity: 0,
          onStart: () => (state.numHideTweens += 1),
          onComplete: () => {
            registerHideTweenComplete();
          },
        }
    );
  };
  
  // *** invoke optional callback (hero carousel)
  const registerShowTweenComplete = (debugMsg) => {
    state.numShowTweensComplete += 1;
    // console.log('/HeroSingle/ -registerShowTweenComplete', state.numShowTweensComplete, debugMsg);
    if (state.numShowTweensComplete === state.numShowTweens) {
      // console.log('/HeroSingle/ -registerShowTweenComplete DONE');
      if (props.showCompleteFn) props.showCompleteFn({ index: props.index });
    }
  };
  
  // *** invoke optional callback (hero carousel)
  const registerHideTweenComplete = () => {
    state.numHideTweensComplete += 1;
    if (state.numHideTweensComplete === state.numHideTweens) {
      if (props.hideCompleteFn) props.hideCompleteFn({ index: props.index });
    }
  };
  
  const triggerAnimation = () => {
    // *** resets for hero carousel context
    gsapWithCSS$1.set(els.el, {
      opacity: 1,
      scale: 1,
      backgroundColor: '#f6f8f9',
    });
    let items = [];
    if (els.image) items.push(els.image);
    if (els.shapes) items.push(els.shapes);
    if (els.buttons.length > 0) {
      items.push(els.buttons);
    }
    if (els.paragraphs.length > 0) items.push(els.paragraphs);
    
    if (items.length === 0) {
      console.warn('/HeroSingle/ -triggerAnimation --nothing to animate');
      return;
    }
    
    //console.log('/HeroSingle/ -triggerAnimation', els.el);
    
    gsapWithCSS$1.set(items, {
      opacity: 1,
    });
    // ...
    
    if (els.shapes) tweenShapes();
    tweenContentItems();
    
    // *** find image and tween on image loaded
    if (els.image) {
      if (state.hasImageLoaded) {
        tweenImage();
        return;
      }
      
      const imageLoadDetector = ImageLoadDetector();
      imageLoadDetector.watch({
        notify: 'last-of',
        els: [els.image],
        uid: null,
      });
      imageLoadDetector.events.on(imageLoadDetector.LOADED, () => {
        tweenImage();
        state.hasImageLoaded = true;
        imageLoadDetector.events.off(imageLoadDetector.LOADED);
      });
    }
  };
  
  const tweenContentItems = () => {
    const time = state.showAnimationBaseTime;
    const predelay = state.showAnimationBaseTime * 1.3;
    const fromX = '+7vw';
    const staggerDivisor = state.contentStaggerDivisor;
    
    const tween = (el, t, d) => {
      gsapWithCSS$1.from(el, t, {
        opacity: 0,
        x: fromX,
        delay: predelay + d,
        ease: 'expo.out',
        overwrite: true,
        onStart: () => (state.numShowTweens += 1),
        onComplete: () => {
          registerShowTweenComplete();
        },
      });
    };
    
    state.contentItems.map((item, index) => {
      if (item) {
        tween(item, time, index / staggerDivisor);
      }
    });
  };
  
  const tweenImage = () => {
    const time = state.showAnimationBaseTime;
    const predelay = state.showAnimationBaseTime;
    
    gsapWithCSS$1.from(els.image, time, {
      opacity: 0,
      x: '10%',
      ease: 'power3.out',
      delay: predelay,
      overwrite: true,
      onStart: () => (state.numShowTweens += 1),
      onComplete: () => {
        registerShowTweenComplete();
      },
    });
  };
  
  const tweenShapes = () => {
    const items = [...els.shapes.children];
    
    const time = state.showAnimationBaseTime;
    const predelay = state.showAnimationBaseTime / 2;
    
    gsapWithCSS$1.from(els.shapes, time, {
      y: '+33vh',
      x: '-10vw',
      ease: 'back.out',
      delay: predelay,
      overwrite: true,
      onStart: () => (state.numShowTweens += 1),
      onComplete: () => {
        registerShowTweenComplete();
      },
    });
    
    items.map((item, index) => {
      const y = `-${(index + 1) * 33}vh`;
      const rotation = `-${(index + 1) * 22}deg`;
      const delay = predelay + index / 8;
      
      gsapWithCSS$1.from(item, time, {
        opacity: 0,
        y,
        rotation,
        ease: 'power3.out',
        delay,
        overwrite: true,
        onStart: () => (state.numShowTweens += 1),
        onComplete: () => {
          registerShowTweenComplete();
        },
      });
    });
  };
  
  return {
    init,
    show,
    hide,
  };
};

/**
 * drives hero carousel animation, not carousel functionality!
 * @param props
 * @return {{init: init}}
 * @constructor
 */
const HeroCarousel = (props) => {
  const classes = {
    heroCarousel: 'hero-carousel',
    heroBanner: 'heroBanner',
    carousel: 'carousel',
    carouselControlStrip: 'carousel-control-strip',
    heroCarouselHasInitialised: 'hero-carousel--has-initialised',
  };

  const state = {
    items: [], // *** heroes
    carouselUid: null,
    currentIndex: 0,
    prevIndex: null,
    animationStates: [],
    preventAnimation: false,

    carouselStateObject: null,
  };

  const els = {
    el: null,
    carousel: null,
    carouselControlStrip: null,
  };

  const init = () => {
    els.el = props.el;

    els.carousel = els.el.querySelector(`.${classes.carousel}`);
    els.carouselControlStrip = els.el.querySelector(
      `.${classes.carouselControlStrip}`
    );
    state.carouselUid = els.carousel.getAttribute('data-behaviour-uid');

    const instance = behaviours.getBehaviourByUid(state.carouselUid);

    // *** settings for carousel
    state.carouselStateObject = instance.options.state;
    state.carouselStateObject.defaultEase = 'power2.out';
    state.carouselStateObject.deferUpdateTime = 275; // *** ms
  
    setTimeout(() => {
      exec();
      els.el.classList.add(classes.heroCarouselHasInitialised);
      window.debugWindow.update(`/HeroCarousel/ START ${els.el}`);
    }, config.features.scrollFader.initDelay); // TODO lose this timeout
    
    // *** CAROUSEL_READY arrives too early for this when removing timeout in carousel, can hopefully fix with better app init pattern
    /*console.log('/HeroCarousel/ -init WAITING');
    props.signal.on(config.eventNames.CAROUSEL_READY, (payload) => {
      console.log('/HeroCarousel/ -CAROUSEL_READY');
      exec();
    });*/

    // *** init heroes
    const exec = () => {
      const heroes = [
        ...els.carousel.querySelectorAll(`.${classes.heroBanner}`),
      ];

      heroes.map((item, index) => {
        const hero = HeroSingle({
          el: item,
          index,
          signal: props.signal,
          auto: false, // *** stops hero running onload
          // *** opportunity to modify speed in hero carousel context:
          showAnimationBaseTime: config.timings.animation.longest() / 1.3,
          contentStaggerDivisor: 48,
          // *** animation state callbacks
          showCompleteFn: (args) => {
            setHeroAnimationState({
              index: args.index,
              type: 'show',
              state: 'end',
            });
          },
          hideCompleteFn: (args) => {
            setHeroAnimationState({
              index: args.index,
              type: 'hide',
              state: 'end',
            });
          },
        });
        hero.init();
        state.items.push(hero);
        state.animationStates.push({});
      });

      // *** this won't catch the event on first fire...
      props.signal.on(config.eventNames.BEHAVIOUR_BINDING_SIGNAL, (payload) => {
        if (payload.sender !== state.carouselUid) return; // *** defeat

        if (payload.type === actions.CAROUSEL_WILL_UPDATE) {
          hide({
            index: state.currentIndex,
            direction: payload.data.direction,
          });
        }

        if (payload.type === actions.CAROUSEL_UPDATING) {
          show(payload.data);
        }
      });

      // *** ...so force update here
      show({ index: 0, direction: null, endStop: 'start' }); // *** trigger first hero
      els.el.classList.add(classes.heroCarouselHasInitialised); // *** smooth first run
    };
  };

  // *** records little matrix of anim start/end to set state
  const setHeroAnimationState = (args) => {
    state.animationStates[args.index] = args;
    const current = state.animationStates[state.currentIndex];
    state.preventAnimation = current.state === 'start';
    state.carouselStateObject.lockNavigation = state.preventAnimation; // *** chokes carousel while animation in progress

    updateControlsUI();
  };

  // *** fades arrows in/out while carousel choked
  const updateControlsUI = () => {
    gsapWithCSS$1.to(els.carouselControlStrip, config.timings.animation.short(), {
      opacity: +!state.preventAnimation,
    });
  };

  const show = (vo) => {
    const item = state.items[vo.index];
    state.currentIndex = vo.index;
    if (state.currentIndex === state.prevIndex) return; // *** defeat
    state.prevIndex = state.currentIndex;

    item.show();
    setHeroAnimationState({ index: vo.index, type: 'show', state: 'start' });
  };

  const hide = (vo) => {
    const item = state.items[vo.index];
    if (state.currentIndex === 0 && vo.direction === 'prev') return; // *** defeat
    if (
      state.currentIndex === state.items.length - 1 &&
      vo.direction === 'next'
    )
      return; // *** defeat

    item.hide();
    setHeroAnimationState({ index: vo.index, type: 'hide', state: 'start' });
  };

  return {
    init,
  };
};

/**
 * inspect heroes on the page (either single or part of hero carousel)
 * @return {{init: init}}
 * @constructor
 */

const HeroBannerAnimator = () => {
  
  const classes = {
    heroCarousel: 'hero-carousel',
    heroBanner: 'heroBanner',
    heroCarouselHasInitialised: 'hero-carousel--has-initialised',
  };
  
  const state = {
    component: null,
  };
  
  const components = {
    single: HeroSingle,
    carousel: HeroCarousel,
  };
  
  const init = (signal) => {
    
    const heroCarousel = document.body.querySelector(`.${classes.heroCarousel}`);
    
    // *** hero carousel fork
    if (heroCarousel) {
      state.component = components.carousel({
        el: heroCarousel,
        signal,
      });
      state.component.init();

    } else {
      
      // *** regular hero fork
      const heroBanner = document.body.querySelector(`.${classes.heroBanner}`);
      if (heroBanner) {
        state.component = components.single({
          el: heroBanner,
          signal,
          auto: true,
        });
        state.component.init();
      }
    }
  };
  
  return {
    init,
  }
};

const mediator = {
  props: {
    els: {},
    components: {
      instantiated: [], // *** WIP
      scripts: [], // *** only child scripts, shared scripts (use: xxx) not stored here
    },
    classes: {
      isInitialising: 'viewport--is-initialising',
      isUsingScrollFader: 'feature--scroll-fader',
    },
  },
  
  init: () => {
    // *** some processes choke on unclassed divs from the CMS, so ensure they have a class
    console.time('divs');
    const divs = [...document.body.querySelectorAll('div')];
    divs.map((item) => {
      if (!item.getAttribute('class')) {
        item.classList.add('class-unset');
      }
    });
    //console.log('/mediator/ -init --processed unclassed divs in:');
    //console.timeEnd('divs');
    //console.log('\n');
    
    // *** clear any added #anchors on startup
    // @ja changed to href from pathname to re-instate hashes
    // history.replaceState('', document.title, window.location.href);
  
    // *** possible fix for weird safari behaviour: https://gomakethings.com/fixing-safaris-back-button-browser-cache-issue-with-vanilla-js/
    /*window.onpageshow = function(e) {
      if (e.persisted) {
        console.log('/mediator/ -onpageshow RELOAD');
        window.location.reload();
      }
    };*/
    
    // *** always start at the top - TODO check this doesn't break anchors
    if ('scrollRestoration' in window.history) window.history.scrollRestoration = 'manual';
    window.scrollTo(0, 0);
    
    // *** NOTE disabled this, but contains some useful functionality
    /*cookieManager.init();
    cookieManager.setRoute();*/
  
    networkListener.init(signal); // *** needs to live at the top to catch early requests
    mediator.injectBehaviours();
    mediator.initComponents();
    resizeNotifier.init(signal);
    mediaQueries.init(signal);

    mediator.getPageAttributes();  
    analytics.init(signal);
    analytics.examine();
    general.lazyLoadImages();
    // *** REMOVE UNLESS TESTING ***
    if (config.features.debugging.debugWindow.isEnabled) {
      window.debugWindow = DebugWindow(signal);
      window.debugWindow.init(config.features.debugging.debugWindow.launchOnPageLoad);
    } else {
      window.debugWindow = function(){};
      window.debugWindow.update = function(){}; // *** calls to update won't break when debug is disabled
    }
    
    const heroBannerAnimator = HeroBannerAnimator();
    heroBannerAnimator.init(signal);
    
    // *** scrollfader is enabled by presence of 'feature--scroll-fader' class on body
    if (
        document.body.classList.contains(
            mediator.props.classes.isUsingScrollFader
        )
    ) {
      setTimeout(() => {
        // FIXIT
        const scrollFader = ScrollFader();
        scrollFader.init();
        document.body.classList.remove(mediator.props.classes.isInitialising);
        // at this point bellow forth and tell carousel to get started
        // signal.emit(config.eventNames.PREPARE_CAROUSEL);
      }, config.features.scrollFader.initDelay);
    } else {
      document.body.classList.remove(mediator.props.classes.isInitialising);
      // setTimeout(() => {
      //   // the carousel is waiting for this call to init itself
      //   signal.emit(config.eventNames.PREPARE_CAROUSEL);
      // }, config.features.scrollFader.initDelay);
    }
  },
  
  // *** get attributes on body tag and shift them off to handler
  // TODO use same format as other helpers and remove this from here
  getPageAttributes: () => {
    pageState.init(signal);
    pageState.examine(
        general.attributeParser(
            document.body.getAttribute('data-page-attributes')
        )
    );
    pageState.examine(
        general.attributeParser(
            document.body.getAttribute('data-page-referrer')
        )
    );
  },
  
  /**
   * method to allow components with their own js to register to the core
   * @param object
   */
  addScript: (script) => {
    let isValid = true;
    
    mediator.props.components.scripts.find((item) => {
      if (item.name === script.name) {
        isValid = false;
      }
    });
    
    if (!isValid) {
      console.error(
          '/mediator/ -addScript *ERROR* script already exists! Please use a unique name',
          `[${script.name}]`
      );
      return;
    }
    
    mediator.props.components.scripts.push(script);
  },
  
  retrieveScript: (name) => {
    if (!name) return null;
    
    const scripts = mediator.props.components.scripts;
    let result = null;
    
    scripts.filter((item) => {
      if (item.name === name) {
        result = item.script;
        return true;
      }
    });
    
    return result;
  },
  
  /**
   * find core behaviours requested by components, e.g. carousel, and delegate to behaviour
   * Note - components need not have their own js to utilise behaviours
   */
  injectBehaviours: () => {
    [...document.body.querySelectorAll('[data-behaviour]')].map(
        (item, index) => {
          behaviours.examine(item, signal);
        }
    );
  },
  
  /**
   * components with their own js that have registered themselves are wrapped with `AbstractComponent`
   * and injected with events, global config etc
   * NB: Default is no dom el is passed in, but can also specify a dom element to search within
   */
  initComponents: (elementNode = null) => {
    const components = elementNode
        ? [...elementNode.querySelectorAll('[data-component]')]
        : [...document.body.querySelectorAll('[data-component]')];
    
    components.map((item, index) => {
      const name = item.getAttribute('data-component');
      const sharedScript = general.attributeParser(name);
      let script;
      
      if (!name.length) {
        console.warn(
            '/mediator/ -initComponents --empty @data-component, quitting process'
        );
        return;
      }
      
      if (Object.keys(sharedScript)[0] === 'use') {
        script = sharedComponentScripts.examine(sharedScript.use);
      } else {
        script = mediator.retrieveScript(name); // *** use regular child component script
      }
      
      item.setAttribute('data-uid', `alchemy-${index}`);
      
      const view = {
        class: AbstractComponent({
          index,
          name,
          script,
          config,
          el: item,
          events: signal,
        }),
      };
      view.class.init();
      mediator.props.components.instantiated.push(view);
      return view;
    });
  },
};

/**
 * Copyright (c) 2014-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

createCommonjsModule(function (module) {
var runtime = (function (exports) {

  var Op = Object.prototype;
  var hasOwn = Op.hasOwnProperty;
  var undefined$1; // More compressible than void 0.
  var $Symbol = typeof Symbol === "function" ? Symbol : {};
  var iteratorSymbol = $Symbol.iterator || "@@iterator";
  var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator";
  var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag";

  function define(obj, key, value) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
    return obj[key];
  }
  try {
    // IE 8 has a broken Object.defineProperty that only works on DOM objects.
    define({}, "");
  } catch (err) {
    define = function(obj, key, value) {
      return obj[key] = value;
    };
  }

  function wrap(innerFn, outerFn, self, tryLocsList) {
    // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator.
    var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator;
    var generator = Object.create(protoGenerator.prototype);
    var context = new Context(tryLocsList || []);

    // The ._invoke method unifies the implementations of the .next,
    // .throw, and .return methods.
    generator._invoke = makeInvokeMethod(innerFn, self, context);

    return generator;
  }
  exports.wrap = wrap;

  // Try/catch helper to minimize deoptimizations. Returns a completion
  // record like context.tryEntries[i].completion. This interface could
  // have been (and was previously) designed to take a closure to be
  // invoked without arguments, but in all the cases we care about we
  // already have an existing method we want to call, so there's no need
  // to create a new function object. We can even get away with assuming
  // the method takes exactly one argument, since that happens to be true
  // in every case, so we don't have to touch the arguments object. The
  // only additional allocation required is the completion record, which
  // has a stable shape and so hopefully should be cheap to allocate.
  function tryCatch(fn, obj, arg) {
    try {
      return { type: "normal", arg: fn.call(obj, arg) };
    } catch (err) {
      return { type: "throw", arg: err };
    }
  }

  var GenStateSuspendedStart = "suspendedStart";
  var GenStateSuspendedYield = "suspendedYield";
  var GenStateExecuting = "executing";
  var GenStateCompleted = "completed";

  // Returning this object from the innerFn has the same effect as
  // breaking out of the dispatch switch statement.
  var ContinueSentinel = {};

  // Dummy constructor functions that we use as the .constructor and
  // .constructor.prototype properties for functions that return Generator
  // objects. For full spec compliance, you may wish to configure your
  // minifier not to mangle the names of these two functions.
  function Generator() {}
  function GeneratorFunction() {}
  function GeneratorFunctionPrototype() {}

  // This is a polyfill for %IteratorPrototype% for environments that
  // don't natively support it.
  var IteratorPrototype = {};
  IteratorPrototype[iteratorSymbol] = function () {
    return this;
  };

  var getProto = Object.getPrototypeOf;
  var NativeIteratorPrototype = getProto && getProto(getProto(values([])));
  if (NativeIteratorPrototype &&
      NativeIteratorPrototype !== Op &&
      hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) {
    // This environment has a native %IteratorPrototype%; use it instead
    // of the polyfill.
    IteratorPrototype = NativeIteratorPrototype;
  }

  var Gp = GeneratorFunctionPrototype.prototype =
    Generator.prototype = Object.create(IteratorPrototype);
  GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype;
  GeneratorFunctionPrototype.constructor = GeneratorFunction;
  GeneratorFunction.displayName = define(
    GeneratorFunctionPrototype,
    toStringTagSymbol,
    "GeneratorFunction"
  );

  // Helper for defining the .next, .throw, and .return methods of the
  // Iterator interface in terms of a single ._invoke method.
  function defineIteratorMethods(prototype) {
    ["next", "throw", "return"].forEach(function(method) {
      define(prototype, method, function(arg) {
        return this._invoke(method, arg);
      });
    });
  }

  exports.isGeneratorFunction = function(genFun) {
    var ctor = typeof genFun === "function" && genFun.constructor;
    return ctor
      ? ctor === GeneratorFunction ||
        // For the native GeneratorFunction constructor, the best we can
        // do is to check its .name property.
        (ctor.displayName || ctor.name) === "GeneratorFunction"
      : false;
  };

  exports.mark = function(genFun) {
    if (Object.setPrototypeOf) {
      Object.setPrototypeOf(genFun, GeneratorFunctionPrototype);
    } else {
      genFun.__proto__ = GeneratorFunctionPrototype;
      define(genFun, toStringTagSymbol, "GeneratorFunction");
    }
    genFun.prototype = Object.create(Gp);
    return genFun;
  };

  // Within the body of any async function, `await x` is transformed to
  // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test
  // `hasOwn.call(value, "__await")` to determine if the yielded value is
  // meant to be awaited.
  exports.awrap = function(arg) {
    return { __await: arg };
  };

  function AsyncIterator(generator, PromiseImpl) {
    function invoke(method, arg, resolve, reject) {
      var record = tryCatch(generator[method], generator, arg);
      if (record.type === "throw") {
        reject(record.arg);
      } else {
        var result = record.arg;
        var value = result.value;
        if (value &&
            typeof value === "object" &&
            hasOwn.call(value, "__await")) {
          return PromiseImpl.resolve(value.__await).then(function(value) {
            invoke("next", value, resolve, reject);
          }, function(err) {
            invoke("throw", err, resolve, reject);
          });
        }

        return PromiseImpl.resolve(value).then(function(unwrapped) {
          // When a yielded Promise is resolved, its final value becomes
          // the .value of the Promise<{value,done}> result for the
          // current iteration.
          result.value = unwrapped;
          resolve(result);
        }, function(error) {
          // If a rejected Promise was yielded, throw the rejection back
          // into the async generator function so it can be handled there.
          return invoke("throw", error, resolve, reject);
        });
      }
    }

    var previousPromise;

    function enqueue(method, arg) {
      function callInvokeWithMethodAndArg() {
        return new PromiseImpl(function(resolve, reject) {
          invoke(method, arg, resolve, reject);
        });
      }

      return previousPromise =
        // If enqueue has been called before, then we want to wait until
        // all previous Promises have been resolved before calling invoke,
        // so that results are always delivered in the correct order. If
        // enqueue has not been called before, then it is important to
        // call invoke immediately, without waiting on a callback to fire,
        // so that the async generator function has the opportunity to do
        // any necessary setup in a predictable way. This predictability
        // is why the Promise constructor synchronously invokes its
        // executor callback, and why async functions synchronously
        // execute code before the first await. Since we implement simple
        // async functions in terms of async generators, it is especially
        // important to get this right, even though it requires care.
        previousPromise ? previousPromise.then(
          callInvokeWithMethodAndArg,
          // Avoid propagating failures to Promises returned by later
          // invocations of the iterator.
          callInvokeWithMethodAndArg
        ) : callInvokeWithMethodAndArg();
    }

    // Define the unified helper method that is used to implement .next,
    // .throw, and .return (see defineIteratorMethods).
    this._invoke = enqueue;
  }

  defineIteratorMethods(AsyncIterator.prototype);
  AsyncIterator.prototype[asyncIteratorSymbol] = function () {
    return this;
  };
  exports.AsyncIterator = AsyncIterator;

  // Note that simple async functions are implemented on top of
  // AsyncIterator objects; they just return a Promise for the value of
  // the final result produced by the iterator.
  exports.async = function(innerFn, outerFn, self, tryLocsList, PromiseImpl) {
    if (PromiseImpl === void 0) PromiseImpl = Promise;

    var iter = new AsyncIterator(
      wrap(innerFn, outerFn, self, tryLocsList),
      PromiseImpl
    );

    return exports.isGeneratorFunction(outerFn)
      ? iter // If outerFn is a generator, return the full iterator.
      : iter.next().then(function(result) {
          return result.done ? result.value : iter.next();
        });
  };

  function makeInvokeMethod(innerFn, self, context) {
    var state = GenStateSuspendedStart;

    return function invoke(method, arg) {
      if (state === GenStateExecuting) {
        throw new Error("Generator is already running");
      }

      if (state === GenStateCompleted) {
        if (method === "throw") {
          throw arg;
        }

        // Be forgiving, per 25.3.3.3.3 of the spec:
        // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume
        return doneResult();
      }

      context.method = method;
      context.arg = arg;

      while (true) {
        var delegate = context.delegate;
        if (delegate) {
          var delegateResult = maybeInvokeDelegate(delegate, context);
          if (delegateResult) {
            if (delegateResult === ContinueSentinel) continue;
            return delegateResult;
          }
        }

        if (context.method === "next") {
          // Setting context._sent for legacy support of Babel's
          // function.sent implementation.
          context.sent = context._sent = context.arg;

        } else if (context.method === "throw") {
          if (state === GenStateSuspendedStart) {
            state = GenStateCompleted;
            throw context.arg;
          }

          context.dispatchException(context.arg);

        } else if (context.method === "return") {
          context.abrupt("return", context.arg);
        }

        state = GenStateExecuting;

        var record = tryCatch(innerFn, self, context);
        if (record.type === "normal") {
          // If an exception is thrown from innerFn, we leave state ===
          // GenStateExecuting and loop back for another invocation.
          state = context.done
            ? GenStateCompleted
            : GenStateSuspendedYield;

          if (record.arg === ContinueSentinel) {
            continue;
          }

          return {
            value: record.arg,
            done: context.done
          };

        } else if (record.type === "throw") {
          state = GenStateCompleted;
          // Dispatch the exception by looping back around to the
          // context.dispatchException(context.arg) call above.
          context.method = "throw";
          context.arg = record.arg;
        }
      }
    };
  }

  // Call delegate.iterator[context.method](context.arg) and handle the
  // result, either by returning a { value, done } result from the
  // delegate iterator, or by modifying context.method and context.arg,
  // setting context.delegate to null, and returning the ContinueSentinel.
  function maybeInvokeDelegate(delegate, context) {
    var method = delegate.iterator[context.method];
    if (method === undefined$1) {
      // A .throw or .return when the delegate iterator has no .throw
      // method always terminates the yield* loop.
      context.delegate = null;

      if (context.method === "throw") {
        // Note: ["return"] must be used for ES3 parsing compatibility.
        if (delegate.iterator["return"]) {
          // If the delegate iterator has a return method, give it a
          // chance to clean up.
          context.method = "return";
          context.arg = undefined$1;
          maybeInvokeDelegate(delegate, context);

          if (context.method === "throw") {
            // If maybeInvokeDelegate(context) changed context.method from
            // "return" to "throw", let that override the TypeError below.
            return ContinueSentinel;
          }
        }

        context.method = "throw";
        context.arg = new TypeError(
          "The iterator does not provide a 'throw' method");
      }

      return ContinueSentinel;
    }

    var record = tryCatch(method, delegate.iterator, context.arg);

    if (record.type === "throw") {
      context.method = "throw";
      context.arg = record.arg;
      context.delegate = null;
      return ContinueSentinel;
    }

    var info = record.arg;

    if (! info) {
      context.method = "throw";
      context.arg = new TypeError("iterator result is not an object");
      context.delegate = null;
      return ContinueSentinel;
    }

    if (info.done) {
      // Assign the result of the finished delegate to the temporary
      // variable specified by delegate.resultName (see delegateYield).
      context[delegate.resultName] = info.value;

      // Resume execution at the desired location (see delegateYield).
      context.next = delegate.nextLoc;

      // If context.method was "throw" but the delegate handled the
      // exception, let the outer generator proceed normally. If
      // context.method was "next", forget context.arg since it has been
      // "consumed" by the delegate iterator. If context.method was
      // "return", allow the original .return call to continue in the
      // outer generator.
      if (context.method !== "return") {
        context.method = "next";
        context.arg = undefined$1;
      }

    } else {
      // Re-yield the result returned by the delegate method.
      return info;
    }

    // The delegate iterator is finished, so forget it and continue with
    // the outer generator.
    context.delegate = null;
    return ContinueSentinel;
  }

  // Define Generator.prototype.{next,throw,return} in terms of the
  // unified ._invoke helper method.
  defineIteratorMethods(Gp);

  define(Gp, toStringTagSymbol, "Generator");

  // A Generator should always return itself as the iterator object when the
  // @@iterator function is called on it. Some browsers' implementations of the
  // iterator prototype chain incorrectly implement this, causing the Generator
  // object to not be returned from this call. This ensures that doesn't happen.
  // See https://github.com/facebook/regenerator/issues/274 for more details.
  Gp[iteratorSymbol] = function() {
    return this;
  };

  Gp.toString = function() {
    return "[object Generator]";
  };

  function pushTryEntry(locs) {
    var entry = { tryLoc: locs[0] };

    if (1 in locs) {
      entry.catchLoc = locs[1];
    }

    if (2 in locs) {
      entry.finallyLoc = locs[2];
      entry.afterLoc = locs[3];
    }

    this.tryEntries.push(entry);
  }

  function resetTryEntry(entry) {
    var record = entry.completion || {};
    record.type = "normal";
    delete record.arg;
    entry.completion = record;
  }

  function Context(tryLocsList) {
    // The root entry object (effectively a try statement without a catch
    // or a finally block) gives us a place to store values thrown from
    // locations where there is no enclosing try statement.
    this.tryEntries = [{ tryLoc: "root" }];
    tryLocsList.forEach(pushTryEntry, this);
    this.reset(true);
  }

  exports.keys = function(object) {
    var keys = [];
    for (var key in object) {
      keys.push(key);
    }
    keys.reverse();

    // Rather than returning an object with a next method, we keep
    // things simple and return the next function itself.
    return function next() {
      while (keys.length) {
        var key = keys.pop();
        if (key in object) {
          next.value = key;
          next.done = false;
          return next;
        }
      }

      // To avoid creating an additional object, we just hang the .value
      // and .done properties off the next function object itself. This
      // also ensures that the minifier will not anonymize the function.
      next.done = true;
      return next;
    };
  };

  function values(iterable) {
    if (iterable) {
      var iteratorMethod = iterable[iteratorSymbol];
      if (iteratorMethod) {
        return iteratorMethod.call(iterable);
      }

      if (typeof iterable.next === "function") {
        return iterable;
      }

      if (!isNaN(iterable.length)) {
        var i = -1, next = function next() {
          while (++i < iterable.length) {
            if (hasOwn.call(iterable, i)) {
              next.value = iterable[i];
              next.done = false;
              return next;
            }
          }

          next.value = undefined$1;
          next.done = true;

          return next;
        };

        return next.next = next;
      }
    }

    // Return an iterator with no values.
    return { next: doneResult };
  }
  exports.values = values;

  function doneResult() {
    return { value: undefined$1, done: true };
  }

  Context.prototype = {
    constructor: Context,

    reset: function(skipTempReset) {
      this.prev = 0;
      this.next = 0;
      // Resetting context._sent for legacy support of Babel's
      // function.sent implementation.
      this.sent = this._sent = undefined$1;
      this.done = false;
      this.delegate = null;

      this.method = "next";
      this.arg = undefined$1;

      this.tryEntries.forEach(resetTryEntry);

      if (!skipTempReset) {
        for (var name in this) {
          // Not sure about the optimal order of these conditions:
          if (name.charAt(0) === "t" &&
              hasOwn.call(this, name) &&
              !isNaN(+name.slice(1))) {
            this[name] = undefined$1;
          }
        }
      }
    },

    stop: function() {
      this.done = true;

      var rootEntry = this.tryEntries[0];
      var rootRecord = rootEntry.completion;
      if (rootRecord.type === "throw") {
        throw rootRecord.arg;
      }

      return this.rval;
    },

    dispatchException: function(exception) {
      if (this.done) {
        throw exception;
      }

      var context = this;
      function handle(loc, caught) {
        record.type = "throw";
        record.arg = exception;
        context.next = loc;

        if (caught) {
          // If the dispatched exception was caught by a catch block,
          // then let that catch block handle the exception normally.
          context.method = "next";
          context.arg = undefined$1;
        }

        return !! caught;
      }

      for (var i = this.tryEntries.length - 1; i >= 0; --i) {
        var entry = this.tryEntries[i];
        var record = entry.completion;

        if (entry.tryLoc === "root") {
          // Exception thrown outside of any try block that could handle
          // it, so set the completion value of the entire function to
          // throw the exception.
          return handle("end");
        }

        if (entry.tryLoc <= this.prev) {
          var hasCatch = hasOwn.call(entry, "catchLoc");
          var hasFinally = hasOwn.call(entry, "finallyLoc");

          if (hasCatch && hasFinally) {
            if (this.prev < entry.catchLoc) {
              return handle(entry.catchLoc, true);
            } else if (this.prev < entry.finallyLoc) {
              return handle(entry.finallyLoc);
            }

          } else if (hasCatch) {
            if (this.prev < entry.catchLoc) {
              return handle(entry.catchLoc, true);
            }

          } else if (hasFinally) {
            if (this.prev < entry.finallyLoc) {
              return handle(entry.finallyLoc);
            }

          } else {
            throw new Error("try statement without catch or finally");
          }
        }
      }
    },

    abrupt: function(type, arg) {
      for (var i = this.tryEntries.length - 1; i >= 0; --i) {
        var entry = this.tryEntries[i];
        if (entry.tryLoc <= this.prev &&
            hasOwn.call(entry, "finallyLoc") &&
            this.prev < entry.finallyLoc) {
          var finallyEntry = entry;
          break;
        }
      }

      if (finallyEntry &&
          (type === "break" ||
           type === "continue") &&
          finallyEntry.tryLoc <= arg &&
          arg <= finallyEntry.finallyLoc) {
        // Ignore the finally entry if control is not jumping to a
        // location outside the try/catch block.
        finallyEntry = null;
      }

      var record = finallyEntry ? finallyEntry.completion : {};
      record.type = type;
      record.arg = arg;

      if (finallyEntry) {
        this.method = "next";
        this.next = finallyEntry.finallyLoc;
        return ContinueSentinel;
      }

      return this.complete(record);
    },

    complete: function(record, afterLoc) {
      if (record.type === "throw") {
        throw record.arg;
      }

      if (record.type === "break" ||
          record.type === "continue") {
        this.next = record.arg;
      } else if (record.type === "return") {
        this.rval = this.arg = record.arg;
        this.method = "return";
        this.next = "end";
      } else if (record.type === "normal" && afterLoc) {
        this.next = afterLoc;
      }

      return ContinueSentinel;
    },

    finish: function(finallyLoc) {
      for (var i = this.tryEntries.length - 1; i >= 0; --i) {
        var entry = this.tryEntries[i];
        if (entry.finallyLoc === finallyLoc) {
          this.complete(entry.completion, entry.afterLoc);
          resetTryEntry(entry);
          return ContinueSentinel;
        }
      }
    },

    "catch": function(tryLoc) {
      for (var i = this.tryEntries.length - 1; i >= 0; --i) {
        var entry = this.tryEntries[i];
        if (entry.tryLoc === tryLoc) {
          var record = entry.completion;
          if (record.type === "throw") {
            var thrown = record.arg;
            resetTryEntry(entry);
          }
          return thrown;
        }
      }

      // The context.catch method must only be called with a location
      // argument that corresponds to a known catch block.
      throw new Error("illegal catch attempt");
    },

    delegateYield: function(iterable, resultName, nextLoc) {
      this.delegate = {
        iterator: values(iterable),
        resultName: resultName,
        nextLoc: nextLoc
      };

      if (this.method === "next") {
        // Deliberately forget the last sent value so that we don't
        // accidentally pass it on to the delegate.
        this.arg = undefined$1;
      }

      return ContinueSentinel;
    }
  };

  // Regardless of whether this script is executing as a CommonJS module
  // or not, return the runtime object so that we can declare the variable
  // regeneratorRuntime in the outer scope, which allows this module to be
  // injected easily by `bin/regenerator --include-runtime script.js`.
  return exports;

}(
  // If this script is executing as a CommonJS module, use module.exports
  // as the regeneratorRuntime namespace. Otherwise create a new empty
  // object. Either way, the resulting object will be used to initialize
  // the regeneratorRuntime variable at the top of this file.
  module.exports 
));

try {
  regeneratorRuntime = runtime;
} catch (accidentalStrictMode) {
  // This module should not be running in strict mode, so the above
  // assignment should always work unless something is misconfigured. Just
  // in case runtime.js accidentally runs in strict mode, we can escape
  // strict mode using a global Function call. This could conceivably fail
  // if a Content Security Policy forbids using Function, but in that case
  // the proper solution is to fix the accidental strict mode problem. If
  // you've misconfigured your bundler to force strict mode and applied a
  // CSP to forbid Function, and you're not willing to fix either of those
  // problems, please detail your unique predicament in a GitHub issue.
  Function("r", "regeneratorRuntime = r")(runtime);
}
});

/*import smoothscroll from 'smoothscroll-polyfill';
smoothscroll.polyfill();*/
// *** TODO still not working in safari!

var app = {
  init: function init() {
    console.log('/********************************************');
    console.warn('/alchemy/ --release :: 2.9.22 v1.0');
    console.log('/********************************************');
    mediator.init();
  },
  add: function add(script) {
    mediator.addScript(script);
  },
  addSharedScripts: function addSharedScripts(elementNode) {
    if (!elementNode) {
      console.error('/app.js/ -addSharedScripts *ERROR* no dom item specified, bailing out..');
      return;
    }

    mediator.initComponents(elementNode);
  }
};
window.addEventListener('DOMContentLoaded', function (event) {
  app.init();
});
window.addEventListener('refreshScripts', function (e) {
  app.addSharedScripts(e.detail.node);
});

/**
 * globally available template literals for on the fly creation of elements to populate tabstack, carousel etc from fetched data
 * use generic keys in templates and provide mapping via invoker of <getTemplate> method
 */
window.alchemyTemplates = {
    // *** special characters to change interpretation of input strings, e.g. "FindOutMoreLabel: "%Print me as you find me here"
    controlChars: {
        literalValuePrefix: '%',
    },

    // === templates start ===

    /*
      -StaffCard-
      mapping reference (see ref impl: frontend/src/components/SMIT-249-board-of-directors)
     */
    StaffCard: (item) => {
        const imageCheck = () => {
            if (item.ImageUrl) {
                return item.ImageUrl;
            } else {
                return '../images/blank.png';
            }
        };

        const jobTitle = item.JobTitle ? item.JobTitle : '';
        const name = item.Name ? item.Name : '';
        const button = () => {
            if (item.Url && item.FindOutMoreLabel) {
                return `<div class="button-group">
                  <a href="${item.Url}" title="${name}" data-analytics='{"type":"standardClick","event":"click","click_element_type":"link","click_text":"${item.FindOutMoreLabel}", "link_url":"${item.Url}"}'>${item.FindOutMoreLabel}</a>
                </div>`;
            } else {
                return '';
            }
        };

        return `
      <aside class="card card--staff">
        <div class="card__content">
        <div class="image-wrapper">
          <img src="${imageCheck()}" alt="${name}, ${jobTitle}">
        </div>
        <div class="text-block">
          <h5 class="card__title">${name}</h5>
          <div class="paragraphs paragraph--small">
            <p>${jobTitle}</p>
          </div>
        </div>
      </div>
      ${button()}
    </aside>
  `;
    },

    /*
    -TreatmentCard-
    mapping reference (see ref impl: frontend/src/components/SMIT-691_patient-treatment-options--BODY/index.html)
   */
    TreatmentCard: (item) => {
        const colour = item.IconColour ? item.IconColour : '';
        const icon = item.Icon ? item.Icon : '';
        const name = item.Name ? item.Name : '';
        const description = item.Description ? item.Description : '';
        const button = () => {
            if (item.CTAButtonLink && item.CTAButtonLabel) {
                return `<div class="button-group button-group--1-6">
          <a class="cta-button cta-button--primary cta-button--medium" href="${item.CTAButtonLink}" target="_self"  data-analytics='{"type":"standardClick","event":"click","click_element_type":"link","click_text":"${item.CTAButtonLabel}", "link_url":"${item.CTAButtonLink}"}'>
            <span class="cta-button__label">${item.CTAButtonLabel}</span>
          </a>
        </div>`;
            } else {
                return '';
            }
        };
        return `
    <aside class="card card--icon-card">
      <div class="card__content">
        <div class="icon-content">
          <div class="icon icon--square"
               data-colour-value="${colour}"
               data-colour-targets="bg">
               <span class="sn-${icon}"></span>
          </div>
        </div>
    
        <div class="text-content">
          <h4 class="card__title">${name}</h4>
          <div class="paragraphs">
            <p>${description}</p>
          </div>
          ${button()}
      </div>
    </aside>
    `;
    },

    /*
  -CaseReviewCard-
  mapping reference (see ref impl: frontend/src/components/SMIT-2100-Video-thumbs-carousel/index.html)
  */
    CaseReviewCard: (item) => {
        const buttonCheck = () => {
            if (item.DownloadLink && item.DownloadLabel) {
                return `
        <div class="button-group">
          <a
          class="cta-button cta-button--primary cta-button--medium"
          href="${item.DownloadLink}"
          target="_self"
          data-analytics='{"type":"standardClick","event":"click","click_element_type":"link","click_text":"${item.DownloadLabel}", "link_url":"${item.DownloadLink}"}'
          >
            <span class="cta-button__label">${item.DownloadLabel}</span>
            <span class="cta-button__icon sn-icon-download"></span>
          </a>
        </div>`;
            } else {
                return '';
            }
        };

        const addToClipboard = () => {
            if (item.ShareableAssetUrl) {
                return `<div
        data-behaviour="clipboard"
        data-behaviour-uid="${window.uid(false)}"
        data-behaviour-config="notificationText: Link copied,
          notificationDuration: 3,
          notificationType: shareUrl,
          notificationDismissable: true"
      >
        <div class="button-group button-group--share"  data-analytics='{
          "type": "standardClick",
          "event": "buttonClick",
          "buttonType": "asset share"}'
        >
          <button class="cta-button cta-button--secondary cta-button--small cta-button--left" data-behaviour="asset-share"  data-behaviour-uid="${window.uid(
                    false
                )}"  data-button-action="to-clipboard-and-notify" data-share-url="${item.ShareableAssetUrl
                    }">
            <span class="cta-button__label">SHARE</span>
            <span class="cta-button__icon sn-icon-share"></span>
          </button>
        </div>
      </div>`;
            } else {
                return '';
            }
        };

        const imageOrVideo = () => {
            const imageUrl = item.ImageUrl ? item.ImageUrl : '';
            if (item.VideoConfig) {
                return `
        <div
          class="image-wrapper image-wrapper--video"
          data-behaviour="video-player"
          data-behaviour-uid="${window.uid(false)}"
          data-behaviour-config="${item.VideoConfig}"
          style="background-image: url('${imageUrl}')"
        >
          <div class="videoplayer__cta videoplayer__cta--play">
            <span class="sn-icon-play"></span>
          </div>
        </div>`;
            } else {
                return `
        <div
          class="image-wrapper"
          style="background-image: url('${imageUrl}')"
        ></div>
        `;
            }
        };

        const description = item.Description ? item.Description : '';
        const title = item.Title ? item.Title : '';

        return `
      <aside class="card card--staff" ">
        <div class="card__content">
          ${imageOrVideo()}
          ${addToClipboard()}
          <div class="text-block">
            <h5 class="card__title">${title}</h5>
            <div class="paragraphs paragraph--small">
              <p>${description}</p>
            </div>
          </div>
        </div>
        ${buttonCheck()}
      </aside>
    `;
    },

    /*
  -ProductCard-
  mapping reference (see ref impl: frontend/src/components/SMIT-666_HCP-products--BODY/index.html)
  */
    ProductCard: (item) => {

        // console.log('/alchemy-templates/ -ProductCard', item);

        const imageUrl = item.ImageUrl ? item.ImageUrl : '';
        const name = item.Name ? item.Name : '';
        const disciplineName = item.DisciplineName ? item.DisciplineName : '';
        const ctaLink = item.CTAButtonLink ? item.CTAButtonLink : '';
        const ctaLabel = item.CTAButtonLabel ? item.CTAButtonLabel : '';
        const ctaQuickLabel = item.QuickviewCTAButtonLabel
            ? item.QuickviewCTAButtonLabel
            : '';
        const description = item.Description ? item.Description : '';
        const prodId = item.ProductId ? item.ProductId : '';

        // *** @as SMIT-3450, note css sets span to display: block
        const getProductDescriptor = () => {
            if (item.ProductDescriptor) {
                return `<span class="card--product__product-descriptor">${item.ProductDescriptor}</span>`
            } else {
                return '';
            }
        }

        return `
   <aside class="card card--product">
        <div class="card__content">
        <div class="image-wrapper">
          <img src="${imageUrl}" alt="${name}">
        </div>
        <div class="text-block">
          <h5 class="card__title">
            ${name}${getProductDescriptor()}
          </h5>
        </div>
      </div>
      <div class="button-group">
        <a class="cta-button cta-button--primary cta-button--medium"  data-analytics='{"type":"standardClick","event":"click","click_element_type":"link","click_text":"${ctaLabel}", "link_url":"${ctaLink}"}' href="${ctaLink}">${ctaLabel}</a>
        
        <button class="cta-button cta-button--secondary cta-button--medium" 
        data-button-action='{"LAUNCH_POPOVER": "",
        "type": "product-quickview",
        "uid": "${prodId}",
        "title": "${name}",
        "productDescriptor": "${item.ProductDescriptor || ''}",
        "url": "${ctaLink}",
        "ctaButtonLabel": "${ctaQuickLabel}",
        "image": "${imageUrl}",
        "description": "${description}"
         }'>
          <span class="cta-button__icon sn-icon-visible-true"></span>
        </button>
      </div>
    </aside>
    `;
    },

    /*
  -News Card -
  mapping reference (see ref impl: frontend/src/components/SMIT-2319-news-cards-tabstack-from-api)
  */
    NewsCard: (item) => {
        const shouldTransformPublishDate = false; // *** false will render as provided without transformation

        //console.log('/alchemy-templates/ -NewsCard', item);
        // *** strip time and replace '/' with '-' to match figma
        const transformPublishDate = (string) => {
            if (!string || string === '') return ''; // bail if we have nothing
            let result = string;
            if (shouldTransformPublishDate) {
                string = string.split(' ');
                if (shouldTransformPublishDate && string[0]) {
                    result = string[0].replace(/[/]+/g, '-');
                }
            }
            return result;
        };

        const imageCheck = () => {
            if (item.ImageUrl) {
                return `
        <div class="image-wrapper"><img src="${item.ImageUrl}" alt=""></div>`;
            } else {
                return '';
            }
        };

        const title = item.Title ? item.Title : '';
        const intro = item.IntroductionText ? item.IntroductionText : '';
        const button = () => {
            if (item.Url && item.CtaLabel) {
                return `
          <div class="button-group">
            <a href="${item.Url}"  data-analytics='{"type":"standardClick","event":"click","click_element_type":"link","click_text":"${item.CtaLabel}", "link_url":"${item.Url}"}'>${item.CtaLabel}</a>
          </div>`;
            } else {
                return '';
            }
        };
        const newsDate = () => {
            const date = transformPublishDate(item.PublishDate);
            if (date !== '') {
                return `<span>${date}</span>`;
            } else {
                return '';
            }
        };

        return `<aside class="card card--news-card">
      <div class="card__content">
        ${imageCheck()}
        <div class="text-content">
          <div class="news-detail">
              <div class="news-date">
                ${newsDate()}
              </div>
            </div>
          <h5 class="card__title">${title}</h5>
          <div class="paragraphs">
            <p>${intro}</p>
          </div>
        </div>
      </div>
      ${button()}
      </aside>`;
    },

    /*
  -Contact Card - TO DO when news API is ready-
  
  */

    ContactCard: (item) => {
        // *** conditional content methods
        const address = (value) => {
            if (value) return `<p>${value}</p>`;
            return '';
        };

        // ...others if required
        const email = () => {
            if (!item.Email || item.Email === '') return '';
            return `<div class="contact-email">
        <span class="sn-icon-email selected" data-attribute-ctatype="email" data-attribute-email="${item.Email}"></span>
      </div>`;
        };

        const fax = () => {
            if (!item.Fax || item.Fax === '') return '';
            return `<div class="contact-fax">
        <span class="sn-icon-fax"  data-attribute-ctatype="fax" data-attribute-fax="${item.Fax}"></span>
      </div>`;
        };

        const phone = () => {
            if (!item.Phone || item.Phone === '') return '';
            return `<div class="contact-phone">
        <span class="sn-icon-phone-call"  data-attribute-ctatype="phone" data-attribute-phone="${item.Phone}"></span>
      </div>`;
        };

        const url = () => {
            if (!item.Url || item.Url === '') return '';
            return `<div class="contact-url">
        <span class="sn-icon-website"  data-attribute-ctatype="url"  data-attribute-url="${item.Url}"></span>
      </div>`;
        };

        const name = item.Name ? item.Name : '';
        const title = item.JobTitle ? item.JobTitle : '';
        const icon = item.Icon ? item.Icon : '';
        const contactAreaTitles = item.ContactAreaTags ? item.ContactAreaTags.map((area) => area.Title).join(" | ") : ''

        return `
      <aside class="card card--contact-card" data-behaviour="contactcard" data-behaviour-uid="contact-card_${window.uid(
            false
        )}"  data-analytics='{
        "type": "contact",
        "event": "contactClick",
        "areaOfContact": "${contactAreaTitles}"}'>
        <div class="card__content">
          <div class="text-content">
            <div class="text-content--left">
              <div class="contact-icon">
                <span class="sn-${icon}"></span>
              </div>
              <div class="contact-titles">
                <h5 class="card__title">${title}</h5>
                <h6 class="card__subtitle">${name}</h6>
              </div>
            </div>
            <div class="text-content--right">
                  <div class="paragraphs">
                    ${address(item.Address)}
                  </div>
                  <div class="contact-options">
                    ${email()}
                    ${fax()}
                    ${phone()}
                    ${url()}
                  </div>
              </div>
          </div>
          <div class="contact-content">
            <a href="mailto:${item.Email}">${item.Email}</a>
          </div>
        </div>
      </aside>`;
    },

    /*
    -ContactHead--
    mapping reference (see ref impl: frontend/src/components/SMIT-666_HCP-products--BODY/index.html)
    */
    ContactHead: (item) => {
        const title = item.Title ? item.Title : '';
        const intro = item.Introduction ? item.Introduction : '';
        const address = item.Address ? item.Address : '';
        const phone = item.PhoneNumber ? item.PhoneNumber : '';
        // console.log('/alchemy-templates/ -ContactHead', item);
        return `
       <div class="contact-module--bottom">
       <div class="contact-module--bottom--details">
         <div class="contact-module--bottom--details--left">
           <h4>${title}</h4>
           <p>${intro}</p>
         </div>
         <div class="contact-module--bottom--details--right">
           <p>${address}</p>
           <a href="tel:${phone}">${phone}</a>
         </div>
       </div>
    `;
    },

    /*
  -Featured Product panel-
  mapping reference (see ref impl: frontend/src/components/SMIT-666_HCP-products--BODY/index.html)
  */
    FeaturedProduct: (item) => {

        //console.log('/alchemy-templates/ -FeaturedProduct ???', item.ProductDescriptor);

        const name = item.Name ? item.Name : '';
        // const productDescriptor = item.ProductDescriptor ? item.ProductDescriptor : '';
        const imageUrl = item.OverviewImageUrl ? item.OverviewImageUrl : '';
        const subTitle = item.SubTitle ? item.SubTitle : '';
        const description = item.Description ? item.Description : '';
        const discName = item.DisciplineName ? item.DisciplineName : '';
        const ctaLink = item.CTAButtonLink ? item.CTAButtonLink : '';
        const ctaLabel = item.CTAButtonLabel ? item.CTAButtonLabel : '';

        const getProductDescriptor = () => {
            if (item.ProductDescriptor) {
                return `<span class="featured-product__product-descriptor">${item.ProductDescriptor}</span>`
            } else {
                return '';
            }
        }

        return `
      <aside class="featured-product">
        <div class="image-wrapper">
          <img src="${imageUrl}" alt="${name}">
        </div>
    
        <div class="featured-product__content">
          <div class="text-block">
            <h3>${subTitle}</h3>
          </div>
          <div class="text-block">
            <h2 class="featured-product__title">${name}${getProductDescriptor()}</h2> <!-- design is calling for h1 size, setting to h2 instead for testing -->
            <div class="paragraphs paragraph--large">
              <p>${description}</p>
            </div>
            <div class="button-group button-group--1-4">
              <a class="cta-button cta-button--primary cta-button--large" data-analytics='{"type":"standardClick","event":"click","click_element_type":"link","click_text":"${ctaLabel}", "link_url":"${ctaLink}"}' href="${ctaLink}">${ctaLabel}</a>
            </div>
          </div>
        </div>
      </aside>
    `;
    },

    /*
  -Investor Downloadable reports-
  mapping reference (see ref impl: frontend/src/components/SMIT-2206-downloadable-reports/)
  */
    DownloadCard: (item) => {
        // *** conditional content methods
        const fileSize = (value) => {
            value = parseInt(value, 10);
            if (value && value !== '')
                return `<div class="file-detail">
        <span>${item.FileSize} Kb</span>
      </div>`;
            return '';
        };

        const ctaButton = (value) => {
            // *** layout breaks if cta not rendered... this stops it breaking, but card should be fixed
            const style = () => {
                return !value || value.length < 3 ? 'visibility: hidden;' : '';
            };
            const url = item.Url ? item.Url : '';
            const button = item.DownloadCTAButtonLabel
                ? item.DownloadCTAButtonLabel
                : '';
            // *** always render card, but visually hide if no url
            return `
        <div class="button-group button-group--4-1 button-download" style="${style()}">
          <a class="cta-button cta-button--primary cta-button--medium"
             href="${url}"
             target="_self"
             data-analytics='{"type":"standardClick","event":"click","click_element_type":"link","click_text":"${button}", "link_url":"${url}"}'>
             <span class="cta-button__label">${button}</span>
             <span class="cta-button__icon sn-icon-download"></span>
           </a>
         </div>`;
        };

        const title = item.Title ? item.Title : '';
        const subTitle = item.SubTitle ? item.SubTitle.toUpperCase() : '';
        const summary = item.Summary ? item.Summary : '';

        // *** render
        return `
      <aside class="card card--download-card">
        <div class="card__content">
          <div class="text-content">
            <h5 class="card__subtitle">${subTitle}</h5>
            <h5 class="card__title">${title}</h5>
            <div class="paragraphs paragraph--medium paragraph--normal">
              <p>${summary}</p>
            </div>
          </div>
          <div class="pdf-download">
            ${fileSize(item.FileSize)}
            ${ctaButton(item.Url)}
          </div>
        </div>
      </aside>
  `;
    },

    // ...templates +

    // === templates end ===

    /**
     * -factory method called from concrete component-
     * @param options {Object}
     * > type : maps to template name in this file, e.g. 'StaffCard' passed via alchemy @data-component-props: "...template: <template name>"
     * > map : defined in alchemy @data-json-mapping. map of [new key]: [source key]
     * > parent : originating html element for debugging
     * @param data {Object} : a single piece of data representing a single item
     * @return {Element} : returns a populated template dom element
     */
    getTemplate: (options, data) => {
        const template = window.alchemyTemplates[options.type]; // *** find template
        const stub = document.createElement('div'); // *** element stub, only output on error

        //console.log('/alchemy-templates/ -getTemplate', options.type, data);


        if (!template || !data || !options.map) {
            console.warn(
                '/alchemy-templates/ -getTemplate --not found, no data, or no mapping defined!',
                options,
                data
            );
            stub.classList.add('blank-item');
            return stub; // *** return something to prevent consumers from breaking
        }

        let mappedData = {};

        // *** transpose keys using provided map
        Object.keys(options.map).map((key) => {
            const originalKey = options.map[key];

            /*if (options.type === 'FeaturedProduct') {
              console.log('/alchemy-templates/ -getTemplate DATA', key);
            }*/

            // *** when intention is to pass literal value
            if (originalKey.includes(window.alchemyTemplates.controlChars.literalValuePrefix)) {
                mappedData[key] = options.map[key].substr(1); // *** remove control character
                return;
            }

            mappedData[key] = data[originalKey];
        });

        /*if (options.type === 'FeaturedProduct') {
          console.log('/alchemy-templates/ -getTemplate DATA', options.type, mappedData);
        }*/

        stub.innerHTML = template(mappedData);
        return stub.firstElementChild; // *** return template as written without stub
    },
};

function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); }

function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); }

function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

/**
 * CoveoSearchShare component script
 * Component scripts are managed by an instance of `src/core/js/view/AbstractComponent.js` which mediates
 * between the core and the components, injecting components with configuration, events etc
 * import statements here are allowed but must be used with caution TODO explain how and why
 *
 * @param {Object} props - TODO
 * @return {{init: init, foo: foo}}
 */
//import analytics from '../../core/js/utils/analytics';
var script = function script(props) {
    // *** mandatory function - called by `AbstractComponent` on instantiation
    var els = {
        el: null,
        targetDiv: null,
        coveoSearchbox: null,
        coveoInputControl: null,
        coveoSearchButton: null
    };

    var init = function init() {
        // console.log('/CoveoSearchShare/ -init ', props);
        window.init = function () {
            reinit();
        };
    };

    var reinit = async function reinit() {
        // console.log('/CoveoSearchShare/ -reinit', props);
        els.el = document.querySelector(".hostedSearchPage");
        els.targetDiv = els.el.querySelector(".coveo-main-section .coveo-results-column");

        var clipboards = _toConsumableArray(els.targetDiv.querySelectorAll("[data-behaviour=\"clipboard\"]"));

        clipboards.map(function (item) {
            props.registerBehaviour(item);
        });

        var shareBTNS = _toConsumableArray(els.targetDiv.querySelectorAll("[data-behaviour=\"asset-share\"]"));

        shareBTNS.map(function (item) {
            props.registerBehaviour(item);
        });
        const fetchOptions = {
            datasource: `api/hcp/GetSelfDeclarationSessionData`,
            caller: props.name,
        };
        const requestParams = props.requestModel.format({
            Key: '', Values: '', // *** simple request doesn't need anything
        });
        const data = await props.fetchData(fetchOptions, requestParams);

        if (data.payload && data.payload['Success']) {
            if ((data.payload.CountryCode.toLowerCase() === 'en-au' && !data.payload.IsAHPRAHCPSelfDeclarationCookieAvailable) || (!data.payload.IsHCPSelfDeclarationSessionAvailable)) {
                openHCPPopup(data.payload);
            }
        }
        //Google Analytics for search
        trackSearchFilters();
    };

    var getFilterName = function getFilterName(label) {
        const arr = label.split(' ');
        const nameArr = arr.slice(0, -2);
        return nameArr.join(' ');
    }

    var trackSearchFilters = function trackSearchFilters() {
        // Analytics for view search results
        const filterSummary = document.querySelector('.CoveoQuerySummary');
        const filterSummaryText = filterSummary.innerText;
        const filterSummaryTextArr = filterSummaryText.split(' ');
        const searchTerm = filterSummaryTextArr.length == 6 ? filterSummaryTextArr[filterSummaryTextArr.length - 1] : '';
        const numberTotalOfSearchResults = filterSummaryTextArr.length == 6 ? filterSummaryTextArr[filterSummaryTextArr.length - 3] : 0;
        if (numberTotalOfSearchResults) {
            const trackingDetails = {
                event: 'view_search_results',
                search_term: searchTerm,
                number_results: numberTotalOfSearchResults,
            }
            window.dataLayer = window.dataLayer || [];
            window.dataLayer.push(trackingDetails);
        }

        //Analytics for item click event
        const itemList = document.querySelectorAll('.CoveoResultLink');
        itemList.forEach((item, index) => {
            const itemSection = item.closest('.coveo-result-frame');
            const shareButton = itemSection.querySelector('.button-group--share');
            const contentType = itemSection.querySelector('.CoveoFieldValue.contenttype')
            shareButton.addEventListener("click", () => {
                const trackingDetails = {
                    event: 'share',
                    method: '',
                    content_type: contentType ? contentType.innerText : '',
                    item_id: item.href || null,
                }
                window.dataLayer = window.dataLayer || [];
                window.dataLayer.push(trackingDetails);

            });

        });

        itemList.forEach((item, index) => {
            item.addEventListener('click', () => {
                const itemSection = item.closest('.coveo-result-frame');
                const contentType = itemSection.querySelector('.CoveoFieldValue.contenttype')
                const clickTrackingDetails = {
                    event: 'search_result_click',
                    search_term: searchTerm || '',
                    content_type: contentType ? contentType.innerText : '',
                    item_id: item.href || null,
                }
                window.dataLayer = window.dataLayer || [];
                window.dataLayer.push(clickTrackingDetails);
            });
        });

        // Analytics for search filter
        const filterCheckboxList = document.querySelectorAll('.coveo-checkbox-button');
        filterCheckboxList.forEach((item, index) => {
            item.addEventListener('click', () => {
                const filterCategory = item.closest('.CoveoDynamicFacet');
                const filterName = getFilterName(item.ariaLabel);
                const eventName = item.ariaChecked == 'true' || item.ariaPressed == 'true' ? 'filter_activate' : 'filter_deactivate';
                const trackingObj = {
                    event: eventName,
                    value: `${filterCategory.dataset.title} | ${filterName}`,
                }
                if (item.ariaChecked == 'true' || item.ariaPressed == 'true') {
                    const labelArr = item.ariaLabel.split(' ');
                    trackingObj['number_results'] = labelArr[labelArr.length - 2];
                }
                window.dataLayer = window.dataLayer || [];
                window.dataLayer.push(trackingObj);
            })
        });
    }

    var openHCPPopup = function openHCPPopup(payload) {
        const openLinkTagList = document.querySelectorAll('.CoveoResultLink.button');
        for (const link of openLinkTagList) {
            const parentEl = link.closest('.coveo-result-frame');
            const piller = parentEl.querySelector('[data-field="@pillar"]');
            const source = parentEl.querySelector('[data-field="@source"]');
            if (piller && source && source.innerText.toLowerCase() === 'shareable assets' && piller.innerText.toLowerCase() === 'hcp') {
                if (!link.dataset.buttonAction) {
                    link.style.display = 'none';
                }
                let btnEl = document.createElement('button');

                btnEl.classList.add('CoveoResultHCPLink');
                btnEl.innerText = link.innerText;
                btnEl.dataset.redirectionUrl = link.href;
                btnEl.dataset.countryCode = payload.CountryCode.toLowerCase();
                if (btnEl.innerText !== link.nextSibling.innerText) {
                    link.parentNode.insertBefore(btnEl, link.nextSibling);
                }

                btnEl.onclick = function (e) {
                    e.preventDefault();
                    const hcpConfirmationYes = document.getElementById('hcpConfirmationYes');
                    hcpConfirmationYes.setAttribute('data-redirection-url', btnEl.dataset.redirectionUrl);
                    const popoverType = btnEl.dataset.countryCode === 'en-au' ? "ahpra-self-declaration" : "self-declaration"
                    props.events.emit('LAUNCH_POPOVER', {
                        args: {
                            "LAUNCH_POPOVER": popoverType,
                        },
                    });
                }
            }
        }
    }

    return {
        init: init,
        reinit: reinit
    };
};

window.app.add({
    name: 'CoveoSearchShare',
    script: script
});

