Monocle = {
  VERSION: "1.0.0"
};


Monocle.pieceLoaded = function (piece) {
  if (typeof onMonoclePiece == 'function') {
    onMonoclePiece(piece);
  }
}


Monocle.defer = function (fn, time) {
  if (fn && typeof fn == "function") {
    return setTimeout(fn, time || 0);
  }
}


Monocle.Browser = { engine: 'W3C' }

Monocle.Browser.is = {
  IE: (!!(window.attachEvent && navigator.userAgent.indexOf('Opera') === -1)) &&
    (Monocle.Browser.engine = "IE"),
  Opera: navigator.userAgent.indexOf('Opera') > -1 &&
    (Monocle.Browser.engine = "Opera"),
  WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1 &&
    (Monocle.Browser.engine = "WebKit"),
  Gecko: navigator.userAgent.indexOf('Gecko') > -1 &&
    navigator.userAgent.indexOf('KHTML') === -1 &&
    (Monocle.Browser.engine = "Gecko"),
  MobileSafari: !!navigator.userAgent.match(/AppleWebKit.*Mobile/)
} // ... with thanks to PrototypeJS.


Monocle.Browser.on = {
  iPhone: navigator.userAgent.indexOf("iPhone") != -1,
  iPad: navigator.userAgent.indexOf("iPad") != -1,
  BlackBerry: navigator.userAgent.indexOf("BlackBerry") != -1,
  Android: navigator.userAgent.indexOf('Android') != -1,
  MacOSX: navigator.userAgent.indexOf('Mac OS X') != -1,
  Kindle3: navigator.userAgent.match(/Kindle\/3/)
}


if (Monocle.Browser.is.MobileSafari) {
  (function () {
    var ver = navigator.userAgent.match(/ OS ([\d_]+)/);
    if (ver) {
      Monocle.Browser.iOSVersion = ver[1].replace(/_/g, '.');
    } else {
      console.warn("Unknown MobileSafari user agent: "+navigator.userAgent);
    }
  })();
}
Monocle.Browser.iOSVersionBelow = function (strOrNum) {
  return Monocle.Browser.iOSVersion && Monocle.Browser.iOSVersion < strOrNum;
}


Monocle.Browser.CSSProps = {
  engines: ["W3C", "WebKit", "Gecko", "Opera", "IE", "Konqueror"],
  prefixes: ["", "-webkit-", "-moz-", "-o-", "-ms-", "-khtml-"],
  domprefixes: ["", "Webkit", "Moz", "O", "ms", "Khtml"],
  guineapig: document.createElement('div')
}


Monocle.Browser.CSSProps.capStr = function (wd) {
  return wd ? wd.charAt(0).toUpperCase() + wd.substr(1) : "";
}


Monocle.Browser.CSSProps.toDOMProps = function (prop, prefix) {
  var parts = prop.split('-');
  for (var i = parts.length; i > 0; --i) {
    parts[i] = Monocle.Browser.CSSProps.capStr(parts[i]);
  }

  if (typeof(prefix) != 'undefined' && prefix != null) {
    if (prefix) {
      parts[0] = Monocle.Browser.CSSProps.capStr(parts[0]);
      return prefix+parts.join('');
    } else {
      return parts.join('');
    }
  }

  var props = [parts.join('')];
  parts[0] = Monocle.Browser.CSSProps.capStr(parts[0]);
  for (i = 0; i < Monocle.Browser.CSSProps.prefixes.length; ++i) {
    var pf = Monocle.Browser.CSSProps.domprefixes[i];
    if (!pf) { continue; }
    props.push(pf+parts.join(''));
  }
  return props;
}


Monocle.Browser.CSSProps.toDOMProp = function (prop) {
  return Monocle.Browser.CSSProps.toDOMProps(
    prop,
    Monocle.Browser.CSSProps.domprefixes[
      Monocle.Browser.CSSProps.engines.indexOf(Monocle.Browser.engine)
    ]
  );
}


Monocle.Browser.CSSProps.isSupported = function (props) {
  for (var i in props) {
    if (Monocle.Browser.CSSProps.guineapig.style[props[i]] !== undefined) {
      return true;
    }
  }
  return false;
} // Thanks modernizr!


Monocle.Browser.CSSProps.isSupportedForAnyPrefix = function (prop) {
  return Monocle.Browser.CSSProps.isSupported(
    Monocle.Browser.CSSProps.toDOMProps(prop)
  );
}


Monocle.Browser.CSSProps.supportsMediaQuery = function (query) {
  var gpid = "monocle_guineapig";
  var div = Monocle.Browser.CSSProps.guineapig;
  div.id = gpid;
  var st = document.createElement('style');
  st.textContent = query+'{#'+gpid+'{height:3px}}';
  (document.head || document.getElementsByTagName('head')[0]).appendChild(st);
  document.documentElement.appendChild(div);

  var result = Monocle.Browser.CSSProps.guineapig.offsetHeight === 3;

  st.parentNode.removeChild(st);
  div.parentNode.removeChild(div);

  return result;
} // Thanks modernizr!


Monocle.Browser.CSSProps.supportsMediaQueryProperty = function (prop) {
  return Monocle.Browser.CSSProps.supportsMediaQuery(
    '@media ('+Monocle.Browser.CSSProps.prefixes.join(prop+'),(')+'monocle__)'
  );
}



Monocle.Browser.has = {}
Monocle.Browser.has.touch = ('ontouchstart' in window) ||
  Monocle.Browser.CSSProps.supportsMediaQueryProperty('touch-enabled');
Monocle.Browser.has.columns = Monocle.Browser.CSSProps.isSupportedForAnyPrefix(
  'column-width'
);
Monocle.Browser.has.transform3d = Monocle.Browser.CSSProps.isSupported([
  'perspectiveProperty',
  'WebkitPerspective',
  'MozPerspective',
  'OPerspective',
  'msPerspective'
]) && Monocle.Browser.CSSProps.supportsMediaQueryProperty('transform-3d');
Monocle.Browser.has.embedded = (top != self);

Monocle.Browser.has.iframeTouchBug = Monocle.Browser.iOSVersionBelow("4.2");

Monocle.Browser.has.selectThruBug = Monocle.Browser.iOSVersionBelow("4.2");

Monocle.Browser.has.mustScrollSheaf = Monocle.Browser.is.MobileSafari;
Monocle.Browser.has.iframeDoubleWidthBug = Monocle.Browser.has.mustScrollSheaf;

Monocle.Browser.has.floatColumnBug = Monocle.Browser.is.WebKit;

Monocle.Browser.has.relativeIframeWidthBug = Monocle.Browser.on.Android;


Monocle.Browser.has.jumpFlickerBug =
  Monocle.Browser.on.MacOSX && Monocle.Browser.is.WebKit;


if (typeof window.console == "undefined") {
  window.console = {
    messages: [],
    log: function (msg) {
      this.messages.push(msg);
    }
  }
}


window.console.compatDir = function (obj) {
  var stringify = function (o) {
    var parts = [];
    for (x in o) {
      parts.push(x + ": " + o[x]);
    }
    return parts.join("; ");
  }

  window.console.log(stringify(obj));
}


if (!Array.prototype.indexOf) {
  Array.prototype.indexOf = function(elt /*, from*/) {
    var len = this.length >>> 0;

    var from = Number(arguments[1]) || 0;
    from = (from < 0)
      ? Math.ceil(from)
      : Math.floor(from);
    if (from < 0) {
      from += len;
    }

    for (; from < len; from++) {
      if (from in this && this[from] === elt) {
        return from;
      }
    }
    return -1;
  };
}


Monocle.pieceLoaded('compat');
Monocle.Factory = function (element, label, index, reader) {

  var API = { constructor: Monocle.Factory };
  var k = API.constants = API.constructor;
  var p = API.properties = {
    element: element,
    label: label,
    index: index,
    reader: reader,
    prefix: reader.properties.classPrefix || ''
  }


  function initialize() {
    var node = p.reader.properties.graph;
    node[p.label] = node[p.label] || [];
    if (typeof p.index == 'undefined' && node[p.label][p.index]) {
      throw('Element already exists in graph: '+p.label+'['+p.index+']');
    } else {
      p.index = p.index || node[p.label].length;
    }
    node[p.label][p.index] = p.element;

    addClass(p.label);
  }


  function find(oLabel, oIndex) {
    if (!p.reader.properties.graph[oLabel]) {
      return null;
    }
    return p.reader.properties.graph[oLabel][oIndex || 0];
  }


  function claim(oElement, oLabel, oIndex) {
    return oElement.dom = new Monocle.Factory(
      oElement,
      oLabel,
      oIndex,
      p.reader
    );
  }


  function make(tagName, oLabel, index_or_options, or_options) {
    var oIndex, options;
    if (arguments.length == 2) {
      oIndex = 0;
      options = {};
    } else if (arguments.length == 4) {
      oIndex = arguments[2];
      options = arguments[3];
    } else if (arguments.length == 3) {
      var lastArg = arguments[arguments.length - 1];
      if (typeof lastArg == "number") {
        oIndex = lastArg;
        options = {};
      } else {
        oIndex = 0;
        options = lastArg;
      }
    }

    var oElement = document.createElement(tagName);
    claim(oElement, oLabel, oIndex);
    if (options['class']) {
      oElement.className += " "+p.prefix+options['class'];
    }
    if (options['html']) {
      oElement.innerHTML = options['html'];
    }
    if (options['text']) {
      oElement.appendChild(document.createTextNode(options['text']));
    }

    return oElement;
  }


  function append(tagName, oLabel, index_or_options, or_options) {
    var oElement = make.apply(this, arguments);
    p.element.appendChild(oElement);
    return oElement;
  }


  function address() {
    return [p.label, p.index, p.reader];
  }


  function setStyles(rules) {
    return Monocle.Styles.applyRules(p.element, rules);
  }


  function setBetaStyle(property, value) {
    return Monocle.Styles.affix(p.element, property, value);
  }



  function hasClass(name) {
    name = p.prefix + name;
    var klass = p.element.className;
    if (!klass) { return false; }
    if (klass == name) { return true; }
    return new RegExp("(^|\\s)"+name+"(\\s|$)").test(klass);
  }


  function addClass(name) {
    if (hasClass(name)) { return; }
    var gap = p.element.className ? ' ' : '';
    return p.element.className += gap+p.prefix+name;
  }


  function removeClass(name) {
    var reName = new RegExp("(^|\\s+)"+p.prefix+name+"(\\s+|$)");
    var reTrim = /^\s+|\s+$/g;
    var klass = p.element.className;
    p.element.className = klass.replace(reName, ' ').replace(reTrim, '');
    return p.element.className;
  }


  API.find = find;
  API.claim = claim;
  API.make = make;
  API.append = append;
  API.address = address;

  API.setStyles = setStyles;
  API.setBetaStyle = setBetaStyle;
  API.hasClass = hasClass;
  API.addClass = addClass;
  API.removeClass = removeClass;

  initialize();

  return API;
}

Monocle.pieceLoaded('factory');
Monocle.Events = {}


Monocle.Events.listen = function (elem, evtType, fn, useCapture) {
  if (elem.addEventListener) {
    return elem.addEventListener(evtType, fn, useCapture || false);
  } else if (elem.attachEvent) {
    return elem.attachEvent('on'+evtType, fn);
  }
}


Monocle.Events.deafen = function (elem, evtType, fn, useCapture) {
  if (elem.removeEventListener) {
    return elem.removeEventListener(evtType, fn, useCapture || false);
  } else if (elem.detachEvent) {
    try {
      return elem.detachEvent('on'+evtType, fn);
    } catch(e) {}
  }
}


