Index: frontend/node_modules/resolve-url-loader/lib/engine/fail-initialisation.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/engine/fail-initialisation.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/engine/fail-initialisation.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,7 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+throw new Error('This "engine" is designed to fail at require time, for testing purposes only');
Index: frontend/node_modules/resolve-url-loader/lib/engine/fail-processing.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/engine/fail-processing.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/engine/fail-processing.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,18 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+/**
+ * Process the given CSS content into reworked CSS content.
+ */
+function process() {
+  return new Promise(function (_, reject) {
+    setTimeout(function () {
+      reject(new Error('This "engine" is designed to fail at processing time, for testing purposes only'));
+    }, 100);
+  });
+}
+
+module.exports = process;
Index: frontend/node_modules/resolve-url-loader/lib/engine/postcss.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/engine/postcss.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/engine/postcss.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,130 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+var os      = require('os'),
+    path    = require('path'),
+    postcss = require('postcss');
+
+var fileProtocol = require('../file-protocol');
+var algerbra     = require('../position-algerbra');
+
+var ORPHAN_CR_REGEX = /\r(?!\n)(.|\n)?/g;
+
+/**
+ * Process the given CSS content into reworked CSS content.
+ *
+ * @param {string} sourceFile The absolute path of the file being processed
+ * @param {string} sourceContent CSS content without source-map
+ * @param {{outputSourceMap: boolean, transformDeclaration:function, absSourceMap:object,
+ *        sourceMapConsumer:object, removeCR:boolean}} params Named parameters
+ * @return {{content: string, map: object}} Reworked CSS and optional source-map
+ */
+function process(sourceFile, sourceContent, params) {
+  // #107 libsass emits orphan CR not considered newline, postcss does consider newline (content vs source-map mismatch)
+  var correctedContent = params.removeCR && (os.EOL !== '\r') ?
+    sourceContent.replace(ORPHAN_CR_REGEX, ' $1') :
+    sourceContent;
+
+  // prepend file protocol to all sources to avoid problems with source map
+  return postcss([
+    postcss.plugin('postcss-resolve-url', postcssPlugin)
+  ])
+    .process(correctedContent, {
+      from: fileProtocol.prepend(sourceFile),
+      map : params.outputSourceMap && {
+        prev          : !!params.absSourceMap && fileProtocol.prepend(params.absSourceMap),
+        inline        : false,
+        annotation    : false,
+        sourcesContent: true  // #98 sourcesContent missing from output map
+      }
+    })
+    .then(result => ({
+      content: result.css,
+      map    : params.outputSourceMap ? fileProtocol.remove(result.map.toJSON()) : null
+    }));
+
+  /**
+   * Plugin for postcss that follows SASS transpilation.
+   */
+  function postcssPlugin() {
+    return function applyPlugin(styles) {
+      styles.walkDecls(eachDeclaration);
+    };
+
+    /**
+     * Process a declaration from the syntax tree.
+     * @param declaration
+     */
+    function eachDeclaration(declaration) {
+      var prefix,
+          isValid = declaration.value && (declaration.value.indexOf('url') >= 0);
+      if (isValid) {
+        prefix = declaration.prop + declaration.raws.between;
+        declaration.value = params.transformDeclaration(declaration.value, getPathsAtChar);
+      }
+
+      /**
+       * Create a hash of base path strings.
+       *
+       * Position in the declaration is supported by postcss at the position of the url() statement.
+       *
+       * @param {number} index Index in the declaration value at which to evaluate
+       * @throws Error on invalid source map
+       * @returns {{subString:string, value:string, property:string, selector:string}} Hash of base path strings
+       */
+      function getPathsAtChar(index) {
+        var subString    = declaration.value.slice(0, index),
+            posSelector  = algerbra.sanitise(declaration.parent.source.start),
+            posProperty  = algerbra.sanitise(declaration.source.start),
+            posValue     = algerbra.add([posProperty, algerbra.strToOffset(prefix)]),
+            posSubString = algerbra.add([posValue, algerbra.strToOffset(subString)]);
+
+        var result = {
+          subString: positionToOriginalDirectory(posSubString),
+          value    : positionToOriginalDirectory(posValue),
+          property : positionToOriginalDirectory(posProperty),
+          selector : positionToOriginalDirectory(posSelector)
+        };
+
+        var isValid = [result.subString, result.value, result.property, result.selector].every(Boolean);
+        if (isValid) {
+          return result;
+        }
+        else if (params.sourceMapConsumer) {
+          throw new Error(
+            'source-map information is not available at url() declaration ' +
+            (ORPHAN_CR_REGEX.test(sourceContent) ? '(found orphan CR, try removeCR option)' : '(no orphan CR found)')
+          );
+        } else {
+          throw new Error('a valid source-map is not present (ensure preceding loaders output a source-map)');
+        }
+      }
+    }
+  }
+
+  /**
+   * Given an apparent position find the directory of the original file.
+   *
+   * @param startPosApparent {{line: number, column: number}}
+   * @returns {false|string} Directory of original file or false on invalid
+   */
+  function positionToOriginalDirectory(startPosApparent) {
+    // reverse the original source-map to find the original source file before transpilation
+    var startPosOriginal =
+      !!params.sourceMapConsumer &&
+      params.sourceMapConsumer.originalPositionFor(startPosApparent);
+
+    // we require a valid directory for the specified file
+    var directory =
+      !!startPosOriginal &&
+      !!startPosOriginal.source &&
+      fileProtocol.remove(path.dirname(startPosOriginal.source));
+
+    return directory;
+  }
+}
+
+module.exports = process;
Index: frontend/node_modules/resolve-url-loader/lib/engine/rework.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/engine/rework.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/engine/rework.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,155 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+var path    = require('path'),
+    convert = require('convert-source-map');
+
+var fileProtocol = require('../file-protocol');
+
+var rework = requireOptionalPeerDependency('rework'),
+    visit  = requireOptionalPeerDependency('rework-visit');
+
+/**
+ * Process the given CSS content into reworked CSS content.
+ *
+ * @param {string} sourceFile The absolute path of the file being processed
+ * @param {string} sourceContent CSS content without source-map
+ * @param {{outputSourceMap: boolean, transformDeclaration:function, absSourceMap:object,
+ *        sourceMapConsumer:object}} params Named parameters
+ * @return {{content: string, map: object}} Reworked CSS and optional source-map
+ */
+function process(sourceFile, sourceContent, params) {
+
+  // embed source-map in css
+  //  prepend file protocol to all sources to avoid problems with source map
+  var contentWithMap = sourceContent + (
+    params.absSourceMap ?
+      convert.fromObject(fileProtocol.prepend(params.absSourceMap)).toComment({multiline: true}) :
+      ''
+  );
+
+  // need to prepend file protocol to source as well to avoid problems with source map
+  var reworked = rework(contentWithMap, {source: fileProtocol.prepend(sourceFile)})
+    .use(reworkPlugin)
+    .toString({
+      sourcemap        : params.outputSourceMap,
+      sourcemapAsObject: params.outputSourceMap
+    });
+
+  // complete with source-map
+  if (params.outputSourceMap) {
+    return {
+      content: reworked.code,
+      map    : fileProtocol.remove(reworked.map)
+    };
+  }
+  // complete without source-map
+  else {
+    return {
+      content: reworked,
+      map    : null
+    };
+  }
+
+  /**
+   * Plugin for css rework that follows SASS transpilation.
+   *
+   * @param {object} stylesheet AST for the CSS output from SASS
+   */
+  function reworkPlugin(stylesheet) {
+
+    // visit each node (selector) in the stylesheet recursively using the official utility method
+    //  each node may have multiple declarations
+    visit(stylesheet, function visitor(declarations) {
+      if (declarations) {
+        declarations.forEach(eachDeclaration);
+      }
+    });
+
+    /**
+     * Process a declaration from the syntax tree.
+     * @param declaration
+     */
+    function eachDeclaration(declaration) {
+      var isValid = declaration.value && (declaration.value.indexOf('url') >= 0);
+      if (isValid) {
+        declaration.value = params.transformDeclaration(declaration.value, getPathsAtChar);
+      }
+
+      /**
+       * Create a hash of base path strings.
+       *
+       * Position in the declaration is not supported since rework does not refine sourcemaps to this detail.
+       *
+       * @throws Error on invalid source map
+       * @returns {{selector:string, property:string}} Hash of base path strings
+       */
+      function getPathsAtChar() {
+        var posSelector = declaration.parent && declaration.parent.position.start,
+            posProperty = declaration.position.start;
+
+        var result = {
+          property: positionToOriginalDirectory(posProperty),
+          selector: positionToOriginalDirectory(posSelector)
+        };
+
+        var isValid = [result.property, result.selector].every(Boolean);
+        if (isValid) {
+          return result;
+        }
+        else if (params.sourceMapConsumer) {
+          throw new Error('source-map information is not available at url() declaration');
+        } else {
+          throw new Error('a valid source-map is not present (ensure preceding loaders output a source-map)');
+        }
+      }
+    }
+  }
+
+  /**
+   * Given an apparent position find the directory of the original file.
+   *
+   * @param startPosApparent {{line: number, column: number}}
+   * @returns {false|string} Directory of original file or false on invalid
+   */
+  function positionToOriginalDirectory(startPosApparent) {
+    // reverse the original source-map to find the original source file before transpilation
+    var startPosOriginal =
+      !!params.sourceMapConsumer &&
+      params.sourceMapConsumer.originalPositionFor(startPosApparent);
+
+    // we require a valid directory for the specified file
+    var directory =
+      !!startPosOriginal &&
+      !!startPosOriginal.source &&
+      fileProtocol.remove(path.dirname(startPosOriginal.source));
+
+    return directory;
+  }
+}
+
+module.exports = process;
+
+/**
+ * Require the given filename but fail with an error that `requireOptionalPeerDependencies` must be installed.
+ *
+ * @param moduleName The module to require
+ * @returns {*} The module
+ * @throws Error when module is not found
+ */
+function requireOptionalPeerDependency(moduleName) {
+  try {
+    return require(moduleName);
+  }
+  catch (error) {
+    if (error.message === 'Cannot find module \'' + moduleName + '\'') {
+      throw new Error('to use the "rework" engine you must install the optionalPeerDependencies');
+    }
+    else {
+      throw error;
+    }
+  }
+}
Index: frontend/node_modules/resolve-url-loader/lib/file-protocol.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/file-protocol.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/file-protocol.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,39 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+/**
+ * Prepend file:// protocol to source path string or source-map sources.
+ */
+function prepend(candidate) {
+  if (typeof candidate === 'string') {
+    return 'file://' + candidate;
+  } else if (candidate && (typeof candidate === 'object') && Array.isArray(candidate.sources)) {
+    return Object.assign({}, candidate, {
+      sources: candidate.sources.map(prepend)
+    });
+  } else {
+    throw new Error('expected string|object');
+  }
+}
+
+exports.prepend = prepend;
+
+/**
+ * Remove file:// protocol from source path string or source-map sources.
+ */
+function remove(candidate) {
+  if (typeof candidate === 'string') {
+    return candidate.replace(/^file:\/{2}/, '');
+  } else if (candidate && (typeof candidate === 'object') && Array.isArray(candidate.sources)) {
+    return Object.assign({}, candidate, {
+      sources: candidate.sources.map(remove)
+    });
+  } else {
+    throw new Error('expected string|object');
+  }
+}
+
+exports.remove = remove;
Index: frontend/node_modules/resolve-url-loader/lib/join-function/debug.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/join-function/debug.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/join-function/debug.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,86 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+const path = require('path');
+
+const PACKAGE_NAME = require('../../package.json').name;
+
+/**
+ * Paths are formatted to have posix style path separators and those within the CWD are made relative to CWD.
+ *
+ * @param {string} absolutePath An absolute path to format
+ * @returns {string} the formatted path
+ */
+const pathToString = (absolutePath) => {
+  if (absolutePath === '') {
+    return '-empty-';
+  } else {
+    const relative = path.relative(process.cwd(), absolutePath).split(path.sep);
+    const segments =
+      (relative[0] !== '..') ? ['.'].concat(relative).filter(Boolean) :
+        (relative.lastIndexOf('..') < 2) ? relative :
+          absolutePath.split(path.sep);
+    return segments.join('/');
+  }
+};
+
+exports.pathToString = pathToString;
+
+/**
+ * Format a debug message.
+ *
+ * @param {string} filename The file being processed by webpack
+ * @param {string} uri A uri path, relative or absolute
+ * @param {Array<{base:string,joined:string,isSuccess:boolean}>} attempts An array of attempts, possibly empty
+ * @return {string} Formatted message
+ */
+const formatJoinMessage = (filename, uri, attempts) => {
+  const attemptToCells = (_, i, array) => {
+    const { base: prev } = (i === 0) ? {} : array[i-1];
+    const { base: curr, joined } = array[i];
+    return [(curr === prev) ? '' : pathToString(curr), pathToString(joined)];
+  };
+
+  const formatCells = (lines) => {
+    const maxWidth = lines.reduce((max, [cellA]) => Math.max(max, cellA.length), 0);
+    return lines.map(([cellA, cellB]) => [cellA.padEnd(maxWidth), cellB]).map((cells) => cells.join(' --> '));
+  };
+
+  return [PACKAGE_NAME + ': ' + pathToString(filename) + ': ' + uri]
+    .concat(attempts.length === 0 ? '-empty-' : formatCells(attempts.map(attemptToCells)))
+    .concat(attempts.some(({ isSuccess }) => isSuccess) ? 'FOUND' : 'NOT FOUND')
+    .join('\n  ');
+};
+
+exports.formatJoinMessage = formatJoinMessage;
+
+/**
+ * A factory for a log function predicated on the given debug parameter.
+ *
+ * The logging function created accepts a function that formats a message and parameters that the function utilises.
+ * Presuming the message function may be expensive we only call it if logging is enabled.
+ *
+ * The log messages are de-duplicated based on the parameters, so it is assumed they are simple types that stringify
+ * well.
+ *
+ * @param {function|boolean} debug A boolean or debug function
+ * @return {function(function, array):void} A logging function possibly degenerate
+ */
+const createDebugLogger = (debug) => {
+  const log = !!debug && ((typeof debug === 'function') ? debug : console.log);
+  const cache = {};
+  return log ?
+    ((msgFn, params) => {
+      const key = Function.prototype.toString.call(msgFn) + JSON.stringify(params);
+      if (!cache[key]) {
+        cache[key] = true;
+        log(msgFn.apply(null, params));
+      }
+    }) :
+    (() => undefined);
+};
+
+exports.createDebugLogger = createDebugLogger;
Index: frontend/node_modules/resolve-url-loader/lib/join-function/fs-utils.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/join-function/fs-utils.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/join-function/fs-utils.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,24 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+const fsUtils = (fs) => {
+  // fs from enhanced-resolver doesn't include fs.existsSync so we need to use fs.statsSync instead
+  const withStats = (fn) => (absolutePath) => {
+    try {
+      return fn(fs.statSync(absolutePath));
+    } catch (e) {
+      return false;
+    }
+  };
+
+  return {
+    isFileSync: withStats((stats) => stats.isFile()),
+    isDirectorySync: withStats((stats) => stats.isDirectory()),
+    existsSync: withStats((stats) => stats.isFile() || stats.isDirectory())
+  };
+};
+
+module.exports = fsUtils;
Index: frontend/node_modules/resolve-url-loader/lib/join-function/index.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/join-function/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/join-function/index.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,235 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+const path = require('path');
+
+const { createDebugLogger, formatJoinMessage } = require('./debug');
+const fsUtils = require('./fs-utils');
+
+const ITERATION_SAFETY_LIMIT = 100e3;
+
+/**
+ * Wrap a function such that it always returns a generator of tuple elements.
+ *
+ * @param {function({uri:string},...):(Array|Iterator)<[string,string]|string>} fn The function to wrap
+ * @returns {function({uri:string},...):(Array|Iterator)<[string,string]>} A function that always returns tuple elements
+ */
+const asGenerator = (fn) => {
+  const toTuple = (defaults) => (value) => {
+    const partial = [].concat(value);
+    return [...partial, ...defaults.slice(partial.length)];
+  };
+
+  const isTupleUnique = (v, i, a) => {
+    const required = v.join(',');
+    return a.findIndex((vv) => vv.join(',') === required) === i;
+  };
+
+  return (item, ...rest) => {
+    const {uri} = item;
+    const mapTuple = toTuple([null, uri]);
+    const pending = fn(item, ...rest);
+    if (Array.isArray(pending)) {
+      return pending.map(mapTuple).filter(isTupleUnique)[Symbol.iterator]();
+    } else if (
+      pending &&
+      (typeof pending === 'object') &&
+      (typeof pending.next === 'function') &&
+      (pending.next.length === 0)
+    ) {
+      return pending;
+    } else {
+      throw new TypeError(`in "join" function expected "generator" to return Array|Iterator`);
+    }
+  };
+};
+
+exports.asGenerator = asGenerator;
+
+/**
+ * A high-level utility to create a join function.
+ *
+ * The `generator` is responsible for ordering possible base paths. The `operation` is responsible for joining a single
+ * `base` path with the given `uri`. The `predicate` is responsible for reporting whether the single joined value is
+ * successful as the overall result.
+ *
+ * Both the `generator` and `operation` may be `function*()` or simply `function(...):Array<string>`.
+ *
+ * @param {function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string,
+ *  selector:string}}, {filename:string, fs:Object, debug:function|boolean, root:string}):
+ *  (Array<string>|Iterator<string>)} generator A function that takes the hash of base paths from the `engine` and
+ *  returns ordered iterable of paths to consider
+ * @returns {function({filename:string, fs:Object, debug:function|boolean, root:string}):
+ *  (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string,
+ *  selector:string}}):string)} join implementation
+ */
+const createJoinImplementation = (generator) => (item, options, loader) => {
+  const { isAbsolute } = item;
+  const { root } = options;
+  const { fs } = loader;
+
+  // generate the iterator
+  const iterator = generator(item, options, loader);
+  const isValidIterator = iterator && typeof iterator === 'object' && typeof iterator.next === 'function';
+  if (!isValidIterator) {
+    throw new Error('expected generator to return Iterator');
+  }
+
+  // run the iterator lazily and record attempts
+  const { isFileSync, isDirectorySync } = fsUtils(fs);
+  const attempts = [];
+  for (let i = 0; i < ITERATION_SAFETY_LIMIT; i++) {
+    const { value, done } = iterator.next();
+    if (done) {
+      break;
+    } else if (value) {
+      const tuple = Array.isArray(value) && value.length === 2 ? value : null;
+      if (!tuple) {
+        throw new Error('expected Iterator values to be tuple of [string,string], do you need asGenerator utility?');
+      }
+
+      // skip elements where base or uri is non-string
+      // noting that we need to support base="" when root=""
+      const [base, uri] = value;
+      if ((typeof base === 'string') && (typeof uri === 'string')) {
+
+        // validate
+        const isValidBase = (isAbsolute && base === root) || (path.isAbsolute(base) && isDirectorySync(base));
+        if (!isValidBase) {
+          throw new Error(`expected "base" to be absolute path to a valid directory, got "${base}"`);
+        }
+
+        // make the attempt
+        const joined = path.normalize(path.join(base, uri));
+        const isFallback = true;
+        const isSuccess = isFileSync(joined);
+        attempts.push({base, uri, joined, isFallback, isSuccess});
+
+        if (isSuccess) {
+          break;
+        }
+
+      // validate any non-strings are falsey
+      } else {
+        const isValidTuple = value.every((v) => (typeof v === 'string') || !v);
+        if (!isValidTuple) {
+          throw new Error('expected Iterator values to be tuple of [string,string]');
+        }
+      }
+    }
+  }
+
+  return attempts;
+};
+
+exports.createJoinImplementation = createJoinImplementation;
+
+/**
+ * A low-level utility to create a join function.
+ *
+ * The `implementation` function processes an individual `item` and returns an Array of attempts. Each attempt consists
+ * of a `base` and a `joined` value with `isSuccessful` and `isFallback` flags.
+ *
+ * In the case that any attempt `isSuccessful` then its `joined` value is the outcome. Otherwise the first `isFallback`
+ * attempt is used. If there is no successful or fallback attempts then `null` is returned indicating no change to the
+ * original URI in the CSS.
+ *
+ * The `attempts` Array is logged to console when in `debug` mode.
+ *
+ * @param {string} name Name for the resulting join function
+ * @param {function({uri:string, query:string, isAbsolute:boolean, bases:{subString:string, value:string,
+ *  property:string, selector:string}}, {filename:string, fs:Object, debug:function|boolean, root:string}):
+ *  Array<{base:string,joined:string,fallback?:string,result?:string}>} implementation A function accepts an item and
+ *  returns a list of attempts
+ * @returns {function({filename:string, fs:Object, debug:function|boolean, root:string}):
+ *  (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string,
+ *  selector:string}}):string)} join function
+ */
+const createJoinFunction = (name, implementation) => {
+  const assertAttempts = (value) => {
+    const isValid =
+      Array.isArray(value) && value.every((v) =>
+        v &&
+        (typeof v === 'object') &&
+        (typeof v.base === 'string') &&
+        (typeof v.uri === 'string') &&
+        (typeof v.joined === 'string') &&
+        (typeof v.isSuccess === 'boolean') &&
+        (typeof v.isFallback === 'boolean')
+      );
+    if (!isValid) {
+      throw new Error(`expected implementation to return Array of {base, uri, joined, isSuccess, isFallback}`);
+    } else {
+      return value;
+    }
+  };
+
+  const assertJoined = (value) => {
+    const isValid = value && (typeof value === 'string') && path.isAbsolute(value) || (value === null);
+    if (!isValid) {
+      throw new Error(`expected "joined" to be absolute path, got "${value}"`);
+    } else {
+      return value;
+    }
+  };
+
+  const join = (options, loader) => {
+    const { debug } = options;
+    const { resourcePath } = loader;
+    const log = createDebugLogger(debug);
+
+    return (item) => {
+      const { uri } = item;
+      const attempts = implementation(item, options, loader);
+      assertAttempts(attempts, !!debug);
+
+      const { joined: fallback } = attempts.find(({ isFallback }) => isFallback) || {};
+      const { joined: result } = attempts.find(({ isSuccess }) => isSuccess) || {};
+
+      log(formatJoinMessage, [resourcePath, uri, attempts]);
+
+      return assertJoined(result || fallback || null);
+    };
+  };
+
+  const toString = () => '[Function ' + name + ']';
+
+  return Object.assign(join, !!name && {
+    toString,
+    toJSON: toString
+  });
+};
+
+exports.createJoinFunction = createJoinFunction;
+
+/**
+ * The default iterable factory will order `subString` then `value` then `property` then `selector`.
+ *
+ * @param {string} uri The uri given in the file webpack is processing
+ * @param {boolean} isAbsolute True for absolute URIs, false for relative URIs
+ * @param {string} subString A possible base path
+ * @param {string} value A possible base path
+ * @param {string} property A possible base path
+ * @param {string} selector A possible base path
+ * @param {string} root The loader options.root value where given
+ * @returns {Array<string>} An iterable of possible base paths in preference order
+ */
+const defaultJoinGenerator = asGenerator(
+  ({ uri, isAbsolute, bases: { subString, value, property, selector } }, { root }) =>
+    isAbsolute ? [root] : [subString, value, property, selector]
+);
+
+exports.defaultJoinGenerator = defaultJoinGenerator;
+
+/**
+ * @type {function({filename:string, fs:Object, debug:function|boolean, root:string}):
+ *  (function({uri:string, isAbsolute:boolean, bases:{subString:string, value:string, property:string,
+ *  selector:string}}):string)} join function
+ */
+exports.defaultJoin = createJoinFunction(
+  'defaultJoin',
+  createJoinImplementation(defaultJoinGenerator)
+);
Index: frontend/node_modules/resolve-url-loader/lib/log-to-test-harness.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/log-to-test-harness.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/log-to-test-harness.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,36 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+var stream = require('stream');
+
+var hasLogged = false;
+
+function logToTestHarness(maybeStream, options) {
+  var doLogging =
+    !hasLogged &&
+    !!maybeStream &&
+    (typeof maybeStream === 'object') &&
+    (maybeStream instanceof stream.Writable);
+
+  if (doLogging) {
+    hasLogged = true; // ensure we log only once
+    Object.keys(options).forEach(eachOptionKey);
+  }
+
+  function eachOptionKey(key) {
+    maybeStream.write(key + ': ' + stringify(options[key]) + '\n');
+  }
+
+  function stringify(value) {
+    try {
+      return JSON.stringify(value) || String(value);
+    } catch (e) {
+      return '-unstringifyable-';
+    }
+  }
+}
+
+module.exports = logToTestHarness;
Index: frontend/node_modules/resolve-url-loader/lib/position-algerbra.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/position-algerbra.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/position-algerbra.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,62 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+/**
+ * Given a sourcemap position create a new maybeObject with only line and column properties.
+ *
+ * @param {*|{line: number, column: number}} maybeObj Possible location hash
+ * @returns {{line: number, column: number}} Location hash with possible NaN values
+ */
+function sanitise(maybeObj) {
+  var obj = !!maybeObj && typeof maybeObj === 'object' && maybeObj || {};
+  return {
+    line: isNaN(obj.line) ? NaN : obj.line,
+    column: isNaN(obj.column) ? NaN : obj.column
+  };
+}
+
+exports.sanitise = sanitise;
+
+/**
+ * Infer a line and position delta based on the linebreaks in the given string.
+ *
+ * @param candidate {string} A string with possible linebreaks
+ * @returns {{line: number, column: number}} A position object where line and column are deltas
+ */
+function strToOffset(candidate) {
+  var split = candidate.split(/\r\n|\n/g);
+  var last  = split[split.length - 1];
+  return {
+    line: split.length - 1,
+    column: last.length
+  };
+}
+
+exports.strToOffset = strToOffset;
+
+/**
+ * Add together a list of position elements.
+ *
+ * Lines are added. If the new line is zero the column is added otherwise it is overwritten.
+ *
+ * @param {{line: number, column: number}[]} list One or more sourcemap position elements to add
+ * @returns {{line: number, column: number}} Resultant position element
+ */
+function add(list) {
+  return list
+    .slice(1)
+    .reduce(
+      function (accumulator, element) {
+        return {
+          line: accumulator.line + element.line,
+          column: element.line > 0 ? element.column : accumulator.column + element.column,
+        };
+      },
+      list[0]
+    );
+}
+
+exports.add = add;
Index: frontend/node_modules/resolve-url-loader/lib/value-processor.js
===================================================================
--- frontend/node_modules/resolve-url-loader/lib/value-processor.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
+++ frontend/node_modules/resolve-url-loader/lib/value-processor.js	(revision 9af201e94b5a79beb92a30858fc54de4bc3b9449)
@@ -0,0 +1,136 @@
+/*
+ * MIT License http://opensource.org/licenses/MIT
+ * Author: Ben Holloway @bholloway
+ */
+'use strict';
+
+var path        = require('path'),
+    loaderUtils = require('loader-utils');
+
+/**
+ * Create a value processing function for a given file path.
+ *
+ * @param {function(Object):string} join The inner join function
+ * @param {string} root The loader options.root value where given
+ * @param {string} directory The directory of the file webpack is currently processing
+ * @return {function} value processing function
+ */
+function valueProcessor({ join, root, directory }) {
+  var URL_STATEMENT_REGEX = /(url\s*\(\s*)(?:(['"])((?:(?!\2).)*)(\2)|([^'"](?:(?!\)).)*[^'"]))(\s*\))/g,
+      QUERY_REGEX         = /([?#])/g;
+
+  /**
+   * Process the given CSS declaration value.
+   *
+   * @param {string} value A declaration value that may or may not contain a url() statement
+   * @param {function(number):Object} getPathsAtChar Given an offset in the declaration value get a
+   *  list of possible absolute path strings
+   */
+  return function transformValue(value, getPathsAtChar) {
+
+    // allow multiple url() values in the declaration
+    //  split by url statements and process the content
+    //  additional capture groups are needed to match quotations correctly
+    //  escaped quotations are not considered
+    return value
+      .split(URL_STATEMENT_REGEX)
+      .map(initialise)
+      .map(eachSplitOrGroup)
+      .join('');
+
+    /**
+     * Ensure all capture group tokens are a valid string.
+     *
+     * @param {string|void} token A capture group or uncaptured token
+     * @returns {string}
+     */
+    function initialise(token) {
+      return typeof token === 'string' ? token : '';
+    }
+
+    /**
+     * An Array reduce function that accumulates string length.
+     */
+    function accumulateLength(accumulator, element) {
+      return accumulator + element.length;
+    }
+
+    /**
+     * Encode the content portion of <code>url()</code> statements.
+     * There are 6 capture groups in the split making every 7th unmatched.
+     *
+     * @param {string} element A single split item
+     * @param {number} i The index of the item in the split
+     * @param {Array} arr The array of split values
+     * @returns {string} Every 3 or 5 items is an encoded url everything else is as is
+     */
+    function eachSplitOrGroup(element, i, arr) {
+
+      // the content of the url() statement is either in group 3 or group 5
+      var mod = i % 7;
+
+      // only one of the capture groups 3 or 5 will match the other will be falsey
+      if (element && ((mod === 3) || (mod === 5))) {
+
+        // calculate the offset of the match from the front of the string
+        var position = arr.slice(0, i - mod + 1).reduce(accumulateLength, 0);
+
+        // detect quoted url and unescape backslashes
+        var before    = arr[i - 1],
+            after     = arr[i + 1],
+            isQuoted  = (before === after) && ((before === '\'') || (before === '"')),
+            unescaped = isQuoted ? element.replace(/\\{2}/g, '\\') : element;
+
+        // split into uri and query/hash and then determine if the uri is some type of file
+        var split      = unescaped.split(QUERY_REGEX),
+            uri        = split[0],
+            query      = split.slice(1).join(''),
+            isRelative = testIsRelative(uri),
+            isAbsolute = testIsAbsolute(uri);
+
+        // file like URIs are processed but not all URIs are files
+        if (isRelative || isAbsolute) {
+          var bases    = getPathsAtChar(position), // construct iterator as late as possible in case sourcemap invalid
+              absolute = join({ uri, query, isAbsolute, bases });
+
+          if (typeof absolute === 'string') {
+            var relative = path.relative(directory, absolute)
+              .replace(/\\/g, '/'); // #6 - backslashes are not legal in URI
+
+            return loaderUtils.urlToRequest(relative + query);
+          }
+        }
+      }
+
+      // everything else, including parentheses and quotation (where present) and media statements
+      return element;
+    }
+  };
+
+  /**
+   * The loaderUtils.isUrlRequest() doesn't support windows absolute paths on principle. We do not subscribe to that
+   * dogma so we add path.isAbsolute() check to allow them.
+   *
+   * We also eliminate module relative (~) paths.
+   *
+   * @param {string|undefined} uri A uri string possibly empty or undefined
+   * @return {boolean} True for relative uri
+   */
+  function testIsRelative(uri) {
+    return !!uri && loaderUtils.isUrlRequest(uri, false) && !path.isAbsolute(uri) && (uri.indexOf('~') !== 0);
+  }
+
+  /**
+   * The loaderUtils.isUrlRequest() doesn't support windows absolute paths on principle. We do not subscribe to that
+   * dogma so we add path.isAbsolute() check to allow them.
+   *
+   * @param {string|undefined} uri A uri string possibly empty or undefined
+   * @return {boolean} True for absolute uri
+   */
+  function testIsAbsolute(uri) {
+    return !!uri && (typeof root === 'string') && loaderUtils.isUrlRequest(uri, root) &&
+      (/^\//.test(uri) || path.isAbsolute(uri));
+  }
+}
+
+module.exports = valueProcessor;
