function stack() {
  var stack = {},
      size = [1280, 720],
      fontSize = 32,
      sectionHeight,
      windowHeight,
      dispatch = d3.dispatch("scroll", "activate", "deactivate"),
      touchy = "ontouchstart" in document,
      resize = touchy ? resizeTouchy : resizeNoTouchy,
      i = NaN,
      y = 0,
      yt,
      scrollRatio = 1 / 6;

  var section = d3.selectAll("section")
      .style("box-sizing", "border-box")
      .each(initialize);

  var n = section.size();

  var body = d3.select("body")
      .style("margin", 0)
      .style("padding", 0)
      .style("background", "#333");

  if (touchy) {
    section
        .style("position", "relative");

    d3.select(window)
        .on("resize.stack", resize)
        .each(resize);
  } else {
    var background = d3.select("body").insert("div", "section")
        .style("background", "#000")
        .style("box-shadow", "0 8px 16px rgba(0,0,0,.3)")
        .style("padding", "1px 0")
        .style("margin-top", "-1px")
        .style("z-index", 0);

    section
        .style("display", "none")
        .style("opacity", 0)
        .style("z-index", 0);

    var sectionAndBackground = d3.selectAll(section[0].concat(background.node()))
        .style("position", "fixed")
        .style("left", 0)
        .style("top", 0)
        .style("width", "100%");

    var indicator = d3.select("body").append("div")
        .attr("class", "indicator")
      .selectAll("div")
        .data(d3.range(section.size()))
      .enter().append("div")
        .style("position", "absolute")
        .style("z-index", 10)
        .style("left", 0)
        .style("width", "3px")
        .style("background", "linear-gradient(to top,black,white)");

    var sectionPrevious = d3.select(null),
        sectionCurrent = d3.select(section[0][0]),
        sectionNext = d3.select(section[0][1]);

    d3.select(window)
        .on("resize.stack", resize)
        .on("scroll.stack", reposition)
        .on("keydown.stack", keydown)
        .each(resize);

    d3.timer(function() {
      reposition();
      return true;
    });
  }

  function dispatchEvent(event, i) {
    var target = section[0][i], sourceEvent = event.sourceEvent = d3.event;
    try {
      d3.event = event;
      dispatch[event.type].call(target, target.__data__, i);
    } finally {
      d3.event = sourceEvent;
    }
  }

  function initialize(d, i) {
    this.__stack__ = {index: i, active: false};
  }

  function activate() {
    if (!this.__stack__.active) {
      this.__stack__.active = true;
      dispatchEvent({type: "activate"}, this.__stack__.index);
    }
  }

  function deactivate() {
    if (this.__stack__.active) {
      this.__stack__.active = false;
      dispatchEvent({type: "deactivate"}, this.__stack__.index);
    }
  }

  function resizeTouchy() {
    var marginBottom = 20;

    sectionHeight = size[1] / size[0] * innerWidth;
    windowHeight = innerHeight;

    section
        .style("height", sectionHeight + "px")
        .style("box-shadow", "0 4px 4px rgba(0,0,0,.3)")
      .filter(function(d, i) { return i < n - 1; })
        .style("margin-bottom", marginBottom + "px");

    body
        .style("font-size", innerWidth / size[0] * fontSize + "px");
  }

  function resizeNoTouchy() {
    if (sectionHeight) var y0 = y;

    sectionHeight = size[1] / size[0] * innerWidth;
    windowHeight = innerHeight;

    sectionAndBackground
        .style("top", (windowHeight - sectionHeight) / 2 + "px")
        .style("height", sectionHeight + "px");

    indicator
        .style("top", function(i) { return (i + (1 - scrollRatio) / 2) * windowHeight + "px"; })
        .style("height", windowHeight * scrollRatio + "px");

    body
        .style("font-size", innerWidth / size[0] * fontSize + "px")
        .style("height", windowHeight * n + "px");

    // Preserve the current scroll position on resize.
    if (!isNaN(y0)) scrollTo(0, (y = y0) * windowHeight);
  }

  function reposition() {
    var y1 = pageYOffset / windowHeight,
        i1 = Math.max(0, Math.min(n - 1, Math.floor(y1 + (1 + scrollRatio) / 2)));

    if (i !== i1) {
      if (i1 === i + 1) { // advance one
        sectionPrevious.interrupt().style("display", "none").style("opacity", 0).style("z-index", 0).each(deactivate);
        sectionPrevious = sectionCurrent.interrupt().style("opacity", 1).style("z-index", 1);
        sectionPrevious.transition().each("end", deactivate);
        sectionCurrent = sectionNext.interrupt().style("opacity", 0).style("z-index", 2).each(activate);
        sectionCurrent.transition().style("opacity", 1);
        sectionNext = d3.select(section[0][i1 + 1]).interrupt().style("display", "block").style("opacity", 0).style("z-index", 0);
      } else if (i1 === i - 1) { // rewind one
        sectionNext.interrupt().style("display", "none").style("opacity", 0).style("z-index", 0).each(deactivate);
        sectionNext = sectionCurrent.interrupt().style("opacity", 1).style("z-index", 1);
        sectionNext.transition().each("end", deactivate);
        sectionCurrent = sectionPrevious.interrupt().style("opacity", 0).style("z-index", 2).each(activate);
        sectionCurrent.transition().style("opacity", 1);
        sectionPrevious = d3.select(section[0][i1 - 1]).interrupt().style("display", "block").style("opacity", 0).style("z-index", 0);
      } else { // skip
        sectionPrevious.interrupt().style("display", "none").style("opacity", 0).style("z-index", 0).each(deactivate);
        sectionCurrent.interrupt().style("display", "none").style("opacity", 0).style("z-index", 0).each(deactivate);
        sectionNext.interrupt().style("display", "none").style("opacity", 0).style("z-index", 0).each(deactivate);
        sectionPrevious = d3.select(section[0][i1 - 1]).interrupt().style("display", "block").style("opacity", 0).style("z-index", 0).each(deactivate);
        sectionCurrent = d3.select(section[0][i1]).interrupt().style("display", "block").style("opacity", 1).style("z-index", 2).each(activate);
        sectionNext = d3.select(section[0][i1 + 1]).interrupt().style("display", "block").style("opacity", 0).style("z-index", 0).each(deactivate);
      }
      i = i1;
    }

    dispatchEvent({type: "scroll", offset: y = y1}, i);
  }

  function keydown() {
    var delta;
    switch (d3.event.keyCode) {
      case 39: // right arrow
      if (d3.event.metaKey) return;
      case 40: // down arrow
      case 34: // page down
      delta = d3.event.metaKey ? Infinity : 1; break;
      case 37: // left arrow
      if (d3.event.metaKey) return;
      case 38: // up arrow
      case 33: // page up
      delta = d3.event.metaKey ? -Infinity : -1; break;
      case 32: // space
      delta = d3.event.shiftKey ? -1 : 1;
      break;
      default: return;
    }

    var y0 = isNaN(yt) ? y : yt;

    yt = Math.max(0, Math.min(n - 1, (delta > 0
        ? Math.floor(y0 + (1 + scrollRatio) / 2)
        : Math.ceil(y0 - (1 - scrollRatio) / 2)) + delta));

    d3.select(document.documentElement)
        .interrupt()
      .transition()
        .duration(500)
        .tween("scroll", function() {
          var i = d3.interpolateNumber(pageYOffset, yt * windowHeight);
          return function(t) { scrollTo(0, i(t)); };
        })
        .each("end", function() { yt = NaN; });

    d3.event.preventDefault();
  }

  stack.size = function(_) {
    return arguments.length ? (size = [+_[0], +_[1]], resize(), stack) : size;
  };

  stack.scrollRatio = function(_) {
    return arguments.length ? (scrollRatio = +_, resize(), stack) : scrollRatio;
  };

  stack.fontSize = function(_) {
    return arguments.length ? (fontSize = +_, resize(), stack) : fontSize;
  };

  d3.rebind(stack, dispatch, "on");

  return stack;
}