Monocle.Events.listenForContact = function (elem, fns, options) {
  var listeners = {};

  var cursorInfo = function (evt, ci) {
    evt.m = {
      pageX: ci.pageX,
      pageY: ci.pageY
    };

    var target = evt.target || window.srcElement;
    while (target.nodeType != 1 && target.parentNode) {
      target = target.parentNode;
    }

    var offset = offsetFor(evt, target);
    evt.m.offsetX = offset[0];
    evt.m.offsetY = offset[1];

    if (evt.currentTarget) {
      offset = offsetFor(evt, evt.currentTarget);
      evt.m.registrantX = offset[0];
      evt.m.registrantY = offset[1];
    }

    return evt;
  }


  var offsetFor = function (evt, elem) {
    var r;
    if (elem.getBoundingClientRect) {
      var er = elem.getBoundingClientRect();
      var dr = document.body.getBoundingClientRect();
      r = { left: er.left - dr.left, top: er.top - dr.top };
    } else {
      r = { left: elem.offsetLeft, top: elem.offsetTop }
      while (elem = elem.parentNode) {
        if (elem.offsetLeft || elem.offsetTop) {
          r.left += elem.offsetLeft;
          r.top += elem.offsetTop;
        }
      }
    }
    return [evt.m.pageX - r.left, evt.m.pageY - r.top];
  }


  var capture = (options && options.useCapture) || false;

  if (!Monocle.Browser.has.touch) {
    if (fns.start) {
      listeners.mousedown = function (evt) {
        if (evt.button != 0) { return; }
        fns.start(cursorInfo(evt, evt));
      }
      Monocle.Events.listen(elem, 'mousedown', listeners.mousedown, capture);
    }
    if (fns.move) {
      listeners.mousemove = function (evt) {
        fns.move(cursorInfo(evt, evt));
      }
      Monocle.Events.listen(elem, 'mousemove', listeners.mousemove, capture);
    }
    if (fns.end) {
      listeners.mouseup = function (evt) {
        fns.end(cursorInfo(evt, evt));
      }
      Monocle.Events.listen(elem, 'mouseup', listeners.mouseup, capture);
    }
    if (fns.cancel) {
      listeners.mouseout = function (evt) {
        obj = evt.relatedTarget || evt.fromElement;
        while (obj && (obj = obj.parentNode)) {
          if (obj == elem) { return; }
        }
        fns.cancel(cursorInfo(evt, evt));
      }
      Monocle.Events.listen(elem, 'mouseout', listeners.mouseout, capture);
    }
  } else {
    if (fns.start) {
      listeners.start = function (evt) {
        if (evt.touches.length > 1) { return; }
        fns.start(cursorInfo(evt, evt.targetTouches[0]));
      }
    }
    if (fns.move) {
      listeners.move = function (evt) {
        if (evt.touches.length > 1) { return; }
        fns.move(cursorInfo(evt, evt.targetTouches[0]));
      }
    }
    if (fns.end) {
      listeners.end = function (evt) {
        fns.end(cursorInfo(evt, evt.changedTouches[0]));
        evt.preventDefault();
      }
    }
    if (fns.cancel) {
      listeners.cancel = function (evt) {
        fns.cancel(cursorInfo(evt, evt.changedTouches[0]));
      }
    }

    if (Monocle.Browser.has.iframeTouchBug) {
      Monocle.Events.tMonitor = Monocle.Events.tMonitor ||
        new Monocle.Events.TouchMonitor();
      Monocle.Events.tMonitor.listen(elem, listeners, options);
    } else {
      for (etype in listeners) {
        Monocle.Events.listen(elem, 'touch'+etype, listeners[etype], capture);
      }
    }
  }

  return listeners;
}


Monocle.Events.deafenForContact = function (elem, listeners) {
  var prefix = "";
  if (Monocle.Browser.has.touch) {
    prefix = Monocle.Browser.has.iframeTouchBug ? "contact" : "touch";
  }

  for (evtType in listeners) {
    Monocle.Events.deafen(elem, prefix + evtType, listeners[evtType]);
  }
}


Monocle.Events.listenForTap = function (elem, fn) {
  var startPos;

  if (Monocle.Browser.on.Kindle3) {
    Monocle.Events.listen(elem, 'click', function () {});
  }

  var annulIfOutOfBounds = function (evt) {
    if (evt.type.match(/^mouse/)) {
      return;
    }
    if (Monocle.Browser.is.MobileSafari && Monocle.Browser.iOSVersion < "3.2") {
      return;
    }
    if (
      evt.m.registrantX < 0 || evt.m.registrantX > elem.offsetWidth ||
      evt.m.registrantY < 0 || evt.m.registrantY > elem.offsetHeight
    ) {
      startPos = null;
    } else {
      evt.preventDefault();
    }
  }

  return Monocle.Events.listenForContact(
    elem,
    {
      start: function (evt) {
        startPos = [evt.m.pageX, evt.m.pageY];
        evt.preventDefault();
      },
      move: annulIfOutOfBounds,
      end: function (evt) {
        annulIfOutOfBounds(evt);
        if (startPos) {
          evt.m.startOffset = startPos;
          fn(evt);
        }
      },
      cancel: function (evt) {
        startPos = null;
      }
    },
    {
      useCapture: false
    }
  );
}


Monocle.Events.deafenForTap = Monocle.Events.deafenForContact;


Monocle.Events.TouchMonitor = function () {
  if (Monocle.Events == this) {
    return new Monocle.Events.TouchMonitor();
  }

  var API = { constructor: Monocle.Events.TouchMonitor }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    touching: null,
    edataPrev: null,
    originator: null,
    brokenModel_4_1: navigator.userAgent.match(/ OS 4_1/)
  }


  function listenOnIframe(iframe) {
    if (iframe.contentDocument) {
      enableTouchProxy(iframe.contentDocument);
      iframe.contentDocument.isTouchFrame = true;
    }

    if (p.brokenModel_4_1) {
      enableTouchProxy(iframe);
    }
  }


  function listen(element, fns, useCapture) {
    for (etype in fns) {
      Monocle.Events.listen(element, 'contact'+etype, fns[etype], useCapture);
    }
    enableTouchProxy(element, useCapture);
  }


  function enableTouchProxy(element, useCapture) {
    if (element.monocleTouchProxy) {
      return;
    }
    element.monocleTouchProxy = true;

    var fn = function (evt) { touchProxyHandler(element, evt) }
    Monocle.Events.listen(element, "touchstart", fn, useCapture);
    Monocle.Events.listen(element, "touchmove", fn, useCapture);
    Monocle.Events.listen(element, "touchend", fn, useCapture);
    Monocle.Events.listen(element, "touchcancel", fn, useCapture);
  }


  function touchProxyHandler(element, evt) {
    var edata = {
      start: evt.type == "touchstart",
      move: evt.type == "touchmove",
      end: evt.type == "touchend" || evt.type == "touchcancel",
      time: new Date().getTime(),
      frame: element.isTouchFrame
    }

    if (!p.touching) {
      p.originator = element;
    }

    var target = element;
    var touch = evt.touches[0] || evt.changedTouches[0];
    target = document.elementFromPoint(touch.screenX, touch.screenY);

    if (target) {
      translateTouchEvent(element, target, evt, edata);
    }
  }


  function translateTouchEvent(element, target, evt, edata) {
    if (
      p.brokenModel_4_1 &&
      !edata.frame &&
      !p.touching &&
      edata.start &&
      p.edataPrev &&
      p.edataPrev.end &&
      (edata.time - p.edataPrev.time) < 30
    ) {
      evt.preventDefault();
      return;
    }

    if (!p.touching && !edata.end) {
      return fireStart(evt, target, edata);
    }

    if (edata.move && p.touching) {
      return fireMove(evt, edata);
    }

    if (p.brokenModel_4_1) {
      if (p.touching && !edata.frame) {
        return fireProvisionalEnd(evt, edata);
      }
    } else {
      if (edata.end && p.touching) {
        return fireProvisionalEnd(evt, edata);
      }
    }

    if (
      p.brokenModel_4_1 &&
      p.originator != element &&
      edata.frame &&
      edata.end
    ) {
      evt.preventDefault();
      return;
    }

    if (edata.frame && edata.end && p.touching) {
      return fireProvisionalEnd(evt, edata);
    }
  }


  function fireStart(evt, target, edata) {
    p.touching = target;
    p.edataPrev = edata;
    return fireTouchEvent(p.touching, 'start', evt);
  }


  function fireMove(evt, edata) {
    clearProvisionalEnd();
    p.edataPrev = edata;
    return fireTouchEvent(p.touching, 'move', evt);
  }


  function fireEnd(evt, edata) {
    var result = fireTouchEvent(p.touching, 'end', evt);
    p.edataPrev = edata;
    p.touching = null;
    return result;
  }


  function fireProvisionalEnd(evt, edata) {
    clearProvisionalEnd();
    var mimicEvt = mimicTouchEvent(p.touching, 'end', evt);
    p.edataPrev = edata;

    p.provisionalEnd = setTimeout(
      function() {
        if (p.touching) {
          p.touching.dispatchEvent(mimicEvt);
          p.touching = null;
        }
      },
      30
    );
  }


  function clearProvisionalEnd() {
    if (p.provisionalEnd) {
      clearTimeout(p.provisionalEnd);
      p.provisionalEnd = null;
    }
  }


  function mimicTouchEvent(target, newtype, evt) {
    var cloneTouch = function (t) {
      return document.createTouch(
        document.defaultView,
        target,
        t.identifier,
        t.screenX,
        t.screenY,
        t.screenX,
        t.screenY
      );
    }

    var findTouch = function (id) {
      for (var i = 0; i < touches.all.length; ++i) {
        if (touches.all[i].identifier == id) {
          return touches.all[i];
        }
      }
    }

    var touches = { all: [], target: [], changed: [] };
    for (var i = 0; i < evt.touches.length; ++i) {
      touches.all.push(cloneTouch(evt.touches[i]));
    }
    for (var i = 0; i < evt.targetTouches.length; ++i) {
      touches.target.push(
        findTouch(evt.targetTouches[i].identifier) ||
        cloneTouch(evt.targetTouches[i])
      );
    }
    for (var i = 0; i < evt.changedTouches.length; ++i) {
      touches.changed.push(
        findTouch(evt.changedTouches[i].identifier) ||
        cloneTouch(evt.changedTouches[i])
      );
    }

    var mimicEvt = document.createEvent('TouchEvent');
    mimicEvt.initTouchEvent(
      "contact"+newtype,
      true,
      true,
      document.defaultView,
      evt.detail,
      evt.screenX,
      evt.screenY,
      evt.screenX,
      evt.screenY,
      evt.ctrlKey,
      evt.altKey,
      evt.shiftKey,
      evt.metaKey,
      document.createTouchList.apply(document, touches.all),
      document.createTouchList.apply(document, touches.target),
      document.createTouchList.apply(document, touches.changed),
      evt.scale,
      evt.rotation
    );

    return mimicEvt;
  }


  function fireTouchEvent(target, newtype, evt) {
    var mimicEvt = mimicTouchEvent(target, newtype, evt);
    var result = target.dispatchEvent(mimicEvt);
    if (!result) {
      evt.preventDefault();
    }
    return result;
  }


  API.listen = listen;
  API.listenOnIframe = listenOnIframe;

  return API;
}


Monocle.Events.listenOnIframe = function (frame) {
  if (!Monocle.Browser.has.iframeTouchBug) {
    return;
  }
  Monocle.Events.tMonitor = Monocle.Events.tMonitor ||
    new Monocle.Events.TouchMonitor();
  Monocle.Events.tMonitor.listenOnIframe(frame);
}

