/*!
 * reveal.js
 * http://revealjs.com
 * MIT licensed
 *
 * Copyright (C) 2019 Hakim El Hattab, http://hakim.se
 */
(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(function () {
      root.Reveal = factory();
      return root.Reveal;
    });
  } else if (typeof exports === 'object') {
    // Node. Does not work with strict CommonJS.
    module.exports = factory();
  } else {
    // Browser globals.
    root.Reveal = factory();
  }
}(this, function () {

  'use strict';

  var Reveal;

  // The reveal.js version
  var VERSION = '3.8.0';

  var SLIDES_SELECTOR = '.slides section',
    HORIZONTAL_SLIDES_SELECTOR = '.slides>section',
    VERTICAL_SLIDES_SELECTOR = '.slides>section.present>section',
    HOME_SLIDE_SELECTOR = '.slides>section:first-of-type',
    UA = navigator.userAgent,

    // Configuration defaults, can be overridden at initialization time
    config = {

      // The "normal" size of the presentation, aspect ratio will be preserved
      // when the presentation is scaled to fit different resolutions
      width: 960,
      height: 700,

      // Factor of the display size that should remain empty around the content
      margin: 0.04,

      // Bounds for smallest/largest possible scale to apply to content
      minScale: 0.2,
      maxScale: 2.0,

      // Display presentation control arrows
      controls: true,

      // Help the user learn the controls by providing hints, for example by
      // bouncing the down arrow when they first encounter a vertical slide
      controlsTutorial: true,

      // Determines where controls appear, "edges" or "bottom-right"
      controlsLayout: 'bottom-right',

      // Visibility rule for backwards navigation arrows; "faded", "hidden"
      // or "visible"
      controlsBackArrows: 'faded',

      // Display a presentation progress bar
      progress: true,

      // Display the page number of the current slide
      // - true:    Show slide number
      // - false:   Hide slide number
      //
      // Can optionally be set as a string that specifies the number formatting:
      // - "h.v":	  Horizontal . vertical slide number (default)
      // - "h/v":	  Horizontal / vertical slide number
      // - "c":	  Flattened slide number
      // - "c/t":	  Flattened slide number / total slides
      //
      // Alternatively, you can provide a function that returns the slide
      // number for the current slide. The function needs to return an array
      // with one string [slideNumber] or three strings [n1,delimiter,n2].
      // See #formatSlideNumber().
      slideNumber: false,

      // Can be used to limit the contexts in which the slide number appears
      // - "all":      Always show the slide number
      // - "print":    Only when printing to PDF
      // - "speaker":  Only in the speaker view
      showSlideNumber: 'all',

      // Use 1 based indexing for # links to match slide number (default is zero
      // based)
      hashOneBasedIndex: false,

      // Add the current slide number to the URL hash so that reloading the
      // page/copying the URL will return you to the same slide
      hash: false,

      // Push each slide change to the browser history.  Implies `hash: true`
      history: false,

      // Enable keyboard shortcuts for navigation
      keyboard: true,

      // Optional function that blocks keyboard events when retuning false
      keyboardCondition: null,

      // Enable the slide overview mode
      overview: true,

      // Disables the default reveal.js slide layout so that you can use
      // custom CSS layout
      disableLayout: false,

      // Vertical centering of slides
      center: true,

      // Enables touch navigation on devices with touch input
      touch: true,

      // Loop the presentation
      loop: false,

      // Change the presentation direction to be RTL
      rtl: false,

      // Changes the behavior of our navigation directions.
      //
      // "default"
      // Left/right arrow keys step between horizontal slides, up/down
      // arrow keys step between vertical slides. Space key steps through
      // all slides (both horizontal and vertical).
      //
      // "linear"
      // Removes the up/down arrows. Left/right arrows step through all
      // slides (both horizontal and vertical).
      //
      // "grid"
      // When this is enabled, stepping left/right from a vertical stack
      // to an adjacent vertical stack will land you at the same vertical
      // index.
      //
      // Consider a deck with six slides ordered in two vertical stacks:
      // 1.1    2.1
      // 1.2    2.2
      // 1.3    2.3
      //
      // If you're on slide 1.3 and navigate right, you will normally move
      // from 1.3 -> 2.1. If "grid" is used, the same navigation takes you
      // from 1.3 -> 2.3.
      navigationMode: 'default',

      // Randomizes the order of slides each time the presentation loads
      shuffle: false,

      // Turns fragments on and off globally
      fragments: true,

      // Flags whether to include the current fragment in the URL,
      // so that reloading brings you to the same fragment position
      fragmentInURL: false,

      // Flags if the presentation is running in an embedded mode,
      // i.e. contained within a limited portion of the screen
      embedded: false,

      // Flags if we should show a help overlay when the question-mark
      // key is pressed
      help: true,

      // Flags if it should be possible to pause the presentation (blackout)
      pause: true,

      // Flags if speaker notes should be visible to all viewers
      showNotes: false,

      // Global override for autolaying embedded media (video/audio/iframe)
      // - null:   Media will only autoplay if data-autoplay is present
      // - true:   All media will autoplay, regardless of individual setting
      // - false:  No media will autoplay, regardless of individual setting
      autoPlayMedia: null,

      // Global override for preloading lazy-loaded iframes
      // - null:   Iframes with data-src AND data-preload will be loaded when within
      //           the viewDistance, iframes with only data-src will be loaded when visible
      // - true:   All iframes with data-src will be loaded when within the viewDistance
      // - false:  All iframes with data-src will be loaded only when visible
      preloadIframes: null,

      // Controls automatic progression to the next slide
      // - 0:      Auto-sliding only happens if the data-autoslide HTML attribute
      //           is present on the current slide or fragment
      // - 1+:     All slides will progress automatically at the given interval
      // - false:  No auto-sliding, even if data-autoslide is present
      autoSlide: 0,

      // Stop auto-sliding after user input
      autoSlideStoppable: true,

      // Use this method for navigation when auto-sliding (defaults to navigateNext)
      autoSlideMethod: null,

      // Specify the average time in seconds that you think you will spend
      // presenting each slide. This is used to show a pacing timer in the
      // speaker view
      defaultTiming: null,

      // Enable slide navigation via mouse wheel
      mouseWheel: false,

      // Apply a 3D roll to links on hover
      rollingLinks: false,

      // Hides the address bar on mobile devices
      hideAddressBar: true,

      // Opens links in an iframe preview overlay
      // Add `data-preview-link` and `data-preview-link="false"` to customise each link
      // individually
      previewLinks: false,

      // Exposes the reveal.js API through window.postMessage
      postMessage: true,

      // Dispatches all reveal.js events to the parent window through postMessage
      postMessageEvents: false,

      // Focuses body when page changes visibility to ensure keyboard shortcuts work
      focusBodyOnPageVisibilityChange: true,

      // Transition style
      transition: 'slide', // none/fade/slide/convex/concave/zoom

      // Transition speed
      transitionSpeed: 'default', // default/fast/slow

      // Transition style for full page slide backgrounds
      backgroundTransition: 'fade', // none/fade/slide/convex/concave/zoom

      // Parallax background image
      parallaxBackgroundImage: '', // CSS syntax, e.g. "a.jpg"

      // Parallax background size
      parallaxBackgroundSize: '', // CSS syntax, e.g. "3000px 2000px"

      // Parallax background repeat
      parallaxBackgroundRepeat: '', // repeat/repeat-x/repeat-y/no-repeat/initial/inherit

      // Parallax background position
      parallaxBackgroundPosition: '', // CSS syntax, e.g. "top left"

      // Amount of pixels to move the parallax background per slide step
      parallaxBackgroundHorizontal: null,
      parallaxBackgroundVertical: null,

      // The maximum number of pages a single slide can expand onto when printing
      // to PDF, unlimited by default
      pdfMaxPagesPerSlide: Number.POSITIVE_INFINITY,

      // Prints each fragment on a separate slide
      pdfSeparateFragments: true,

      // Offset used to reduce the height of content within exported PDF pages.
      // This exists to account for environment differences based on how you
      // print to PDF. CLI printing options, like phantomjs and wkpdf, can end
      // on precisely the total height of the document whereas in-browser
      // printing has to end one pixel before.
      pdfPageHeightOffset: -1,

      // Number of slides away from the current that are visible
      viewDistance: 3,

      // The display mode that will be used to show slides
      display: 'block',

      // Hide cursor if inactive
      hideInactiveCursor: true,

      // Time before the cursor is hidden (in ms)
      hideCursorTime: 5000,

      // Script dependencies to load
      dependencies: []

    },

    // Flags if Reveal.initialize() has been called
    initialized = false,

    // Flags if reveal.js is loaded (has dispatched the 'ready' event)
    loaded = false,

    // Flags if the overview mode is currently active
    overview = false,

    // Holds the dimensions of our overview slides, including margins
    overviewSlideWidth = null,
    overviewSlideHeight = null,

    // The horizontal and vertical index of the currently active slide
    indexh,
    indexv,

    // The previous and current slide HTML elements
    previousSlide,
    currentSlide,

    previousBackground,

    // Remember which directions that the user has navigated towards
    hasNavigatedRight = false,
    hasNavigatedDown = false,

    // Slides may hold a data-state attribute which we pick up and apply
    // as a class to the body. This list contains the combined state of
    // all current slides.
    state = [],

    // The current scale of the presentation (see width/height config)
    scale = 1,

    // CSS transform that is currently applied to the slides container,
    // split into two groups
    slidesTransform = { layout: '', overview: '' },

    // Cached references to DOM elements
    dom = {},

    // A list of registered reveal.js plugins
    plugins = {},

    // List of asynchronously loaded reveal.js dependencies
    asyncDependencies = [],

    // Features supported by the browser, see #checkCapabilities()
    features = {},

    // Client is a mobile device, see #checkCapabilities()
    isMobileDevice,

    // Client is a desktop Chrome, see #checkCapabilities()
    isChrome,

    // Throttles mouse wheel navigation
    lastMouseWheelStep = 0,

    // Delays updates to the URL due to a Chrome thumbnailer bug
    writeURLTimeout = 0,

    // Is the mouse pointer currently hidden from view
    cursorHidden = false,

    // Timeout used to determine when the cursor is inactive
    cursorInactiveTimeout = 0,

    // Flags if the interaction event listeners are bound
    eventsAreBound = false,

    // The current auto-slide duration
    autoSlide = 0,

    // Auto slide properties
    autoSlidePlayer,
    autoSlideTimeout = 0,
    autoSlideStartTime = -1,
    autoSlidePaused = false,

    // Holds information about the currently ongoing touch input
    touch = {
      startX: 0,
      startY: 0,
      startCount: 0,
      captured: false,
      threshold: 40
    },

    // A key:value map of shortcut keyboard keys and descriptions of
    // the actions they trigger, generated in #configure()
    keyboardShortcuts = {},

    // Holds custom key code mappings
    registeredKeyBindings = {};

	/**
	 * Starts up the presentation if the client is capable.
	 */
  function initialize(options) {

    // Make sure we only initialize once
    if (initialized === true) return;

    initialized = true;

    checkCapabilities();

    if (!features.transforms2d && !features.transforms3d) {
      document.body.setAttribute('class', 'no-transforms');

      // Since JS won't be running any further, we load all lazy
      // loading elements upfront
      var images = toArray(document.getElementsByTagName('img')),
        iframes = toArray(document.getElementsByTagName('iframe'));

      var lazyLoadable = images.concat(iframes);

      for (var i = 0, len = lazyLoadable.length; i < len; i++) {
        var element = lazyLoadable[i];
        if (element.getAttribute('data-src')) {
          element.setAttribute('src', element.getAttribute('data-src'));
          element.removeAttribute('data-src');
        }
      }

      // If the browser doesn't support core features we won't be
      // using JavaScript to control the presentation
      return;
    }

    // Cache references to key DOM elements
    dom.wrapper = document.querySelector('.reveal');
    dom.slides = document.querySelector('.reveal .slides');

    // Force a layout when the whole page, incl fonts, has loaded
    window.addEventListener('load', layout, false);

    var query = Reveal.getQueryHash();

    // Do not accept new dependencies via query config to avoid
    // the potential of malicious script injection
    if (typeof query['dependencies'] !== 'undefined') delete query['dependencies'];

    // Copy options over to our config object
    extend(config, options);
    extend(config, query);

    // Hide the address bar in mobile browsers
    hideAddressBar();

    // Loads dependencies and continues to #start() once done
    load();

  }

	/**
	 * Inspect the client to see what it's capable of, this
	 * should only happens once per runtime.
	 */
  function checkCapabilities() {

    isMobileDevice = /(iphone|ipod|ipad|android)/gi.test(UA);
    isChrome = /chrome/i.test(UA) && !/edge/i.test(UA);

    var testElement = document.createElement('div');

    features.transforms3d = 'WebkitPerspective' in testElement.style ||
      'MozPerspective' in testElement.style ||
      'msPerspective' in testElement.style ||
      'OPerspective' in testElement.style ||
      'perspective' in testElement.style;

    features.transforms2d = 'WebkitTransform' in testElement.style ||
      'MozTransform' in testElement.style ||
      'msTransform' in testElement.style ||
      'OTransform' in testElement.style ||
      'transform' in testElement.style;

    features.requestAnimationFrameMethod = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame;
    features.requestAnimationFrame = typeof features.requestAnimationFrameMethod === 'function';

    features.canvas = !!document.createElement('canvas').getContext;

    // Transitions in the overview are disabled in desktop and
    // Safari due to lag
    features.overviewTransitions = !/Version\/[\d\.]+.*Safari/.test(UA);

    // Flags if we should use zoom instead of transform to scale
    // up slides. Zoom produces crisper results but has a lot of
    // xbrowser quirks so we only use it in whitelsited browsers.
    features.zoom = 'zoom' in testElement.style && !isMobileDevice &&
      (isChrome || /Version\/[\d\.]+.*Safari/.test(UA));

  }

	/**
	 * Loads the dependencies of reveal.js. Dependencies are
	 * defined via the configuration option 'dependencies'
	 * and will be loaded prior to starting/binding reveal.js.
	 * Some dependencies may have an 'async' flag, if so they
	 * will load after reveal.js has been started up.
	 */
  function load() {

    var scripts = [],
      scriptsToLoad = 0;

    config.dependencies.forEach(function (s) {
      // Load if there's no condition or the condition is truthy
      if (!s.condition || s.condition()) {
        if (s.async) {
          asyncDependencies.push(s);
        }
        else {
          scripts.push(s);
        }
      }
    });

    if (scripts.length) {
      scriptsToLoad = scripts.length;

      // Load synchronous scripts
      scripts.forEach(function (s) {
        loadScript(s.src, function () {

          if (typeof s.callback === 'function') s.callback();

          if (--scriptsToLoad === 0) {
            initPlugins();
          }

        });
      });
    }
    else {
      initPlugins();
    }

  }

	/**
	 * Initializes our plugins and waits for them to be ready
	 * before proceeding.
	 */
  function initPlugins() {

    var pluginsToInitialize = Object.keys(plugins).length;

    // If there are no plugins, skip this step
    if (pluginsToInitialize === 0) {
      loadAsyncDependencies();
    }
    // ... otherwise initialize plugins
    else {

      var afterPlugInitialized = function () {
        if (--pluginsToInitialize === 0) {
          loadAsyncDependencies();
        }
      };

      for (var i in plugins) {

        var plugin = plugins[i];

        // If the plugin has an 'init' method, invoke it
        if (typeof plugin.init === 'function') {
          var callback = plugin.init();

          // If the plugin returned a Promise, wait for it
          if (callback && typeof callback.then === 'function') {
            callback.then(afterPlugInitialized);
          }
          else {
            afterPlugInitialized();
          }
        }
        else {
          afterPlugInitialized();
        }

      }

    }

  }

	/**
	 * Loads all async reveal.js dependencies.
	 */
  function loadAsyncDependencies() {

    if (asyncDependencies.length) {
      asyncDependencies.forEach(function (s) {
        loadScript(s.src, s.callback);
      });
    }

    start();

  }

	/**
	 * Loads a JavaScript file from the given URL and executes it.
	 *
	 * @param {string} url Address of the .js file to load
	 * @param {function} callback Method to invoke when the script
	 * has loaded and executed
	 */
  function loadScript(url, callback) {

    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.async = false;
    script.defer = false;
    script.src = url;

    if (callback) {

      // Success callback
      script.onload = script.onreadystatechange = function (event) {
        if (event.type === "load" || (/loaded|complete/.test(script.readyState))) {

          // Kill event listeners
          script.onload = script.onreadystatechange = script.onerror = null;

          callback();

        }
      };

      // Error callback
      script.onerror = function (err) {

        // Kill event listeners
        script.onload = script.onreadystatechange = script.onerror = null;

        callback(new Error('Failed loading script: ' + script.src + '\n' + err));

      };

    }

    // Append the script at the end of <head>
    var head = document.querySelector('head');
    head.insertBefore(script, head.lastChild);

  }

	/**
	 * Starts up reveal.js by binding input events and navigating
	 * to the current URL deeplink if there is one.
	 */
  function start() {

    loaded = true;

    // Make sure we've got all the DOM elements we need
    setupDOM();

    // Listen to messages posted to this window
    setupPostMessage();

    // Prevent the slides from being scrolled out of view
    setupScrollPrevention();

    // Resets all vertical slides so that only the first is visible
    resetVerticalSlides();

    // Updates the presentation to match the current configuration values
    configure();

    // Read the initial hash
    readURL();

    // Update all backgrounds
    updateBackground(true);

    // Notify listeners that the presentation is ready but use a 1ms
    // timeout to ensure it's not fired synchronously after #initialize()
    setTimeout(function () {
      // Enable transitions now that we're loaded
      dom.slides.classList.remove('no-transition');

      dom.wrapper.classList.add('ready');

      dispatchEvent('ready', {
        'indexh': indexh,
        'indexv': indexv,
        'currentSlide': currentSlide
      });
    }, 1);

    // Special setup and config is required when printing to PDF
    if (isPrintingPDF()) {
      removeEventListeners();

      // The document needs to have loaded for the PDF layout
      // measurements to be accurate
      if (document.readyState === 'complete') {
        setupPDF();
      }
      else {
        window.addEventListener('load', setupPDF);
      }
    }

  }

	/**
	 * Finds and stores references to DOM elements which are
	 * required by the presentation. If a required element is
	 * not found, it is created.
	 */
  function setupDOM() {

    // Prevent transitions while we're loading
    dom.slides.classList.add('no-transition');

    if (isMobileDevice) {
      dom.wrapper.classList.add('no-hover');
    }
    else {
      dom.wrapper.classList.remove('no-hover');
    }

    if (/iphone/gi.test(UA)) {
      dom.wrapper.classList.add('ua-iphone');
    }
    else {
      dom.wrapper.classList.remove('ua-iphone');
    }

    // Background element
    dom.background = createSingletonNode(dom.wrapper, 'div', 'backgrounds', null);

    // Progress bar
    dom.progress = createSingletonNode(dom.wrapper, 'div', 'progress', '<span></span>');
    dom.progressbar = dom.progress.querySelector('span');

    // Arrow controls
    dom.controls = createSingletonNode(dom.wrapper, 'aside', 'controls',
      '<button class="navigate-left" aria-label="previous slide"><div class="controls-arrow"></div></button>' +
      '<button class="navigate-right" aria-label="next slide"><div class="controls-arrow"></div></button>' +
      '<button class="navigate-up" aria-label="above slide"><div class="controls-arrow"></div></button>' +
      '<button class="navigate-down" aria-label="below slide"><div class="controls-arrow"></div></button>');

    // Slide number
    dom.slideNumber = createSingletonNode(dom.wrapper, 'div', 'slide-number', '');

    // Element containing notes that are visible to the audience
    dom.speakerNotes = createSingletonNode(dom.wrapper, 'div', 'speaker-notes', null);
    dom.speakerNotes.setAttribute('data-prevent-swipe', '');
    dom.speakerNotes.setAttribute('tabindex', '0');

    // Overlay graphic which is displayed during the paused mode
    dom.pauseOverlay = createSingletonNode(dom.wrapper, 'div', 'pause-overlay', config.controls ? '<button class="resume-button">Resume presentation</button>' : null);

    dom.wrapper.setAttribute('role', 'application');

    // There can be multiple instances of controls throughout the page
    dom.controlsLeft = toArray(document.querySelectorAll('.navigate-left'));
    dom.controlsRight = toArray(document.querySelectorAll('.navigate-right'));
    dom.controlsUp = toArray(document.querySelectorAll('.navigate-up'));
    dom.controlsDown = toArray(document.querySelectorAll('.navigate-down'));
    dom.controlsPrev = toArray(document.querySelectorAll('.navigate-prev'));
    dom.controlsNext = toArray(document.querySelectorAll('.navigate-next'));

    // The right and down arrows in the standard reveal.js controls
    dom.controlsRightArrow = dom.controls.querySelector('.navigate-right');
    dom.controlsDownArrow = dom.controls.querySelector('.navigate-down');

    dom.statusDiv = createStatusDiv();
  }

	/**
	 * Creates a hidden div with role aria-live to announce the
	 * current slide content. Hide the div off-screen to make it
	 * available only to Assistive Technologies.
	 *
	 * @return {HTMLElement}
	 */
  function createStatusDiv() {

    var statusDiv = document.getElementById('aria-status-div');
    if (!statusDiv) {
      statusDiv = document.createElement('div');
      statusDiv.style.position = 'absolute';
      statusDiv.style.height = '1px';
      statusDiv.style.width = '1px';
      statusDiv.style.overflow = 'hidden';
      statusDiv.style.clip = 'rect( 1px, 1px, 1px, 1px )';
      statusDiv.setAttribute('id', 'aria-status-div');
      statusDiv.setAttribute('aria-live', 'polite');
      statusDiv.setAttribute('aria-atomic', 'true');
      dom.wrapper.appendChild(statusDiv);
    }
    return statusDiv;

  }

	/**
	 * Converts the given HTML element into a string of text
	 * that can be announced to a screen reader. Hidden
	 * elements are excluded.
	 */
  function getStatusText(node) {

    var text = '';

    // Text node
    if (node.nodeType === 3) {
      text += node.textContent;
    }
    // Element node
    else if (node.nodeType === 1) {

      var isAriaHidden = node.getAttribute('aria-hidden');
      var isDisplayHidden = window.getComputedStyle(node)['display'] === 'none';
      if (isAriaHidden !== 'true' && !isDisplayHidden) {

        toArray(node.childNodes).forEach(function (child) {
          text += getStatusText(child);
        });

      }

    }

    return text;

  }

	/**
	 * Configures the presentation for printing to a static
	 * PDF.
	 */
  function setupPDF() {

    var slideSize = getComputedSlideSize(window.innerWidth, window.innerHeight);

    // Dimensions of the PDF pages
    var pageWidth = Math.floor(slideSize.width * (1 + config.margin)),
      pageHeight = Math.floor(slideSize.height * (1 + config.margin));

    // Dimensions of slides within the pages
    var slideWidth = slideSize.width,
      slideHeight = slideSize.height;

    // Let the browser know what page size we want to print
    injectStyleSheet('@page{size:' + pageWidth + 'px ' + pageHeight + 'px; margin: 0px;}');

    // Limit the size of certain elements to the dimensions of the slide
    injectStyleSheet('.reveal section>img, .reveal section>video, .reveal section>iframe{max-width: ' + slideWidth + 'px; max-height:' + slideHeight + 'px}');

    document.body.classList.add('print-pdf');
    document.body.style.width = pageWidth + 'px';
    document.body.style.height = pageHeight + 'px';

    // Make sure stretch elements fit on slide
    layoutSlideContents(slideWidth, slideHeight);

    // Add each slide's index as attributes on itself, we need these
    // indices to generate slide numbers below
    toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR)).forEach(function (hslide, h) {
      hslide.setAttribute('data-index-h', h);

      if (hslide.classList.contains('stack')) {
        toArray(hslide.querySelectorAll('section')).forEach(function (vslide, v) {
          vslide.setAttribute('data-index-h', h);
          vslide.setAttribute('data-index-v', v);
        });
      }
    });

    // Slide and slide background layout
    toArray(dom.wrapper.querySelectorAll(SLIDES_SELECTOR)).forEach(function (slide) {

      // Vertical stacks are not centred since their section
      // children will be
      if (slide.classList.contains('stack') === false) {
        // Center the slide inside of the page, giving the slide some margin
        var left = (pageWidth - slideWidth) / 2,
          top = (pageHeight - slideHeight) / 2;

        var contentHeight = slide.scrollHeight;
        var numberOfPages = Math.max(Math.ceil(contentHeight / pageHeight), 1);

        // Adhere to configured pages per slide limit
        numberOfPages = Math.min(numberOfPages, config.pdfMaxPagesPerSlide);

        // Center slides vertically
        if (numberOfPages === 1 && config.center || slide.classList.contains('center')) {
          top = Math.max((pageHeight - contentHeight) / 2, 0);
        }

        // Wrap the slide in a page element and hide its overflow
        // so that no page ever flows onto another
        var page = document.createElement('div');
        page.className = 'pdf-page';
        page.style.height = ((pageHeight + config.pdfPageHeightOffset) * numberOfPages) + 'px';
        slide.parentNode.insertBefore(page, slide);
        page.appendChild(slide);

        // Position the slide inside of the page
        slide.style.left = left + 'px';
        slide.style.top = top + 'px';
        slide.style.width = slideWidth + 'px';

        if (slide.slideBackgroundElement) {
          page.insertBefore(slide.slideBackgroundElement, slide);
        }

        // Inject notes if `showNotes` is enabled
        if (config.showNotes) {

          // Are there notes for this slide?
          var notes = getSlideNotes(slide);
          if (notes) {

            var notesSpacing = 8;
            var notesLayout = typeof config.showNotes === 'string' ? config.showNotes : 'inline';
            var notesElement = document.createElement('div');
            notesElement.classList.add('speaker-notes');
            notesElement.classList.add('speaker-notes-pdf');
            notesElement.setAttribute('data-layout', notesLayout);
            notesElement.innerHTML = notes;

            if (notesLayout === 'separate-page') {
              page.parentNode.insertBefore(notesElement, page.nextSibling);
            }
            else {
              notesElement.style.left = notesSpacing + 'px';
              notesElement.style.bottom = notesSpacing + 'px';
              notesElement.style.width = (pageWidth - notesSpacing * 2) + 'px';
              page.appendChild(notesElement);
            }

          }

        }

        // Inject slide numbers if `slideNumbers` are enabled
        if (config.slideNumber && /all|print/i.test(config.showSlideNumber)) {
          var slideNumberH = parseInt(slide.getAttribute('data-index-h'), 10) + 1,
            slideNumberV = parseInt(slide.getAttribute('data-index-v'), 10) + 1;

          var numberElement = document.createElement('div');
          numberElement.classList.add('slide-number');
          numberElement.classList.add('slide-number-pdf');
          numberElement.innerHTML = formatSlideNumber(slideNumberH, '.', slideNumberV);
          page.appendChild(numberElement);
        }

        // Copy page and show fragments one after another
        if (config.pdfSeparateFragments) {

          // Each fragment 'group' is an array containing one or more
          // fragments. Multiple fragments that appear at the same time
          // are part of the same group.
          var fragmentGroups = sortFragments(page.querySelectorAll('.fragment'), true);

          var previousFragmentStep;
          var previousPage;

          fragmentGroups.forEach(function (fragments) {

            // Remove 'current-fragment' from the previous group
            if (previousFragmentStep) {
              previousFragmentStep.forEach(function (fragment) {
                fragment.classList.remove('current-fragment');
              });
            }

            // Show the fragments for the current index
            fragments.forEach(function (fragment) {
              fragment.classList.add('visible', 'current-fragment');
            });

            // Create a separate page for the current fragment state
            var clonedPage = page.cloneNode(true);
            page.parentNode.insertBefore(clonedPage, (previousPage || page).nextSibling);

            previousFragmentStep = fragments;
            previousPage = clonedPage;

          });

          // Reset the first/original page so that all fragments are hidden
          fragmentGroups.forEach(function (fragments) {
            fragments.forEach(function (fragment) {
              fragment.classList.remove('visible', 'current-fragment');
            });
          });

        }
        // Show all fragments
        else {
          toArray(page.querySelectorAll('.fragment:not(.fade-out)')).forEach(function (fragment) {
            fragment.classList.add('visible');
          });
        }

      }

    });

    // Notify subscribers that the PDF layout is good to go
    dispatchEvent('pdf-ready');

  }

	/**
	 * This is an unfortunate necessity. Some actions – such as
	 * an input field being focused in an iframe or using the
	 * keyboard to expand text selection beyond the bounds of
	 * a slide – can trigger our content to be pushed out of view.
	 * This scrolling can not be prevented by hiding overflow in
	 * CSS (we already do) so we have to resort to repeatedly
	 * checking if the slides have been offset :(
	 */
  function setupScrollPrevention() {

    setInterval(function () {
      if (dom.wrapper.scrollTop !== 0 || dom.wrapper.scrollLeft !== 0) {
        dom.wrapper.scrollTop = 0;
        dom.wrapper.scrollLeft = 0;
      }
    }, 1000);

  }

	/**
	 * Creates an HTML element and returns a reference to it.
	 * If the element already exists the existing instance will
	 * be returned.
	 *
	 * @param {HTMLElement} container
	 * @param {string} tagname
	 * @param {string} classname
	 * @param {string} innerHTML
	 *
	 * @return {HTMLElement}
	 */
  function createSingletonNode(container, tagname, classname, innerHTML) {

    // Find all nodes matching the description
    var nodes = container.querySelectorAll('.' + classname);

    // Check all matches to find one which is a direct child of
    // the specified container
    for (var i = 0; i < nodes.length; i++) {
      var testNode = nodes[i];
      if (testNode.parentNode === container) {
        return testNode;
      }
    }

    // If no node was found, create it now
    var node = document.createElement(tagname);
    node.className = classname;
    if (typeof innerHTML === 'string') {
      node.innerHTML = innerHTML;
    }
    container.appendChild(node);

    return node;

  }

	/**
	 * Creates the slide background elements and appends them
	 * to the background container. One element is created per
	 * slide no matter if the given slide has visible background.
	 */
  function createBackgrounds() {

    var printMode = isPrintingPDF();

    // Clear prior backgrounds
    dom.background.innerHTML = '';
    dom.background.classList.add('no-transition');

    // Iterate over all horizontal slides
    toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR)).forEach(function (slideh) {

      var backgroundStack = createBackground(slideh, dom.background);

      // Iterate over all vertical slides
      toArray(slideh.querySelectorAll('section')).forEach(function (slidev) {

        createBackground(slidev, backgroundStack);

        backgroundStack.classList.add('stack');

      });

    });

    // Add parallax background if specified
    if (config.parallaxBackgroundImage) {

      dom.background.style.backgroundImage = 'url("' + config.parallaxBackgroundImage + '")';
      dom.background.style.backgroundSize = config.parallaxBackgroundSize;
      dom.background.style.backgroundRepeat = config.parallaxBackgroundRepeat;
      dom.background.style.backgroundPosition = config.parallaxBackgroundPosition;

      // Make sure the below properties are set on the element - these properties are
      // needed for proper transitions to be set on the element via CSS. To remove
      // annoying background slide-in effect when the presentation starts, apply
      // these properties after short time delay
      setTimeout(function () {
        dom.wrapper.classList.add('has-parallax-background');
      }, 1);

    }
    else {

      dom.background.style.backgroundImage = '';
      dom.wrapper.classList.remove('has-parallax-background');

    }

  }

	/**
	 * Creates a background for the given slide.
	 *
	 * @param {HTMLElement} slide
	 * @param {HTMLElement} container The element that the background
	 * should be appended to
	 * @return {HTMLElement} New background div
	 */
  function createBackground(slide, container) {


    // Main slide background element
    var element = document.createElement('div');
    element.className = 'slide-background ' + slide.className.replace(/present|past|future/, '');

    // Inner background element that wraps images/videos/iframes
    var contentElement = document.createElement('div');
    contentElement.className = 'slide-background-content';

    element.appendChild(contentElement);
    container.appendChild(element);

    slide.slideBackgroundElement = element;
    slide.slideBackgroundContentElement = contentElement;

    // Syncs the background to reflect all current background settings
    syncBackground(slide);

    return element;

  }

	/**
	 * Renders all of the visual properties of a slide background
	 * based on the various background attributes.
	 *
	 * @param {HTMLElement} slide
	 */
  function syncBackground(slide) {

    var element = slide.slideBackgroundElement,
      contentElement = slide.slideBackgroundContentElement;

    // Reset the prior background state in case this is not the
    // initial sync
    slide.classList.remove('has-dark-background');
    slide.classList.remove('has-light-background');

    element.removeAttribute('data-loaded');
    element.removeAttribute('data-background-hash');
    element.removeAttribute('data-background-size');
    element.removeAttribute('data-background-transition');
    element.style.backgroundColor = '';

    contentElement.style.backgroundSize = '';
    contentElement.style.backgroundRepeat = '';
    contentElement.style.backgroundPosition = '';
    contentElement.style.backgroundImage = '';
    contentElement.style.opacity = '';
    contentElement.innerHTML = '';

    var data = {
      background: slide.getAttribute('data-background'),
      backgroundSize: slide.getAttribute('data-background-size'),
      backgroundImage: slide.getAttribute('data-background-image'),
      backgroundVideo: slide.getAttribute('data-background-video'),
      backgroundIframe: slide.getAttribute('data-background-iframe'),
      backgroundColor: slide.getAttribute('data-background-color'),
      backgroundRepeat: slide.getAttribute('data-background-repeat'),
      backgroundPosition: slide.getAttribute('data-background-position'),
      backgroundTransition: slide.getAttribute('data-background-transition'),
      backgroundOpacity: slide.getAttribute('data-background-opacity')
    };

    if (data.background) {
      // Auto-wrap image urls in url(...)
      if (/^(http|file|\/\/)/gi.test(data.background) || /\.(svg|png|jpg|jpeg|gif|bmp)([?#\s]|$)/gi.test(data.background)) {
        slide.setAttribute('data-background-image', data.background);
      }
      else {
        element.style.background = data.background;
      }
    }

    // Create a hash for this combination of background settings.
    // This is used to determine when two slide backgrounds are
    // the same.
    if (data.background || data.backgroundColor || data.backgroundImage || data.backgroundVideo || data.backgroundIframe) {
      element.setAttribute('data-background-hash', data.background +
        data.backgroundSize +
        data.backgroundImage +
        data.backgroundVideo +
        data.backgroundIframe +
        data.backgroundColor +
        data.backgroundRepeat +
        data.backgroundPosition +
        data.backgroundTransition +
        data.backgroundOpacity);
    }

    // Additional and optional background properties
    if (data.backgroundSize) element.setAttribute('data-background-size', data.backgroundSize);
    if (data.backgroundColor) element.style.backgroundColor = data.backgroundColor;
    if (data.backgroundTransition) element.setAttribute('data-background-transition', data.backgroundTransition);

    // Background image options are set on the content wrapper
    if (data.backgroundSize) contentElement.style.backgroundSize = data.backgroundSize;
    if (data.backgroundRepeat) contentElement.style.backgroundRepeat = data.backgroundRepeat;
    if (data.backgroundPosition) contentElement.style.backgroundPosition = data.backgroundPosition;
    if (data.backgroundOpacity) contentElement.style.opacity = data.backgroundOpacity;

    // If this slide has a background color, we add a class that
    // signals if it is light or dark. If the slide has no background
    // color, no class will be added
    var contrastColor = data.backgroundColor;

    // If no bg color was found, check the computed background
    if (!contrastColor) {
      var computedBackgroundStyle = window.getComputedStyle(element);
      if (computedBackgroundStyle && computedBackgroundStyle.backgroundColor) {
        contrastColor = computedBackgroundStyle.backgroundColor;
      }
    }

    if (contrastColor) {
      var rgb = colorToRgb(contrastColor);

      // Ignore fully transparent backgrounds. Some browsers return
      // rgba(0,0,0,0) when reading the computed background color of
      // an element with no background
      if (rgb && rgb.a !== 0) {
        if (colorBrightness(contrastColor) < 128) {
          slide.classList.add('has-dark-background');
        }
        else {
          slide.classList.add('has-light-background');
        }
      }
    }

  }

	/**
	 * Registers a listener to postMessage events, this makes it
	 * possible to call all reveal.js API methods from another
	 * window. For example:
	 *
	 * revealWindow.postMessage( JSON.stringify({
	 *   method: 'slide',
	 *   args: [ 2 ]
	 * }), '*' );
	 */
  function setupPostMessage() {

    if (config.postMessage) {
      window.addEventListener('message', function (event) {
        var data = event.data;

        // Make sure we're dealing with JSON
        if (typeof data === 'string' && data.charAt(0) === '{' && data.charAt(data.length - 1) === '}') {
          data = JSON.parse(data);

          // Check if the requested method can be found
          if (data.method && typeof Reveal[data.method] === 'function') {
            Reveal[data.method].apply(Reveal, data.args);
          }
        }
      }, false);
    }

  }

	/**
	 * Applies the configuration settings from the config
	 * object. May be called multiple times.
	 *
	 * @param {object} options
	 */
  function configure(options) {

    var oldTransition = config.transition;

    // New config options may be passed when this method
    // is invoked through the API after initialization
    if (typeof options === 'object') extend(config, options);

    // Abort if reveal.js hasn't finished loading, config
    // changes will be applied automatically once loading
    // finishes
    if (loaded === false) return;

    var numberOfSlides = dom.wrapper.querySelectorAll(SLIDES_SELECTOR).length;

    // Remove the previously configured transition class
    dom.wrapper.classList.remove(oldTransition);

    // Force linear transition based on browser capabilities
    if (features.transforms3d === false) config.transition = 'linear';

    dom.wrapper.classList.add(config.transition);

    dom.wrapper.setAttribute('data-transition-speed', config.transitionSpeed);
    dom.wrapper.setAttribute('data-background-transition', config.backgroundTransition);

    dom.controls.style.display = config.controls ? 'block' : 'none';
    dom.progress.style.display = config.progress ? 'block' : 'none';

    dom.controls.setAttribute('data-controls-layout', config.controlsLayout);
    dom.controls.setAttribute('data-controls-back-arrows', config.controlsBackArrows);

    if (config.shuffle) {
      shuffle();
    }

    if (config.rtl) {
      dom.wrapper.classList.add('rtl');
    }
    else {
      dom.wrapper.classList.remove('rtl');
    }

    if (config.center) {
      dom.wrapper.classList.add('center');
    }
    else {
      dom.wrapper.classList.remove('center');
    }

    // Exit the paused mode if it was configured off
    if (config.pause === false) {
      resume();
    }

    if (config.showNotes) {
      dom.speakerNotes.setAttribute('data-layout', typeof config.showNotes === 'string' ? config.showNotes : 'inline');
    }

    if (config.mouseWheel) {
      document.addEventListener('DOMMouseScroll', onDocumentMouseScroll, false); // FF
      document.addEventListener('mousewheel', onDocumentMouseScroll, false);
    }
    else {
      document.removeEventListener('DOMMouseScroll', onDocumentMouseScroll, false); // FF
      document.removeEventListener('mousewheel', onDocumentMouseScroll, false);
    }

    // Rolling 3D links
    if (config.rollingLinks) {
      enableRollingLinks();
    }
    else {
      disableRollingLinks();
    }

    // Auto-hide the mouse pointer when its inactive
    if (config.hideInactiveCursor) {
      document.addEventListener('mousemove', onDocumentCursorActive, false);
      document.addEventListener('mousedown', onDocumentCursorActive, false);
    }
    else {
      showCursor();

      document.removeEventListener('mousemove', onDocumentCursorActive, false);
      document.removeEventListener('mousedown', onDocumentCursorActive, false);
    }

    // Iframe link previews
    if (config.previewLinks) {
      enablePreviewLinks();
      disablePreviewLinks('[data-preview-link=false]');
    }
    else {
      disablePreviewLinks();
      enablePreviewLinks('[data-preview-link]:not([data-preview-link=false])');
    }

    // Remove existing auto-slide controls
    if (autoSlidePlayer) {
      autoSlidePlayer.destroy();
      autoSlidePlayer = null;
    }

    // Generate auto-slide controls if needed
    if (numberOfSlides > 1 && config.autoSlide && config.autoSlideStoppable && features.canvas && features.requestAnimationFrame) {
      autoSlidePlayer = new Playback(dom.wrapper, function () {
        return Math.min(Math.max((Date.now() - autoSlideStartTime) / autoSlide, 0), 1);
      });

      autoSlidePlayer.on('click', onAutoSlidePlayerClick);
      autoSlidePaused = false;
    }

    // When fragments are turned off they should be visible
    if (config.fragments === false) {
      toArray(dom.slides.querySelectorAll('.fragment')).forEach(function (element) {
        element.classList.add('visible');
        element.classList.remove('current-fragment');
      });
    }

    // Slide numbers
    var slideNumberDisplay = 'none';
    if (config.slideNumber && !isPrintingPDF()) {
      if (config.showSlideNumber === 'all') {
        slideNumberDisplay = 'block';
      }
      else if (config.showSlideNumber === 'speaker' && isSpeakerNotes()) {
        slideNumberDisplay = 'block';
      }
    }

    dom.slideNumber.style.display = slideNumberDisplay;

    // Add the navigation mode to the DOM so we can adjust styling
    if (config.navigationMode !== 'default') {
      dom.wrapper.setAttribute('data-navigation-mode', config.navigationMode);
    }
    else {
      dom.wrapper.removeAttribute('data-navigation-mode');
    }

    // Define our contextual list of keyboard shortcuts
    if (config.navigationMode === 'linear') {
      keyboardShortcuts['&#8594;  ,  &#8595;  ,  SPACE  ,  N  ,  L  ,  J'] = 'Next slide';
      keyboardShortcuts['&#8592;  ,  &#8593;  ,  P  ,  H  ,  K'] = 'Previous slide';
    }
    else {
      keyboardShortcuts['N  ,  SPACE'] = 'Next slide';
      keyboardShortcuts['P'] = 'Previous slide';
      keyboardShortcuts['&#8592;  ,  H'] = 'Navigate left';
      keyboardShortcuts['&#8594;  ,  L'] = 'Navigate right';
      keyboardShortcuts['&#8593;  ,  K'] = 'Navigate up';
      keyboardShortcuts['&#8595;  ,  J'] = 'Navigate down';
    }

    keyboardShortcuts['Home  ,  &#8984;/CTRL &#8592;'] = 'First slide';
    keyboardShortcuts['End  ,  &#8984;/CTRL &#8594;'] = 'Last slide';
    keyboardShortcuts['B  ,  .'] = 'Pause';
    keyboardShortcuts['F'] = 'Fullscreen';
    keyboardShortcuts['ESC, O'] = 'Slide overview';

    sync();

  }

	/**
	 * Binds all event listeners.
	 */
  function addEventListeners() {

    eventsAreBound = true;

    window.addEventListener('hashchange', onWindowHashChange, false);
    window.addEventListener('resize', onWindowResize, false);

    if (config.touch) {
      if ('onpointerdown' in window) {
        // Use W3C pointer events
        dom.wrapper.addEventListener('pointerdown', onPointerDown, false);
        dom.wrapper.addEventListener('pointermove', onPointerMove, false);
        dom.wrapper.addEventListener('pointerup', onPointerUp, false);
      }
      else if (window.navigator.msPointerEnabled) {
        // IE 10 uses prefixed version of pointer events
        dom.wrapper.addEventListener('MSPointerDown', onPointerDown, false);
        dom.wrapper.addEventListener('MSPointerMove', onPointerMove, false);
        dom.wrapper.addEventListener('MSPointerUp', onPointerUp, false);
      }
      else {
        // Fall back to touch events
        dom.wrapper.addEventListener('touchstart', onTouchStart, false);
        dom.wrapper.addEventListener('touchmove', onTouchMove, false);
        dom.wrapper.addEventListener('touchend', onTouchEnd, false);
      }
    }

    if (config.keyboard) {
      document.addEventListener('keydown', onDocumentKeyDown, false);
      document.addEventListener('keypress', onDocumentKeyPress, false);
    }

    if (config.progress && dom.progress) {
      dom.progress.addEventListener('click', onProgressClicked, false);
    }

    dom.pauseOverlay.addEventListener('click', resume, false);

    if (config.focusBodyOnPageVisibilityChange) {
      var visibilityChange;

      if ('hidden' in document) {
        visibilityChange = 'visibilitychange';
      }
      else if ('msHidden' in document) {
        visibilityChange = 'msvisibilitychange';
      }
      else if ('webkitHidden' in document) {
        visibilityChange = 'webkitvisibilitychange';
      }

      if (visibilityChange) {
        document.addEventListener(visibilityChange, onPageVisibilityChange, false);
      }
    }

    // Listen to both touch and click events, in case the device
    // supports both
    var pointerEvents = ['touchstart', 'click'];

    // Only support touch for Android, fixes double navigations in
    // stock browser
    if (UA.match(/android/gi)) {
      pointerEvents = ['touchstart'];
    }

    pointerEvents.forEach(function (eventName) {
      dom.controlsLeft.forEach(function (el) { el.addEventListener(eventName, onNavigateLeftClicked, false); });
      dom.controlsRight.forEach(function (el) { el.addEventListener(eventName, onNavigateRightClicked, false); });
      dom.controlsUp.forEach(function (el) { el.addEventListener(eventName, onNavigateUpClicked, false); });
      dom.controlsDown.forEach(function (el) { el.addEventListener(eventName, onNavigateDownClicked, false); });
      dom.controlsPrev.forEach(function (el) { el.addEventListener(eventName, onNavigatePrevClicked, false); });
      dom.controlsNext.forEach(function (el) { el.addEventListener(eventName, onNavigateNextClicked, false); });
    });

  }

	/**
	 * Unbinds all event listeners.
	 */
  function removeEventListeners() {

    eventsAreBound = false;

    document.removeEventListener('keydown', onDocumentKeyDown, false);
    document.removeEventListener('keypress', onDocumentKeyPress, false);
    window.removeEventListener('hashchange', onWindowHashChange, false);
    window.removeEventListener('resize', onWindowResize, false);

    dom.wrapper.removeEventListener('pointerdown', onPointerDown, false);
    dom.wrapper.removeEventListener('pointermove', onPointerMove, false);
    dom.wrapper.removeEventListener('pointerup', onPointerUp, false);

    dom.wrapper.removeEventListener('MSPointerDown', onPointerDown, false);
    dom.wrapper.removeEventListener('MSPointerMove', onPointerMove, false);
    dom.wrapper.removeEventListener('MSPointerUp', onPointerUp, false);

    dom.wrapper.removeEventListener('touchstart', onTouchStart, false);
    dom.wrapper.removeEventListener('touchmove', onTouchMove, false);
    dom.wrapper.removeEventListener('touchend', onTouchEnd, false);

    dom.pauseOverlay.removeEventListener('click', resume, false);

    if (config.progress && dom.progress) {
      dom.progress.removeEventListener('click', onProgressClicked, false);
    }

    ['touchstart', 'click'].forEach(function (eventName) {
      dom.controlsLeft.forEach(function (el) { el.removeEventListener(eventName, onNavigateLeftClicked, false); });
      dom.controlsRight.forEach(function (el) { el.removeEventListener(eventName, onNavigateRightClicked, false); });
      dom.controlsUp.forEach(function (el) { el.removeEventListener(eventName, onNavigateUpClicked, false); });
      dom.controlsDown.forEach(function (el) { el.removeEventListener(eventName, onNavigateDownClicked, false); });
      dom.controlsPrev.forEach(function (el) { el.removeEventListener(eventName, onNavigatePrevClicked, false); });
      dom.controlsNext.forEach(function (el) { el.removeEventListener(eventName, onNavigateNextClicked, false); });
    });

  }

	/**
	 * Registers a new plugin with this reveal.js instance.
	 *
	 * reveal.js waits for all regisered plugins to initialize
	 * before considering itself ready, as long as the plugin
	 * is registered before calling `Reveal.initialize()`.
	 */
  function registerPlugin(id, plugin) {

    if (plugins[id] === undefined) {
      plugins[id] = plugin;

      // If a plugin is registered after reveal.js is loaded,
      // initialize it right away
      if (loaded && typeof plugin.init === 'function') {
        plugin.init();
      }
    }
    else {
      console.warn('reveal.js: "' + id + '" plugin has already been registered');
    }

  }

	/**
	 * Checks if a specific plugin has been registered.
	 *
	 * @param {String} id Unique plugin identifier
	 */
  function hasPlugin(id) {

    return !!plugins[id];

  }

	/**
	 * Returns the specific plugin instance, if a plugin
	 * with the given ID has been registered.
	 *
	 * @param {String} id Unique plugin identifier
	 */
  function getPlugin(id) {

    return plugins[id];

  }

	/**
	 * Add a custom key binding with optional description to
	 * be added to the help screen.
	 */
  function addKeyBinding(binding, callback) {

    if (typeof binding === 'object' && binding.keyCode) {
      registeredKeyBindings[binding.keyCode] = {
        callback: callback,
        key: binding.key,
        description: binding.description
      };
    }
    else {
      registeredKeyBindings[binding] = {
        callback: callback,
        key: null,
        description: null
      };
    }

  }

	/**
	 * Removes the specified custom key binding.
	 */
  function removeKeyBinding(keyCode) {

    delete registeredKeyBindings[keyCode];

  }

	/**
	 * Extend object a with the properties of object b.
	 * If there's a conflict, object b takes precedence.
	 *
	 * @param {object} a
	 * @param {object} b
	 */
  function extend(a, b) {

    for (var i in b) {
      a[i] = b[i];
    }

    return a;

  }

	/**
	 * Converts the target object to an array.
	 *
	 * @param {object} o
	 * @return {object[]}
	 */
  function toArray(o) {

    return Array.prototype.slice.call(o);

  }

	/**
	 * Utility for deserializing a value.
	 *
	 * @param {*} value
	 * @return {*}
	 */
  function deserialize(value) {

    if (typeof value === 'string') {
      if (value === 'null') return null;
      else if (value === 'true') return true;
      else if (value === 'false') return false;
      else if (value.match(/^-?[\d\.]+$/)) return parseFloat(value);
    }

    return value;

  }

	/**
	 * Measures the distance in pixels between point a
	 * and point b.
	 *
	 * @param {object} a point with x/y properties
	 * @param {object} b point with x/y properties
	 *
	 * @return {number}
	 */
  function distanceBetween(a, b) {

    var dx = a.x - b.x,
      dy = a.y - b.y;

    return Math.sqrt(dx * dx + dy * dy);

  }

	/**
	 * Applies a CSS transform to the target element.
	 *
	 * @param {HTMLElement} element
	 * @param {string} transform
	 */
  function transformElement(element, transform) {

    element.style.WebkitTransform = transform;
    element.style.MozTransform = transform;
    element.style.msTransform = transform;
    element.style.transform = transform;

  }

	/**
	 * Applies CSS transforms to the slides container. The container
	 * is transformed from two separate sources: layout and the overview
	 * mode.
	 *
	 * @param {object} transforms
	 */
  function transformSlides(transforms) {

    // Pick up new transforms from arguments
    if (typeof transforms.layout === 'string') slidesTransform.layout = transforms.layout;
    if (typeof transforms.overview === 'string') slidesTransform.overview = transforms.overview;

    // Apply the transforms to the slides container
    if (slidesTransform.layout) {
      transformElement(dom.slides, slidesTransform.layout + ' ' + slidesTransform.overview);
    }
    else {
      transformElement(dom.slides, slidesTransform.overview);
    }

  }

	/**
	 * Injects the given CSS styles into the DOM.
	 *
	 * @param {string} value
	 */
  function injectStyleSheet(value) {

    var tag = document.createElement('style');
    tag.type = 'text/css';
    if (tag.styleSheet) {
      tag.styleSheet.cssText = value;
    }
    else {
      tag.appendChild(document.createTextNode(value));
    }
    document.getElementsByTagName('head')[0].appendChild(tag);

  }

	/**
	 * Find the closest parent that matches the given
	 * selector.
	 *
	 * @param {HTMLElement} target The child element
	 * @param {String} selector The CSS selector to match
	 * the parents against
	 *
	 * @return {HTMLElement} The matched parent or null
	 * if no matching parent was found
	 */
  function closestParent(target, selector) {

    var parent = target.parentNode;

    while (parent) {

      // There's some overhead doing this each time, we don't
      // want to rewrite the element prototype but should still
      // be enough to feature detect once at startup...
      var matchesMethod = parent.matches || parent.matchesSelector || parent.msMatchesSelector;

      // If we find a match, we're all set
      if (matchesMethod && matchesMethod.call(parent, selector)) {
        return parent;
      }

      // Keep searching
      parent = parent.parentNode;

    }

    return null;

  }

	/**
	 * Converts various color input formats to an {r:0,g:0,b:0} object.
	 *
	 * @param {string} color The string representation of a color
	 * @example
	 * colorToRgb('#000');
	 * @example
	 * colorToRgb('#000000');
	 * @example
	 * colorToRgb('rgb(0,0,0)');
	 * @example
	 * colorToRgb('rgba(0,0,0)');
	 *
	 * @return {{r: number, g: number, b: number, [a]: number}|null}
	 */
  function colorToRgb(color) {

    var hex3 = color.match(/^#([0-9a-f]{3})$/i);
    if (hex3 && hex3[1]) {
      hex3 = hex3[1];
      return {
        r: parseInt(hex3.charAt(0), 16) * 0x11,
        g: parseInt(hex3.charAt(1), 16) * 0x11,
        b: parseInt(hex3.charAt(2), 16) * 0x11
      };
    }

    var hex6 = color.match(/^#([0-9a-f]{6})$/i);
    if (hex6 && hex6[1]) {
      hex6 = hex6[1];
      return {
        r: parseInt(hex6.substr(0, 2), 16),
        g: parseInt(hex6.substr(2, 2), 16),
        b: parseInt(hex6.substr(4, 2), 16)
      };
    }

    var rgb = color.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
    if (rgb) {
      return {
        r: parseInt(rgb[1], 10),
        g: parseInt(rgb[2], 10),
        b: parseInt(rgb[3], 10)
      };
    }

    var rgba = color.match(/^rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\,\s*([\d]+|[\d]*.[\d]+)\s*\)$/i);
    if (rgba) {
      return {
        r: parseInt(rgba[1], 10),
        g: parseInt(rgba[2], 10),
        b: parseInt(rgba[3], 10),
        a: parseFloat(rgba[4])
      };
    }

    return null;

  }

	/**
	 * Calculates brightness on a scale of 0-255.
	 *
	 * @param {string} color See colorToRgb for supported formats.
	 * @see {@link colorToRgb}
	 */
  function colorBrightness(color) {

    if (typeof color === 'string') color = colorToRgb(color);

    if (color) {
      return (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
    }

    return null;

  }

	/**
	 * Returns the remaining height within the parent of the
	 * target element.
	 *
	 * remaining height = [ configured parent height ] - [ current parent height ]
	 *
	 * @param {HTMLElement} element
	 * @param {number} [height]
	 */
  function getRemainingHeight(element, height) {

    height = height || 0;

    if (element) {
      var newHeight, oldHeight = element.style.height;

      // Change the .stretch element height to 0 in order find the height of all
      // the other elements
      element.style.height = '0px';

      // In Overview mode, the parent (.slide) height is set of 700px.
      // Restore it temporarily to its natural height.
      element.parentNode.style.height = 'auto';

      newHeight = height - element.parentNode.offsetHeight;

      // Restore the old height, just in case
      element.style.height = oldHeight + 'px';

      // Clear the parent (.slide) height. .removeProperty works in IE9+
      element.parentNode.style.removeProperty('height');

      return newHeight;
    }

    return height;

  }

	/**
	 * Checks if this instance is being used to print a PDF.
	 */
  function isPrintingPDF() {

    return (/print-pdf/gi).test(window.location.search);

  }

	/**
	 * Hides the address bar if we're on a mobile device.
	 */
  function hideAddressBar() {

    if (config.hideAddressBar && isMobileDevice) {
      // Events that should trigger the address bar to hide
      window.addEventListener('load', removeAddressBar, false);
      window.addEventListener('orientationchange', removeAddressBar, false);
    }

  }

	/**
	 * Causes the address bar to hide on mobile devices,
	 * more vertical space ftw.
	 */
  function removeAddressBar() {

    setTimeout(function () {
      window.scrollTo(0, 1);
    }, 10);

  }

	/**
	 * Dispatches an event of the specified type from the
	 * reveal DOM element.
	 */
  function dispatchEvent(type, args) {

    var event = document.createEvent('HTMLEvents', 1, 2);
    event.initEvent(type, true, true);
    extend(event, args);
    dom.wrapper.dispatchEvent(event);

    // If we're in an iframe, post each reveal.js event to the
    // parent window. Used by the notes plugin
    if (config.postMessageEvents && window.parent !== window.self) {
      window.parent.postMessage(JSON.stringify({ namespace: 'reveal', eventName: type, state: getState() }), '*');
    }

  }

	/**
	 * Wrap all links in 3D goodness.
	 */
  function enableRollingLinks() {

    if (features.transforms3d && !('msPerspective' in document.body.style)) {
      var anchors = dom.wrapper.querySelectorAll(SLIDES_SELECTOR + ' a');

      for (var i = 0, len = anchors.length; i < len; i++) {
        var anchor = anchors[i];

        if (anchor.textContent && !anchor.querySelector('*') && (!anchor.className || !anchor.classList.contains(anchor, 'roll'))) {
          var span = document.createElement('span');
          span.setAttribute('data-title', anchor.text);
          span.innerHTML = anchor.innerHTML;

          anchor.classList.add('roll');
          anchor.innerHTML = '';
          anchor.appendChild(span);
        }
      }
    }

  }

	/**
	 * Unwrap all 3D links.
	 */
  function disableRollingLinks() {

    var anchors = dom.wrapper.querySelectorAll(SLIDES_SELECTOR + ' a.roll');

    for (var i = 0, len = anchors.length; i < len; i++) {
      var anchor = anchors[i];
      var span = anchor.querySelector('span');

      if (span) {
        anchor.classList.remove('roll');
        anchor.innerHTML = span.innerHTML;
      }
    }

  }

	/**
	 * Bind preview frame links.
	 *
	 * @param {string} [selector=a] - selector for anchors
	 */
  function enablePreviewLinks(selector) {

    var anchors = toArray(document.querySelectorAll(selector ? selector : 'a'));

    anchors.forEach(function (element) {
      if (/^(http|www)/gi.test(element.getAttribute('href'))) {
        element.addEventListener('click', onPreviewLinkClicked, false);
      }
    });

  }

	/**
	 * Unbind preview frame links.
	 */
  function disablePreviewLinks(selector) {

    var anchors = toArray(document.querySelectorAll(selector ? selector : 'a'));

    anchors.forEach(function (element) {
      if (/^(http|www)/gi.test(element.getAttribute('href'))) {
        element.removeEventListener('click', onPreviewLinkClicked, false);
      }
    });

  }

	/**
	 * Opens a preview window for the target URL.
	 *
	 * @param {string} url - url for preview iframe src
	 */
  function showPreview(url) {

    closeOverlay();

    dom.overlay = document.createElement('div');
    dom.overlay.classList.add('overlay');
    dom.overlay.classList.add('overlay-preview');
    dom.wrapper.appendChild(dom.overlay);

    dom.overlay.innerHTML = [
      '<header>',
      '<a class="close" href="#"><span class="icon"></span></a>',
      '<a class="external" href="' + url + '" target="_blank"><span class="icon"></span></a>',
      '</header>',
      '<div class="spinner"></div>',
      '<div class="viewport">',
      '<iframe src="' + url + '"></iframe>',
      '<small class="viewport-inner">',
      '<span class="x-frame-error">Unable to load iframe. This is likely due to the site\'s policy (x-frame-options).</span>',
      '</small>',
      '</div>'
    ].join('');

    dom.overlay.querySelector('iframe').addEventListener('load', function (event) {
      dom.overlay.classList.add('loaded');
    }, false);

    dom.overlay.querySelector('.close').addEventListener('click', function (event) {
      closeOverlay();
      event.preventDefault();
    }, false);

    dom.overlay.querySelector('.external').addEventListener('click', function (event) {
      closeOverlay();
    }, false);

    setTimeout(function () {
      dom.overlay.classList.add('visible');
    }, 1);

  }

	/**
	 * Open or close help overlay window.
	 *
	 * @param {Boolean} [override] Flag which overrides the
	 * toggle logic and forcibly sets the desired state. True means
	 * help is open, false means it's closed.
	 */
  function toggleHelp(override) {

    if (typeof override === 'boolean') {
      override ? showHelp() : closeOverlay();
    }
    else {
      if (dom.overlay) {
        closeOverlay();
      }
      else {
        showHelp();
      }
    }
  }

	/**
	 * Opens an overlay window with help material.
	 */
  function showHelp() {

    if (config.help) {

      closeOverlay();

      dom.overlay = document.createElement('div');
      dom.overlay.classList.add('overlay');
      dom.overlay.classList.add('overlay-help');
      dom.wrapper.appendChild(dom.overlay);

      var html = '<p class="title">Keyboard Shortcuts</p><br/>';

      html += '<table><th>KEY</th><th>ACTION</th>';
      for (var key in keyboardShortcuts) {
        html += '<tr><td>' + key + '</td><td>' + keyboardShortcuts[key] + '</td></tr>';
      }

      // Add custom key bindings that have associated descriptions
      for (var binding in registeredKeyBindings) {
        if (registeredKeyBindings[binding].key && registeredKeyBindings[binding].description) {
          html += '<tr><td>' + registeredKeyBindings[binding].key + '</td><td>' + registeredKeyBindings[binding].description + '</td></tr>';
        }
      }

      html += '</table>';

      dom.overlay.innerHTML = [
        '<header>',
        '<a class="close" href="#"><span class="icon"></span></a>',
        '</header>',
        '<div class="viewport">',
        '<div class="viewport-inner">' + html + '</div>',
        '</div>'
      ].join('');

      dom.overlay.querySelector('.close').addEventListener('click', function (event) {
        closeOverlay();
        event.preventDefault();
      }, false);

      setTimeout(function () {
        dom.overlay.classList.add('visible');
      }, 1);

    }

  }

	/**
	 * Closes any currently open overlay.
	 */
  function closeOverlay() {

    if (dom.overlay) {
      dom.overlay.parentNode.removeChild(dom.overlay);
      dom.overlay = null;
    }

  }

	/**
	 * Applies JavaScript-controlled layout rules to the
	 * presentation.
	 */
  function layout() {

    if (dom.wrapper && !isPrintingPDF()) {

      if (!config.disableLayout) {

        // On some mobile devices '100vh' is taller than the visible
        // viewport which leads to part of the presentation being
        // cut off. To work around this we define our own '--vh' custom
        // property where 100x adds up to the correct height.
        //
        // https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
        if (isMobileDevice) {
          document.documentElement.style.setProperty('--vh', (window.innerHeight * 0.01) + 'px');
        }

        var size = getComputedSlideSize();

        var oldScale = scale;

        // Layout the contents of the slides
        layoutSlideContents(config.width, config.height);

        dom.slides.style.width = size.width + 'px';
        dom.slides.style.height = size.height + 'px';

        // Determine scale of content to fit within available space
        scale = Math.min(size.presentationWidth / size.width, size.presentationHeight / size.height);

        // Respect max/min scale settings
        scale = Math.max(scale, config.minScale);
        scale = Math.min(scale, config.maxScale);

        // Don't apply any scaling styles if scale is 1
        if (scale === 1) {
          dom.slides.style.zoom = '';
          dom.slides.style.left = '';
          dom.slides.style.top = '';
          dom.slides.style.bottom = '';
          dom.slides.style.right = '';
          transformSlides({ layout: '' });
        }
        else {
          // Prefer zoom for scaling up so that content remains crisp.
          // Don't use zoom to scale down since that can lead to shifts
          // in text layout/line breaks.
          if (scale > 1 && features.zoom) {
            dom.slides.style.zoom = scale;
            dom.slides.style.left = '';
            dom.slides.style.top = '';
            dom.slides.style.bottom = '';
            dom.slides.style.right = '';
            transformSlides({ layout: '' });
          }
          // Apply scale transform as a fallback
          else {
            dom.slides.style.zoom = '';
            dom.slides.style.left = '50%';
            dom.slides.style.top = '50%';
            dom.slides.style.bottom = 'auto';
            dom.slides.style.right = 'auto';
            transformSlides({ layout: 'translate(-50%, -50%) scale(' + scale + ')' });
          }
        }

        // Select all slides, vertical and horizontal
        var slides = toArray(dom.wrapper.querySelectorAll(SLIDES_SELECTOR));

        for (var i = 0, len = slides.length; i < len; i++) {
          var slide = slides[i];

          // Don't bother updating invisible slides
          if (slide.style.display === 'none') {
            continue;
          }

          if (config.center || slide.classList.contains('center')) {
            // Vertical stacks are not centred since their section
            // children will be
            if (slide.classList.contains('stack')) {
              slide.style.top = 0;
            }
            else {
              slide.style.top = Math.max((size.height - slide.scrollHeight) / 2, 0) + 'px';
            }
          }
          else {
            slide.style.top = '';
          }

        }

        if (oldScale !== scale) {
          dispatchEvent('resize', {
            'oldScale': oldScale,
            'scale': scale,
            'size': size
          });
        }
      }

      updateProgress();
      updateParallax();

      if (isOverview()) {
        updateOverview();
      }

    }

  }

	/**
	 * Applies layout logic to the contents of all slides in
	 * the presentation.
	 *
	 * @param {string|number} width
	 * @param {string|number} height
	 */
  function layoutSlideContents(width, height) {

    // Handle sizing of elements with the 'stretch' class
    toArray(dom.slides.querySelectorAll('section > .stretch')).forEach(function (element) {

      // Determine how much vertical space we can use
      var remainingHeight = getRemainingHeight(element, height);

      // Consider the aspect ratio of media elements
      if (/(img|video)/gi.test(element.nodeName)) {
        var nw = element.naturalWidth || element.videoWidth,
          nh = element.naturalHeight || element.videoHeight;

        var es = Math.min(width / nw, remainingHeight / nh);

        element.style.width = (nw * es) + 'px';
        element.style.height = (nh * es) + 'px';

      }
      else {
        element.style.width = width + 'px';
        element.style.height = remainingHeight + 'px';
      }

    });

  }

	/**
	 * Calculates the computed pixel size of our slides. These
	 * values are based on the width and height configuration
	 * options.
	 *
	 * @param {number} [presentationWidth=dom.wrapper.offsetWidth]
	 * @param {number} [presentationHeight=dom.wrapper.offsetHeight]
	 */
  function getComputedSlideSize(presentationWidth, presentationHeight) {

    var size = {
      // Slide size
      width: config.width,
      height: config.height,

      // Presentation size
      presentationWidth: presentationWidth || dom.wrapper.offsetWidth,
      presentationHeight: presentationHeight || dom.wrapper.offsetHeight
    };

    // Reduce available space by margin
    size.presentationWidth -= (size.presentationWidth * config.margin);
    size.presentationHeight -= (size.presentationHeight * config.margin);

    // Slide width may be a percentage of available width
    if (typeof size.width === 'string' && /%$/.test(size.width)) {
      size.width = parseInt(size.width, 10) / 100 * size.presentationWidth;
    }

    // Slide height may be a percentage of available height
    if (typeof size.height === 'string' && /%$/.test(size.height)) {
      size.height = parseInt(size.height, 10) / 100 * size.presentationHeight;
    }

    return size;

  }

	/**
	 * Stores the vertical index of a stack so that the same
	 * vertical slide can be selected when navigating to and
	 * from the stack.
	 *
	 * @param {HTMLElement} stack The vertical stack element
	 * @param {string|number} [v=0] Index to memorize
	 */
  function setPreviousVerticalIndex(stack, v) {

    if (typeof stack === 'object' && typeof stack.setAttribute === 'function') {
      stack.setAttribute('data-previous-indexv', v || 0);
    }

  }

	/**
	 * Retrieves the vertical index which was stored using
	 * #setPreviousVerticalIndex() or 0 if no previous index
	 * exists.
	 *
	 * @param {HTMLElement} stack The vertical stack element
	 */
  function getPreviousVerticalIndex(stack) {

    if (typeof stack === 'object' && typeof stack.setAttribute === 'function' && stack.classList.contains('stack')) {
      // Prefer manually defined start-indexv
      var attributeName = stack.hasAttribute('data-start-indexv') ? 'data-start-indexv' : 'data-previous-indexv';

      return parseInt(stack.getAttribute(attributeName) || 0, 10);
    }

    return 0;

  }

	/**
	 * Displays the overview of slides (quick nav) by scaling
	 * down and arranging all slide elements.
	 */
  function activateOverview() {

    // Only proceed if enabled in config
    if (config.overview && !isOverview()) {

      overview = true;

      dom.wrapper.classList.add('overview');
      dom.wrapper.classList.remove('overview-deactivating');

      if (features.overviewTransitions) {
        setTimeout(function () {
          dom.wrapper.classList.add('overview-animated');
        }, 1);
      }

      // Don't auto-slide while in overview mode
      cancelAutoSlide();

      // Move the backgrounds element into the slide container to
      // that the same scaling is applied
      dom.slides.appendChild(dom.background);

      // Clicking on an overview slide navigates to it
      toArray(dom.wrapper.querySelectorAll(SLIDES_SELECTOR)).forEach(function (slide) {
        if (!slide.classList.contains('stack')) {
          slide.addEventListener('click', onOverviewSlideClicked, true);
        }
      });

      // Calculate slide sizes
      var margin = 70;
      var slideSize = getComputedSlideSize();
      overviewSlideWidth = slideSize.width + margin;
      overviewSlideHeight = slideSize.height + margin;

      // Reverse in RTL mode
      if (config.rtl) {
        overviewSlideWidth = -overviewSlideWidth;
      }

      updateSlidesVisibility();
      layoutOverview();
      updateOverview();

      layout();

      // Notify observers of the overview showing
      dispatchEvent('overviewshown', {
        'indexh': indexh,
        'indexv': indexv,
        'currentSlide': currentSlide
      });

    }

  }

	/**
	 * Uses CSS transforms to position all slides in a grid for
	 * display inside of the overview mode.
	 */
  function layoutOverview() {

    // Layout slides
    toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR)).forEach(function (hslide, h) {
      hslide.setAttribute('data-index-h', h);
      transformElement(hslide, 'translate3d(' + (h * overviewSlideWidth) + 'px, 0, 0)');

      if (hslide.classList.contains('stack')) {

        toArray(hslide.querySelectorAll('section')).forEach(function (vslide, v) {
          vslide.setAttribute('data-index-h', h);
          vslide.setAttribute('data-index-v', v);

          transformElement(vslide, 'translate3d(0, ' + (v * overviewSlideHeight) + 'px, 0)');
        });

      }
    });

    // Layout slide backgrounds
    toArray(dom.background.childNodes).forEach(function (hbackground, h) {
      transformElement(hbackground, 'translate3d(' + (h * overviewSlideWidth) + 'px, 0, 0)');

      toArray(hbackground.querySelectorAll('.slide-background')).forEach(function (vbackground, v) {
        transformElement(vbackground, 'translate3d(0, ' + (v * overviewSlideHeight) + 'px, 0)');
      });
    });

  }

	/**
	 * Moves the overview viewport to the current slides.
	 * Called each time the current slide changes.
	 */
  function updateOverview() {

    var vmin = Math.min(window.innerWidth, window.innerHeight);
    var scale = Math.max(vmin / 5, 150) / vmin;

    transformSlides({
      overview: [
        'scale(' + scale + ')',
        'translateX(' + (-indexh * overviewSlideWidth) + 'px)',
        'translateY(' + (-indexv * overviewSlideHeight) + 'px)'
      ].join(' ')
    });

  }

	/**
	 * Exits the slide overview and enters the currently
	 * active slide.
	 */
  function deactivateOverview() {

    // Only proceed if enabled in config
    if (config.overview) {

      overview = false;

      dom.wrapper.classList.remove('overview');
      dom.wrapper.classList.remove('overview-animated');

      // Temporarily add a class so that transitions can do different things
      // depending on whether they are exiting/entering overview, or just
      // moving from slide to slide
      dom.wrapper.classList.add('overview-deactivating');

      setTimeout(function () {
        dom.wrapper.classList.remove('overview-deactivating');
      }, 1);

      // Move the background element back out
      dom.wrapper.appendChild(dom.background);

      // Clean up changes made to slides
      toArray(dom.wrapper.querySelectorAll(SLIDES_SELECTOR)).forEach(function (slide) {
        transformElement(slide, '');

        slide.removeEventListener('click', onOverviewSlideClicked, true);
      });

      // Clean up changes made to backgrounds
      toArray(dom.background.querySelectorAll('.slide-background')).forEach(function (background) {
        transformElement(background, '');
      });

      transformSlides({ overview: '' });

      slide(indexh, indexv);

      layout();

      cueAutoSlide();

      // Notify observers of the overview hiding
      dispatchEvent('overviewhidden', {
        'indexh': indexh,
        'indexv': indexv,
        'currentSlide': currentSlide
      });

    }
  }

	/**
	 * Toggles the slide overview mode on and off.
	 *
	 * @param {Boolean} [override] Flag which overrides the
	 * toggle logic and forcibly sets the desired state. True means
	 * overview is open, false means it's closed.
	 */
  function toggleOverview(override) {

    if (typeof override === 'boolean') {
      override ? activateOverview() : deactivateOverview();
    }
    else {
      isOverview() ? deactivateOverview() : activateOverview();
    }

  }

	/**
	 * Checks if the overview is currently active.
	 *
	 * @return {Boolean} true if the overview is active,
	 * false otherwise
	 */
  function isOverview() {

    return overview;

  }

	/**
	 * Return a hash URL that will resolve to the current slide location.
	 */
  function locationHash() {

    var url = '/';

    // Attempt to create a named link based on the slide's ID
    var id = currentSlide ? currentSlide.getAttribute('id') : null;
    if (id) {
      id = encodeURIComponent(id);
    }

    var indexf;
    if (config.fragmentInURL) {
      indexf = getIndices().f;
    }

    // If the current slide has an ID, use that as a named link,
    // but we don't support named links with a fragment index
    if (typeof id === 'string' && id.length && indexf === undefined) {
      url = '/' + id;
    }
    // Otherwise use the /h/v index
    else {
      var hashIndexBase = config.hashOneBasedIndex ? 1 : 0;
      if (indexh > 0 || indexv > 0 || indexf !== undefined) url += indexh + hashIndexBase;
      if (indexv > 0 || indexf !== undefined) url += '/' + (indexv + hashIndexBase);
      if (indexf !== undefined) url += '/' + indexf;
    }

    return url;

  }

	/**
	 * Checks if the current or specified slide is vertical
	 * (nested within another slide).
	 *
	 * @param {HTMLElement} [slide=currentSlide] The slide to check
	 * orientation of
	 * @return {Boolean}
	 */
  function isVerticalSlide(slide) {

    // Prefer slide argument, otherwise use current slide
    slide = slide ? slide : currentSlide;

    return slide && slide.parentNode && !!slide.parentNode.nodeName.match(/section/i);

  }

	/**
	 * Handling the fullscreen functionality via the fullscreen API
	 *
	 * @see http://fullscreen.spec.whatwg.org/
	 * @see https://developer.mozilla.org/en-US/docs/DOM/Using_fullscreen_mode
	 */
  function enterFullscreen() {

    var element = document.documentElement;

    // Check which implementation is available
    var requestMethod = element.requestFullscreen ||
      element.webkitRequestFullscreen ||
      element.webkitRequestFullScreen ||
      element.mozRequestFullScreen ||
      element.msRequestFullscreen;

    if (requestMethod) {
      requestMethod.apply(element);
    }

  }

	/**
	 * Shows the mouse pointer after it has been hidden with
	 * #hideCursor.
	 */
  function showCursor() {

    if (cursorHidden) {
      cursorHidden = false;
      dom.wrapper.style.cursor = '';
    }

  }

	/**
	 * Hides the mouse pointer when it's on top of the .reveal
	 * container.
	 */
  function hideCursor() {

    if (cursorHidden === false) {
      cursorHidden = true;
      dom.wrapper.style.cursor = 'none';
    }

  }

	/**
	 * Enters the paused mode which fades everything on screen to
	 * black.
	 */
  function pause() {

    if (config.pause) {
      var wasPaused = dom.wrapper.classList.contains('paused');

      cancelAutoSlide();
      dom.wrapper.classList.add('paused');

      if (wasPaused === false) {
        dispatchEvent('paused');
      }
    }

  }

	/**
	 * Exits from the paused mode.
	 */
  function resume() {

    var wasPaused = dom.wrapper.classList.contains('paused');
    dom.wrapper.classList.remove('paused');

    cueAutoSlide();

    if (wasPaused) {
      dispatchEvent('resumed');
    }

  }

	/**
	 * Toggles the paused mode on and off.
	 */
  function togglePause(override) {

    if (typeof override === 'boolean') {
      override ? pause() : resume();
    }
    else {
      isPaused() ? resume() : pause();
    }

  }

	/**
	 * Checks if we are currently in the paused mode.
	 *
	 * @return {Boolean}
	 */
  function isPaused() {

    return dom.wrapper.classList.contains('paused');

  }

	/**
	 * Toggles the auto slide mode on and off.
	 *
	 * @param {Boolean} [override] Flag which sets the desired state.
	 * True means autoplay starts, false means it stops.
	 */

  function toggleAutoSlide(override) {

    if (typeof override === 'boolean') {
      override ? resumeAutoSlide() : pauseAutoSlide();
    }

    else {
      autoSlidePaused ? resumeAutoSlide() : pauseAutoSlide();
    }

  }

	/**
	 * Checks if the auto slide mode is currently on.
	 *
	 * @return {Boolean}
	 */
  function isAutoSliding() {

    return !!(autoSlide && !autoSlidePaused);

  }

	/**
	 * Steps from the current point in the presentation to the
	 * slide which matches the specified horizontal and vertical
	 * indices.
	 *
	 * @param {number} [h=indexh] Horizontal index of the target slide
	 * @param {number} [v=indexv] Vertical index of the target slide
	 * @param {number} [f] Index of a fragment within the
	 * target slide to activate
	 * @param {number} [o] Origin for use in multimaster environments
	 */
  function slide(h, v, f, o) {

    // Remember where we were at before
    previousSlide = currentSlide;

    // Query all horizontal slides in the deck
    var horizontalSlides = dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR);

    // Abort if there are no slides
    if (horizontalSlides.length === 0) return;

    // If no vertical index is specified and the upcoming slide is a
    // stack, resume at its previous vertical index
    if (v === undefined && !isOverview()) {
      v = getPreviousVerticalIndex(horizontalSlides[h]);
    }

    // If we were on a vertical stack, remember what vertical index
    // it was on so we can resume at the same position when returning
    if (previousSlide && previousSlide.parentNode && previousSlide.parentNode.classList.contains('stack')) {
      setPreviousVerticalIndex(previousSlide.parentNode, indexv);
    }

    // Remember the state before this slide
    var stateBefore = state.concat();

    // Reset the state array
    state.length = 0;

    var indexhBefore = indexh || 0,
      indexvBefore = indexv || 0;

    // Activate and transition to the new slide
    indexh = updateSlides(HORIZONTAL_SLIDES_SELECTOR, h === undefined ? indexh : h);
    indexv = updateSlides(VERTICAL_SLIDES_SELECTOR, v === undefined ? indexv : v);

    // Update the visibility of slides now that the indices have changed
    updateSlidesVisibility();

    layout();

    // Update the overview if it's currently active
    if (isOverview()) {
      updateOverview();
    }

    // Find the current horizontal slide and any possible vertical slides
    // within it
    var currentHorizontalSlide = horizontalSlides[indexh],
      currentVerticalSlides = currentHorizontalSlide.querySelectorAll('section');

    // Store references to the previous and current slides
    currentSlide = currentVerticalSlides[indexv] || currentHorizontalSlide;

    // Show fragment, if specified
    if (typeof f !== 'undefined') {
      navigateFragment(f);
    }

    // Dispatch an event if the slide changed
    var slideChanged = (indexh !== indexhBefore || indexv !== indexvBefore);
    if (!slideChanged) {
      // Ensure that the previous slide is never the same as the current
      previousSlide = null;
    }

    // Solves an edge case where the previous slide maintains the
    // 'present' class when navigating between adjacent vertical
    // stacks
    if (previousSlide && previousSlide !== currentSlide) {
      previousSlide.classList.remove('present');
      previousSlide.setAttribute('aria-hidden', 'true');

      // Reset all slides upon navigate to home
      // Issue: #285
      if (dom.wrapper.querySelector(HOME_SLIDE_SELECTOR).classList.contains('present')) {
        // Launch async task
        setTimeout(function () {
          var slides = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR + '.stack')), i;
          for (i in slides) {
            if (slides[i]) {
              // Reset stack
              setPreviousVerticalIndex(slides[i], 0);
            }
          }
        }, 0);
      }
    }

    // Apply the new state
    stateLoop: for (var i = 0, len = state.length; i < len; i++) {
      // Check if this state existed on the previous slide. If it
      // did, we will avoid adding it repeatedly
      for (var j = 0; j < stateBefore.length; j++) {
        if (stateBefore[j] === state[i]) {
          stateBefore.splice(j, 1);
          continue stateLoop;
        }
      }

      document.documentElement.classList.add(state[i]);

      // Dispatch custom event matching the state's name
      dispatchEvent(state[i]);
    }

    // Clean up the remains of the previous state
    while (stateBefore.length) {
      document.documentElement.classList.remove(stateBefore.pop());
    }

    if (slideChanged) {
      dispatchEvent('slidechanged', {
        'indexh': indexh,
        'indexv': indexv,
        'previousSlide': previousSlide,
        'currentSlide': currentSlide,
        'origin': o
      });
    }

    // Handle embedded content
    if (slideChanged || !previousSlide) {
      stopEmbeddedContent(previousSlide);
      startEmbeddedContent(currentSlide);
    }

    // Announce the current slide contents, for screen readers
    dom.statusDiv.textContent = getStatusText(currentSlide);

    updateControls();
    updateProgress();
    updateBackground();
    updateParallax();
    updateSlideNumber();
    updateNotes();
    updateFragments();

    // Update the URL hash
    writeURL();

    cueAutoSlide();

  }

	/**
	 * Syncs the presentation with the current DOM. Useful
	 * when new slides or control elements are added or when
	 * the configuration has changed.
	 */
  function sync() {

    // Subscribe to input
    removeEventListeners();
    addEventListeners();

    // Force a layout to make sure the current config is accounted for
    layout();

    // Reflect the current autoSlide value
    autoSlide = config.autoSlide;

    // Start auto-sliding if it's enabled
    cueAutoSlide();

    // Re-create the slide backgrounds
    createBackgrounds();

    // Write the current hash to the URL
    writeURL();

    sortAllFragments();

    updateControls();
    updateProgress();
    updateSlideNumber();
    updateSlidesVisibility();
    updateBackground(true);
    updateNotesVisibility();
    updateNotes();

    formatEmbeddedContent();

    // Start or stop embedded content depending on global config
    if (config.autoPlayMedia === false) {
      stopEmbeddedContent(currentSlide, { unloadIframes: false });
    }
    else {
      startEmbeddedContent(currentSlide);
    }

    if (isOverview()) {
      layoutOverview();
    }

  }

	/**
	 * Updates reveal.js to keep in sync with new slide attributes. For
	 * example, if you add a new `data-background-image` you can call
	 * this to have reveal.js render the new background image.
	 *
	 * Similar to #sync() but more efficient when you only need to
	 * refresh a specific slide.
	 *
	 * @param {HTMLElement} slide
	 */
  function syncSlide(slide) {

    // Default to the current slide
    slide = slide || currentSlide;

    syncBackground(slide);
    syncFragments(slide);

    updateBackground();
    updateNotes();

    loadSlide(slide);

  }

	/**
	 * Formats the fragments on the given slide so that they have
	 * valid indices. Call this if fragments are changed in the DOM
	 * after reveal.js has already initialized.
	 *
	 * @param {HTMLElement} slide
	 * @return {Array} a list of the HTML fragments that were synced
	 */
  function syncFragments(slide) {

    // Default to the current slide
    slide = slide || currentSlide;

    return sortFragments(slide.querySelectorAll('.fragment'));

  }

	/**
	 * Resets all vertical slides so that only the first
	 * is visible.
	 */
  function resetVerticalSlides() {

    var horizontalSlides = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR));
    horizontalSlides.forEach(function (horizontalSlide) {

      var verticalSlides = toArray(horizontalSlide.querySelectorAll('section'));
      verticalSlides.forEach(function (verticalSlide, y) {

        if (y > 0) {
          verticalSlide.classList.remove('present');
          verticalSlide.classList.remove('past');
          verticalSlide.classList.add('future');
          verticalSlide.setAttribute('aria-hidden', 'true');
        }

      });

    });

  }

	/**
	 * Sorts and formats all of fragments in the
	 * presentation.
	 */
  function sortAllFragments() {

    var horizontalSlides = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR));
    horizontalSlides.forEach(function (horizontalSlide) {

      var verticalSlides = toArray(horizontalSlide.querySelectorAll('section'));
      verticalSlides.forEach(function (verticalSlide, y) {

        sortFragments(verticalSlide.querySelectorAll('.fragment'));

      });

      if (verticalSlides.length === 0) sortFragments(horizontalSlide.querySelectorAll('.fragment'));

    });

  }

	/**
	 * Randomly shuffles all slides in the deck.
	 */
  function shuffle() {

    var slides = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR));

    slides.forEach(function (slide) {

      // Insert this slide next to another random slide. This may
      // cause the slide to insert before itself but that's fine.
      dom.slides.insertBefore(slide, slides[Math.floor(Math.random() * slides.length)]);

    });

  }

	/**
	 * Updates one dimension of slides by showing the slide
	 * with the specified index.
	 *
	 * @param {string} selector A CSS selector that will fetch
	 * the group of slides we are working with
	 * @param {number} index The index of the slide that should be
	 * shown
	 *
	 * @return {number} The index of the slide that is now shown,
	 * might differ from the passed in index if it was out of
	 * bounds.
	 */
  function updateSlides(selector, index) {

    // Select all slides and convert the NodeList result to
    // an array
    var slides = toArray(dom.wrapper.querySelectorAll(selector)),
      slidesLength = slides.length;

    var printMode = isPrintingPDF();

    if (slidesLength) {

      // Should the index loop?
      if (config.loop) {
        index %= slidesLength;

        if (index < 0) {
          index = slidesLength + index;
        }
      }

      // Enforce max and minimum index bounds
      index = Math.max(Math.min(index, slidesLength - 1), 0);

      for (var i = 0; i < slidesLength; i++) {
        var element = slides[i];

        var reverse = config.rtl && !isVerticalSlide(element);

        element.classList.remove('past');
        element.classList.remove('present');
        element.classList.remove('future');

        // http://www.w3.org/html/wg/drafts/html/master/editing.html#the-hidden-attribute
        element.setAttribute('hidden', '');
        element.setAttribute('aria-hidden', 'true');

        // If this element contains vertical slides
        if (element.querySelector('section')) {
          element.classList.add('stack');
        }

        // If we're printing static slides, all slides are "present"
        if (printMode) {
          element.classList.add('present');
          continue;
        }

        if (i < index) {
          // Any element previous to index is given the 'past' class
          element.classList.add(reverse ? 'future' : 'past');

          if (config.fragments) {
            // Show all fragments in prior slides
            toArray(element.querySelectorAll('.fragment')).forEach(function (fragment) {
              fragment.classList.add('visible');
              fragment.classList.remove('current-fragment');
            });
          }
        }
        else if (i > index) {
          // Any element subsequent to index is given the 'future' class
          element.classList.add(reverse ? 'past' : 'future');

          if (config.fragments) {
            // Hide all fragments in future slides
            toArray(element.querySelectorAll('.fragment.visible')).forEach(function (fragment) {
              fragment.classList.remove('visible');
              fragment.classList.remove('current-fragment');
            });
          }
        }
      }

      // Mark the current slide as present
      slides[index].classList.add('present');
      slides[index].removeAttribute('hidden');
      slides[index].removeAttribute('aria-hidden');

      // If this slide has a state associated with it, add it
      // onto the current state of the deck
      var slideState = slides[index].getAttribute('data-state');
      if (slideState) {
        state = state.concat(slideState.split(' '));
      }

    }
    else {
      // Since there are no slides we can't be anywhere beyond the
      // zeroth index
      index = 0;
    }

    return index;

  }

	/**
	 * Optimization method; hide all slides that are far away
	 * from the present slide.
	 */
  function updateSlidesVisibility() {

    // Select all slides and convert the NodeList result to
    // an array
    var horizontalSlides = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR)),
      horizontalSlidesLength = horizontalSlides.length,
      distanceX,
      distanceY;

    if (horizontalSlidesLength && typeof indexh !== 'undefined') {

      // The number of steps away from the present slide that will
      // be visible
      var viewDistance = isOverview() ? 10 : config.viewDistance;

      // Limit view distance on weaker devices
      if (isMobileDevice) {
        viewDistance = isOverview() ? 6 : 2;
      }

      // All slides need to be visible when exporting to PDF
      if (isPrintingPDF()) {
        viewDistance = Number.MAX_VALUE;
      }

      for (var x = 0; x < horizontalSlidesLength; x++) {
        var horizontalSlide = horizontalSlides[x];

        var verticalSlides = toArray(horizontalSlide.querySelectorAll('section')),
          verticalSlidesLength = verticalSlides.length;

        // Determine how far away this slide is from the present
        distanceX = Math.abs((indexh || 0) - x) || 0;

        // If the presentation is looped, distance should measure
        // 1 between the first and last slides
        if (config.loop) {
          distanceX = Math.abs(((indexh || 0) - x) % (horizontalSlidesLength - viewDistance)) || 0;
        }

        // Show the horizontal slide if it's within the view distance
        if (distanceX < viewDistance) {
          loadSlide(horizontalSlide);
        }
        else {
          unloadSlide(horizontalSlide);
        }

        if (verticalSlidesLength) {

          var oy = getPreviousVerticalIndex(horizontalSlide);

          for (var y = 0; y < verticalSlidesLength; y++) {
            var verticalSlide = verticalSlides[y];

            distanceY = x === (indexh || 0) ? Math.abs((indexv || 0) - y) : Math.abs(y - oy);

            if (distanceX + distanceY < viewDistance) {
              loadSlide(verticalSlide);
            }
            else {
              unloadSlide(verticalSlide);
            }
          }

        }
      }

      // Flag if there are ANY vertical slides, anywhere in the deck
      if (dom.wrapper.querySelectorAll('.slides>section>section').length) {
        dom.wrapper.classList.add('has-vertical-slides');
      }
      else {
        dom.wrapper.classList.remove('has-vertical-slides');
      }

      // Flag if there are ANY horizontal slides, anywhere in the deck
      if (dom.wrapper.querySelectorAll('.slides>section').length > 1) {
        dom.wrapper.classList.add('has-horizontal-slides');
      }
      else {
        dom.wrapper.classList.remove('has-horizontal-slides');
      }

    }

  }

	/**
	 * Pick up notes from the current slide and display them
	 * to the viewer.
	 *
	 * @see {@link config.showNotes}
	 */
  function updateNotes() {

    if (config.showNotes && dom.speakerNotes && currentSlide && !isPrintingPDF()) {

      dom.speakerNotes.innerHTML = getSlideNotes() || '<span class="notes-placeholder">No notes on this slide.</span>';

    }

  }

	/**
	 * Updates the visibility of the speaker notes sidebar that
	 * is used to share annotated slides. The notes sidebar is
	 * only visible if showNotes is true and there are notes on
	 * one or more slides in the deck.
	 */
  function updateNotesVisibility() {

    if (config.showNotes && hasNotes()) {
      dom.wrapper.classList.add('show-notes');
    }
    else {
      dom.wrapper.classList.remove('show-notes');
    }

  }

	/**
	 * Checks if there are speaker notes for ANY slide in the
	 * presentation.
	 */
  function hasNotes() {

    return dom.slides.querySelectorAll('[data-notes], aside.notes').length > 0;

  }

	/**
	 * Updates the progress bar to reflect the current slide.
	 */
  function updateProgress() {

    // Update progress if enabled
    if (config.progress && dom.progressbar) {

      dom.progressbar.style.width = getProgress() * dom.wrapper.offsetWidth + 'px';

    }

  }


	/**
	 * Updates the slide number to match the current slide.
	 */
  function updateSlideNumber() {

    // Update slide number if enabled
    if (config.slideNumber && dom.slideNumber) {

      var value;
      var format = 'h.v';

      if (typeof config.slideNumber === 'function') {
        value = config.slideNumber();
      }
      else {
        // Check if a custom number format is available
        if (typeof config.slideNumber === 'string') {
          format = config.slideNumber;
        }

        // If there are ONLY vertical slides in this deck, always use
        // a flattened slide number
        if (!/c/.test(format) && dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR).length === 1) {
          format = 'c';
        }

        value = [];
        switch (format) {
          case 'c':
            value.push(getSlidePastCount() + 1);
            break;
          case 'c/t':
            value.push(getSlidePastCount() + 1, '/', getTotalSlides());
            break;
          case 'h/v':
            value.push(indexh + 1);
            if (isVerticalSlide()) value.push('/', indexv + 1);
            break;
          default:
            value.push(indexh + 1);
            if (isVerticalSlide()) value.push('.', indexv + 1);
        }
      }

      dom.slideNumber.innerHTML = formatSlideNumber(value[0], value[1], value[2]);
    }

  }

	/**
	 * Applies HTML formatting to a slide number before it's
	 * written to the DOM.
	 *
	 * @param {number} a Current slide
	 * @param {string} delimiter Character to separate slide numbers
	 * @param {(number|*)} b Total slides
	 * @return {string} HTML string fragment
	 */
  function formatSlideNumber(a, delimiter, b) {

    var url = '#' + locationHash();
    if (typeof b === 'number' && !isNaN(b)) {
      return '<a href="' + url + '">' +
        '<span class="slide-number-a">' + a + '</span>' +
        '<span class="slide-number-delimiter">' + delimiter + '</span>' +
        '<span class="slide-number-b">' + b + '</span>' +
        '</a>';
    }
    else {
      return '<a href="' + url + '">' +
        '<span class="slide-number-a">' + a + '</span>' +
        '</a>';
    }

  }

	/**
	 * Updates the state of all control/navigation arrows.
	 */
  function updateControls() {

    var routes = availableRoutes();
    var fragments = availableFragments();

    // Remove the 'enabled' class from all directions
    dom.controlsLeft.concat(dom.controlsRight)
      .concat(dom.controlsUp)
      .concat(dom.controlsDown)
      .concat(dom.controlsPrev)
      .concat(dom.controlsNext).forEach(function (node) {
        node.classList.remove('enabled');
        node.classList.remove('fragmented');

        // Set 'disabled' attribute on all directions
        node.setAttribute('disabled', 'disabled');
      });

    // Add the 'enabled' class to the available routes; remove 'disabled' attribute to enable buttons
    if (routes.left) dom.controlsLeft.forEach(function (el) { el.classList.add('enabled'); el.removeAttribute('disabled'); });
    if (routes.right) dom.controlsRight.forEach(function (el) { el.classList.add('enabled'); el.removeAttribute('disabled'); });
    if (routes.up) dom.controlsUp.forEach(function (el) { el.classList.add('enabled'); el.removeAttribute('disabled'); });
    if (routes.down) dom.controlsDown.forEach(function (el) { el.classList.add('enabled'); el.removeAttribute('disabled'); });

    // Prev/next buttons
    if (routes.left || routes.up) dom.controlsPrev.forEach(function (el) { el.classList.add('enabled'); el.removeAttribute('disabled'); });
    if (routes.right || routes.down) dom.controlsNext.forEach(function (el) { el.classList.add('enabled'); el.removeAttribute('disabled'); });

    // Highlight fragment directions
    if (currentSlide) {

      // Always apply fragment decorator to prev/next buttons
      if (fragments.prev) dom.controlsPrev.forEach(function (el) { el.classList.add('fragmented', 'enabled'); el.removeAttribute('disabled'); });
      if (fragments.next) dom.controlsNext.forEach(function (el) { el.classList.add('fragmented', 'enabled'); el.removeAttribute('disabled'); });

      // Apply fragment decorators to directional buttons based on
      // what slide axis they are in
      if (isVerticalSlide(currentSlide)) {
        if (fragments.prev) dom.controlsUp.forEach(function (el) { el.classList.add('fragmented', 'enabled'); el.removeAttribute('disabled'); });
        if (fragments.next) dom.controlsDown.forEach(function (el) { el.classList.add('fragmented', 'enabled'); el.removeAttribute('disabled'); });
      }
      else {
        if (fragments.prev) dom.controlsLeft.forEach(function (el) { el.classList.add('fragmented', 'enabled'); el.removeAttribute('disabled'); });
        if (fragments.next) dom.controlsRight.forEach(function (el) { el.classList.add('fragmented', 'enabled'); el.removeAttribute('disabled'); });
      }

    }

    if (config.controlsTutorial) {

      // Highlight control arrows with an animation to ensure
      // that the viewer knows how to navigate
      if (!hasNavigatedDown && routes.down) {
        dom.controlsDownArrow.classList.add('highlight');
      }
      else {
        dom.controlsDownArrow.classList.remove('highlight');

        if (!hasNavigatedRight && routes.right && indexv === 0) {
          dom.controlsRightArrow.classList.add('highlight');
        }
        else {
          dom.controlsRightArrow.classList.remove('highlight');
        }
      }

    }

  }

	/**
	 * Updates the background elements to reflect the current
	 * slide.
	 *
	 * @param {boolean} includeAll If true, the backgrounds of
	 * all vertical slides (not just the present) will be updated.
	 */
  function updateBackground(includeAll) {

    var currentBackground = null;

    // Reverse past/future classes when in RTL mode
    var horizontalPast = config.rtl ? 'future' : 'past',
      horizontalFuture = config.rtl ? 'past' : 'future';

    // Update the classes of all backgrounds to match the
    // states of their slides (past/present/future)
    toArray(dom.background.childNodes).forEach(function (backgroundh, h) {

      backgroundh.classList.remove('past');
      backgroundh.classList.remove('present');
      backgroundh.classList.remove('future');

      if (h < indexh) {
        backgroundh.classList.add(horizontalPast);
      }
      else if (h > indexh) {
        backgroundh.classList.add(horizontalFuture);
      }
      else {
        backgroundh.classList.add('present');

        // Store a reference to the current background element
        currentBackground = backgroundh;
      }

      if (includeAll || h === indexh) {
        toArray(backgroundh.querySelectorAll('.slide-background')).forEach(function (backgroundv, v) {

          backgroundv.classList.remove('past');
          backgroundv.classList.remove('present');
          backgroundv.classList.remove('future');

          if (v < indexv) {
            backgroundv.classList.add('past');
          }
          else if (v > indexv) {
            backgroundv.classList.add('future');
          }
          else {
            backgroundv.classList.add('present');

            // Only if this is the present horizontal and vertical slide
            if (h === indexh) currentBackground = backgroundv;
          }

        });
      }

    });

    // Stop content inside of previous backgrounds
    if (previousBackground) {

      stopEmbeddedContent(previousBackground);

    }

    // Start content in the current background
    if (currentBackground) {

      startEmbeddedContent(currentBackground);

      var currentBackgroundContent = currentBackground.querySelector('.slide-background-content');
      if (currentBackgroundContent) {

        var backgroundImageURL = currentBackgroundContent.style.backgroundImage || '';

        // Restart GIFs (doesn't work in Firefox)
        if (/\.gif/i.test(backgroundImageURL)) {
          currentBackgroundContent.style.backgroundImage = '';
          window.getComputedStyle(currentBackgroundContent).opacity;
          currentBackgroundContent.style.backgroundImage = backgroundImageURL;
        }

      }

      // Don't transition between identical backgrounds. This
      // prevents unwanted flicker.
      var previousBackgroundHash = previousBackground ? previousBackground.getAttribute('data-background-hash') : null;
      var currentBackgroundHash = currentBackground.getAttribute('data-background-hash');
      if (currentBackgroundHash && currentBackgroundHash === previousBackgroundHash && currentBackground !== previousBackground) {
        dom.background.classList.add('no-transition');
      }

      previousBackground = currentBackground;

    }

    // If there's a background brightness flag for this slide,
    // bubble it to the .reveal container
    if (currentSlide) {
      ['has-light-background', 'has-dark-background'].forEach(function (classToBubble) {
        if (currentSlide.classList.contains(classToBubble)) {
          dom.wrapper.classList.add(classToBubble);
        }
        else {
          dom.wrapper.classList.remove(classToBubble);
        }
      });
    }

    // Allow the first background to apply without transition
    setTimeout(function () {
      dom.background.classList.remove('no-transition');
    }, 1);

  }

	/**
	 * Updates the position of the parallax background based
	 * on the current slide index.
	 */
  function updateParallax() {

    if (config.parallaxBackgroundImage) {

      var horizontalSlides = dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR),
        verticalSlides = dom.wrapper.querySelectorAll(VERTICAL_SLIDES_SELECTOR);

      var backgroundSize = dom.background.style.backgroundSize.split(' '),
        backgroundWidth, backgroundHeight;

      if (backgroundSize.length === 1) {
        backgroundWidth = backgroundHeight = parseInt(backgroundSize[0], 10);
      }
      else {
        backgroundWidth = parseInt(backgroundSize[0], 10);
        backgroundHeight = parseInt(backgroundSize[1], 10);
      }

      var slideWidth = dom.background.offsetWidth,
        horizontalSlideCount = horizontalSlides.length,
        horizontalOffsetMultiplier,
        horizontalOffset;

      if (typeof config.parallaxBackgroundHorizontal === 'number') {
        horizontalOffsetMultiplier = config.parallaxBackgroundHorizontal;
      }
      else {
        horizontalOffsetMultiplier = horizontalSlideCount > 1 ? (backgroundWidth - slideWidth) / (horizontalSlideCount - 1) : 0;
      }

      horizontalOffset = horizontalOffsetMultiplier * indexh * -1;

      var slideHeight = dom.background.offsetHeight,
        verticalSlideCount = verticalSlides.length,
        verticalOffsetMultiplier,
        verticalOffset;

      if (typeof config.parallaxBackgroundVertical === 'number') {
        verticalOffsetMultiplier = config.parallaxBackgroundVertical;
      }
      else {
        verticalOffsetMultiplier = (backgroundHeight - slideHeight) / (verticalSlideCount - 1);
      }

      verticalOffset = verticalSlideCount > 0 ? verticalOffsetMultiplier * indexv : 0;

      dom.background.style.backgroundPosition = horizontalOffset + 'px ' + -verticalOffset + 'px';

    }

  }

	/**
	 * Should the given element be preloaded?
	 * Decides based on local element attributes and global config.
	 *
	 * @param {HTMLElement} element
	 */
  function shouldPreload(element) {

    // Prefer an explicit global preload setting
    var preload = config.preloadIframes;

    // If no global setting is available, fall back on the element's
    // own preload setting
    if (typeof preload !== 'boolean') {
      preload = element.hasAttribute('data-preload');
    }

    return preload;
  }

	/**
	 * Called when the given slide is within the configured view
	 * distance. Shows the slide element and loads any content
	 * that is set to load lazily (data-src).
	 *
	 * @param {HTMLElement} slide Slide to show
	 */
  function loadSlide(slide, options) {

    options = options || {};

    // Show the slide element
    slide.style.display = config.display;

    // Media elements with data-src attributes
    toArray(slide.querySelectorAll('img[data-src], video[data-src], audio[data-src], iframe[data-src]')).forEach(function (element) {
      if (element.tagName !== 'IFRAME' || shouldPreload(element)) {
        element.setAttribute('src', element.getAttribute('data-src'));
        element.setAttribute('data-lazy-loaded', '');
        element.removeAttribute('data-src');
      }
    });

    // Media elements with <source> children
    toArray(slide.querySelectorAll('video, audio')).forEach(function (media) {
      var sources = 0;

      toArray(media.querySelectorAll('source[data-src]')).forEach(function (source) {
        source.setAttribute('src', source.getAttribute('data-src'));
        source.removeAttribute('data-src');
        source.setAttribute('data-lazy-loaded', '');
        sources += 1;
      });

      // If we rewrote sources for this video/audio element, we need
      // to manually tell it to load from its new origin
      if (sources > 0) {
        media.load();
      }
    });


    // Show the corresponding background element
    var background = slide.slideBackgroundElement;
    if (background) {
      background.style.display = 'block';

      var backgroundContent = slide.slideBackgroundContentElement;

      // If the background contains media, load it
      if (background.hasAttribute('data-loaded') === false) {
        background.setAttribute('data-loaded', 'true');

        var backgroundImage = slide.getAttribute('data-background-image'),
          backgroundVideo = slide.getAttribute('data-background-video'),
          backgroundVideoLoop = slide.hasAttribute('data-background-video-loop'),
          backgroundVideoMuted = slide.hasAttribute('data-background-video-muted'),
          backgroundIframe = slide.getAttribute('data-background-iframe');

        // Images
        if (backgroundImage) {
          backgroundContent.style.backgroundImage = 'url(' + encodeURI(backgroundImage) + ')';
        }
        // Videos
        else if (backgroundVideo && !isSpeakerNotes()) {
          var video = document.createElement('video');

          if (backgroundVideoLoop) {
            video.setAttribute('loop', '');
          }

          if (backgroundVideoMuted) {
            video.muted = true;
          }

          // Inline video playback works (at least in Mobile Safari) as
          // long as the video is muted and the `playsinline` attribute is
          // present
          if (isMobileDevice) {
            video.muted = true;
            video.autoplay = true;
            video.setAttribute('playsinline', '');
          }

          // Support comma separated lists of video sources
          backgroundVideo.split(',').forEach(function (source) {
            video.innerHTML += '<source src="' + source + '">';
          });

          backgroundContent.appendChild(video);
        }
        // Iframes
        else if (backgroundIframe && options.excludeIframes !== true) {
          var iframe = document.createElement('iframe');
          iframe.setAttribute('allowfullscreen', '');
          iframe.setAttribute('mozallowfullscreen', '');
          iframe.setAttribute('webkitallowfullscreen', '');

          // Only load autoplaying content when the slide is shown to
          // avoid having it play in the background
          if (/autoplay=(1|true|yes)/gi.test(backgroundIframe)) {
            iframe.setAttribute('data-src', backgroundIframe);
          }
          else {
            iframe.setAttribute('src', backgroundIframe);
          }

          iframe.style.width = '100%';
          iframe.style.height = '100%';
          iframe.style.maxHeight = '100%';
          iframe.style.maxWidth = '100%';

          backgroundContent.appendChild(iframe);
        }
      }

    }

  }

	/**
	 * Unloads and hides the given slide. This is called when the
	 * slide is moved outside of the configured view distance.
	 *
	 * @param {HTMLElement} slide
	 */
  function unloadSlide(slide) {

    // Hide the slide element
    slide.style.display = 'none';

    // Hide the corresponding background element
    var background = getSlideBackground(slide);
    if (background) {
      background.style.display = 'none';
    }

    // Reset lazy-loaded media elements with src attributes
    toArray(slide.querySelectorAll('video[data-lazy-loaded][src], audio[data-lazy-loaded][src], iframe[data-lazy-loaded][src]')).forEach(function (element) {
      element.setAttribute('data-src', element.getAttribute('src'));
      element.removeAttribute('src');
    });

    // Reset lazy-loaded media elements with <source> children
    toArray(slide.querySelectorAll('video[data-lazy-loaded] source[src], audio source[src]')).forEach(function (source) {
      source.setAttribute('data-src', source.getAttribute('src'));
      source.removeAttribute('src');
    });

  }

	/**
	 * Determine what available routes there are for navigation.
	 *
	 * @return {{left: boolean, right: boolean, up: boolean, down: boolean}}
	 */
  function availableRoutes() {

    var horizontalSlides = dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR),
      verticalSlides = dom.wrapper.querySelectorAll(VERTICAL_SLIDES_SELECTOR);

    var routes = {
      left: indexh > 0,
      right: indexh < horizontalSlides.length - 1,
      up: indexv > 0,
      down: indexv < verticalSlides.length - 1
    };

    // Looped presentations can always be navigated as long as
    // there are slides available
    if (config.loop) {
      if (horizontalSlides.length > 1) {
        routes.left = true;
        routes.right = true;
      }

      if (verticalSlides.length > 1) {
        routes.up = true;
        routes.down = true;
      }
    }

    // Reverse horizontal controls for rtl
    if (config.rtl) {
      var left = routes.left;
      routes.left = routes.right;
      routes.right = left;
    }

    return routes;

  }

	/**
	 * Returns an object describing the available fragment
	 * directions.
	 *
	 * @return {{prev: boolean, next: boolean}}
	 */
  function availableFragments() {

    if (currentSlide && config.fragments) {
      var fragments = currentSlide.querySelectorAll('.fragment');
      var hiddenFragments = currentSlide.querySelectorAll('.fragment:not(.visible)');

      return {
        prev: fragments.length - hiddenFragments.length > 0,
        next: !!hiddenFragments.length
      };
    }
    else {
      return { prev: false, next: false };
    }

  }

	/**
	 * Enforces origin-specific format rules for embedded media.
	 */
  function formatEmbeddedContent() {

    var _appendParamToIframeSource = function (sourceAttribute, sourceURL, param) {
      toArray(dom.slides.querySelectorAll('iframe[' + sourceAttribute + '*="' + sourceURL + '"]')).forEach(function (el) {
        var src = el.getAttribute(sourceAttribute);
        if (src && src.indexOf(param) === -1) {
          el.setAttribute(sourceAttribute, src + (!/\?/.test(src) ? '?' : '&') + param);
        }
      });
    };

    // YouTube frames must include "?enablejsapi=1"
    _appendParamToIframeSource('src', 'youtube.com/embed/', 'enablejsapi=1');
    _appendParamToIframeSource('data-src', 'youtube.com/embed/', 'enablejsapi=1');

    // Vimeo frames must include "?api=1"
    _appendParamToIframeSource('src', 'player.vimeo.com/', 'api=1');
    _appendParamToIframeSource('data-src', 'player.vimeo.com/', 'api=1');

  }

	/**
	 * Start playback of any embedded content inside of
	 * the given element.
	 *
	 * @param {HTMLElement} element
	 */
  function startEmbeddedContent(element) {

    if (element && !isSpeakerNotes()) {

      // Restart GIFs
      toArray(element.querySelectorAll('img[src$=".gif"]')).forEach(function (el) {
        // Setting the same unchanged source like this was confirmed
        // to work in Chrome, FF & Safari
        el.setAttribute('src', el.getAttribute('src'));
      });

      // HTML5 media elements
      toArray(element.querySelectorAll('video, audio')).forEach(function (el) {
        if (closestParent(el, '.fragment') && !closestParent(el, '.fragment.visible')) {
          return;
        }

        // Prefer an explicit global autoplay setting
        var autoplay = config.autoPlayMedia;

        // If no global setting is available, fall back on the element's
        // own autoplay setting
        if (typeof autoplay !== 'boolean') {
          autoplay = el.hasAttribute('data-autoplay') || !!closestParent(el, '.slide-background');
        }

        if (autoplay && typeof el.play === 'function') {

          // If the media is ready, start playback
          if (el.readyState > 1) {
            startEmbeddedMedia({ target: el });
          }
          // Mobile devices never fire a loaded event so instead
          // of waiting, we initiate playback
          else if (isMobileDevice) {
            var promise = el.play();

            // If autoplay does not work, ensure that the controls are visible so
            // that the viewer can start the media on their own
            if (promise && typeof promise.catch === 'function' && el.controls === false) {
              promise.catch(function () {
                el.controls = true;

                // Once the video does start playing, hide the controls again
                el.addEventListener('play', function () {
                  el.controls = false;
                });
              });
            }
          }
          // If the media isn't loaded, wait before playing
          else {
            el.removeEventListener('loadeddata', startEmbeddedMedia); // remove first to avoid dupes
            el.addEventListener('loadeddata', startEmbeddedMedia);
          }

        }
      });

      // Normal iframes
      toArray(element.querySelectorAll('iframe[src]')).forEach(function (el) {
        if (closestParent(el, '.fragment') && !closestParent(el, '.fragment.visible')) {
          return;
        }

        startEmbeddedIframe({ target: el });
      });

      // Lazy loading iframes
      toArray(element.querySelectorAll('iframe[data-src]')).forEach(function (el) {
        if (closestParent(el, '.fragment') && !closestParent(el, '.fragment.visible')) {
          return;
        }

        if (el.getAttribute('src') !== el.getAttribute('data-src')) {
          el.removeEventListener('load', startEmbeddedIframe); // remove first to avoid dupes
          el.addEventListener('load', startEmbeddedIframe);
          el.setAttribute('src', el.getAttribute('data-src'));
        }
      });

    }

  }

	/**
	 * Starts playing an embedded video/audio element after
	 * it has finished loading.
	 *
	 * @param {object} event
	 */
  function startEmbeddedMedia(event) {

    var isAttachedToDOM = !!closestParent(event.target, 'html'),
      isVisible = !!closestParent(event.target, '.present');

    if (isAttachedToDOM && isVisible) {
      event.target.currentTime = 0;
      event.target.play();
    }

    event.target.removeEventListener('loadeddata', startEmbeddedMedia);

  }

	/**
	 * "Starts" the content of an embedded iframe using the
	 * postMessage API.
	 *
	 * @param {object} event
	 */
  function startEmbeddedIframe(event) {

    var iframe = event.target;

    if (iframe && iframe.contentWindow) {

      var isAttachedToDOM = !!closestParent(event.target, 'html'),
        isVisible = !!closestParent(event.target, '.present');

      if (isAttachedToDOM && isVisible) {

        // Prefer an explicit global autoplay setting
        var autoplay = config.autoPlayMedia;

        // If no global setting is available, fall back on the element's
        // own autoplay setting
        if (typeof autoplay !== 'boolean') {
          autoplay = iframe.hasAttribute('data-autoplay') || !!closestParent(iframe, '.slide-background');
        }

        // YouTube postMessage API
        if (/youtube\.com\/embed\//.test(iframe.getAttribute('src')) && autoplay) {
          iframe.contentWindow.postMessage('{"event":"command","func":"playVideo","args":""}', '*');
        }
        // Vimeo postMessage API
        else if (/player\.vimeo\.com\//.test(iframe.getAttribute('src')) && autoplay) {
          iframe.contentWindow.postMessage('{"method":"play"}', '*');
        }
        // Generic postMessage API
        else {
          iframe.contentWindow.postMessage('slide:start', '*');
        }

      }

    }

  }

	/**
	 * Stop playback of any embedded content inside of
	 * the targeted slide.
	 *
	 * @param {HTMLElement} element
	 */
  function stopEmbeddedContent(element, options) {

    options = extend({
      // Defaults
      unloadIframes: true
    }, options || {});

    if (element && element.parentNode) {
      // HTML5 media elements
      toArray(element.querySelectorAll('video, audio')).forEach(function (el) {
        if (!el.hasAttribute('data-ignore') && typeof el.pause === 'function') {
          el.setAttribute('data-paused-by-reveal', '');
          el.pause();
        }
      });

      // Generic postMessage API for non-lazy loaded iframes
      toArray(element.querySelectorAll('iframe')).forEach(function (el) {
        if (el.contentWindow) el.contentWindow.postMessage('slide:stop', '*');
        el.removeEventListener('load', startEmbeddedIframe);
      });

      // YouTube postMessage API
      toArray(element.querySelectorAll('iframe[src*="youtube.com/embed/"]')).forEach(function (el) {
        if (!el.hasAttribute('data-ignore') && el.contentWindow && typeof el.contentWindow.postMessage === 'function') {
          el.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
        }
      });

      // Vimeo postMessage API
      toArray(element.querySelectorAll('iframe[src*="player.vimeo.com/"]')).forEach(function (el) {
        if (!el.hasAttribute('data-ignore') && el.contentWindow && typeof el.contentWindow.postMessage === 'function') {
          el.contentWindow.postMessage('{"method":"pause"}', '*');
        }
      });

      if (options.unloadIframes === true) {
        // Unload lazy-loaded iframes
        toArray(element.querySelectorAll('iframe[data-src]')).forEach(function (el) {
          // Only removing the src doesn't actually unload the frame
          // in all browsers (Firefox) so we set it to blank first
          el.setAttribute('src', 'about:blank');
          el.removeAttribute('src');
        });
      }
    }

  }

	/**
	 * Returns the number of past slides. This can be used as a global
	 * flattened index for slides.
	 *
	 * @return {number} Past slide count
	 */
  function getSlidePastCount() {

    var horizontalSlides = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR));

    // The number of past slides
    var pastCount = 0;

    // Step through all slides and count the past ones
    mainLoop: for (var i = 0; i < horizontalSlides.length; i++) {

      var horizontalSlide = horizontalSlides[i];
      var verticalSlides = toArray(horizontalSlide.querySelectorAll('section'));

      for (var j = 0; j < verticalSlides.length; j++) {

        // Stop as soon as we arrive at the present
        if (verticalSlides[j].classList.contains('present')) {
          break mainLoop;
        }

        pastCount++;

      }

      // Stop as soon as we arrive at the present
      if (horizontalSlide.classList.contains('present')) {
        break;
      }

      // Don't count the wrapping section for vertical slides
      if (horizontalSlide.classList.contains('stack') === false) {
        pastCount++;
      }

    }

    return pastCount;

  }

	/**
	 * Returns a value ranging from 0-1 that represents
	 * how far into the presentation we have navigated.
	 *
	 * @return {number}
	 */
  function getProgress() {

    // The number of past and total slides
    var totalCount = getTotalSlides();
    var pastCount = getSlidePastCount();

    if (currentSlide) {

      var allFragments = currentSlide.querySelectorAll('.fragment');

      // If there are fragments in the current slide those should be
      // accounted for in the progress.
      if (allFragments.length > 0) {
        var visibleFragments = currentSlide.querySelectorAll('.fragment.visible');

        // This value represents how big a portion of the slide progress
        // that is made up by its fragments (0-1)
        var fragmentWeight = 0.9;

        // Add fragment progress to the past slide count
        pastCount += (visibleFragments.length / allFragments.length) * fragmentWeight;
      }

    }

    return Math.min(pastCount / (totalCount - 1), 1);

  }

	/**
	 * Checks if this presentation is running inside of the
	 * speaker notes window.
	 *
	 * @return {boolean}
	 */
  function isSpeakerNotes() {

    return !!window.location.search.match(/receiver/gi);

  }

	/**
	 * Reads the current URL (hash) and navigates accordingly.
	 */
  function readURL() {

    var hash = window.location.hash;

    // Attempt to parse the hash as either an index or name
    var bits = hash.slice(2).split('/'),
      name = hash.replace(/#|\//gi, '');

    // If the first bit is not fully numeric and there is a name we
    // can assume that this is a named link
    if (!/^[0-9]*$/.test(bits[0]) && name.length) {
      var element;

      // Ensure the named link is a valid HTML ID attribute
      try {
        element = document.getElementById(decodeURIComponent(name));
      }
      catch (error) { }

      // Ensure that we're not already on a slide with the same name
      var isSameNameAsCurrentSlide = currentSlide ? currentSlide.getAttribute('id') === name : false;

      if (element) {
        // If the slide exists and is not the current slide...
        if (!isSameNameAsCurrentSlide) {
          // ...find the position of the named slide and navigate to it
          var indices = Reveal.getIndices(element);
          slide(indices.h, indices.v);
        }
      }
      // If the slide doesn't exist, navigate to the current slide
      else {
        slide(indexh || 0, indexv || 0);
      }
    }
    else {
      var hashIndexBase = config.hashOneBasedIndex ? 1 : 0;

      // Read the index components of the hash
      var h = (parseInt(bits[0], 10) - hashIndexBase) || 0,
        v = (parseInt(bits[1], 10) - hashIndexBase) || 0,
        f;

      if (config.fragmentInURL) {
        f = parseInt(bits[2], 10);
        if (isNaN(f)) {
          f = undefined;
        }
      }

      if (h !== indexh || v !== indexv || f !== undefined) {
        slide(h, v, f);
      }
    }

  }

	/**
	 * Updates the page URL (hash) to reflect the current
	 * state.
	 *
	 * @param {number} delay The time in ms to wait before
	 * writing the hash
	 */
  function writeURL(delay) {

    // Make sure there's never more than one timeout running
    clearTimeout(writeURLTimeout);

    // If a delay is specified, timeout this call
    if (typeof delay === 'number') {
      writeURLTimeout = setTimeout(writeURL, delay);
    }
    else if (currentSlide) {
      // If we're configured to push to history OR the history
      // API is not avaialble.
      if (config.history || !window.history) {
        window.location.hash = locationHash();
      }
      // If we're configured to reflect the current slide in the
      // URL without pushing to history.
      else if (config.hash) {
        window.history.replaceState(null, null, '#' + locationHash());
      }
      // If history and hash are both disabled, a hash may still
      // be added to the URL by clicking on a href with a hash
      // target. Counter this by always removing the hash.
      else {
        window.history.replaceState(null, null, window.location.pathname + window.location.search);
      }
    }

  }
	/**
	 * Retrieves the h/v location and fragment of the current,
	 * or specified, slide.
	 *
	 * @param {HTMLElement} [slide] If specified, the returned
	 * index will be for this slide rather than the currently
	 * active one
	 *
	 * @return {{h: number, v: number, f: number}}
	 */
  function getIndices(slide) {

    // By default, return the current indices
    var h = indexh,
      v = indexv,
      f;

    // If a slide is specified, return the indices of that slide
    if (slide) {
      var isVertical = isVerticalSlide(slide);
      var slideh = isVertical ? slide.parentNode : slide;

      // Select all horizontal slides
      var horizontalSlides = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR));

      // Now that we know which the horizontal slide is, get its index
      h = Math.max(horizontalSlides.indexOf(slideh), 0);

      // Assume we're not vertical
      v = undefined;

      // If this is a vertical slide, grab the vertical index
      if (isVertical) {
        v = Math.max(toArray(slide.parentNode.querySelectorAll('section')).indexOf(slide), 0);
      }
    }

    if (!slide && currentSlide) {
      var hasFragments = currentSlide.querySelectorAll('.fragment').length > 0;
      if (hasFragments) {
        var currentFragment = currentSlide.querySelector('.current-fragment');
        if (currentFragment && currentFragment.hasAttribute('data-fragment-index')) {
          f = parseInt(currentFragment.getAttribute('data-fragment-index'), 10);
        }
        else {
          f = currentSlide.querySelectorAll('.fragment.visible').length - 1;
        }
      }
    }

    return { h: h, v: v, f: f };

  }

	/**
	 * Retrieves all slides in this presentation.
	 */
  function getSlides() {

    return toArray(dom.wrapper.querySelectorAll(SLIDES_SELECTOR + ':not(.stack)'));

  }

	/**
	 * Returns an array of objects where each object represents the
	 * attributes on its respective slide.
	 */
  function getSlidesAttributes() {

    return getSlides().map(function (slide) {

      var attributes = {};
      for (var i = 0; i < slide.attributes.length; i++) {
        var attribute = slide.attributes[i];
        attributes[attribute.name] = attribute.value;
      }
      return attributes;

    });

  }

	/**
	 * Retrieves the total number of slides in this presentation.
	 *
	 * @return {number}
	 */
  function getTotalSlides() {

    return getSlides().length;

  }

	/**
	 * Returns the slide element matching the specified index.
	 *
	 * @return {HTMLElement}
	 */
  function getSlide(x, y) {

    var horizontalSlide = dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR)[x];
    var verticalSlides = horizontalSlide && horizontalSlide.querySelectorAll('section');

    if (verticalSlides && verticalSlides.length && typeof y === 'number') {
      return verticalSlides ? verticalSlides[y] : undefined;
    }

    return horizontalSlide;

  }

	/**
	 * Returns the background element for the given slide.
	 * All slides, even the ones with no background properties
	 * defined, have a background element so as long as the
	 * index is valid an element will be returned.
	 *
	 * @param {mixed} x Horizontal background index OR a slide
	 * HTML element
	 * @param {number} y Vertical background index
	 * @return {(HTMLElement[]|*)}
	 */
  function getSlideBackground(x, y) {

    var slide = typeof x === 'number' ? getSlide(x, y) : x;
    if (slide) {
      return slide.slideBackgroundElement;
    }

    return undefined;

  }

	/**
	 * Retrieves the speaker notes from a slide. Notes can be
	 * defined in two ways:
	 * 1. As a data-notes attribute on the slide <section>
	 * 2. As an <aside class="notes"> inside of the slide
	 *
	 * @param {HTMLElement} [slide=currentSlide]
	 * @return {(string|null)}
	 */
  function getSlideNotes(slide) {

    // Default to the current slide
    slide = slide || currentSlide;

    // Notes can be specified via the data-notes attribute...
    if (slide.hasAttribute('data-notes')) {
      return slide.getAttribute('data-notes');
    }

    // ... or using an <aside class="notes"> element
    var notesElement = slide.querySelector('aside.notes');
    if (notesElement) {
      return notesElement.innerHTML;
    }

    return null;

  }

	/**
	 * Retrieves the current state of the presentation as
	 * an object. This state can then be restored at any
	 * time.
	 *
	 * @return {{indexh: number, indexv: number, indexf: number, paused: boolean, overview: boolean}}
	 */
  function getState() {

    var indices = getIndices();

    return {
      indexh: indices.h,
      indexv: indices.v,
      indexf: indices.f,
      paused: isPaused(),
      overview: isOverview()
    };

  }

	/**
	 * Restores the presentation to the given state.
	 *
	 * @param {object} state As generated by getState()
	 * @see {@link getState} generates the parameter `state`
	 */
  function setState(state) {

    if (typeof state === 'object') {
      slide(deserialize(state.indexh), deserialize(state.indexv), deserialize(state.indexf));

      var pausedFlag = deserialize(state.paused),
        overviewFlag = deserialize(state.overview);

      if (typeof pausedFlag === 'boolean' && pausedFlag !== isPaused()) {
        togglePause(pausedFlag);
      }

      if (typeof overviewFlag === 'boolean' && overviewFlag !== isOverview()) {
        toggleOverview(overviewFlag);
      }
    }

  }

	/**
	 * Return a sorted fragments list, ordered by an increasing
	 * "data-fragment-index" attribute.
	 *
	 * Fragments will be revealed in the order that they are returned by
	 * this function, so you can use the index attributes to control the
	 * order of fragment appearance.
	 *
	 * To maintain a sensible default fragment order, fragments are presumed
	 * to be passed in document order. This function adds a "fragment-index"
	 * attribute to each node if such an attribute is not already present,
	 * and sets that attribute to an integer value which is the position of
	 * the fragment within the fragments list.
	 *
	 * @param {object[]|*} fragments
	 * @param {boolean} grouped If true the returned array will contain
	 * nested arrays for all fragments with the same index
	 * @return {object[]} sorted Sorted array of fragments
	 */
  function sortFragments(fragments, grouped) {

    fragments = toArray(fragments);

    var ordered = [],
      unordered = [],
      sorted = [];

    // Group ordered and unordered elements
    fragments.forEach(function (fragment, i) {
      if (fragment.hasAttribute('data-fragment-index')) {
        var index = parseInt(fragment.getAttribute('data-fragment-index'), 10);

        if (!ordered[index]) {
          ordered[index] = [];
        }

        ordered[index].push(fragment);
      }
      else {
        unordered.push([fragment]);
      }
    });

    // Append fragments without explicit indices in their
    // DOM order
    ordered = ordered.concat(unordered);

    // Manually count the index up per group to ensure there
    // are no gaps
    var index = 0;

    // Push all fragments in their sorted order to an array,
    // this flattens the groups
    ordered.forEach(function (group) {
      group.forEach(function (fragment) {
        sorted.push(fragment);
        fragment.setAttribute('data-fragment-index', index);
      });

      index++;
    });

    return grouped === true ? ordered : sorted;

  }

	/**
	 * Refreshes the fragments on the current slide so that they
	 * have the appropriate classes (.visible + .current-fragment).
	 *
	 * @param {number} [index] The index of the current fragment
	 * @param {array} [fragments] Array containing all fragments
	 * in the current slide
	 *
	 * @return {{shown: array, hidden: array}}
	 */
  function updateFragments(index, fragments) {

    var changedFragments = {
      shown: [],
      hidden: []
    };

    if (currentSlide && config.fragments) {

      fragments = fragments || sortFragments(currentSlide.querySelectorAll('.fragment'));

      if (fragments.length) {

        if (typeof index !== 'number') {
          var currentFragment = sortFragments(currentSlide.querySelectorAll('.fragment.visible')).pop();
          if (currentFragment) {
            index = parseInt(currentFragment.getAttribute('data-fragment-index') || 0, 10);
          }
        }

        toArray(fragments).forEach(function (el, i) {

          if (el.hasAttribute('data-fragment-index')) {
            i = parseInt(el.getAttribute('data-fragment-index'), 10);
          }

          // Visible fragments
          if (i <= index) {
            if (!el.classList.contains('visible')) changedFragments.shown.push(el);
            el.classList.add('visible');
            el.classList.remove('current-fragment');

            // Announce the fragments one by one to the Screen Reader
            dom.statusDiv.textContent = getStatusText(el);

            if (i === index) {
              el.classList.add('current-fragment');
              startEmbeddedContent(el);
            }
          }
          // Hidden fragments
          else {
            if (el.classList.contains('visible')) changedFragments.hidden.push(el);
            el.classList.remove('visible');
            el.classList.remove('current-fragment');
          }

        });

      }

    }

    return changedFragments;

  }

	/**
	 * Navigate to the specified slide fragment.
	 *
	 * @param {?number} index The index of the fragment that
	 * should be shown, -1 means all are invisible
	 * @param {number} offset Integer offset to apply to the
	 * fragment index
	 *
	 * @return {boolean} true if a change was made in any
	 * fragments visibility as part of this call
	 */
  function navigateFragment(index, offset) {

    if (currentSlide && config.fragments) {

      var fragments = sortFragments(currentSlide.querySelectorAll('.fragment'));
      if (fragments.length) {

        // If no index is specified, find the current
        if (typeof index !== 'number') {
          var lastVisibleFragment = sortFragments(currentSlide.querySelectorAll('.fragment.visible')).pop();

          if (lastVisibleFragment) {
            index = parseInt(lastVisibleFragment.getAttribute('data-fragment-index') || 0, 10);
          }
          else {
            index = -1;
          }
        }

        // If an offset is specified, apply it to the index
        if (typeof offset === 'number') {
          index += offset;
        }

        var changedFragments = updateFragments(index, fragments);

        if (changedFragments.hidden.length) {
          dispatchEvent('fragmenthidden', { fragment: changedFragments.hidden[0], fragments: changedFragments.hidden });
        }

        if (changedFragments.shown.length) {
          dispatchEvent('fragmentshown', { fragment: changedFragments.shown[0], fragments: changedFragments.shown });
        }

        updateControls();
        updateProgress();

        if (config.fragmentInURL) {
          writeURL();
        }

        return !!(changedFragments.shown.length || changedFragments.hidden.length);

      }

    }

    return false;

  }

	/**
	 * Navigate to the next slide fragment.
	 *
	 * @return {boolean} true if there was a next fragment,
	 * false otherwise
	 */
  function nextFragment() {

    return navigateFragment(null, 1);

  }

	/**
	 * Navigate to the previous slide fragment.
	 *
	 * @return {boolean} true if there was a previous fragment,
	 * false otherwise
	 */
  function previousFragment() {

    return navigateFragment(null, -1);

  }

	/**
	 * Cues a new automated slide if enabled in the config.
	 */
  function cueAutoSlide() {

    cancelAutoSlide();

    if (currentSlide && config.autoSlide !== false) {

      var fragment = currentSlide.querySelector('.current-fragment');

      // When the slide first appears there is no "current" fragment so
      // we look for a data-autoslide timing on the first fragment
      if (!fragment) fragment = currentSlide.querySelector('.fragment');

      var fragmentAutoSlide = fragment ? fragment.getAttribute('data-autoslide') : null;
      var parentAutoSlide = currentSlide.parentNode ? currentSlide.parentNode.getAttribute('data-autoslide') : null;
      var slideAutoSlide = currentSlide.getAttribute('data-autoslide');

      // Pick value in the following priority order:
      // 1. Current fragment's data-autoslide
      // 2. Current slide's data-autoslide
      // 3. Parent slide's data-autoslide
      // 4. Global autoSlide setting
      if (fragmentAutoSlide) {
        autoSlide = parseInt(fragmentAutoSlide, 10);
      }
      else if (slideAutoSlide) {
        autoSlide = parseInt(slideAutoSlide, 10);
      }
      else if (parentAutoSlide) {
        autoSlide = parseInt(parentAutoSlide, 10);
      }
      else {
        autoSlide = config.autoSlide;
      }

      // If there are media elements with data-autoplay,
      // automatically set the autoSlide duration to the
      // length of that media. Not applicable if the slide
      // is divided up into fragments.
      // playbackRate is accounted for in the duration.
      if (currentSlide.querySelectorAll('.fragment').length === 0) {
        toArray(currentSlide.querySelectorAll('video, audio')).forEach(function (el) {
          if (el.hasAttribute('data-autoplay')) {
            if (autoSlide && (el.duration * 1000 / el.playbackRate) > autoSlide) {
              autoSlide = (el.duration * 1000 / el.playbackRate) + 1000;
            }
          }
        });
      }

      // Cue the next auto-slide if:
      // - There is an autoSlide value
      // - Auto-sliding isn't paused by the user
      // - The presentation isn't paused
      // - The overview isn't active
      // - The presentation isn't over
      if (autoSlide && !autoSlidePaused && !isPaused() && !isOverview() && (!Reveal.isLastSlide() || availableFragments().next || config.loop === true)) {
        autoSlideTimeout = setTimeout(function () {
          typeof config.autoSlideMethod === 'function' ? config.autoSlideMethod() : navigateNext();
          cueAutoSlide();
        }, autoSlide);
        autoSlideStartTime = Date.now();
      }

      if (autoSlidePlayer) {
        autoSlidePlayer.setPlaying(autoSlideTimeout !== -1);
      }

    }

  }

	/**
	 * Cancels any ongoing request to auto-slide.
	 */
  function cancelAutoSlide() {

    clearTimeout(autoSlideTimeout);
    autoSlideTimeout = -1;

  }

  function pauseAutoSlide() {

    if (autoSlide && !autoSlidePaused) {
      autoSlidePaused = true;
      dispatchEvent('autoslidepaused');
      clearTimeout(autoSlideTimeout);

      if (autoSlidePlayer) {
        autoSlidePlayer.setPlaying(false);
      }
    }

  }

  function resumeAutoSlide() {

    if (autoSlide && autoSlidePaused) {
      autoSlidePaused = false;
      dispatchEvent('autoslideresumed');
      cueAutoSlide();
    }

  }

  function navigateLeft() {

    // Reverse for RTL
    if (config.rtl) {
      if ((isOverview() || nextFragment() === false) && availableRoutes().left) {
        slide(indexh + 1, config.navigationMode === 'grid' ? indexv : undefined);
      }
    }
    // Normal navigation
    else if ((isOverview() || previousFragment() === false) && availableRoutes().left) {
      slide(indexh - 1, config.navigationMode === 'grid' ? indexv : undefined);
    }

  }

  function navigateRight() {

    hasNavigatedRight = true;

    // Reverse for RTL
    if (config.rtl) {
      if ((isOverview() || previousFragment() === false) && availableRoutes().right) {
        slide(indexh - 1, config.navigationMode === 'grid' ? indexv : undefined);
      }
    }
    // Normal navigation
    else if ((isOverview() || nextFragment() === false) && availableRoutes().right) {
      slide(indexh + 1, config.navigationMode === 'grid' ? indexv : undefined);
    }

  }

  function navigateUp() {

    // Prioritize hiding fragments
    if ((isOverview() || previousFragment() === false) && availableRoutes().up) {
      slide(indexh, indexv - 1);
    }

  }

  function navigateDown() {

    hasNavigatedDown = true;

    // Prioritize revealing fragments
    if ((isOverview() || nextFragment() === false) && availableRoutes().down) {
      slide(indexh, indexv + 1);
    }

  }

	/**
	 * Navigates backwards, prioritized in the following order:
	 * 1) Previous fragment
	 * 2) Previous vertical slide
	 * 3) Previous horizontal slide
	 */
  function navigatePrev() {

    // Prioritize revealing fragments
    if (previousFragment() === false) {
      if (availableRoutes().up) {
        navigateUp();
      }
      else {
        // Fetch the previous horizontal slide, if there is one
        var previousSlide;

        if (config.rtl) {
          previousSlide = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR + '.future')).pop();
        }
        else {
          previousSlide = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR + '.past')).pop();
        }

        if (previousSlide) {
          var v = (previousSlide.querySelectorAll('section').length - 1) || undefined;
          var h = indexh - 1;
          slide(h, v);
        }
      }
    }

  }

	/**
	 * The reverse of #navigatePrev().
	 */
  function navigateNext() {

    hasNavigatedRight = true;
    hasNavigatedDown = true;

    // Prioritize revealing fragments
    if (nextFragment() === false) {

      var routes = availableRoutes();

      // When looping is enabled `routes.down` is always available
      // so we need a separate check for when we've reached the
      // end of a stack and should move horizontally
      if (routes.down && routes.right && config.loop && Reveal.isLastVerticalSlide(currentSlide)) {
        routes.down = false;
      }

      if (routes.down) {
        navigateDown();
      }
      else if (config.rtl) {
        navigateLeft();
      }
      else {
        navigateRight();
      }
    }

  }

	/**
	 * Checks if the target element prevents the triggering of
	 * swipe navigation.
	 */
  function isSwipePrevented(target) {

    while (target && typeof target.hasAttribute === 'function') {
      if (target.hasAttribute('data-prevent-swipe')) return true;
      target = target.parentNode;
    }

    return false;

  }


  // --------------------------------------------------------------------//
  // ----------------------------- EVENTS -------------------------------//
  // --------------------------------------------------------------------//

	/**
	 * Called by all event handlers that are based on user
	 * input.
	 *
	 * @param {object} [event]
	 */
  function onUserInput(event) {

    if (config.autoSlideStoppable) {
      pauseAutoSlide();
    }

  }

	/**
	 * Called whenever there is mouse input at the document level
	 * to determine if the cursor is active or not.
	 *
	 * @param {object} event
	 */
  function onDocumentCursorActive(event) {

    showCursor();

    clearTimeout(cursorInactiveTimeout);

    cursorInactiveTimeout = setTimeout(hideCursor, config.hideCursorTime);

  }

	/**
	 * Handler for the document level 'keypress' event.
	 *
	 * @param {object} event
	 */
  function onDocumentKeyPress(event) {

    // Check if the pressed key is question mark
    if (event.shiftKey && event.charCode === 63) {
      toggleHelp();
    }

  }

	/**
	 * Handler for the document level 'keydown' event.
	 *
	 * @param {object} event
	 */
  function onDocumentKeyDown(event) {

    // If there's a condition specified and it returns false,
    // ignore this event
    if (typeof config.keyboardCondition === 'function' && config.keyboardCondition(event) === false) {
      return true;
    }

    // Shorthand
    var keyCode = event.keyCode;

    // Remember if auto-sliding was paused so we can toggle it
    var autoSlideWasPaused = autoSlidePaused;

    onUserInput(event);

    // Is there a focused element that could be using the keyboard?
    var activeElementIsCE = document.activeElement && document.activeElement.contentEditable !== 'inherit';
    var activeElementIsInput = document.activeElement && document.activeElement.tagName && /input|textarea/i.test(document.activeElement.tagName);
    var activeElementIsNotes = document.activeElement && document.activeElement.className && /speaker-notes/i.test(document.activeElement.className);

    // Whitelist specific modified + keycode combinations
    var prevSlideShortcut = event.shiftKey && event.keyCode === 32;
    var firstSlideShortcut = (event.metaKey || event.ctrlKey) && keyCode === 37;
    var lastSlideShortcut = (event.metaKey || event.ctrlKey) && keyCode === 39;

    // Prevent all other events when a modifier is pressed
    var unusedModifier = !prevSlideShortcut && !firstSlideShortcut && !lastSlideShortcut &&
      (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey);

    // Disregard the event if there's a focused element or a
    // keyboard modifier key is present
    if (activeElementIsCE || activeElementIsInput || activeElementIsNotes || unusedModifier) return;

    // While paused only allow resume keyboard events; 'b', 'v', '.'
    var resumeKeyCodes = [66, 86, 190, 191];
    var key;

    // Custom key bindings for togglePause should be able to resume
    if (typeof config.keyboard === 'object') {
      for (key in config.keyboard) {
        if (config.keyboard[key] === 'togglePause') {
          resumeKeyCodes.push(parseInt(key, 10));
        }
      }
    }

    if (isPaused() && resumeKeyCodes.indexOf(keyCode) === -1) {
      return false;
    }

    var triggered = false;

    // 1. User defined key bindings
    if (typeof config.keyboard === 'object') {

      for (key in config.keyboard) {

        // Check if this binding matches the pressed key
        if (parseInt(key, 10) === keyCode) {

          var value = config.keyboard[key];

          // Callback function
          if (typeof value === 'function') {
            value.apply(null, [event]);
          }
          // String shortcuts to reveal.js API
          else if (typeof value === 'string' && typeof Reveal[value] === 'function') {
            Reveal[value].call();
          }

          triggered = true;

        }

      }

    }

    // 2. Registered custom key bindings
    if (triggered === false) {

      for (key in registeredKeyBindings) {

        // Check if this binding matches the pressed key
        if (parseInt(key, 10) === keyCode) {

          var action = registeredKeyBindings[key].callback;

          // Callback function
          if (typeof action === 'function') {
            action.apply(null, [event]);
          }
          // String shortcuts to reveal.js API
          else if (typeof action === 'string' && typeof Reveal[action] === 'function') {
            Reveal[action].call();
          }

          triggered = true;
        }
      }
    }

    // 3. System defined key bindings
    if (triggered === false) {

      // Assume true and try to prove false
      triggered = true;

      // P, PAGE UP
      if (keyCode === 80 || keyCode === 33) {
        navigatePrev();
      }
      // N, PAGE DOWN
      else if (keyCode === 78 || keyCode === 34) {
        navigateNext();
      }
      // H, LEFT
      else if (keyCode === 72 || keyCode === 37) {
        if (firstSlideShortcut) {
          slide(0);
        }
        else if (!isOverview() && config.navigationMode === 'linear') {
          navigatePrev();
        }
        else {
          navigateLeft();
        }
      }
      // L, RIGHT
      else if (keyCode === 76 || keyCode === 39) {
        if (lastSlideShortcut) {
          slide(Number.MAX_VALUE);
        }
        else if (!isOverview() && config.navigationMode === 'linear') {
          navigateNext();
        }
        else {
          navigateRight();
        }
      }
      // K, UP
      else if (keyCode === 75 || keyCode === 38) {
        if (!isOverview() && config.navigationMode === 'linear') {
          navigatePrev();
        }
        else {
          navigateUp();
        }
      }
      // J, DOWN
      else if (keyCode === 74 || keyCode === 40) {
        if (!isOverview() && config.navigationMode === 'linear') {
          navigateNext();
        }
        else {
          navigateDown();
        }
      }
      // HOME
      else if (keyCode === 36) {
        slide(0);
      }
      // END
      else if (keyCode === 35) {
        slide(Number.MAX_VALUE);
      }
      // SPACE
      else if (keyCode === 32) {
        if (isOverview()) {
          deactivateOverview();
        }
        if (event.shiftKey) {
          navigatePrev();
        }
        else {
          navigateNext();
        }
      }
      // TWO-SPOT, SEMICOLON, B, V, PERIOD, LOGITECH PRESENTER TOOLS "BLACK SCREEN" BUTTON
      else if (keyCode === 58 || keyCode === 59 || keyCode === 66 || keyCode === 86 || keyCode === 190 || keyCode === 191) {
        togglePause();
      }
      // F
      else if (keyCode === 70) {
        enterFullscreen();
      }
      // A
      else if (keyCode === 65) {
        if (config.autoSlideStoppable) {
          toggleAutoSlide(autoSlideWasPaused);
        }
      }
      else {
        triggered = false;
      }

    }

    // If the input resulted in a triggered action we should prevent
    // the browsers default behavior
    if (triggered) {
      event.preventDefault && event.preventDefault();
    }
    // ESC or O key
    else if ((keyCode === 27 || keyCode === 79) && features.transforms3d) {
      if (dom.overlay) {
        closeOverlay();
      }
      else {
        toggleOverview();
      }

      event.preventDefault && event.preventDefault();
    }

    // If auto-sliding is enabled we need to cue up
    // another timeout
    cueAutoSlide();

  }

	/**
	 * Handler for the 'touchstart' event, enables support for
	 * swipe and pinch gestures.
	 *
	 * @param {object} event
	 */
  function onTouchStart(event) {

    if (isSwipePrevented(event.target)) return true;

    touch.startX = event.touches[0].clientX;
    touch.startY = event.touches[0].clientY;
    touch.startCount = event.touches.length;

  }

	/**
	 * Handler for the 'touchmove' event.
	 *
	 * @param {object} event
	 */
  function onTouchMove(event) {

    if (isSwipePrevented(event.target)) return true;

    // Each touch should only trigger one action
    if (!touch.captured) {
      onUserInput(event);

      var currentX = event.touches[0].clientX;
      var currentY = event.touches[0].clientY;

      // There was only one touch point, look for a swipe
      if (event.touches.length === 1 && touch.startCount !== 2) {

        var deltaX = currentX - touch.startX,
          deltaY = currentY - touch.startY;

        if (deltaX > touch.threshold && Math.abs(deltaX) > Math.abs(deltaY)) {
          touch.captured = true;
          navigateLeft();
        }
        else if (deltaX < -touch.threshold && Math.abs(deltaX) > Math.abs(deltaY)) {
          touch.captured = true;
          navigateRight();
        }
        else if (deltaY > touch.threshold) {
          touch.captured = true;
          navigateUp();
        }
        else if (deltaY < -touch.threshold) {
          touch.captured = true;
          navigateDown();
        }

        // If we're embedded, only block touch events if they have
        // triggered an action
        if (config.embedded) {
          if (touch.captured || isVerticalSlide(currentSlide)) {
            event.preventDefault();
          }
        }
        // Not embedded? Block them all to avoid needless tossing
        // around of the viewport in iOS
        else {
          event.preventDefault();
        }

      }
    }
    // There's a bug with swiping on some Android devices unless
    // the default action is always prevented
    else if (UA.match(/android/gi)) {
      event.preventDefault();
    }

  }

	/**
	 * Handler for the 'touchend' event.
	 *
	 * @param {object} event
	 */
  function onTouchEnd(event) {

    touch.captured = false;

  }

	/**
	 * Convert pointer down to touch start.
	 *
	 * @param {object} event
	 */
  function onPointerDown(event) {

    if (event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch") {
      event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
      onTouchStart(event);
    }

  }

	/**
	 * Convert pointer move to touch move.
	 *
	 * @param {object} event
	 */
  function onPointerMove(event) {

    if (event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch") {
      event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
      onTouchMove(event);
    }

  }

	/**
	 * Convert pointer up to touch end.
	 *
	 * @param {object} event
	 */
  function onPointerUp(event) {

    if (event.pointerType === event.MSPOINTER_TYPE_TOUCH || event.pointerType === "touch") {
      event.touches = [{ clientX: event.clientX, clientY: event.clientY }];
      onTouchEnd(event);
    }

  }

	/**
	 * Handles mouse wheel scrolling, throttled to avoid skipping
	 * multiple slides.
	 *
	 * @param {object} event
	 */
  function onDocumentMouseScroll(event) {

    if (Date.now() - lastMouseWheelStep > 600) {

      lastMouseWheelStep = Date.now();

      var delta = event.detail || -event.wheelDelta;
      if (delta > 0) {
        navigateNext();
      }
      else if (delta < 0) {
        navigatePrev();
      }

    }

  }

	/**
	 * Clicking on the progress bar results in a navigation to the
	 * closest approximate horizontal slide using this equation:
	 *
	 * ( clickX / presentationWidth ) * numberOfSlides
	 *
	 * @param {object} event
	 */
  function onProgressClicked(event) {

    onUserInput(event);

    event.preventDefault();

    var slidesTotal = toArray(dom.wrapper.querySelectorAll(HORIZONTAL_SLIDES_SELECTOR)).length;
    var slideIndex = Math.floor((event.clientX / dom.wrapper.offsetWidth) * slidesTotal);

    if (config.rtl) {
      slideIndex = slidesTotal - slideIndex;
    }

    slide(slideIndex);

  }

	/**
	 * Event handler for navigation control buttons.
	 */
  function onNavigateLeftClicked(event) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigatePrev() : navigateLeft(); }
  function onNavigateRightClicked(event) { event.preventDefault(); onUserInput(); config.navigationMode === 'linear' ? navigateNext() : navigateRight(); }
  function onNavigateUpClicked(event) { event.preventDefault(); onUserInput(); navigateUp(); }
  function onNavigateDownClicked(event) { event.preventDefault(); onUserInput(); navigateDown(); }
  function onNavigatePrevClicked(event) { event.preventDefault(); onUserInput(); navigatePrev(); }
  function onNavigateNextClicked(event) { event.preventDefault(); onUserInput(); navigateNext(); }

	/**
	 * Handler for the window level 'hashchange' event.
	 *
	 * @param {object} [event]
	 */
  function onWindowHashChange(event) {

    readURL();

  }

	/**
	 * Handler for the window level 'resize' event.
	 *
	 * @param {object} [event]
	 */
  function onWindowResize(event) {

    layout();

  }

	/**
	 * Handle for the window level 'visibilitychange' event.
	 *
	 * @param {object} [event]
	 */
  function onPageVisibilityChange(event) {

    var isHidden = document.webkitHidden ||
      document.msHidden ||
      document.hidden;

    // If, after clicking a link or similar and we're coming back,
    // focus the document.body to ensure we can use keyboard shortcuts
    if (isHidden === false && document.activeElement !== document.body) {
      // Not all elements support .blur() - SVGs among them.
      if (typeof document.activeElement.blur === 'function') {
        document.activeElement.blur();
      }
      document.body.focus();
    }

  }

	/**
	 * Invoked when a slide is and we're in the overview.
	 *
	 * @param {object} event
	 */
  function onOverviewSlideClicked(event) {

    // TODO There's a bug here where the event listeners are not
    // removed after deactivating the overview.
    if (eventsAreBound && isOverview()) {
      event.preventDefault();

      var element = event.target;

      while (element && !element.nodeName.match(/section/gi)) {
        element = element.parentNode;
      }

      if (element && !element.classList.contains('disabled')) {

        deactivateOverview();

        if (element.nodeName.match(/section/gi)) {
          var h = parseInt(element.getAttribute('data-index-h'), 10),
            v = parseInt(element.getAttribute('data-index-v'), 10);

          slide(h, v);
        }

      }
    }

  }

	/**
	 * Handles clicks on links that are set to preview in the
	 * iframe overlay.
	 *
	 * @param {object} event
	 */
  function onPreviewLinkClicked(event) {

    if (event.currentTarget && event.currentTarget.hasAttribute('href')) {
      var url = event.currentTarget.getAttribute('href');
      if (url) {
        showPreview(url);
        event.preventDefault();
      }
    }

  }

	/**
	 * Handles click on the auto-sliding controls element.
	 *
	 * @param {object} [event]
	 */
  function onAutoSlidePlayerClick(event) {

    // Replay
    if (Reveal.isLastSlide() && config.loop === false) {
      slide(0, 0);
      resumeAutoSlide();
    }
    // Resume
    else if (autoSlidePaused) {
      resumeAutoSlide();
    }
    // Pause
    else {
      pauseAutoSlide();
    }

  }


  // --------------------------------------------------------------------//
  // ------------------------ PLAYBACK COMPONENT ------------------------//
  // --------------------------------------------------------------------//


	/**
	 * Constructor for the playback component, which displays
	 * play/pause/progress controls.
	 *
	 * @param {HTMLElement} container The component will append
	 * itself to this
	 * @param {function} progressCheck A method which will be
	 * called frequently to get the current progress on a range
	 * of 0-1
	 */
  function Playback(container, progressCheck) {

    // Cosmetics
    this.diameter = 100;
    this.diameter2 = this.diameter / 2;
    this.thickness = 6;

    // Flags if we are currently playing
    this.playing = false;

    // Current progress on a 0-1 range
    this.progress = 0;

    // Used to loop the animation smoothly
    this.progressOffset = 1;

    this.container = container;
    this.progressCheck = progressCheck;

    this.canvas = document.createElement('canvas');
    this.canvas.className = 'playback';
    this.canvas.width = this.diameter;
    this.canvas.height = this.diameter;
    this.canvas.style.width = this.diameter2 + 'px';
    this.canvas.style.height = this.diameter2 + 'px';
    this.context = this.canvas.getContext('2d');

    this.container.appendChild(this.canvas);

    this.render();

  }

	/**
	 * @param value
	 */
  Playback.prototype.setPlaying = function (value) {

    var wasPlaying = this.playing;

    this.playing = value;

    // Start repainting if we weren't already
    if (!wasPlaying && this.playing) {
      this.animate();
    }
    else {
      this.render();
    }

  };

  Playback.prototype.animate = function () {

    var progressBefore = this.progress;

    this.progress = this.progressCheck();

    // When we loop, offset the progress so that it eases
    // smoothly rather than immediately resetting
    if (progressBefore > 0.8 && this.progress < 0.2) {
      this.progressOffset = this.progress;
    }

    this.render();

    if (this.playing) {
      features.requestAnimationFrameMethod.call(window, this.animate.bind(this));
    }

  };

	/**
	 * Renders the current progress and playback state.
	 */
  Playback.prototype.render = function () {

    var progress = this.playing ? this.progress : 0,
      radius = (this.diameter2) - this.thickness,
      x = this.diameter2,
      y = this.diameter2,
      iconSize = 28;

    // Ease towards 1
    this.progressOffset += (1 - this.progressOffset) * 0.1;

    var endAngle = (- Math.PI / 2) + (progress * (Math.PI * 2));
    var startAngle = (- Math.PI / 2) + (this.progressOffset * (Math.PI * 2));

    this.context.save();
    this.context.clearRect(0, 0, this.diameter, this.diameter);

    // Solid background color
    this.context.beginPath();
    this.context.arc(x, y, radius + 4, 0, Math.PI * 2, false);
    this.context.fillStyle = 'rgba( 0, 0, 0, 0.4 )';
    this.context.fill();

    // Draw progress track
    this.context.beginPath();
    this.context.arc(x, y, radius, 0, Math.PI * 2, false);
    this.context.lineWidth = this.thickness;
    this.context.strokeStyle = 'rgba( 255, 255, 255, 0.2 )';
    this.context.stroke();

    if (this.playing) {
      // Draw progress on top of track
      this.context.beginPath();
      this.context.arc(x, y, radius, startAngle, endAngle, false);
      this.context.lineWidth = this.thickness;
      this.context.strokeStyle = '#fff';
      this.context.stroke();
    }

    this.context.translate(x - (iconSize / 2), y - (iconSize / 2));

    // Draw play/pause icons
    if (this.playing) {
      this.context.fillStyle = '#fff';
      this.context.fillRect(0, 0, iconSize / 2 - 4, iconSize);
      this.context.fillRect(iconSize / 2 + 4, 0, iconSize / 2 - 4, iconSize);
    }
    else {
      this.context.beginPath();
      this.context.translate(4, 0);
      this.context.moveTo(0, 0);
      this.context.lineTo(iconSize - 4, iconSize / 2);
      this.context.lineTo(0, iconSize);
      this.context.fillStyle = '#fff';
      this.context.fill();
    }

    this.context.restore();

  };

  Playback.prototype.on = function (type, listener) {
    this.canvas.addEventListener(type, listener, false);
  };

  Playback.prototype.off = function (type, listener) {
    this.canvas.removeEventListener(type, listener, false);
  };

  Playback.prototype.destroy = function () {

    this.playing = false;

    if (this.canvas.parentNode) {
      this.container.removeChild(this.canvas);
    }

  };


  // --------------------------------------------------------------------//
  // ------------------------------- API --------------------------------//
  // --------------------------------------------------------------------//


  Reveal = {
    VERSION: VERSION,

    initialize: initialize,
    configure: configure,

    sync: sync,
    syncSlide: syncSlide,
    syncFragments: syncFragments,

    // Navigation methods
    slide: slide,
    left: navigateLeft,
    right: navigateRight,
    up: navigateUp,
    down: navigateDown,
    prev: navigatePrev,
    next: navigateNext,

    // Fragment methods
    navigateFragment: navigateFragment,
    prevFragment: previousFragment,
    nextFragment: nextFragment,

    // Deprecated aliases
    navigateTo: slide,
    navigateLeft: navigateLeft,
    navigateRight: navigateRight,
    navigateUp: navigateUp,
    navigateDown: navigateDown,
    navigatePrev: navigatePrev,
    navigateNext: navigateNext,

    // Forces an update in slide layout
    layout: layout,

    // Randomizes the order of slides
    shuffle: shuffle,

    // Returns an object with the available routes as booleans (left/right/top/bottom)
    availableRoutes: availableRoutes,

    // Returns an object with the available fragments as booleans (prev/next)
    availableFragments: availableFragments,

    // Toggles a help overlay with keyboard shortcuts
    toggleHelp: toggleHelp,

    // Toggles the overview mode on/off
    toggleOverview: toggleOverview,

    // Toggles the "black screen" mode on/off
    togglePause: togglePause,

    // Toggles the auto slide mode on/off
    toggleAutoSlide: toggleAutoSlide,

    // State checks
    isOverview: isOverview,
    isPaused: isPaused,
    isAutoSliding: isAutoSliding,
    isSpeakerNotes: isSpeakerNotes,

    // Slide preloading
    loadSlide: loadSlide,
    unloadSlide: unloadSlide,

    // Adds or removes all internal event listeners (such as keyboard)
    addEventListeners: addEventListeners,
    removeEventListeners: removeEventListeners,

    // Facility for persisting and restoring the presentation state
    getState: getState,
    setState: setState,

    // Presentation progress
    getSlidePastCount: getSlidePastCount,

    // Presentation progress on range of 0-1
    getProgress: getProgress,

    // Returns the indices of the current, or specified, slide
    getIndices: getIndices,

    // Returns an Array of all slides
    getSlides: getSlides,

    // Returns an Array of objects representing the attributes on
    // the slides
    getSlidesAttributes: getSlidesAttributes,

    // Returns the total number of slides
    getTotalSlides: getTotalSlides,

    // Returns the slide element at the specified index
    getSlide: getSlide,

    // Returns the slide background element at the specified index
    getSlideBackground: getSlideBackground,

    // Returns the speaker notes string for a slide, or null
    getSlideNotes: getSlideNotes,

    // Returns the previous slide element, may be null
    getPreviousSlide: function () {
      return previousSlide;
    },

    // Returns the current slide element
    getCurrentSlide: function () {
      return currentSlide;
    },

    // Returns the current scale of the presentation content
    getScale: function () {
      return scale;
    },

    // Returns the current configuration object
    getConfig: function () {
      return config;
    },

    // Helper method, retrieves query string as a key/value hash
    getQueryHash: function () {
      var query = {};

      location.search.replace(/[A-Z0-9]+?=([\w\.%-]*)/gi, function (a) {
        query[a.split('=').shift()] = a.split('=').pop();
      });

      // Basic deserialization
      for (var i in query) {
        var value = query[i];

        query[i] = deserialize(unescape(value));
      }

      return query;
    },

    // Returns the top-level DOM element
    getRevealElement: function () {
      return dom.wrapper || document.querySelector('.reveal');
    },

    // Returns a hash with all registered plugins
    getPlugins: function () {
      return plugins;
    },

    // Returns true if we're currently on the first slide
    isFirstSlide: function () {
      return (indexh === 0 && indexv === 0);
    },

    // Returns true if we're currently on the last slide
    isLastSlide: function () {
      if (currentSlide) {
        // Does this slide have a next sibling?
        if (currentSlide.nextElementSibling) return false;

        // If it's vertical, does its parent have a next sibling?
        if (isVerticalSlide(currentSlide) && currentSlide.parentNode.nextElementSibling) return false;

        return true;
      }

      return false;
    },

    // Returns true if we're on the last slide in the current
    // vertical stack
    isLastVerticalSlide: function () {
      if (currentSlide && isVerticalSlide(currentSlide)) {
        // Does this slide have a next sibling?
        if (currentSlide.nextElementSibling) return false;

        return true;
      }

      return false;
    },

    // Checks if reveal.js has been loaded and is ready for use
    isReady: function () {
      return loaded;
    },

    // Forward event binding to the reveal DOM element
    addEventListener: function (type, listener, useCapture) {
      if ('addEventListener' in window) {
        Reveal.getRevealElement().addEventListener(type, listener, useCapture);
      }
    },
    removeEventListener: function (type, listener, useCapture) {
      if ('addEventListener' in window) {
        Reveal.getRevealElement().removeEventListener(type, listener, useCapture);
      }
    },

    // Adds/removes a custom key binding
    addKeyBinding: addKeyBinding,
    removeKeyBinding: removeKeyBinding,

    // API for registering and retrieving plugins
    registerPlugin: registerPlugin,
    hasPlugin: hasPlugin,
    getPlugin: getPlugin,

    // Programmatically triggers a keyboard event
    triggerKey: function (keyCode) {
      onDocumentKeyDown({ keyCode: keyCode });
    },

    // Registers a new shortcut to include in the help overlay
    registerKeyboardShortcut: function (key, value) {
      keyboardShortcuts[key] = value;
    }
  };

  return Reveal;

}));