Monocle.pieceLoaded('events');
Monocle.Styles = {
  applyRules: function (elem, rules) {
    if (typeof rules != 'string') {
      var parts = [];
      for (var declaration in rules) {
        parts.push(declaration+": "+rules[declaration]+";")
      }
      rules = parts.join(" ");
    }
    elem.style.cssText += ';'+rules;
    return elem.style.cssText;
  },

  affix: function (elem, property, value) {
    var target = elem.style ? elem.style : elem;
    target[Monocle.Browser.CSSProps.toDOMProp(property)] = value;
  },

  setX: function (elem, x) {
    var s = elem.style;
    if (typeof x == "number") { x += "px"; }
    if (Monocle.Browser.has.transform3d) {
      s.webkitTransform = "translate3d("+x+", 0, 0)";
    } else {
      s.webkitTransform = "translateX("+x+")";
    }
    s.MozTransform = s.OTransform = s.transform = "translateX("+x+")";
    return x;
  },

  setY: function (elem, y) {
    var s = elem.style;
    if (typeof y == "number") { y += "px"; }
    if (Monocle.Browser.has.transform3d) {
      s.webkitTransform = "translate3d(0, "+y+", 0)";
    } else {
      s.webkitTransform = "translateY("+y+")";
    }
    s.MozTransform = s.OTransform = s.transform = "translateY("+y+")";
    return y;
  }
}


Monocle.Styles.container = {
  "position": "absolute",
  "top": "0",
  "left": "0",
  "bottom": "0",
  "right": "0"
}

Monocle.Styles.page = {
  "position": "absolute",
  "z-index": "1",
  "-webkit-user-select": "none",
  "-moz-user-select": "none",
  "user-select": "none",
  "-webkit-transform": "translate3d(0,0,0)"

  /*
  "background": "white",
  "top": "0",
  "left": "0",
  "bottom": "0",
  "right": "0"
  */
}

Monocle.Styles.sheaf = {
  "position": "absolute",
  "overflow": "hidden" // Required by MobileSafari to constrain inner iFrame.

  /*
  "top": "0",
  "left": "0",
  "bottom": "0",
  "right": "0"
  */
}

Monocle.Styles.component = {
  "display": "block",
  "width": "100%",
  "height": "100%",
  "border": "none",
  "overflow": "hidden",
  "-webkit-user-select": "none",
  "-moz-user-select": "none",
  "user-select": "none"
}

Monocle.Styles.control = {
  "z-index": "100",
  "cursor": "pointer"
}

Monocle.Styles.overlay = {
  "position": "absolute",
  "display": "none",
  "width": "100%",
  "height": "100%",
  "z-index": "1000"
}



Monocle.pieceLoaded('styles');
Monocle.Reader = function (node, bookData, options, onLoadCallback) {
  if (Monocle == this) {
    return new Monocle.Reader(node, bookData, options, onLoadCallback);
  }

  var API = { constructor: Monocle.Reader }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    initialized: false,

    book: null,

    graph: {},

    pageStylesheets: [],

    systemId: (options ? options.systemId : null) || k.DEFAULT_SYSTEM_ID,

    classPrefix: k.DEFAULT_CLASS_PREFIX,

    controls: [],

    resizeTimer: null
  }

  var dom;


  function initialize(node, bookData, options, onLoadCallback) {
    var box = typeof(node) == "string" ?  document.getElementById(node) : node;
    dom = API.dom = box.dom = new Monocle.Factory(box, 'box', 0, API);

    options = options || {}

    dispatchEvent("monocle:initializing");

    var bk;
    if (bookData) {
      bk = new Monocle.Book(bookData);
    } else {
      bk = Monocle.Book.fromNodes([box.cloneNode(true)]);
    }
    box.innerHTML = "";

    positionBox();

    attachFlipper(options.flipper);

    createReaderElements();

    p.defaultStyles = addPageStyles(k.DEFAULT_STYLE_RULES, false);

    primeFrames(options.primeURL, function () {
      applyStyles();

      listen('monocle:componentchange', persistPageStylesOnComponentChange);

      p.flipper.listenForInteraction(options.panels);

      setBook(bk, options.place, function () {
        p.initialized = true;
        if (onLoadCallback) { onLoadCallback(API); }
        dispatchEvent("monocle:loaded");
      });
    });
  }


  function positionBox() {
    var currPosVal;
    var box = dom.find('box');
    if (document.defaultView) {
      var currStyle = document.defaultView.getComputedStyle(box, null);
      currPosVal = currStyle.getPropertyValue('position');
    } else if (box.currentStyle) {
      currPosVal = box.currentStyle.position
    }
    if (["absolute", "relative"].indexOf(currPosVal) == -1) {
      box.style.position = "relative";
    }
  }


  function attachFlipper(flipperClass) {
    if (!Monocle.Browser.has.columns) {
      flipperClass = Monocle.Flippers[k.FLIPPER_LEGACY_CLASS];
      if (!flipperClass) {
        return dom.append(
          'div',
          'abortMsg',
          { 'class': k.abortMessage.CLASSNAME, 'html': k.abortMessage.TEXT }
        );
      }
    } else if (!flipperClass) {
      flipperClass = Monocle.Flippers[k.FLIPPER_DEFAULT_CLASS];
      if (!flipperClass) {
        throw("No flipper class");
      }
    }
    p.flipper = new flipperClass(API, null, p.readerOptions);
  }


  function createReaderElements() {
    var cntr = dom.append('div', 'container');
    for (var i = 0; i < p.flipper.pageCount; ++i) {
      var page = cntr.dom.append('div', 'page', i);
      page.m = { reader: API, pageIndex: i, place: null }
      page.m.sheafDiv = page.dom.append('div', 'sheaf', i);
      page.m.activeFrame = page.m.sheafDiv.dom.append('iframe', 'component', i);
      page.m.activeFrame.m = { 'pageDiv': page }
      p.flipper.addPage(page);
      Monocle.Events.listenOnIframe(page.m.activeFrame);
    }
    dom.append('div', 'overlay');
    dispatchEvent("monocle:loading");
  }


  function primeFrames(url, callback) {
    url = url || "about:blank";

    var pageMax = p.flipper.pageCount;
    var pageCount = 0;

    var cb = function (evt) {
      var frame = evt.target || evt.srcElement;
      Monocle.Events.deafen(frame, 'load', cb);
      if (Monocle.Browser.is.WebKit) {
        frame.contentDocument.documentElement.style.overflow = "hidden";
      }
      if ((pageCount += 1) == pageMax) {
        Monocle.defer(callback);
      }
    }

    for (var i = 0; i < pageMax; ++i) {
      var page = dom.find('page', i);
      page.m.activeFrame.style.visibility = "hidden";
      page.m.activeFrame.setAttribute('frameBorder', 0);
      page.m.activeFrame.setAttribute('scrolling', 'no');
      Monocle.Events.listen(page.m.activeFrame, 'load', cb);
      page.m.activeFrame.src = url;
    }
  }


  function applyStyles() {
    dom.find('container').dom.setStyles(Monocle.Styles.container);
    for (var i = 0; i < p.flipper.pageCount; ++i) {
      var page = dom.find('page', i);
      page.dom.setStyles(Monocle.Styles.page);
      dom.find('sheaf', i).dom.setStyles(Monocle.Styles.sheaf);
      var cmpt = dom.find('component', i)
      cmpt.dom.setStyles(Monocle.Styles.component);
      Monocle.Styles.applyRules(cmpt.contentDocument.body, Monocle.Styles.body);
    }
    lockFrameWidths();
    dom.find('overlay').dom.setStyles(Monocle.Styles.overlay);
    dispatchEvent('monocle:styles');
  }


  function lockingFrameWidths() {
    if (!Monocle.Browser.has.relativeIframeWidthBug) { return; }
    for (var i = 0, cmpt; cmpt = dom.find('component', i); ++i) {
      cmpt.style.display = "none";
    }
  }


  function lockFrameWidths() {
    if (!Monocle.Browser.has.relativeIframeWidthBug) { return; }
    for (var i = 0, cmpt; cmpt = dom.find('component', i); ++i) {
      cmpt.style.width = cmpt.parentNode.offsetWidth+"px";
      cmpt.style.display = "block";
    }
  }


  function setBook(bk, place, callback) {
    p.book = bk;
    var pageCount = 0;
    if (typeof callback == 'function') {
      var watcher = function (evt) {
        if ((pageCount += 1) == p.flipper.pageCount) {
          deafen('monocle:componentchange', watcher);
          callback();
        }
      }
      listen('monocle:componentchange', watcher);
    }
    p.flipper.moveTo(place || { page: 1 });
  }


  function getBook() {
    return p.book;
  }


  function resized() {
    if (!p.initialized) {
      console.warn('Attempt to resize book before initialization.');
    }
    lockingFrameWidths();
    if (!dispatchEvent("monocle:resizing", {}, true)) {
      return;
    }
    clearTimeout(p.resizeTimer);
    p.resizeTimer = setTimeout(
      function () {
        lockFrameWidths();
        p.flipper.moveTo({ page: pageNumber() });
        dispatchEvent("monocle:resize");
      },
      k.durations.RESIZE_DELAY
    );
  }


  function pageNumber(pageDiv) {
    var place = getPlace(pageDiv);
    return place ? (place.pageNumber() || 1) : 1;
  }


  function getPlace(pageDiv) {
    if (!p.initialized) {
      console.warn('Attempt to access place before initialization.');
    }
    return p.flipper.getPlace(pageDiv);
  }


  function moveTo(locus, callback) {
    if (!p.initialized) {
      console.warn('Attempt to move place before initialization.');
    }
    var fn = callback;
    if (!locus.direction) {
      dispatchEvent('monocle:jumping', { locus: locus });
      fn = function () {
        dispatchEvent('monocle:jump', { locus: locus });
        if (callback) { callback(); }
      }
    }
    p.flipper.moveTo(locus, fn);
  }


  function skipToChapter(src) {
    var locus = p.book.locusOfChapter(src);
    if (locus) {
      moveTo(locus);
      return true;
    } else {
      dispatchEvent("monocle:notfound", { href: src });
      return false;
    }
  }


  function addControl(ctrl, cType, options) {
    for (var i = 0; i < p.controls.length; ++i) {
      if (p.controls[i].control == ctrl) {
        console.warn("Already added control: " + ctrl);
        return;
      }
    }

    options = options || {};

    var ctrlData = {
      control: ctrl,
      elements: [],
      controlType: cType
    }
    p.controls.push(ctrlData);

    var ctrlElem;
    var cntr = dom.find('container'), overlay = dom.find('overlay');
    if (!cType || cType == "standard") {
      ctrlElem = ctrl.createControlElements(cntr);
      cntr.appendChild(ctrlElem);
      ctrlData.elements.push(ctrlElem);
    } else if (cType == "page") {
      for (var i = 0; i < p.flipper.pageCount; ++i) {
        var page = dom.find('page', i);
        var runner = ctrl.createControlElements(page);
        page.appendChild(runner);
        ctrlData.elements.push(runner);
      }
    } else if (cType == "modal" || cType == "popover") {
      ctrlElem = ctrl.createControlElements(overlay);
      overlay.appendChild(ctrlElem);
      ctrlData.elements.push(ctrlElem);
      ctrlData.usesOverlay = true;
    } else if (cType == "invisible") {
      if (
        typeof(ctrl.createControlElements) == "function" &&
        (ctrlElem = ctrl.createControlElements(cntr))
      ) {
        cntr.appendChild(ctrlElem);
        ctrlData.elements.push(ctrlElem);
      }
    } else {
      console.warn("Unknown control type: " + cType);
    }

    for (var i = 0; i < ctrlData.elements.length; ++i) {
      Monocle.Styles.applyRules(ctrlData.elements[i], Monocle.Styles.control);
    }

    if (options.hidden) {
      hideControl(ctrl);
    } else {
      showControl(ctrl);
    }

    if (typeof ctrl.assignToReader == 'function') {
      ctrl.assignToReader(API);
    }

    return ctrl;
  }


  function dataForControl(ctrl) {
    for (var i = 0; i < p.controls.length; ++i) {
      if (p.controls[i].control == ctrl) {
        return p.controls[i];
      }
    }
  }


  function hideControl(ctrl) {
    var controlData = dataForControl(ctrl);
    if (!controlData) {
      console.warn("No data for control: " + ctrl);
      return;
    }
    if (controlData.hidden) {
      return;
    }
    for (var i = 0; i < controlData.elements.length; ++i) {
      controlData.elements[i].style.display = "none";
    }
    if (controlData.usesOverlay) {
      var overlay = dom.find('overlay');
      overlay.style.display = "none";
      Monocle.Events.deafenForContact(overlay, overlay.listeners);
    }
    controlData.hidden = true;
    if (ctrl.properties) {
      ctrl.properties.hidden = true;
    }
    dispatchEvent('controlhide', ctrl, false);
  }


  function showControl(ctrl) {
    var controlData = dataForControl(ctrl);
    if (!controlData) {
      console.warn("No data for control: " + ctrl);
      return;
    }
    if (controlData.hidden == false) {
      return;
    }
    for (var i = 0; i < controlData.elements.length; ++i) {
      controlData.elements[i].style.display = "block";
    }
    var overlay = dom.find('overlay');
    if (controlData.usesOverlay) {
      overlay.style.display = "block";
    }
    if (controlData.controlType == "popover") {
      overlay.listeners = Monocle.Events.listenForContact(
        overlay,
        {
          start: function (evt) {
            obj = evt.target || window.event.srcElement;
            do {
              if (obj == controlData.elements[0]) { return true; }
            } while (obj && (obj = obj.parentNode));
            hideControl(ctrl);
          },
          move: function (evt) {
            evt.preventDefault();
          }
        }
      );
    }
    controlData.hidden = false;
    if (ctrl.properties) {
      ctrl.properties.hidden = false;
    }
    dispatchEvent('controlshow', ctrl, false);
  }


  function dispatchEvent(evtType, data, cancelable) {
    if (!document.createEvent) {
      return true;
    }
    var evt = document.createEvent("Events");
    evt.initEvent(evtType, false, cancelable || false);
    evt.m = data;
    try {
      return dom.find('box').dispatchEvent(evt);
    } catch(e) {
      console.warn("Failed to dispatch event: " + evtType);
      return false;
    }
  }


  function listen(evtType, fn, useCapture) {
    Monocle.Events.listen(dom.find('box'), evtType, fn, useCapture);
  }


  function deafen(evtType, fn) {
    Monocle.Events.deafen(dom.find('box'), evtType, fn);
  }


  /* PAGE STYLESHEETS */

  function addPageStyles(styleRules, restorePlace) {
    return changingStylesheet(function () {
      p.pageStylesheets.push(styleRules);
      var sheetIndex = p.pageStylesheets.length - 1;

      for (var i = 0; i < p.flipper.pageCount; ++i) {
        var doc = dom.find('component', i).contentDocument;
        addPageStylesheet(doc, sheetIndex);
      }
      return sheetIndex;
    }, restorePlace);
  }


  function updatePageStyles(sheetIndex, styleRules, restorePlace) {
    return changingStylesheet(function () {
      p.pageStylesheets[sheetIndex] = styleRules;
      if (typeof styleRules.join == "function") {
        styleRules = styleRules.join("\n");
      }
      for (var i = 0; i < p.flipper.pageCount; ++i) {
        var doc = dom.find('component', i).contentDocument;
        var styleTag = doc.getElementById('monStylesheet'+sheetIndex);
        if (!styleTag) {
          console.warn('No such stylesheet: ' + sheetIndex);
          return;
        }
        if (styleTag.styleSheet) {
          styleTag.styleSheet.cssText = styleRules;
        } else {
          styleTag.replaceChild(
            doc.createTextNode(styleRules),
            styleTag.firstChild
          );
        }
      }
    }, restorePlace);
  }


  function removePageStyles(sheetIndex, restorePlace) {
    return changingStylesheet(function () {
      p.pageStylesheets[sheetIndex] = null;
      for (var i = 0; i < p.flipper.pageCount; ++i) {
        var doc = dom.find('component', i).contentDocument;
        var styleTag = doc.getElementById('monStylesheet'+sheetIndex);
        styleTag.parentNode.removeChild(styleTag);
      }
    }, restorePlace);
  }


  function persistPageStylesOnComponentChange(evt) {
    var doc = evt.m['document'];
    doc.documentElement.id = p.systemId;
    for (var i = 0; i < p.pageStylesheets.length; ++i) {
      if (p.pageStylesheets[i]) {
        addPageStylesheet(doc, i);
      }
    }
  }


  function changingStylesheet(callback, restorePlace) {
    restorePlace = (restorePlace === false) ? false : true;
    if (restorePlace) {
      dispatchEvent("monocle:stylesheetchanging", {});
    }
    var result = callback();
    if (restorePlace) {
      p.flipper.moveTo({ page: pageNumber() });
      Monocle.defer(
        function () { dispatchEvent("monocle:stylesheetchange", {}); }
      );
    }
    return result;
  }


  function addPageStylesheet(doc, sheetIndex) {
    var styleRules = p.pageStylesheets[sheetIndex];

    if (!styleRules) {
      return;
    }

    var head = doc.getElementsByTagName('head')[0];
    if (!head) {
      if (!doc.documentElement) { return; } // FIXME: IE doesn't like docElem.
      head = doc.createElement('head');
      doc.documentElement.appendChild(head);
    }

    if (typeof styleRules.join == "function") {
      styleRules = styleRules.join("\n");
    }

    var styleTag = doc.createElement('style');
    styleTag.type = 'text/css';
    styleTag.id = "monStylesheet"+sheetIndex;
    if (styleTag.styleSheet) {
      styleTag.styleSheet.cssText = styleRules;
    } else {
      styleTag.appendChild(doc.createTextNode(styleRules));
    }

    head.appendChild(styleTag);

    return styleTag;
  }


  function visiblePages() {
    return p.flipper.visiblePages ? p.flipper.visiblePages() : [dom.find('page')];
  }


  API.getBook = getBook;
  API.getPlace = getPlace;
  API.moveTo = moveTo;
  API.skipToChapter = skipToChapter;
  API.resized = resized;
  API.addControl = addControl;
  API.hideControl = hideControl;
  API.showControl = showControl;
  API.dispatchEvent = dispatchEvent;
  API.listen = listen;
  API.deafen = deafen;
  API.addPageStyles = addPageStyles;
  API.updatePageStyles = updatePageStyles;
  API.removePageStyles = removePageStyles;
  API.visiblePages = visiblePages;

  initialize(node, bookData, options, onLoadCallback);

  return API;
}

Monocle.Reader.durations = {
  RESIZE_DELAY: 100
}
Monocle.Reader.abortMessage = {
  CLASSNAME: "monocleAbortMessage",
  TEXT: "Your browser does not support this technology."
}
Monocle.Reader.DEFAULT_SYSTEM_ID = 'RS:monocle'
Monocle.Reader.DEFAULT_CLASS_PREFIX = 'monelem_'
Monocle.Reader.FLIPPER_DEFAULT_CLASS = "Slider";
Monocle.Reader.FLIPPER_LEGACY_CLASS = "Legacy";
Monocle.Reader.DEFAULT_STYLE_RULES = [
  "html * {" +
    "text-rendering: auto !important;" +
    "word-wrap: break-word !important;" +
    (Monocle.Browser.has.floatColumnBug ? "float: none !important;" : "") +
  "}" +
  "body {" +
    "margin: 0 !important;" +
    "padding: 0 !important;" +
    "-webkit-text-size-adjust: none;" +
  "}" +
  "table, img {" +
    "max-width: 100% !important;" +
    "max-height: 90% !important;" +
  "}"
]


Monocle.pieceLoaded('reader');
/* BOOK */

/* The Book handles movement through the content by the reader page elements.
 *
 * It's responsible for instantiating components as they are required,
 * and for calculating which component and page number to move to (based on
 * requests from the Reader).
 *
 * It should set and know the place of each page element too.
 *
 */
Monocle.Book = function (dataSource) {
  if (Monocle == this) { return new Monocle.Book(dataSource); }

  var API = { constructor: Monocle.Book }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    dataSource: dataSource,
    components: [],
    chapters: {} // flat arrays of chapters per component
  }


  function initialize() {
    p.componentIds = dataSource.getComponents();
    p.contents = dataSource.getContents();
    p.lastCIndex = p.componentIds.length - 1;
  }


  function pageNumberAt(pageDiv, locus) {
    locus.load = false;
    var currComponent = pageDiv.m.activeFrame ?
      pageDiv.m.activeFrame.m.component :
      null;
    var component = null;
    var cIndex = p.componentIds.indexOf(locus.componentId);
    if (cIndex < 0 && !currComponent) {
      locus.load = true;
      locus.componentId = p.componentIds[0];
      return locus;
    } else if (cIndex < 0) {
      component = currComponent;
      locus.componentId = pageDiv.m.activeFrame.m.component.properties.id;
      cIndex = p.componentIds.indexOf(locus.componentId);
    } else if (!p.components[cIndex] || p.components[cIndex] != currComponent) {
      locus.load = true;
      return locus;
    } else {
      component = currComponent;
    }

    var result = { load: false, componentId: locus.componentId, page: 1 }

    var lastPageNum = { 'old': component.lastPageNumber() }
    var changedDims = component.updateDimensions(pageDiv);
    lastPageNum['new'] = component.lastPageNumber();

    if (typeof(locus.page) == "number") {
      result.page = locus.page;
    } else if (typeof(locus.pagesBack) == "number") {
      result.page = lastPageNum['new'] + locus.pagesBack;
    } else if (typeof(locus.percent) == "number") {
      var place = new Monocle.Place();
      place.setPlace(component, 1);
      result.page = place.pageAtPercentageThrough(locus.percent);
    } else if (typeof(locus.direction) == "number") {
      if (!pageDiv.m.place) {
        console.warn("Can't move in a direction if pageDiv has no place.");
      }
      result.page = pageDiv.m.place.pageNumber();
      result.page += locus.direction;
    } else if (typeof(locus.anchor) == "string") {
      result.page = component.pageForChapter(locus.anchor, pageDiv);
    } else if (typeof(locus.position) == "string") {
      if (locus.position == "start") {
        result.page = 1;
      } else if (locus.position == "end") {
        result.page = lastPageNum['new'];
      }
    } else {
      console.warn("Unrecognised locus: " + locus);
    }

    if (changedDims && lastPageNum['old']) {
      result.page = Math.round(
        lastPageNum['new'] * (result.page / lastPageNum['old'])
      );
    }

    if (result.page < 1) {
      if (cIndex == 0) {
        result.page = 1;
      } else {
        result.load = true;
        result.componentId = p.componentIds[cIndex - 1];
        result.pagesBack = result.page;
        result.page = null;
      }
    } else if (result.page > lastPageNum['new']) {
      if (cIndex == p.lastCIndex) {
        result.page = lastPageNum['new'];
      } else {
        result.load = true;
        result.componentId = p.componentIds[cIndex + 1];
        result.page -= lastPageNum['new'];
      }
    }

    return result;
  }


  function setPageAt(pageDiv, locus) {
    locus = pageNumberAt(pageDiv, locus);
    if (!locus.load) {
      var component = p.components[p.componentIds.indexOf(locus.componentId)];
      pageDiv.m.place = pageDiv.m.place || new Monocle.Place();
      pageDiv.m.place.setPlace(component, locus.page);

      var evtData = {
        page: pageDiv,
        locus: locus,
        pageNumber: pageDiv.m.place.pageNumber(),
        componentId: locus.componentId
      }
      pageDiv.m.reader.dispatchEvent("monocle:pagechange", evtData);
    }
    return locus;
  }


  function loadPageAt(pageDiv, locus, callback, progressCallback) {
    var cIndex = p.componentIds.indexOf(locus.componentId);
    if (!locus.load || cIndex < 0) {
      locus = pageNumberAt(pageDiv, locus);
    }

    if (!locus.load) {
      callback(locus);
      return;
    }

    var findPageNumber = function () {
      locus = setPageAt(pageDiv, locus);
      if (locus.load) {
        loadPageAt(pageDiv, locus, callback, progressCallback)
      } else {
        callback(locus);
      }
    }

    var pgFindPageNumber = function () {
      progressCallback ? progressCallback(findPageNumber) : findPageNumber();
    }

    var applyComponent = function (component) {
      component.applyTo(pageDiv, pgFindPageNumber);
    }

    var pgApplyComponent = function (component) {
      progressCallback ?
        progressCallback(function () { applyComponent(component) }) :
        applyComponent(component);
    }

    loadComponent(cIndex, pgApplyComponent, pageDiv);
  }


  function setOrLoadPageAt(pageDiv, locus, callback, progressCallback) {
    locus = setPageAt(pageDiv, locus);
    if (locus.load) {
      loadPageAt(pageDiv, locus, callback, progressCallback);
    } else {
      callback(locus);
    }
  }


  function loadComponent(index, callback, pageDiv) {
    if (p.components[index]) {
      return callback(p.components[index]);
    }
    var cmptId = p.componentIds[index];
    if (pageDiv) {
      var evtData = { 'page': pageDiv, 'component': cmptId, 'index': index };
      pageDiv.m.reader.dispatchEvent('monocle:componentloading', evtData);
    }
    var fn = function (cmptSource) {
      if (pageDiv) {
        evtData['source'] = cmptSource;
        pageDiv.m.reader.dispatchEvent('monocle:componentloaded', evtData);
        html = evtData['html'];
      }
      p.components[index] = new Monocle.Component(
        API,
        cmptId,
        index,
        chaptersForComponent(cmptId),
        cmptSource
      );
      callback(p.components[index]);
    }
    var cmptSource = p.dataSource.getComponent(cmptId, fn);
    if (cmptSource && !p.components[index]) {
      fn(cmptSource);
    }
  }


  function chaptersForComponent(cmptId) {
    if (p.chapters[cmptId]) {
      return p.chapters[cmptId];
    }
    p.chapters[cmptId] = [];
    var matcher = new RegExp('^'+cmptId+"(\#(.+)|$)");
    var matches;
    var recurser = function (chp) {
      if (matches = chp.src.match(matcher)) {
        p.chapters[cmptId].push({
          title: chp.title,
          fragment: matches[2] || null
        });
      }
      if (chp.children) {
        for (var i = 0; i < chp.children.length; ++i) {
          recurser(chp.children[i]);
        }
      }
    }

    for (var i = 0; i < p.contents.length; ++i) {
      recurser(p.contents[i]);
    }
    return p.chapters[cmptId];
  }


  function locusOfChapter(src) {
    var matcher = new RegExp('^(.+?)(#(.*))?$');
    var matches = src.match(matcher);
    if (!matches) { return null; }
    var cmptId = componentIdMatching(matches[1]);
    if (!cmptId) { return null; }
    var locus = { componentId: cmptId }
    matches[3] ? locus.anchor = matches[3] : locus.position = "start";
    return locus;
  }


  function componentIdMatching(str) {
    return p.componentIds.indexOf(str) >= 0 ? str : null;
  }


  API.getMetaData = dataSource.getMetaData;
  API.pageNumberAt = pageNumberAt;
  API.setPageAt = setPageAt;
  API.loadPageAt = loadPageAt;
  API.setOrLoadPageAt = setOrLoadPageAt;
  API.chaptersForComponent = chaptersForComponent;
  API.locusOfChapter = locusOfChapter;

  initialize();

  return API;
}


Monocle.Book.fromNodes = function (nodes) {
  var bookData = {
    getComponents: function () {
      return ['anonymous'];
    },
    getContents: function () {
      return [];
    },
    getComponent: function (n) {
      return { 'nodes': nodes };
    },
    getMetaData: function (key) {
    }
  }

  return new Monocle.Book(bookData);
}

Monocle.pieceLoaded('book');

Monocle.Place = function () {

  var API = { constructor: Monocle.Place }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    component: null,
    percent: null
  }


  function setPlace(cmpt, pageN) {
    p.component = cmpt;
    p.percent = pageN / cmpt.lastPageNumber();
    p.chapter = null;
  }


  function setPercentageThrough(cmpt, percent) {
    p.component = cmpt;
    p.percent = percent;
    p.chapter = null;
  }


  function componentId() {
    return p.component.properties.id;
  }


  function percentageThrough() {
    return p.percent;
  }


  function pageAtPercentageThrough(pc) {
    return Math.max(Math.round(p.component.lastPageNumber() * pc), 1);
  }


  function pageNumber() {
    return pageAtPercentageThrough(p.percent);
  }


  function chapterInfo() {
    if (p.chapter) {
      return p.chapter;
    }
    return p.chapter = p.component.chapterForPage(pageNumber());
  }


  function chapterTitle() {
    var chp = chapterInfo();
    return chp ? chp.title : null;
  }


  function chapterSrc() {
    var src = componentId();
    var cinfo = chapterInfo();
    if (cinfo && cinfo.fragment) {
      src += "#" + cinfo.fragment;
    }
    return src;
  }


  function getLocus(options) {
    options = options || {};
    var locus = {
      page: pageNumber(),
      componentId: componentId()
    }
    if (options.direction) {
      locus.page += options.direction;
    }
    return locus;
  }


  function percentageOfBook() {
    componentIds = p.component.properties.book.properties.componentIds;
    componentSize = 1.0 / componentIds.length;
    var pc = componentIds.indexOf(componentId()) * componentSize;
    pc += componentSize * p.percent;
    return pc;
  }


  function onFirstPageOfBook() {
    return p.component.properties.index == 0 && pageNumber() == 1;
  }


  function onLastPageOfBook() {
    return (
      p.component.properties.index ==
        p.component.properties.book.properties.lastCIndex &&
      pageNumber() == p.component.lastPageNumber()
    );
  }


  API.setPlace = setPlace;
  API.setPercentageThrough = setPercentageThrough;
  API.componentId = componentId;
  API.percentageThrough = percentageThrough;
  API.pageAtPercentageThrough = pageAtPercentageThrough;
  API.pageNumber = pageNumber;
  API.chapterInfo = chapterInfo;
  API.chapterTitle = chapterTitle;
  API.chapterSrc = chapterSrc;
  API.getLocus = getLocus;
  API.percentageOfBook = percentageOfBook;
  API.onFirstPageOfBook = onFirstPageOfBook;
  API.onLastPageOfBook = onLastPageOfBook;

  return API;
}


Monocle.Place.FromPageNumber = function (component, pageNumber) {
  var place = new Monocle.Place();
  place.setPlace(component, pageNumber);
  return place;
}

Monocle.Place.FromPercentageThrough = function (component, percent) {
  var place = new Monocle.Place();
  place.setPercentageThrough(component, percent);
  return place;
}

Monocle.pieceLoaded('place');
/* COMPONENT */

Monocle.Component = function (book, id, index, chapters, source) {

  var API = { constructor: Monocle.Component }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    book: book,

    id: id,

    index: index,

    chapters: chapters,

    source: source
  }


  function applyTo(pageDiv, callback) {
    var evtData = { 'page': pageDiv, 'source': p.source };
    pageDiv.m.reader.dispatchEvent('monocle:componentchanging', evtData);

    return loadFrame(
      pageDiv,
      function () {
        setupFrame(pageDiv, pageDiv.m.activeFrame);
        callback(pageDiv, API);
      }
    );
  }


  function loadFrame(pageDiv, callback) {
    var frame = pageDiv.m.activeFrame;

    frame.m.component = API;

    frame.style.visibility = "hidden";


    if (p.source.html || (typeof p.source == "string")) {   // HTML
      return loadFrameFromHTML(p.source.html || p.source, frame, callback);
    } else if (p.source.url) {                              // URL
      return loadFrameFromURL(p.source.url, frame, callback);
    } else if (p.source.nodes) {                            // NODES
      return loadFrameFromNodes(p.source.nodes, frame, callback);
    } else if (p.source.doc) {                              // DOCUMENT
      return loadFrameFromDocument(p.source.doc, frame, callback);
    }
  }


  function loadFrameFromHTML(src, frame, callback) {
    src = src.replace(/\s+/g, ' ');

    src = src.replace(/\'/g, '\\\'');


    if (Monocle.Browser.is.Gecko) {
      var doctypeFragment = "<!DOCTYPE[^>]*>";
      src = src.replace(new RegExp(doctypeFragment, 'm'), '');
    }

    src = "javascript: '" + src + "';";

    frame.onload = function () {
      frame.onload = null;
      Monocle.defer(callback);
    }
    frame.src = src;
  }


  function loadFrameFromURL(url, frame, callback) {
    if (!url.match(/^\//)) {
      var link = document.createElement('a');
      link.setAttribute('href', url);
      url = link.href;
      delete(link);
    }
    frame.onload = function () {
      frame.onload = null;
      Monocle.defer(callback);
    }
    frame.contentWindow.location.replace(url);
  }


  function loadFrameFromNodes(nodes, frame, callback) {
    var destDoc = frame.contentDocument;
    destDoc.documentElement.innerHTML = "";
    var destHd = destDoc.createElement("head");
    var destBdy = destDoc.createElement("body");

    for (var i = 0; i < nodes.length; ++i) {
      var node = destDoc.importNode(nodes[i], true);
      destBdy.appendChild(node);
    }

    var oldHead = destDoc.getElementsByTagName('head')[0];
    if (oldHead) {
      destDoc.documentElement.replaceChild(destHd, oldHead);
    } else {
      destDoc.documentElement.appendChild(destHd);
    }
    if (destDoc.body) {
      destDoc.documentElement.replaceChild(destBdy, destDoc.body);
    } else {
      destDoc.documentElement.appendChild(destBdy);
    }

    if (callback) { callback(); }
  }


  function loadFrameFromDocument(srcDoc, frame, callback) {
    var destDoc = frame.contentDocument;

    var srcBases = srcDoc.getElementsByTagName('base');
    if (srcBases[0]) {
      var head = destDoc.getElementsByTagName('head')[0];
      if (!head) {
        try {
          head = destDoc.createElement('head');
          if (destDoc.body) {
            destDoc.insertBefore(head, destDoc.body);
          } else {
            destDoc.appendChild(head);
          }
        } catch (e) {
          head = destDoc.body;
        }
      }
      var bases = destDoc.getElementsByTagName('base');
      var base = bases[0] ? bases[0] : destDoc.createElement('base');
      base.setAttribute('href', srcBases[0].getAttribute('href'));
      head.appendChild(base);
    }

    destDoc.replaceChild(
      destDoc.importNode(srcDoc.documentElement, true),
      destDoc.documentElement
    );


    Monocle.defer(callback);
  }


  function setupFrame(pageDiv, frame) {
    Monocle.Events.listenOnIframe(frame);

    var evtData = {
      'page': pageDiv,
      'document': frame.contentDocument,
      'component': API
    };
    pageDiv.m.reader.dispatchEvent('monocle:componentchange', evtData);

    var doc = frame.contentDocument;
    var win = doc.defaultView;
    var currStyle = win.getComputedStyle(doc.body, null);
    var lh = parseFloat(currStyle.getPropertyValue('line-height'));
    var fs = parseFloat(currStyle.getPropertyValue('font-size'));
    doc.body.style.lineHeight = lh / fs;

    p.pageLength = pageDiv.m.dimensions.measure();
    frame.style.visibility = "visible";

    locateChapters(pageDiv);
  }


  function updateDimensions(pageDiv) {
    if (pageDiv.m.dimensions.hasChanged()) {
      p.pageLength = pageDiv.m.dimensions.measure();
      return true;
    } else {
      return false;
    }
  }


  function locateChapters(pageDiv) {
    if (p.chapters[0] && typeof p.chapters[0].percent == "number") {
      return;
    }
    for (var i = 0; i < p.chapters.length; ++i) {
      var chp = p.chapters[i];
      chp.percent = 0;
      if (chp.fragment) {
        chp.percent = pageDiv.m.dimensions.percentageThroughOfId(chp.fragment);
      }
    }
    return p.chapters;
  }


  function chapterForPage(pageN) {
    var cand = null;
    var percent = (pageN - 1) / p.pageLength;
    for (var i = 0; i < p.chapters.length; ++i) {
      if (percent >= p.chapters[i].percent) {
        cand = p.chapters[i];
      } else {
        return cand;
      }
    }
    return cand;
  }


  function pageForChapter(fragment, pageDiv) {
    if (!fragment) {
      return 1;
    }
    var pc2pn = function (pc) { return Math.floor(pc * p.pageLength) + 1 }
    for (var i = 0; i < p.chapters.length; ++i) {
      if (p.chapters[i].fragment == fragment) {
        return pc2pn(p.chapters[i].percent);
      }
    }
    var percent = pageDiv.m.dimensions.percentageThroughOfId(fragment);
    return pc2pn(percent);
  }


  function lastPageNumber() {
    return p.pageLength;
  }


  API.applyTo = applyTo;
  API.updateDimensions = updateDimensions;
  API.chapterForPage = chapterForPage;
  API.pageForChapter = pageForChapter;
  API.lastPageNumber = lastPageNumber;

  return API;
}

Monocle.pieceLoaded('component');

Monocle.Dimensions = {}
Monocle.Controls = {};
Monocle.Flippers = {};
Monocle.Panels = {};

Monocle.Controls.Panel = function () {

  var API = { constructor: Monocle.Controls.Panel }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    evtCallbacks: {}
  }

  function createControlElements(cntr) {
    p.div = cntr.dom.make('div', k.CLS.panel);
    p.div.dom.setStyles(k.DEFAULT_STYLES);
    Monocle.Events.listenForContact(
      p.div,
      {
        'start': start,
        'move': move,
        'end': end,
        'cancel': cancel
      },
      { useCapture: false }
    );
    return p.div;
  }


  function listenTo(evtCallbacks) {
    p.evtCallbacks = evtCallbacks;
  }


  function deafen() {
    p.evtCallbacks = {}
  }


  function start(evt) {
    p.contact = true;
    evt.m.offsetX += p.div.offsetLeft;
    evt.m.offsetY += p.div.offsetTop;
    expand();
    invoke('start', evt);
  }


  function move(evt) {
    if (!p.contact) {
      return;
    }
    invoke('move', evt);
  }


  function end(evt) {
    if (!p.contact) {
      return;
    }
    Monocle.Events.deafenForContact(p.div, p.listeners);
    contract();
    p.contact = false;
    invoke('end', evt);
  }


  function cancel(evt) {
    if (!p.contact) {
      return;
    }
    Monocle.Events.deafenForContact(p.div, p.listeners);
    contract();
    p.contact = false;
    invoke('cancel', evt);
  }


  function invoke(evtType, evt) {
    if (p.evtCallbacks[evtType]) {
      p.evtCallbacks[evtType](API, evt.m.offsetX, evt.m.offsetY);
    }
    evt.preventDefault();
  }


  function expand() {
    if (p.expanded) {
      return;
    }
    p.div.dom.addClass(k.CLS.expanded);
    p.expanded = true;
  }


  function contract(evt) {
    if (!p.expanded) {
      return;
    }
    p.div.dom.removeClass(k.CLS.expanded);
    p.expanded = false;
  }


  API.createControlElements = createControlElements;
  API.listenTo = listenTo;
  API.deafen = deafen;
  API.expand = expand;
  API.contract = contract;

  return API;
}


Monocle.Controls.Panel.CLS = {
  panel: 'panel',
  expanded: 'controls_panel_expanded'
}
Monocle.Controls.Panel.DEFAULT_STYLES = {
  position: 'absolute',
  height: '100%'
}


Monocle.pieceLoaded('controls/panel');
Monocle.Panels.TwoPane = function (flipper, evtCallbacks) {

  var API = { constructor: Monocle.Panels.TwoPane }
  var k = API.constants = API.constructor;
  var p = API.properties = {}


  function initialize() {
    p.panels = {
      forwards: new Monocle.Controls.Panel(),
      backwards: new Monocle.Controls.Panel()
    }

    for (dir in p.panels) {
      flipper.properties.reader.addControl(p.panels[dir]);
      p.panels[dir].listenTo(evtCallbacks);
      p.panels[dir].properties.direction = flipper.constants[dir.toUpperCase()];
      var style = { "width": k.WIDTH };
      style[(dir == "forwards" ? "right" : "left")] = 0;
      p.panels[dir].properties.div.dom.setStyles(style);
    }
  }


  initialize();

  return API;
}

Monocle.Panels.TwoPane.WIDTH = "50%";

Monocle.pieceLoaded('panels/twopane');
Monocle.Dimensions.Vert = function (pageDiv) {

  var API = { constructor: Monocle.Dimensions.Vert }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    page: pageDiv,
    reader: pageDiv.m.reader
  }


  function initialize() {
    p.reader.listen('monocle:componentchange', componentChanged);
  }


  function hasChanged() {
    return getBodyHeight() != p.bodyHeight || getPageHeight != p.pageHeight;
  }


  function measure() {
    p.bodyHeight = getBodyHeight();
    p.pageHeight = getPageHeight();
    p.length = Math.ceil(p.bodyHeight / p.pageHeight);
    return p.length;
  }


  function pages() {
    return p.length;
  }


  function getBodyHeight() {
    return p.page.m.activeFrame.contentDocument.body.scrollHeight;
  }


  function getPageHeight() {
    return p.page.m.activeFrame.offsetHeight - k.GUTTER;
  }


  function percentageThroughOfId(id) {
    var doc = p.page.m.activeFrame.contentDocument;
    var target = doc.getElementById(id);
    var offset = 0;
    if (target.getBoundingClientRect) {
      offset = target.getBoundingClientRect().top;
      offset -= doc.body.getBoundingClientRect().top;
    } else {
      var oldScrollTop = doc.body.scrollTop;
      target.scrollIntoView();
      offset = doc.body.scrollTop;
      doc.body.scrollLeft = 0;
      doc.body.scrollTop = oldScrollTop;
    }

    var percent = offset / p.bodyHeight;
    return percent;
  }


  function componentChanged(evt) {
    if (evt.m['page'] != p.page) { return; }
    var sheaf = p.page.m.sheafDiv;
    var cmpt = p.page.m.activeFrame;
    sheaf.dom.setStyles(k.SHEAF_STYLES);
    cmpt.dom.setStyles(k.COMPONENT_STYLES);
    var doc = evt.m['document'];
    doc.documentElement.style.overflow = 'hidden';
    doc.body.style.marginRight = '10px !important';
    cmpt.contentWindow.scrollTo(0,0);
  }


  function locusToOffset(locus) {
    return p.pageHeight * (locus.page - 1);
  }


  API.hasChanged = hasChanged;
  API.measure = measure;
  API.pages = pages;
  API.percentageThroughOfId = percentageThroughOfId;
  API.locusToOffset = locusToOffset;

  initialize();

  return API;
}

Monocle.Dimensions.Vert.GUTTER = 10;
Monocle.Flippers.Legacy = function (reader) {

  var API = { constructor: Monocle.Flippers.Legacy }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    pageCount: 1,
    divs: {}
  }


  function initialize() {
    p.reader = reader;
  }


  function addPage(pageDiv) {
    pageDiv.m.dimensions = new Monocle.Dimensions.Vert(pageDiv);
  }


  function getPlace() {
    return page().m.place;
  }


  function moveTo(locus, callback) {
    var fn = frameToLocus;
    if (typeof callback == "function") {
      fn = function (locus) { frameToLocus(locus); callback(locus); }
    }
    p.reader.getBook().setOrLoadPageAt(page(), locus, fn);
  }


  function listenForInteraction(panelClass) {
    if (typeof panelClass != "function") {
      panelClass = k.DEFAULT_PANELS_CLASS;
      if (!panelClass) {
        console.warn("Invalid panel class.")
      }
    }
    p.panels = new panelClass(API, { 'end': turn });
  }


  function page() {
    return p.reader.dom.find('page');
  }


  function turn(panel) {
    var dir = panel.properties.direction;
    var place = getPlace();
    if (
      (dir < 0 && place.onFirstPageOfBook()) ||
      (dir > 0 && place.onLastPageOfBook())
    ) { return; }
    moveTo({ page: getPlace().pageNumber() + dir });
  }


  function frameToLocus(locus) {
    var cmpt = p.reader.dom.find('component');
    var win = cmpt.contentWindow;
    var srcY = scrollPos(win);
    var dims = page().m.dimensions;
    var pageHeight = dims.properties.pageHeight;
    var destY = dims.locusToOffset(locus);

    if (Math.abs(destY - srcY) > pageHeight) {
      return win.scrollTo(0, destY);
    }

    showIndicator(win, srcY < destY ? srcY + pageHeight : srcY);
    Monocle.defer(
      function () { smoothScroll(win, srcY, destY, 300, scrollingFinished); },
      150
    );
  }


  function scrollPos(win) {
    if (win.pageYOffset) {
      return win.pageYOffset;
    }
    if (win.document.documentElement && win.document.documentElement.scrollTop) {
      return win.document.documentElement.scrollTop;
    }
    if (win.document.body.scrollTop) {
      return win.document.body.scrollTop;
    }
    return 0;
  }


  function smoothScroll(win, currY, finalY, duration, callback) {
    clearTimeout(win.smoothScrollInterval);
    var stamp = (new Date()).getTime();
    var frameRate = 40;
    var step = (finalY - currY) * (frameRate / duration);
    var stepFn = function () {
      var destY = currY + step;
      if (
        (new Date()).getTime() - stamp > duration ||
        Math.abs(currY - finalY) < Math.abs((currY + step) - finalY)
      ) {
        clearTimeout(win.smoothScrollInterval);
        win.scrollTo(0, finalY);
        if (callback) { callback(); }
      } else {
        win.scrollTo(0, destY);
        currY = destY;
      }
    }
    win.smoothScrollInterval = setInterval(stepFn, frameRate);
  }


  function scrollingFinished() {
    hideIndicator(page().m.activeFrame.contentWindow);
    p.reader.dispatchEvent('monocle:turn');
  }


  function showIndicator(win, pos) {
    if (p.hideTO) { clearTimeout(p.hideTO); }

    var doc = win.document;
    if (!doc.body.indicator) {
      doc.body.indicator = createIndicator(doc);
      doc.body.appendChild(doc.body.indicator);
    }
    doc.body.indicator.line.style.display = "block";
    doc.body.indicator.style.opacity = 1;
    positionIndicator(pos);
  }


  function hideIndicator(win) {
    var doc = win.document;
    p.hideTO = Monocle.defer(
      function () {
        if (!doc.body.indicator) {
          doc.body.indicator = createIndicator(doc);
          doc.body.appendChild(doc.body.indicator);
        }
        var dims = page().m.dimensions;
        positionIndicator(
          dims.locusToOffset(getPlace().getLocus()) + dims.properties.pageHeight
        )
        doc.body.indicator.line.style.display = "none";
        doc.body.indicator.style.opacity = 0.5;
      },
      600
    );
  }


  function createIndicator(doc) {
    var iBox = doc.createElement('div');
    doc.body.appendChild(iBox);
    Monocle.Styles.applyRules(iBox, k.STYLES.iBox);

    iBox.arrow = doc.createElement('div');
    iBox.appendChild(iBox.arrow);
    Monocle.Styles.applyRules(iBox.arrow, k.STYLES.arrow);

    iBox.line = doc.createElement('div');
    iBox.appendChild(iBox.line);
    Monocle.Styles.applyRules(iBox.line, k.STYLES.line);

    return iBox;
  }


  function positionIndicator(y) {
    var p = page();
    var doc = p.m.activeFrame.contentDocument;
    var maxHeight = p.m.dimensions.properties.bodyHeight;
    maxHeight -= doc.body.indicator.offsetHeight;
    if (y > maxHeight) {
      y = maxHeight;
    }
    doc.body.indicator.style.top = y + "px";
  }


  API.pageCount = p.pageCount;
  API.addPage = addPage;
  API.getPlace = getPlace;
  API.moveTo = moveTo;
  API.listenForInteraction = listenForInteraction;

  initialize();

  return API;
}

Monocle.Flippers.Legacy.FORWARDS = 1;
Monocle.Flippers.Legacy.BACKWARDS = -1;
Monocle.Flippers.Legacy.DEFAULT_PANELS_CLASS = Monocle.Panels.TwoPane;

Monocle.Flippers.Legacy.STYLES = {
  iBox: {
    'position': 'absolute',
    'right': 0,
    'left': 0,
    'height': '10px'
  },
  arrow: {
    'position': 'absolute',
    'right': 0,
    'height': '10px',
    'width': '10px',
    'background': '#333',
    'border-radius': '6px'
  },
  line: {
    'width': '100%',
    'border-top': '2px dotted #333',
    'margin-top': '5px'
  }
}

Monocle.pieceLoaded('flippers/legacy');
Monocle.Dimensions.Columns = function (pageDiv) {

  var API = { constructor: Monocle.Dimensions.Columns }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    page: pageDiv,
    reader: pageDiv.m.reader,
    dirty: true
  }


  function initialize() {
    p.reader.listen('monocle:componentchange', componentChanged);
  }


  function hasChanged() {
    if (p.dirty) { return true; }
    var newMeasurements = rawMeasurements();
    return (
      (!p.measurements) ||
      (p.measurements.width != newMeasurements.width) ||
      (p.measurements.height != newMeasurements.height) ||
      (p.measurements.scrollWidth != newMeasurements.scrollWidth) ||
      (p.measurements.fontSize != newMeasurements.fontSize)
    );
  }


  function measure() {
    setColumnWidth();
    p.measurements = rawMeasurements();

    if (
      Monocle.Browser.has.iframeDoubleWidthBug &&
      p.measurements.scrollWidth == p.measurements.width * 2
    ) {
      var doc = p.page.m.activeFrame.contentDocument;
      var lc;
      for (var i = doc.body.childNodes.length - 1; i >= 0; --i) {
        lc = doc.body.childNodes[i];
        if (lc.getBoundingClientRect) { break; }
      }
      if (!lc || !lc.getBoundingClientRect) {
        console.warn('Empty document for page['+p.page.m.pageIndex+']');
        p.measurements.scrollWidth = p.measurements.width;
      } else if (lc.getBoundingClientRect().bottom > p.measurements.height) {
        p.measurements.scrollWidth = p.measurements.width * 2;
      } else {
        p.measurements.scrollWidth = p.measurements.width;
      }
    }

    p.length = Math.ceil(p.measurements.scrollWidth / p.measurements.width);
    p.dirty = false;
    return p.length;
  }


  function pages() {
    if (p.dirty) {
      console.warn('Accessing pages() when dimensions are dirty.')
      return 0;
    }
    return p.length;
  }


  function percentageThroughOfId(id) {
    var doc = p.page.m.activeFrame.contentDocument;
    var target = doc.getElementById(id);
    if (!target) {
      return 0;
    }
    var offset = 0;
    if (target.getBoundingClientRect) {
      offset = target.getBoundingClientRect().left;
      offset -= doc.body.getBoundingClientRect().left;
    } else {
      var scroller = scrollerElement();
      var oldScrollLeft = scroller.scrollLeft;
      target.scrollIntoView();
      offset = scroller.scrollLeft;
      scroller.scrollTop = 0;
      scroller.scrollLeft = oldScrollLeft;
    }

    var percent = offset / p.measurements.scrollWidth;
    return percent;
  }


  function componentChanged(evt) {
    if (evt.m['page'] != p.page) { return; }
    var doc = evt.m['document'];
    Monocle.Styles.applyRules(doc.body, k.BODY_STYLES);

    if (Monocle.Browser.is.WebKit) {
      doc.documentElement.style.overflow = 'hidden';
    }
    p.dirty = true;
  }


  function setColumnWidth() {
    var cw = p.page.m.sheafDiv.clientWidth;
    var doc = p.page.m.activeFrame.contentDocument;
    if (currBodyStyleValue('column-width') != cw+"px") {
      Monocle.Styles.affix(doc.body, 'column-width', cw+"px");
      p.dirty = true;
    }
  }


  function rawMeasurements() {
    var sheaf = p.page.m.sheafDiv;
    return {
      width: sheaf.clientWidth,
      height: sheaf.clientHeight,
      scrollWidth: scrollerWidth(),
      fontSize: currBodyStyleValue('font-size')
    }
  }


  function scrollerElement() {
    if (Monocle.Browser.has.mustScrollSheaf) {
      return p.page.m.sheafDiv;
    } else {
      return p.page.m.activeFrame.contentDocument.body;
    }
  }


  function scrollerWidth() {
    var bdy = p.page.m.activeFrame.contentDocument.body;
    if (Monocle.Browser.has.iframeDoubleWidthBug) {
      if (Monocle.Browser.on.Android) {
        return bdy.scrollWidth * 1.5; // I actually have no idea why 1.5.
      } else if (Monocle.Browser.iOSVersion < "4.1") {
        var hbw = bdy.scrollWidth / 2;
        var sew = scrollerElement().scrollWidth;
        return Math.max(sew, hbw);
      } else {
        bdy.scrollWidth; // Throw one away. Nuts.
        var hbw = bdy.scrollWidth / 2;
        return hbw;
      }
    } else if (Monocle.Browser.is.Gecko) {
      var lc = bdy.lastChild;
      while (lc && lc.nodeType != 1) {
        lc = lc.previousSibling;
      }
      if (lc && lc.getBoundingClientRect) {
        return lc.getBoundingClientRect().right;
      }
    }
    return scrollerElement().scrollWidth;
  }


  function currBodyStyleValue(property) {
    var win = p.page.m.activeFrame.contentWindow;
    var doc = win.document;
    if (!doc.body) { return null; }
    var currStyle = win.getComputedStyle(doc.body, null);
    return currStyle.getPropertyValue(property);
  }


  function locusToOffset(locus) {
    return 0 - (p.measurements.width * (locus.page - 1));
  }


  function translateToLocus(locus) {
    var offset = locusToOffset(locus);
    var bdy = p.page.m.activeFrame.contentDocument.body;
    Monocle.Styles.affix(bdy, "transform", "translateX("+offset+"px)");
    return offset;
  }


  API.hasChanged = hasChanged;
  API.measure = measure;
  API.pages = pages;
  API.percentageThroughOfId = percentageThroughOfId;

  API.locusToOffset = locusToOffset;
  API.translateToLocus = translateToLocus;

  initialize();

  return API;
}


Monocle.Dimensions.Columns.BODY_STYLES = {
  "position": "absolute",
  "height": "100%",
  "-webkit-column-gap": "0",
  "-webkit-column-fill": "auto",
  "-moz-column-gap": "0",
  "-moz-column-fill": "auto",
  "column-gap": "0",
  "column-fill": "auto"
}

if (Monocle.Browser.has.iframeDoubleWidthBug) {
  Monocle.Dimensions.Columns.BODY_STYLES["min-width"] = "200%";
} else {
  Monocle.Dimensions.Columns.BODY_STYLES["width"] = "100%";
}
Monocle.Flippers.Slider = function (reader) {
  if (Monocle.Flippers == this) {
    return new Monocle.Flippers.Slider(reader);
  }

  var API = { constructor: Monocle.Flippers.Slider }
  var k = API.constants = API.constructor;
  var p = API.properties = {
    pageCount: 2,
    activeIndex: 1,
    turnData: {}
  }


  function initialize() {
    p.reader = reader;
  }


  function addPage(pageDiv) {
    pageDiv.m.dimensions = new Monocle.Dimensions.Columns(pageDiv);
  }


  function visiblePages() {
    return [upperPage()];
  }


  function listenForInteraction(panelClass) {
    interactiveMode(true);
    interactiveMode(false);

    if (typeof panelClass != "function") {
      panelClass = k.DEFAULT_PANELS_CLASS;
      if (!panelClass) {
        console.warn("Invalid panel class.")
      }
    }
    var q = function (action, panel, x) {
      var dir = panel.properties.direction;
      if (action == "lift") {
        lift(dir, x);
      } else if (action == "release") {
        release(dir, x);
      }
    }
    p.panels = new panelClass(
      API,
      {
        'start': function (panel, x) { q('lift', panel, x); },
        'move': function (panel, x) { turning(panel.properties.direction, x); },
        'end': function (panel, x) { q('release', panel, x); },
        'cancel': function (panel, x) { q('release', panel, x); }
      }
    );
  }


  function interactiveMode(bState) {
    if (!Monocle.Browser.has.selectThruBug) {
      return;
    }
    if (p.interactive = bState) {
      if (p.activeIndex != 0) {
        var place = getPlace();
        if (place) {
          setPage(
            p.reader.dom.find('page', 0),
            place.getLocus(),
            function () {
              flipPages();
              prepareNextPage();
            }
          );
        } else {
          flipPages();
        }
      }
    }
  }


  function getPlace(pageDiv) {
    pageDiv = pageDiv || upperPage();
    return pageDiv.m ? pageDiv.m.place : null;
  }


  function moveTo(locus, callback) {
    var fn = function () {
      prepareNextPage(announceTurn);
      if (typeof callback == "function") {
        callback();
      }
    }
    setPage(upperPage(), locus, fn);
  }


  function setPage(pageDiv, locus, callback) {
    ensureWaitControl();
    p.reader.getBook().setOrLoadPageAt(
      pageDiv,
      locus,
      function (locus) {
        pageDiv.m.dimensions.translateToLocus(locus);
        if (callback) { callback(); }
      }
    );
  }


  function upperPage() {
    return p.reader.dom.find('page', p.activeIndex);
  }


  function lowerPage() {
    return p.reader.dom.find('page', (p.activeIndex + 1) % 2);
  }


  function flipPages() {
    upperPage().style.zIndex = 1;
    lowerPage().style.zIndex = 2;
    return p.activeIndex = (p.activeIndex + 1) % 2;
  }


  function lift(dir, boxPointX) {
    if (p.turnData.lifting || p.turnData.releasing) { return; }

    p.turnData.points = {
      start: boxPointX,
      min: boxPointX,
      max: boxPointX
    }
    p.turnData.lifting = true;

    if (dir == k.FORWARDS) {
      if (getPlace().onLastPageOfBook()) {
        resetTurnData();
        return;
      }
      onGoingForward(boxPointX);
    } else if (dir == k.BACKWARDS) {
      if (getPlace().onFirstPageOfBook()) {
        resetTurnData();
        return;
      }
      onGoingBackward(boxPointX);
    } else {
      console.warn("Invalid direction: " + dir);
    }
  }


  function turning(dir, boxPointX) {
    if (!p.turnData.points) { return; }
    if (p.turnData.lifting || p.turnData.releasing) { return; }
    checkPoint(boxPointX);
    slideToCursor(boxPointX, null, "0");
  }


  function release(dir, boxPointX) {
    if (!p.turnData.points) {
      return;
    }
    if (p.turnData.lifting) {
      p.turnData.releaseArgs = [dir, boxPointX];
      return;
    }
    if (p.turnData.releasing) {
      return;
    }

    checkPoint(boxPointX);

    p.turnData.releasing = true;
    showWaitControl(lowerPage());

    if (dir == k.FORWARDS) {
      if (
        p.turnData.points.tap ||
        p.turnData.points.start - boxPointX > 60 ||
        p.turnData.points.min >= boxPointX
      ) {
        slideOut(afterGoingForward);
      } else {
        slideIn(afterCancellingForward);
      }
    } else if (dir == k.BACKWARDS) {
      if (
        p.turnData.points.tap ||
        boxPointX - p.turnData.points.start > 60 ||
        p.turnData.points.max <= boxPointX
      ) {
        slideIn(afterGoingBackward);
      } else {
        slideOut(afterCancellingBackward);
      }
    } else {
      console.warn("Invalid direction: " + dir);
    }
  }


  function checkPoint(boxPointX) {
    p.turnData.points.min = Math.min(p.turnData.points.min, boxPointX);
    p.turnData.points.max = Math.max(p.turnData.points.max, boxPointX);
    p.turnData.points.tap = p.turnData.points.max - p.turnData.points.min < 10;
  }


  function onGoingForward(x) {
    lifted(x);
  }


  function onGoingBackward(x) {
    var lp = lowerPage(), up = upperPage();
    showWaitControl(up);
    jumpOut(lp, // move lower page off-screen
      function () {
        flipPages(); // flip lower to upper
        setPage( // set upper page to previous
          lp,
          getPlace(lowerPage()).getLocus({ direction: k.BACKWARDS }),
          function () {
            lifted(x);
            hideWaitControl(up);
          }
        );
      }
    );
  }


  function afterGoingForward() {
    var up = upperPage(), lp = lowerPage();
    if (p.interactive) {
      showWaitControl(up);
      showWaitControl(lp);
      setPage( // set upper (off screen) to current
        up,
        getPlace().getLocus({ direction: k.FORWARDS }),
        function () {
          jumpIn(up, function () { prepareNextPage(announceTurn); });
        }
      );
    } else {
      showWaitControl(lp);
      flipPages();
      jumpIn(up, function () { prepareNextPage(announceTurn); });
    }
  }


  function afterGoingBackward() {
    if (p.interactive) {
      setPage( // set lower page to current
        lowerPage(),
        getPlace().getLocus(),
        function () {
          flipPages(); // flip lower to upper
          prepareNextPage(announceTurn);
        }
      );
    } else {
      announceTurn();
    }
  }


  function afterCancellingForward() {
    resetTurnData();
  }


  function afterCancellingBackward() {
    flipPages(); // flip upper to lower
    jumpIn( // move lower back onto screen
      lowerPage(),
      function () { prepareNextPage(resetTurnData); }
    );
  }


  function prepareNextPage(callback) {
    setPage(
      lowerPage(),
      getPlace().getLocus({ direction: k.FORWARDS }),
      callback
    );
  }


  function lifted(x) {
    p.turnData.lifting = false;
    var releaseArgs = p.turnData.releaseArgs;
    if (releaseArgs) {
      p.turnData.releaseArgs = null;
      release(releaseArgs[0], releaseArgs[1]);
    } else if (x) {
      slideToCursor(x);
    }
  }


  function announceTurn() {
    hideWaitControl(upperPage());
    hideWaitControl(lowerPage());
    p.reader.dispatchEvent('monocle:turn');
    resetTurnData();
  }


  function resetTurnData() {
    p.turnData = {};
  }


  function setX(elem, x, options, callback) {
    var duration;

    if (!options.duration) {
      duration = 0;
    } else {
      duration = parseInt(options['duration']);
    }

    if (typeof(x) == "number") { x = x + "px"; }

    if (typeof WebKitTransitionEvent != "undefined") {
      if (duration) {
        transition = '-webkit-transform';
        transition += ' ' + duration + "ms";
        transition += ' ' + (options['timing'] || 'linear');
        transition += ' ' + (options['delay'] || 0) + 'ms';
      } else {
        transition = 'none';
      }
      elem.style.webkitTransition = transition;
      if (Monocle.Browser.has.transform3d) {
        elem.style.webkitTransform = "translate3d("+x+",0,0)";
      } else {
        elem.style.webkitTransform = "translateX("+x+")";
      }

    } else if (duration > 0) {
      clearTimeout(elem.setXTransitionInterval)

      var stamp = (new Date()).getTime();
      var frameRate = 40;
      var finalX = parseInt(x);
      var currX = getX(elem);
      var step = (finalX - currX) * (frameRate / duration);
      var stepFn = function () {
        var destX = currX + step;
        if (
          (new Date()).getTime() - stamp > duration ||
          Math.abs(currX - finalX) <= Math.abs((currX + step) - finalX)
        ) {
          clearTimeout(elem.setXTransitionInterval)
          Monocle.Styles.setX(elem, finalX);
          if (elem.setXTCB) {
            elem.setXTCB();
          }
        } else {
          Monocle.Styles.setX(elem, destX);
          currX = destX;
        }
      }

      elem.setXTransitionInterval = setInterval(stepFn, frameRate);
    } else {
      Monocle.Styles.setX(elem, x);
    }

    if (elem.setXTCB) {
      Monocle.Events.deafen(elem, 'webkitTransitionEnd', elem.setXTCB);
      elem.setXTCB = null;
    }

    elem.setXTCB = function () {
      if (callback) { callback(); }
    }

    var sX = getX(elem);
    if (!duration || sX == parseInt(x)) {
      elem.setXTCB();
    } else {
      Monocle.Events.listen(elem, 'webkitTransitionEnd', elem.setXTCB);
    }
  }


  /*
  function setX(elem, x, options, callback) {
    var duration, transition;

    if (!Monocle.Browser.has.transitions) {
      duration = 0;
    } else if (!options.duration) {
      duration = 0;
    } else {
      duration = parseInt(options['duration']);
    }

    if (typeof(x) == "number") { x = x + "px"; }

    if (duration) {
      transition = duration + "ms";
      transition += ' ' + (options['timing'] || 'linear');
      transition += ' ' + (options['delay'] || 0) + 'ms';
    } else {
      transition = "none";
    }

    if (elem.setXTCB) {
      Monocle.Events.deafen(elem, 'webkitTransitionEnd', elem.setXTCB);
      Monocle.Events.deafen(elem, 'transitionend', elem.setXTCB);
      elem.setXTCB = null;
    }

    elem.setXTCB = function () {
      if (callback) { callback(); }
    }

    elem.dom.setBetaStyle('transition', transition);
    if (Monocle.Browser.has.transform3d) {
      elem.dom.setBetaStyle('transform', 'translate3d('+x+',0,0)');
    } else {
      elem.dom.setBetaStyle('transform', 'translateX('+x+')');
    }

    if (!duration) {
      elem.setXTCB();
    } else {
      Monocle.Events.listen(elem, 'webkitTransitionEnd', elem.setXTCB);
      Monocle.Events.listen(elem, 'transitionend', elem.setXTCB);
    }
  }
  */


  function getX(elem) {
    if (typeof WebKitCSSMatrix == "object") {
      var matrix = window.getComputedStyle(elem).webkitTransform;
      matrix = new WebKitCSSMatrix(matrix);
      return matrix.m41;
    } else {
      var prop = elem.style.MozTransform;
      if (!prop || prop == "") { return 0; }
      return parseFloat((/translateX\((\-?.*)px\)/).exec(prop)[1]) || 0;
    }
  }


  function jumpIn(pageDiv, callback) {
    var dur = Monocle.Browser.has.jumpFlickerBug ? 1 : 0;
    setX(pageDiv, 0, { duration: dur }, callback);
  }


  function jumpOut(pageDiv, callback) {
    var dur = Monocle.Browser.has.jumpFlickerBug ? 1 : 0;
    setX(pageDiv, 0 - pageDiv.offsetWidth, { duration: dur }, callback);
  }



  function slideIn(callback) {
    var slideOpts = {
      duration: k.durations.SLIDE,
      timing: 'ease-in'
    };
    setX(upperPage(), 0, slideOpts, callback);
  }


  function slideOut(callback) {
    var slideOpts = {
      duration: k.durations.SLIDE,
      timing: 'ease-in'
    };
    setX(upperPage(), 0 - upperPage().offsetWidth, slideOpts, callback);
  }


  function slideToCursor(cursorX, callback, duration) {
    setX(
      upperPage(),
      Math.min(0, cursorX - upperPage().offsetWidth),
      { duration: duration || k.durations.FOLLOW_CURSOR },
      callback
    );
  }


  function ensureWaitControl() {
    if (p.waitControl) { return; }
    p.waitControl = {
      createControlElements: function (holder) {
        return holder.dom.make('div', 'flippers_slider_wait');
      }
    }
    p.reader.addControl(p.waitControl, 'page');
  }


  function showWaitControl(page) {
    var ctrl = p.reader.dom.find('flippers_slider_wait', page.m.pageIndex);
    ctrl.style.opacity = 0.5;
  }


  function hideWaitControl(page) {
    var ctrl = p.reader.dom.find('flippers_slider_wait', page.m.pageIndex);
    ctrl.style.opacity = 0;
  }

  API.pageCount = p.pageCount;
  API.addPage = addPage;
  API.getPlace = getPlace;
  API.moveTo = moveTo;
  API.listenForInteraction = listenForInteraction;

  API.visiblePages = visiblePages;
  API.interactiveMode = interactiveMode;

  initialize();

  return API;
}


Monocle.Flippers.Slider.DEFAULT_PANELS_CLASS = Monocle.Panels.TwoPane;
Monocle.Flippers.Slider.FORWARDS = 1;
Monocle.Flippers.Slider.BACKWARDS = -1;
Monocle.Flippers.Slider.durations = {
  SLIDE: 220,
  FOLLOW_CURSOR: 100
}

Monocle.pieceLoaded('flippers/slider');

Monocle.pieceLoaded('monocle');
