diff --git a/README.md b/README.md
index 1e79276a0..3a9992acd 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,7 @@ Whether you're novice or pro, the CodeCombat team is ready to help you implement
 ![Catherine Weresow](http://codecombat.com/images/pages/about/cat_small.png)
 ![Maka Gradin](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Maka%20Gradin/maka_gradin_100.png)
 ![Rob Blanckaert](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Rob%20Blanckaert/rob_blanckaert_100.png)
+![Josh Callebaut](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Josh%20Callebaut/josh_callebaut_100.png)
 ![Michael Schmatz](http://codecombat.com/images/pages/about/michael_small.png)
 ![Josh Lee](http://codecombat.com/images/pages/about/josh_small.png)
 ![Alex Cotsarelis](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Cotsarelis/alex_100.png)
diff --git a/app/assets/images/pages/about/josh_c_small.png b/app/assets/images/pages/about/josh_c_small.png
new file mode 100644
index 000000000..6a04f9e2e
Binary files /dev/null and b/app/assets/images/pages/about/josh_c_small.png differ
diff --git a/app/core/d3_utils.coffee b/app/core/d3_utils.coffee
new file mode 100644
index 000000000..843d6da81
--- /dev/null
+++ b/app/core/d3_utils.coffee
@@ -0,0 +1,127 @@
+# Caller needs require 'vendor/d3'
+
+module.exports.createContiguousDays = (timeframeDays) ->
+  # Return list of last 'timeframeDays' contiguous days in yyyy-mm-dd format
+  days = []
+  currentDate = new Date()
+  currentDate.setUTCDate(currentDate.getUTCDate() - timeframeDays)
+  for i in [0..timeframeDays]
+    currentDay = currentDate.toISOString().substr(0, 10)
+    days.push(currentDay)
+    currentDate.setUTCDate(currentDate.getUTCDate() + 1)
+  days
+
+module.exports.createLineChart = (containerSelector, chartLines) ->
+  # Creates a line chart within 'containerSelector' based on chartLines
+  return unless chartLines?.length > 0 and containerSelector
+
+  margin = 20
+  keyHeight = 20
+  xAxisHeight = 20
+  yAxisWidth = 40
+  containerWidth = $(containerSelector).width()
+  containerHeight = $(containerSelector).height()
+
+  yScaleCount = 0
+  yScaleCount++ for line in chartLines when line.showYScale
+  svg = d3.select(containerSelector).append("svg")
+    .attr("width", containerWidth)
+    .attr("height", containerHeight)
+  width = containerWidth - margin * 2 - yAxisWidth * yScaleCount
+  height = containerHeight - margin * 2 - xAxisHeight - keyHeight * chartLines.length
+  currentLine = 0
+  currentYScale = 0
+
+  # Horizontal guidelines
+  marks = (Math.round(i * height / 5) for i in [1..5])
+  yRange = d3.scale.linear().range([height, 0]).domain([0, height])
+  svg.selectAll(".line")
+    .data(marks)
+    .enter()
+    .append("line")
+    .attr("x1", margin + yAxisWidth * yScaleCount)
+    .attr("y1", (d) -> margin + yRange(d))
+    .attr("x2", margin + yAxisWidth * yScaleCount + width)
+    .attr("y2", (d) -> margin + yRange(d))
+    .attr("stroke", 'gray')
+    .style("opacity", "0.3")
+
+  for line in chartLines
+    # continue unless line.enabled
+    xRange = d3.scale.linear().range([0, width]).domain([d3.min(line.points, (d) -> d.x), d3.max(line.points, (d) -> d.x)])
+    yRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
+
+    # x-Axis
+    if currentLine is 0
+      startDay = new Date(line.points[0].day)
+      endDay = new Date(line.points[line.points.length - 1].day)
+      xAxisRange = d3.time.scale()
+        .domain([startDay, endDay])
+        .range([0, width])
+      xAxis = d3.svg.axis()
+        .scale(xAxisRange)
+      svg.append("g")
+        .attr("class", "x axis")
+        .call(xAxis)
+        .selectAll("text")
+        .attr("dy", ".35em")
+        .attr("transform", "translate(" + (margin + yAxisWidth) + "," + (height + margin) + ")")
+        .style("text-anchor", "start")
+
+    if line.showYScale
+      # y-Axis
+      yAxisRange = d3.scale.linear().range([height, 0]).domain([line.min, line.max])
+      yAxis = d3.svg.axis()
+        .scale(yRange)
+        .orient("left")
+      svg.append("g")
+        .attr("class", "y axis")
+        .attr("transform", "translate(" + (margin + yAxisWidth * currentYScale) + "," + margin + ")")
+        .style("color", line.lineColor)
+        .call(yAxis)
+        .selectAll("text")
+        .attr("y", 0)
+        .attr("x", 0)
+        .attr("fill", line.lineColor)
+        .style("text-anchor", "start")
+      currentYScale++
+
+    # Key
+    svg.append("line")
+      .attr("x1", margin)
+      .attr("y1", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
+      .attr("x2", margin + 40)
+      .attr("y2", margin + height + xAxisHeight + keyHeight * currentLine + keyHeight / 2)
+      .attr("stroke", line.lineColor)
+      .attr("class", "key-line")
+    svg.append("text")
+      .attr("x", margin + 40 + 10)
+      .attr("y", margin + height + xAxisHeight + keyHeight * currentLine + (keyHeight + 10) / 2)
+      .attr("fill", line.lineColor)
+      .attr("class", "key-text")
+      .text(line.description)
+
+    # Path and points
+    svg.selectAll(".circle")
+      .data(line.points)
+      .enter()
+      .append("circle")
+      .attr("transform", "translate(" + (margin + yAxisWidth * yScaleCount) + "," + margin + ")")
+      .attr("cx", (d) -> xRange(d.x))
+      .attr("cy", (d) -> yRange(d.y))
+      .attr("r", 2)
+      .attr("fill", line.lineColor)
+      .attr("stroke-width", 1)
+      .attr("class", "graph-point")
+      .attr("data-pointid", (d) -> "#{line.lineID}#{d.x}")
+    d3line = d3.svg.line()
+      .x((d) -> xRange(d.x))
+      .y((d) -> yRange(d.y))
+      .interpolate("linear")
+    svg.append("path")
+      .attr("d", d3line(line.points))
+      .attr("transform", "translate(" + (margin + yAxisWidth * yScaleCount) + "," + margin + ")")
+      .style("stroke-width", line.strokeWidth)
+      .style("stroke", line.lineColor)
+      .style("fill", "none")
+    currentLine++
diff --git a/app/core/utils.coffee b/app/core/utils.coffee
index 94cbb152b..782d487d7 100644
--- a/app/core/utils.coffee
+++ b/app/core/utils.coffee
@@ -248,3 +248,39 @@ module.exports.getPrepaidCodeAmount = getPrepaidCodeAmount = (price=999, users=0
   return 0 unless users > 0 and months > 0
   total = price * users * months
   total
+
+module.exports.filterMarkdownCodeLanguages = (text) ->
+  currentLanguage = me.get('aceConfig')?.language or 'python'
+  excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'io'], currentLanguage
+  exclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
+  text.replace exclusionRegex, ''
+
+module.exports.aceEditModes = aceEditModes =
+  'javascript': 'ace/mode/javascript'
+  'coffeescript': 'ace/mode/coffee'
+  'python': 'ace/mode/python'
+  'clojure': 'ace/mode/clojure'
+  'lua': 'ace/mode/lua'
+  'io': 'ace/mode/text'
+
+module.exports.initializeACE = (el, codeLanguage) ->
+  contents = $(el).text().trim()
+  editor = ace.edit el
+  editor.setOptions maxLines: Infinity
+  editor.setReadOnly true
+  editor.setTheme 'ace/theme/textmate'
+  editor.setShowPrintMargin false
+  editor.setShowFoldWidgets false
+  editor.setHighlightActiveLine false
+  editor.setHighlightActiveLine false
+  editor.setBehavioursEnabled false
+  editor.renderer.setShowGutter false
+  editor.setValue contents
+  editor.clearSelection()
+  session = editor.getSession()
+  session.setUseWorker false
+  session.setMode aceEditModes[codeLanguage]
+  session.setWrapLimitRange null
+  session.setUseWrapMode true
+  session.setNewLineMode 'unix'
+  return editor
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index 5b4674098..0a3f008b4 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -603,8 +603,8 @@
     rob_blurb: "Codes things and stuff"
     josh_c_title: "Game Designer"
     josh_c_blurb: "Designs games"
-    carlos_title: "Region Manager"
-    carlos_blurb: "CodeCombat Brazil"
+    carlos_title: "Region Manager, Brazil"
+    carlos_blurb: "Celery Man"
 
   teachers:
     more_info: "More Info for Teachers"
diff --git a/app/schemas/models/level_session.coffee b/app/schemas/models/level_session.coffee
index 66a3bddb8..e2caf8b87 100644
--- a/app/schemas/models/level_session.coffee
+++ b/app/schemas/models/level_session.coffee
@@ -79,15 +79,15 @@ _.extend LevelSessionSchema.properties,
       currentScriptOffset:
         type: 'number'
 
-    selected:
+    selected:  # Not tracked any more, delete with old level types
       type: [
         'null'
         'string'
       ]
     playing:
-      type: 'boolean'  # Not tracked any more
+      type: 'boolean'  # Not tracked any more, delete with old level types
     frame:
-      type: 'number'  # Not tracked any more
+      type: 'number'  # Not tracked any more, delete with old level types
     thangs:   # ... what is this? Is this used?
       type: 'object'
       additionalProperties:
diff --git a/app/styles/admin/analytics.sass b/app/styles/admin/analytics.sass
index 04f72ee5e..14df067d2 100644
--- a/app/styles/admin/analytics.sass
+++ b/app/styles/admin/analytics.sass
@@ -14,3 +14,15 @@
     font-size: 70pt
   .description
     font-size: 8pt
+
+  .line-chart-container
+    height: 500px
+    width: 100%
+    .x.axis
+      font-size: 9pt
+      path
+        display: none
+    .y.axis
+      font-size: 9pt
+      path
+        display: none
diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index 82ad555d4..ad2a515c5 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -72,7 +72,13 @@ $level-resize-transition-time: 0.5s
     width: 55%
     position: relative
     overflow: hidden
-    @include transition($level-resize-transition-time ease-out)
+    @include transition(all $level-resize-transition-time ease-out, z-index 1.2s linear)
+    z-index: 0
+
+    &.preview-overlay
+      z-index: 20
+      #goals-view
+        visibility: hidden
     
   canvas#webgl-surface
     background-color: #333
diff --git a/app/styles/play/level/loading.sass b/app/styles/play/level/loading.sass
index 10c62bee3..5c8d6942b 100644
--- a/app/styles/play/level/loading.sass
+++ b/app/styles/play/level/loading.sass
@@ -8,24 +8,48 @@
   background-position: top $backgroundPosition
   background-size: contain
 
+$UNVEIL_TIME: 1.2s
+
 #level-loading-view
   width: 100%
   height: 100%
   position: absolute
   z-index: 20
-  $UNVEIL_TIME: 1.2s
 
   &.unveiled
     pointer-events: none
-  
-  .loading-details
+
+  &.preview-screen
+    background-color: rgba(0, 0, 0, 0.5)
+
+  .left-wing, .right-wing
+    width: 100%
+    height: 100%
+    position: absolute
+    pointer-events: none
+
+  .left-wing
+    @include wing-background('/images/level/loading_left_wing_1920.jpg', right)
+    @media screen and ( max-width: 1366px )
+      @include wing-background('/images/level/loading_left_wing_1366.jpg', right)
+    left: -50%
+    @include transition(all $UNVEIL_TIME ease)
+
+  .right-wing
+    @include wing-background('/images/level/loading_right_wing_1920.jpg', left)
+    @media screen and ( max-width: 1366px )
+      @include wing-background('/images/level/loading_right_wing_1366.jpg', left)
+    right: -50%
+    @include transition(all $UNVEIL_TIME ease)
+
+  #loading-details
     position: absolute
     top: 86px
-    left: 50%
+    right: 50%
     $WIDTH: 450px
     width: $WIDTH
     height: 450px
-    margin-left: (-$WIDTH / 2)
+    margin-right: (-$WIDTH / 2)
     z-index: 100
     background: transparent url(/images/level/code_editor_background.png) no-repeat
     background-size: 100% 100%
@@ -34,9 +58,22 @@
     padding: 80px 80px 40px 80px
     text-align: center
     // http://matthewlein.com/ceaser/  Bounce down a bit, then snap up.
-    @include transition(top $UNVEIL_TIME cubic-bezier(0.285, -0.595, 0.670, -0.600))
+    @include transition($UNVEIL_TIME cubic-bezier(0.285, -0.595, 0.670, -0.600))
     font-family: 'Open Sans Condensed'
 
+    &.preview
+      top: 0
+      right: 0
+      margin-right: 0
+      width: 45%
+      height: auto
+      pointer-events: all
+      @include transition($UNVEIL_TIME ease-in-out)
+
+      padding: 80px 70px 40px 50px
+      .progress-or-start-container.intro-footer
+        bottom: 30px
+
     .level-loading-goals
       text-align: left
 
@@ -49,12 +86,21 @@
         font-size: 20px
         color: black
 
+    .intro-doc
+      text-align: left
+      font-size: 16px
+      overflow: scroll
+
+      img
+        max-width: 100%
+
     .progress-or-start-container
       position: absolute
       bottom: 95px
-      width: 325px
       height: 80px
       left: 48px
+      right: 77px
+      @include transition(bottom $UNVEIL_TIME ease-out)
 
       .load-progress
         width: 100%
@@ -131,21 +177,7 @@
       width: 401px
       color: #666
 
-  .left-wing, .right-wing
-    width: 100%
-    height: 100%
-    position: absolute
-
-  .left-wing
-    @include wing-background('/images/level/loading_left_wing_1920.jpg', right)
-    @media screen and ( max-width: 1366px )
-      @include wing-background('/images/level/loading_left_wing_1366.jpg', right)
-    left: -50%
-    @include transition(all $UNVEIL_TIME ease)
-
-  .right-wing
-    @include wing-background('/images/level/loading_right_wing_1920.jpg', left)
-    @media screen and ( max-width: 1366px )
-      @include wing-background('/images/level/loading_right_wing_1366.jpg', left)
-    right: -50%
-    @include transition(all $UNVEIL_TIME ease)
+    &.preview #tip-wrapper
+      left: 48px
+      right: 77px
+      width: auto
diff --git a/app/styles/play/menu/guide-view.sass b/app/styles/play/menu/guide-view.sass
index 503097adf..25ce5b57b 100644
--- a/app/styles/play/menu/guide-view.sass
+++ b/app/styles/play/menu/guide-view.sass
@@ -52,3 +52,14 @@
       border-image: url(/images/level/code_toolbar_submit_button_zazz_pressed.png) 14 20 20 20 fill round
       padding: 2px 0 0 2px
       color: white
+
+#guide-view
+  pre.ace_editor
+    padding: 2px 4px
+    border-radius: 4px
+    background-color: #f9f2f4
+    font-size: 12px
+    font-family: Monaco, Menlo, Ubuntu Mono, Consolas, "source-code-pro", monospace !important
+
+    .ace_cursor, .ace_bracket
+      display: none
diff --git a/app/templates/about.jade b/app/templates/about.jade
index 7a086a093..1f5a98f07 100644
--- a/app/templates/about.jade
+++ b/app/templates/about.jade
@@ -184,8 +184,8 @@ block content
               | Compiler Engineer
             p(data-i18n="about.rob_blurb")
               | Codes things and stuff.
-              
-          img(src="/images/pages/about/placeholder.png").img-thumbnail
+
+          img(src="/images/pages/about/josh_c_small.png").img-thumbnail
           .team_bio
             h4.team_name
               | Josh Callebaut
@@ -201,6 +201,6 @@ block content
             h4.team_name
               | Carlos Maia
             p(data-i18n="about.carlos_title")
-              | Region Manager
+              | Region Manager, Brazil
             p(data-i18n="about.carlos_blurb")
-              | CodeCombat Brazil
+              | Celery Man
diff --git a/app/templates/admin/analytics.jade b/app/templates/admin/analytics.jade
index 6c2fc8f4a..87de02c12 100644
--- a/app/templates/admin/analytics.jade
+++ b/app/templates/admin/analytics.jade
@@ -1,7 +1,9 @@
 extends /templates/base
 
 block content
-  
+
+  //- NOTE: do not localize / i18n
+
   if me.isAdmin()
     .container-fluid
       .row
@@ -18,6 +20,21 @@ block content
             div.description 30-day Active Users
             div.count= activeUsers[0].monthlyCount
 
+    h3 KPI 60 days
+    .kpi-recent-chart.line-chart-container
+
+    h3 KPI 300 days
+    .kpi-chart.line-chart-container
+
+    h3 Active Classes 90 days
+    .active-classes-chart.line-chart-container
+
+    h3 Recurring Revenue 90 days
+    .recurring-revenue-chart.line-chart-container
+
+    h3 Active Users 90 days
+    .active-users-chart.line-chart-container
+
     h1 Active Classes
     table.table.table-striped.table-condensed
       tr
diff --git a/app/templates/play/level/level_loading.jade b/app/templates/play/level/level_loading.jade
index 693cfc297..71ef46fd3 100644
--- a/app/templates/play/level/level_loading.jade
+++ b/app/templates/play/level/level_loading.jade
@@ -2,7 +2,7 @@
 
 .right-wing
 
-.loading-details.loading-container
+#loading-details.loading-container
 
   .level-loading-goals.secret
     .goals-title(data-i18n="play_level.goals") Goals
@@ -10,6 +10,8 @@
 
   .errors
 
+  .intro-doc
+
   .progress-or-start-container
     button.start-level-button.btn.btn-lg.btn-success.btn-illustrated.header-font.needsclick(data-i18n="play_level.loading_start") Start Level
 
diff --git a/app/views/admin/AnalyticsView.coffee b/app/views/admin/AnalyticsView.coffee
index 390c39673..66d88a293 100644
--- a/app/views/admin/AnalyticsView.coffee
+++ b/app/views/admin/AnalyticsView.coffee
@@ -1,3 +1,5 @@
+require 'vendor/d3'
+d3Utils = require 'core/d3_utils'
 RootView = require 'views/core/RootView'
 template = require 'templates/admin/analytics'
 utils = require 'core/utils'
@@ -5,86 +7,11 @@ utils = require 'core/utils'
 module.exports = class AnalyticsView extends RootView
   id: 'admin-analytics-view'
   template: template
+  lineColors: ['red', 'blue', 'green', 'purple', 'goldenrod', 'brown', 'darkcyan']
 
   constructor: (options) ->
     super options
-
-    @supermodel.addRequestResource('active_classes', {
-      url: '/db/analytics_perday/-/active_classes'
-      method: 'POST'
-      success: (data) =>
-        @activeClassGroups = {}
-        dayEventsMap = {}
-        for activeClass in data
-          dayEventsMap[activeClass.day] ?= {}
-          dayEventsMap[activeClass.day]['Total'] = 0
-          for event, val of activeClass.classes
-            @activeClassGroups[event] = true
-            dayEventsMap[activeClass.day][event] = val
-            dayEventsMap[activeClass.day]['Total'] += val
-        @activeClassGroups = Object.keys(@activeClassGroups)
-        @activeClassGroups.push 'Total'
-        for day of dayEventsMap
-          for event in @activeClassGroups
-            dayEventsMap[day][event] ?= 0
-        @activeClasses = []
-        for day of dayEventsMap
-          data = day: day, groups: []
-          for group in @activeClassGroups
-            data.groups.push(dayEventsMap[day][group] ? 0)
-          @activeClasses.push data
-        @activeClasses.sort (a, b) -> b.day.localeCompare(a.day)
-        @render?()
-    }, 0).load()
-
-    @supermodel.addRequestResource('active_users', {
-      url: '/db/analytics_perday/-/active_users'
-      method: 'POST'
-      success: (data) =>
-        @activeUsers = data
-        @activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
-        @render?()
-    }, 0).load()
-
-    @supermodel.addRequestResource('recurring_revenue', {
-      url: '/db/analytics_perday/-/recurring_revenue'
-      method: 'POST'
-      success: (data) =>
-        @revenueGroups = {}
-        dayGroupCountMap = {}
-        for dailyRevenue in data
-          dayGroupCountMap[dailyRevenue.day] ?= {}
-          dayGroupCountMap[dailyRevenue.day]['Daily'] = 0
-          for group, val of dailyRevenue.groups
-            @revenueGroups[group] = true
-            dayGroupCountMap[dailyRevenue.day][group] = val
-            dayGroupCountMap[dailyRevenue.day]['Daily'] += val
-        @revenueGroups = Object.keys(@revenueGroups)
-        @revenueGroups.push 'Daily'
-        @revenueGroups.push 'Monthly'
-        for day of dayGroupCountMap
-          for group in @revenueGroups
-            dayGroupCountMap[day][group] ?= 0
-        @revenue = []
-        for day of dayGroupCountMap
-          data = day: day, groups: []
-          for group in @revenueGroups
-            data.groups.push(dayGroupCountMap[day][group] ? 0)
-          @revenue.push data
-        @revenue.sort (a, b) -> b.day.localeCompare(a.day)
-        monthlyValues = []
-
-        return unless @revenue.length > 0
-
-        for i in [@revenue.length-1..0]
-          dailyTotal = @revenue[i].groups[@revenue[i].groups.length - 2]
-          monthlyValues.push(dailyTotal)
-          monthlyValues.shift() if monthlyValues.length > 30
-          if monthlyValues.length is 30
-            monthlyIndex = @revenue[i].groups.length - 1
-            @revenue[i].groups[monthlyIndex] = _.reduce(monthlyValues, (s, num) -> s + num)
-        @render?()
-    }, 0).load()
+    @loadData()
 
   getRenderData: ->
     context = super()
@@ -94,3 +21,303 @@ module.exports = class AnalyticsView extends RootView
     context.revenue = @revenue ? []
     context.revenueGroups = @revenueGroups ? {}
     context
+
+  afterRender: ->
+    super()
+    @createLineCharts()
+
+  loadData: ->
+    @supermodel.addRequestResource('active_classes', {
+      url: '/db/analytics_perday/-/active_classes'
+      method: 'POST'
+      success: (data) =>
+        # Organize data by day, then group
+        groupMap = {}
+        dayGroupMap = {}
+        for activeClass in data
+          dayGroupMap[activeClass.day] ?= {}
+          dayGroupMap[activeClass.day]['Total'] = 0
+          for group, val of activeClass.classes
+            groupMap[group] = true
+            dayGroupMap[activeClass.day][group] = val
+            dayGroupMap[activeClass.day]['Total'] += val
+        @activeClassGroups = Object.keys(groupMap)
+        @activeClassGroups.push 'Total'
+        # Build list of active classes, where each entry is a day of individual group values
+        @activeClasses = []
+        for day of dayGroupMap
+          dashedDay = "#{day.substring(0, 4)}-#{day.substring(4, 6)}-#{day.substring(6, 8)}"
+          data = day: dashedDay, groups: []
+          for group in @activeClassGroups
+            data.groups.push(dayGroupMap[day][group] ? 0)
+          @activeClasses.push data
+        @activeClasses.sort (a, b) -> b.day.localeCompare(a.day)
+
+        @updateAllKPIChartData()
+        @updateActiveClassesChartData()
+        @render?()
+    }, 0).load()
+
+    @supermodel.addRequestResource('active_users', {
+      url: '/db/analytics_perday/-/active_users'
+      method: 'POST'
+      success: (data) =>
+        @activeUsers = data.map (a) ->
+          a.day = "#{a.day.substring(0, 4)}-#{a.day.substring(4, 6)}-#{a.day.substring(6, 8)}"
+          a
+        @activeUsers.sort (a, b) -> b.day.localeCompare(a.day)
+
+        @updateAllKPIChartData()
+        @updateActiveUsersChartData()
+        @render?()
+    }, 0).load()
+
+    @supermodel.addRequestResource('recurring_revenue', {
+      url: '/db/analytics_perday/-/recurring_revenue'
+      method: 'POST'
+      success: (data) =>
+        # Organize data by day, then group
+        groupMap = {}
+        dayGroupCountMap = {}
+        for dailyRevenue in data
+          dayGroupCountMap[dailyRevenue.day] ?= {}
+          dayGroupCountMap[dailyRevenue.day]['Daily'] = 0
+          for group, val of dailyRevenue.groups
+            groupMap[group] = true
+            dayGroupCountMap[dailyRevenue.day][group] = val
+            dayGroupCountMap[dailyRevenue.day]['Daily'] += val
+        @revenueGroups = Object.keys(groupMap)
+        @revenueGroups.push 'Daily'
+        # Build list of recurring revenue entries, where each entry is a day of individual group values
+        @revenue = []
+        for day of dayGroupCountMap
+          dashedDay = "#{day.substring(0, 4)}-#{day.substring(4, 6)}-#{day.substring(6, 8)}"
+          data = day: dashedDay, groups: []
+          for group in @revenueGroups
+            data.groups.push(dayGroupCountMap[day][group] ? 0)
+          @revenue.push data
+        @revenue.sort (a, b) -> b.day.localeCompare(a.day)
+
+        return unless @revenue.length > 0
+
+        # Add monthly recurring revenue values
+        @revenueGroups.push 'Monthly'
+        monthlyValues = []
+        for i in [@revenue.length-1..0]
+          dailyTotal = @revenue[i].groups[@revenue[i].groups.length - 1]
+          monthlyValues.push(dailyTotal)
+          monthlyValues.shift() while monthlyValues.length > 30
+          if monthlyValues.length is 30
+            @revenue[i].groups.push(_.reduce(monthlyValues, (s, num) -> s + num))
+
+        @updateAllKPIChartData()
+        @updateRevenueChartData()
+        @render?()
+    }, 0).load()
+
+  createLineChartPoints: (days, data) ->
+    points = []
+    for entry, i in data
+      points.push
+        x: i
+        y: entry.value
+        day: entry.day
+
+    # Ensure points for each day
+    for day, i in days
+      if points.length <= i or points[i].day isnt day
+        prevY = if i > 0 then points[i - 1].y else 0.0
+        points.splice i, 0,
+          y: prevY
+          day: day
+      points[i].x = i
+
+    points.splice(0, points.length - days.length) if points.length > days.length
+    points
+
+  createLineCharts: ->
+    d3Utils.createLineChart('.kpi-recent-chart', @kpiRecentChartLines)
+    d3Utils.createLineChart('.kpi-chart', @kpiChartLines)
+    d3Utils.createLineChart('.active-classes-chart', @activeClassesChartLines)
+    d3Utils.createLineChart('.active-users-chart', @activeUsersChartLines)
+    d3Utils.createLineChart('.recurring-revenue-chart', @revenueChartLines)
+
+  updateAllKPIChartData: ->
+    @kpiRecentChartLines = []
+    @kpiChartLines = []
+    @updateKPIChartData(60, @kpiRecentChartLines)
+    @updateKPIChartData(300, @kpiChartLines)
+
+  updateKPIChartData: (timeframeDays, chartLines) ->
+    days = d3Utils.createContiguousDays(timeframeDays)
+
+    if @activeClasses?.length > 0
+      data = []
+      for entry in @activeClasses
+        data.push
+          day: entry.day
+          value: entry.groups[entry.groups.length - 1]
+      data.reverse()
+      points = @createLineChartPoints(days, data)
+      chartLines.push
+        points: points
+        description: '30-day Active Classes'
+        lineColor: 'blue'
+        strokeWidth: 1
+        min: 0
+        max: _.max(points, 'y').y
+        showYScale: true
+
+    if @revenue?.length > 0
+      data = []
+      for entry in @revenue
+        data.push
+          day: entry.day
+          value: entry.groups[entry.groups.length - 1] / 100000
+      data.reverse()
+      points = @createLineChartPoints(days, data)
+      chartLines.push
+        points: points
+        description: '30-day Recurring Revenue (in thousands)'
+        lineColor: 'green'
+        strokeWidth: 1
+        min: 0
+        max: _.max(points, 'y').y
+        showYScale: true
+
+    if @activeUsers?.length > 0
+      data = []
+      for entry in @activeUsers
+        break unless entry.monthlyCount
+        data.push
+          day: entry.day
+          value: entry.monthlyCount / 1000
+      data.reverse()
+      points = @createLineChartPoints(days, data)
+      chartLines.push
+        points: points
+        description: '30-day Active Users (in thousands)'
+        lineColor: 'red'
+        strokeWidth: 1
+        min: 0
+        max: _.max(points, 'y').y
+        showYScale: true
+
+  updateActiveClassesChartData: ->
+    @activeClassesChartLines = []
+    return unless @activeClasses?.length
+    days = d3Utils.createContiguousDays(90)
+
+    groupDayMap = {}
+    for entry in @activeClasses
+      for count, i in entry.groups
+        groupDayMap[@activeClassGroups[i]] ?= {}
+        groupDayMap[@activeClassGroups[i]][entry.day] ?= 0
+        groupDayMap[@activeClassGroups[i]][entry.day] += count
+
+    lines = []
+    colorIndex = 0
+    totalMax = 0
+    for group, entries of groupDayMap
+      data = []
+      for day, count of entries
+        data.push
+          day: day
+          value: count
+      data.reverse()
+      points = @createLineChartPoints(days, data)
+      @activeClassesChartLines.push
+        points: points
+        description: group.replace('Active classes ', '')
+        lineColor: @lineColors[colorIndex++ % @lineColors.length]
+        strokeWidth: 1
+        min: 0
+        showYScale: group is 'Total'
+      totalMax = _.max(points, 'y').y if group is 'Total'
+    line.max = totalMax for line in @activeClassesChartLines
+
+  updateActiveUsersChartData: ->
+    @activeUsersChartLines = []
+    return unless @activeUsers?.length
+    days = d3Utils.createContiguousDays(90)
+
+    dailyData = []
+    monthlyData = []
+    dausmausData = []
+    colorIndex = 0
+    for entry in @activeUsers
+      dailyData.push
+        day: entry.day
+        value: entry.dailyCount / 1000
+      if entry.monthlyCount
+        monthlyData.push
+          day: entry.day
+          value: entry.monthlyCount / 1000
+        dausmausData.push
+          day: entry.day
+          value: Math.round(entry.dailyCount / entry.monthlyCount * 100)
+    dailyData.reverse()
+    monthlyData.reverse()
+    dausmausData.reverse()
+    dailyPoints = @createLineChartPoints(days, dailyData)
+    monthlyPoints = @createLineChartPoints(days, monthlyData)
+    dausmausPoints = @createLineChartPoints(days, dausmausData)
+    @activeUsersChartLines.push
+      points: dailyPoints
+      description: 'Daily active users (in thousands)'
+      lineColor: @lineColors[colorIndex++ % @lineColors.length]
+      strokeWidth: 1
+      min: 0
+      max: _.max(dailyPoints, 'y').y
+      showYScale: true
+    @activeUsersChartLines.push
+      points: monthlyPoints
+      description: 'Monthly active users (in thousands)'
+      lineColor: @lineColors[colorIndex++ % @lineColors.length]
+      strokeWidth: 1
+      min: 0
+      max: _.max(monthlyPoints, 'y').y
+      showYScale: true
+    @activeUsersChartLines.push
+      points: dausmausPoints
+      description: 'DAUs/MAUs %'
+      lineColor: @lineColors[colorIndex++ % @lineColors.length]
+      strokeWidth: 1
+      min: 0
+      max: _.max(dausmausPoints, 'y').y
+      showYScale: true
+
+  updateRevenueChartData: ->
+    @revenueChartLines = []
+    return unless @revenue?.length
+    days = d3Utils.createContiguousDays(90)
+
+    groupDayMap = {}
+    for entry in @revenue
+      for count, i in entry.groups
+        groupDayMap[@revenueGroups[i]] ?= {}
+        groupDayMap[@revenueGroups[i]][entry.day] ?= 0
+        groupDayMap[@revenueGroups[i]][entry.day] += count
+
+    lines = []
+    colorIndex = 0
+    dailyMax = 0
+    for group, entries of groupDayMap
+      data = []
+      for day, count of entries
+        data.push
+          day: day
+          value: count / 100
+      data.reverse()
+      points = @createLineChartPoints(days, data)
+      @revenueChartLines.push
+        points: points
+        description: group.replace('DRR ', '')
+        lineColor: @lineColors[colorIndex++ % @lineColors.length]
+        strokeWidth: 1
+        min: 0
+        max: _.max(points, 'y').y
+        showYScale: group in ['Daily', 'Monthly']
+      dailyMax = _.max(points, 'y').y if group is 'Daily'
+      for line in @revenueChartLines when line.description isnt 'Monthly'
+        line.max = dailyMax
diff --git a/app/views/contribute/ArchmageView.coffee b/app/views/contribute/ArchmageView.coffee
index 7a428f5f5..93cdc9401 100644
--- a/app/views/contribute/ArchmageView.coffee
+++ b/app/views/contribute/ArchmageView.coffee
@@ -7,6 +7,7 @@ module.exports = class ArchmageView extends ContributeClassView
   contributorClassName: 'archmage'
 
   contributors: [
+    {id: '547acbb2af18b03c0563fdb3', name: 'David Liu', github: 'trotod'}
     {id: '52ccfc9bd3eb6b5a4100b60d', name: 'Glen De Cauwsemaecker', github: 'GlenDC'}
     {id: '52bfc3ecb7ec628868001297', name: 'Tom Steinbrecher', github: 'TomSteinbrecher'}
     {id: '5272806093680c5817033f73', name: 'Sébastien Moratinos', github: 'smoratinos'}
@@ -27,8 +28,8 @@ module.exports = class ArchmageView extends ContributeClassView
     {id: '531258b5e0789d4609614110', name: 'Ruben Vereecken', github: 'rubenvereecken'}
     {id: '5276ad5dcf83207a2801d3b4', name: 'Zach Martin', github: 'zachster01'}
     {id: '530df0cbc06854403ba67c15', name: 'Alexandru Caciulescu', github: 'Darredevil'}
-    {id: '5268d9baa39d7db617000b18', name: 'Thanish Muhammed', github: 'mnmtanish'}   
-    {id: '53232f458e54704b074b271d', name: 'Bang Honam', github: 'walkingtospace'}  
+    {id: '5268d9baa39d7db617000b18', name: 'Thanish Muhammed', github: 'mnmtanish'}
+    {id: '53232f458e54704b074b271d', name: 'Bang Honam', github: 'walkingtospace'}
     {id: '52d16c1dc931e2544d001daa', name: 'David Pendray', github: 'dpen2000'}
     {id: '53132ea1828a1706108ebb38', name: 'Dominik Kundel'}
     {id: '530eb29347a891b3518b3990', name: 'Ian Li'}
diff --git a/app/views/play/level/LevelLoadingView.coffee b/app/views/play/level/LevelLoadingView.coffee
index 3a7211d9a..fde29cd63 100644
--- a/app/views/play/level/LevelLoadingView.coffee
+++ b/app/views/play/level/LevelLoadingView.coffee
@@ -14,6 +14,7 @@ module.exports = class LevelLoadingView extends CocoView
 
   subscriptions:
     'level:loaded': 'onLevelLoaded'  # If Level loads after level loading view.
+    'level:session-loaded': 'onSessionLoaded'
     'level:subscription-required': 'onSubscriptionRequired'  # If they'd need a subscription to start playing.
     'level:course-membership-required': 'onCourseMembershipRequired'  # If they'd need a subscription to start playing.
     'subscribe-modal:subscribed': 'onSubscribed'
@@ -44,6 +45,14 @@ module.exports = class LevelLoadingView extends CocoView
 
   onLevelLoaded: (e) ->
     @level = e.level
+    @prepareGoals()
+    @prepareTip()
+    @prepareIntro()
+
+  onSessionLoaded: (e) ->
+    @session = e.session if e.session.get('creator') is me.id
+
+  prepareGoals: ->
     goalContainer = @$el.find('.level-loading-goals')
     goalList = goalContainer.find('ul')
     goalCount = 0
@@ -55,57 +64,121 @@ module.exports = class LevelLoadingView extends CocoView
       goalContainer.removeClass('secret')
       if goalCount is 1
         goalContainer.find('.panel-heading').text $.i18n.t 'play_level.goal'  # Not plural
+
+  prepareTip: ->
     tip = @$el.find('.tip')
     if @level.get('loadingTip')
       loadingTip = utils.i18n @level.attributes, 'loadingTip'
       tip.text(loadingTip)
     tip.removeClass('secret')
 
+  prepareIntro: ->
+    @docs = @level.get('documentation') ? {}
+    specific = @docs.specificArticles or []
+    @intro = _.find specific, name: 'Intro'
+
   showReady: ->
     return if @shownReady
     @shownReady = true
-    _.delay @finishShowingReady, 1500  # Let any blocking JS hog the main thread before we show that we're done.
+    _.delay @finishShowingReady, 100  # Let any blocking JS hog the main thread before we show that we're done.
 
   finishShowingReady: =>
     return if @destroyed
-    if @options.autoUnveil
+    showIntro = @getQueryVariable('intro')
+    autoUnveil = not showIntro and (@options.autoUnveil or @session?.get('state').complete)
+    if autoUnveil
       @startUnveiling()
-      @unveil()
+      @unveil true
     else
       @playSound 'level_loaded', 0.75  # old: loading_ready
       @$el.find('.progress').hide()
       @$el.find('.start-level-button').show()
+      @unveil false
 
   startUnveiling: (e) ->
     @playSound 'menu-button-click'
+    @unveiling = true
     Backbone.Mediator.publish 'level:loading-view-unveiling', {}
     _.delay @onClickStartLevel, 1000  # If they never mouse-up for the click (or a modal shows up and interrupts the click), do it anyway.
 
   onClickStartLevel: (e) =>
     return if @destroyed
-    @unveil()
+    @unveil true
 
   onEnterPressed: (e) ->
-    return unless @shownReady and not @$el.hasClass 'unveiled'
+    return unless @shownReady and not @unveiled
     @startUnveiling()
     @onClickStartLevel()
 
-  unveil: ->
-    return if @$el.hasClass 'unveiled'
-    @$el.addClass 'unveiled'
-    loadingDetails = @$el.find('.loading-details')
-    duration = parseFloat loadingDetails.css 'transition-duration'
-    loadingDetails.css 'top', -loadingDetails.outerHeight(true)
+  unveil: (full) ->
+    return if @destroyed or @unveiled
+    @unveiled = full
+    @$loadingDetails = @$el.find('#loading-details')
+    duration = parseFloat(@$loadingDetails.css 'transition-duration') * 1000
+    unless @$el.hasClass 'unveiled'
+      @$el.addClass 'unveiled'
+      @unveilWings duration
+    if full
+      @unveilLoadingFull()
+      _.delay @onUnveilEnded, duration
+    else
+      @unveilLoadingPreview duration
+
+  unveilLoadingFull: ->
+    # Get rid of the loading details screen entirely--the level is totally ready.
+    unless @unveiling
+      Backbone.Mediator.publish 'level:loading-view-unveiling', {}
+      @unveiling = true
+    if @$el.hasClass 'preview-screen'
+      @$loadingDetails.css 'right', -@$loadingDetails.outerWidth(true)
+    else
+      @$loadingDetails.css 'top', -@$loadingDetails.outerHeight(true)
+    @$el.removeClass 'preview-screen'
+    $('#canvas-wrapper').removeClass 'preview-overlay'
+
+  unveilLoadingPreview: (duration) ->
+    # Move the loading details screen over the code editor to preview the level.
+    return if @$el.hasClass 'preview-screen'
+    $('#canvas-wrapper').addClass 'preview-overlay'
+    @$el.addClass('preview-screen')
+    @$loadingDetails.addClass('preview')
+    @resize()
+    @onWindowResize = _.debounce @onWindowResize, 700  # Wait a bit for other views to resize before we resize
+    $(window).on 'resize', @onWindowResize
+    if @intro
+      @$el.find('.progress-or-start-container').addClass('intro-footer')
+      @$el.find('#tip-wrapper').remove()
+      _.delay @unveilIntro, duration
+
+  resize: ->
+    maxHeight = $('#page-container').outerHeight(true)
+    minHeight = $('#code-area').outerHeight(true)
+    @$el.css height: maxHeight
+    @$loadingDetails.css minHeight: minHeight, maxHeight: maxHeight
+    $intro = @$el.find('.intro-doc')
+    $intro.css maxHeight: minHeight - $intro.offset().top - @$el.find('.progress-or-start-container').outerHeight() - 30 - 20
+
+  unveilWings: (duration) ->
+    @playSound 'loading-view-unveil', 0.5
     @$el.find('.left-wing').css left: '-100%', backgroundPosition: 'right -400px top 0'
     @$el.find('.right-wing').css right: '-100%', backgroundPosition: 'left -400px top 0'
-    @playSound 'loading-view-unveil', 0.5
-    _.delay @onUnveilEnded, duration * 1000
-    $('#level-footer-background').detach().appendTo('#page-container').slideDown(duration * 1000)
+    $('#level-footer-background').detach().appendTo('#page-container').slideDown(duration)
+
+  unveilIntro: =>
+    return if @destroyed or not @intro or @unveiled
+    html = marked utils.filterMarkdownCodeLanguages(utils.i18n(@intro, 'body'))
+    @$el.find('.intro-doc').html html
+    @resize()
 
   onUnveilEnded: =>
     return if @destroyed
     Backbone.Mediator.publish 'level:loading-view-unveiled', view: @
 
+  onWindowResize: (e) =>
+    return if @destroyed
+    @$loadingDetails.css transition: 'none'
+    @resize()
+
   onSubscriptionRequired: (e) ->
     @$el.find('.level-loading-goals, .tip, .load-progress').hide()
     @$el.find('.subscription-required').show()
@@ -120,3 +193,7 @@ module.exports = class LevelLoadingView extends CocoView
 
   onSubscribed: ->
     document.location.reload()
+
+  destroy: ->
+    $(window).off 'resize', @onWindowResize
+    super()
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 627245cb3..78828d08d 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -155,7 +155,7 @@ module.exports = class PlayLevelView extends RootView
   afterRender: ->
     super()
     window.onPlayLevelViewLoaded? @  # still a hack
-    @insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil or @observing, level: @levelLoader?.level ? @level  # May not have @level loaded yet
+    @insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil or @observing, level: @levelLoader?.level ? @level, session: @levelLoader?.session ? @session  # May not have @level loaded yet
     @$el.find('#level-done-button').hide()
     $('body').addClass('is-playing')
     $('body').bind('touchmove', false) if @isIPadApp()
@@ -177,7 +177,6 @@ module.exports = class PlayLevelView extends RootView
     @initVolume()
     @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
 
-    @originalSessionState = $.extend(true, {}, @session.get('state'))
     @register()
     @controlBar.setBus(@bus)
     @initScriptManager()
@@ -341,14 +340,16 @@ module.exports = class PlayLevelView extends RootView
     if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal
       return Backbone.Mediator.subscribeOnce 'modal:closed', @onLevelStarted, @
     @surface.showLevel()
-    if @isEditorPreview or @observing
+    Backbone.Mediator.publish 'level:set-time', time: 0
+    if (@isEditorPreview or @observing) and not @getQueryVariable('intro')
       @loadingView.startUnveiling()
-      @loadingView.unveil()
+      @loadingView.unveil true
 
   onLoadingViewUnveiling: (e) ->
-    @restoreSessionState()
+    @selectHero()
 
   onLoadingViewUnveiled: (e) ->
+    Backbone.Mediator.publish 'level:set-playing', playing: true
     @loadingView.$el.remove()
     @removeSubView @loadingView
     @loadingView = null
@@ -372,21 +373,11 @@ module.exports = class PlayLevelView extends RootView
     @ambientSound = createjs.Sound.play src, loop: -1, volume: 0.1
     createjs.Tween.get(@ambientSound).to({volume: 1.0}, 10000)
 
-  restoreSessionState: ->
-    return if @alreadyLoadedState
-    @alreadyLoadedState = true
-    state = @originalSessionState
-    if not @level or @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder']
-      Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true
-      Backbone.Mediator.publish 'tome:select-primary-sprite', {}
-      Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false
-      @surface.focusOnHero()
-      Backbone.Mediator.publish 'level:set-time', time: 0
-      Backbone.Mediator.publish 'level:set-playing', playing: true
-    else
-      if state.selected
-        # TODO: Should also restore selected spell here by saving spellName
-        Backbone.Mediator.publish 'level:select-sprite', thangID: state.selected, spellName: null
+  selectHero: ->
+    Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: true
+    Backbone.Mediator.publish 'tome:select-primary-sprite', {}
+    Backbone.Mediator.publish 'level:suppress-selection-sounds', suppress: false
+    @surface.focusOnHero()
 
   # callbacks
 
diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee
index 27c505502..9cdc15491 100644
--- a/app/views/play/level/tome/SpellPaletteEntryView.coffee
+++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee
@@ -3,7 +3,7 @@ template = require 'templates/play/level/tome/spell_palette_entry'
 {me} = require 'core/auth'
 filters = require 'lib/image_filter'
 DocFormatter = require './DocFormatter'
-SpellView = require 'views/play/level/tome/SpellView'
+utils = require 'core/utils'
 
 module.exports = class SpellPaletteEntryView extends CocoView
   tagName: 'div'  # Could also try <code> instead of <div>, but would need to adjust colors
@@ -59,26 +59,8 @@ module.exports = class SpellPaletteEntryView extends CocoView
       @aceEditors = []
       aceEditors = @aceEditors
       popover?.$tip?.find('.docs-ace').each ->
-        contents = $(@).text()
-        editor = ace.edit @
-        editor.setOptions maxLines: Infinity
-        editor.setReadOnly true
-        editor.setTheme 'ace/theme/textmate'
-        editor.setShowPrintMargin false
-        editor.setShowFoldWidgets false
-        editor.setHighlightActiveLine false
-        editor.setHighlightActiveLine false
-        editor.setBehavioursEnabled false
-        editor.renderer.setShowGutter false
-        editor.setValue contents
-        editor.clearSelection()
-        session = editor.getSession()
-        session.setUseWorker false
-        session.setMode SpellView.editModes[codeLanguage]
-        session.setWrapLimitRange null
-        session.setUseWrapMode true
-        session.setNewLineMode 'unix'
-        aceEditors.push editor
+        aceEditor = utils.initializeACE @, codeLanguage
+        aceEditors.push aceEditor
 
   onMouseEnter: (e) ->
     # Make sure the doc has the updated Thang so it can regenerate its prop value
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 1e7214182..95be903e7 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -9,6 +9,7 @@ SpellDebugView = require './SpellDebugView'
 SpellToolbarView = require './SpellToolbarView'
 LevelComponent = require 'models/LevelComponent'
 UserCodeProblem = require 'models/UserCodeProblem'
+utils = require 'core/utils'
 
 module.exports = class SpellView extends CocoView
   id: 'spell-view'
@@ -18,14 +19,6 @@ module.exports = class SpellView extends CocoView
   eventsSuppressed: true
   writable: true
 
-  @editModes:
-    'javascript': 'ace/mode/javascript'
-    'coffeescript': 'ace/mode/coffee'
-    'python': 'ace/mode/python'
-    'clojure': 'ace/mode/clojure'
-    'lua': 'ace/mode/lua'
-    'io': 'ace/mode/text'
-
   keyBindings:
     'default': null
     'vim': 'ace/keyboard/vim'
@@ -93,7 +86,7 @@ module.exports = class SpellView extends CocoView
     @aceSession = @ace.getSession()
     @aceDoc = @aceSession.getDocument()
     @aceSession.setUseWorker false
-    @aceSession.setMode SpellView.editModes[@spell.language]
+    @aceSession.setMode utils.aceEditModes[@spell.language]
     @aceSession.setWrapLimitRange null
     @aceSession.setUseWrapMode true
     @aceSession.setNewLineMode 'unix'
@@ -479,7 +472,7 @@ module.exports = class SpellView extends CocoView
 
     # window.zatannaInstance = @zatanna  # For debugging. Make sure to not leave active when committing.
     # window.snippetEntries = snippetEntries
-    lang = SpellView.editModes[e.language].substr 'ace/mode/'.length
+    lang = utils.aceEditModes[e.language].substr 'ace/mode/'.length
     @zatanna.addSnippets snippetEntries, lang
     @editorLang = lang
 
@@ -1138,8 +1131,8 @@ module.exports = class SpellView extends CocoView
 
   onChangeLanguage: (e) ->
     return unless @spell.canWrite()
-    @aceSession.setMode SpellView.editModes[e.language]
-    @zatanna?.set 'language', SpellView.editModes[e.language].substr('ace/mode/')
+    @aceSession.setMode utils.aceEditModes[e.language]
+    @zatanna?.set 'language', utils.aceEditModes[e.language].substr('ace/mode/')
     wasDefault = @getSource() is @spell.originalSource
     @spell.setLanguage e.language
     @reloadCode true if wasDefault
diff --git a/app/views/play/menu/GuideView.coffee b/app/views/play/menu/GuideView.coffee
index 606c586b4..b1d727081 100644
--- a/app/views/play/menu/GuideView.coffee
+++ b/app/views/play/menu/GuideView.coffee
@@ -4,8 +4,6 @@ Article = require 'models/Article'
 SubscribeModal = require 'views/core/SubscribeModal'
 utils = require 'core/utils'
 
-# let's implement this once we have the docs database schema set up
-
 module.exports = class LevelGuideView extends CocoView
   template: template
   id: 'guide-view'
@@ -41,10 +39,10 @@ module.exports = class LevelGuideView extends CocoView
     @docs = specific.concat(general)
     @docs = $.extend(true, [], @docs)
     @docs = [@docs[0]] if @firstOnly and @docs[0]
-    doc.html = marked(@filterCodeLanguages(utils.i18n(doc, 'body'))) for doc in @docs
+    doc.html = marked(utils.filterMarkdownCodeLanguages(utils.i18n(doc, 'body'))) for doc in @docs
     doc.name = (utils.i18n doc, 'name') for doc in @docs
     doc.slug = _.string.slugify(doc.name) for doc in @docs
-    super()
+    super options
 
   destroy: ->
     if @vimeoListenerAttached
@@ -52,6 +50,7 @@ module.exports = class LevelGuideView extends CocoView
         window.removeEventListener('message', @onMessageReceived, false)
       else
         window.detachEvent('onmessage', @onMessageReceived, false)
+    oldEditor.destroy() for oldEditor in @aceEditors ? []
     super()
 
   getRenderData: ->
@@ -70,13 +69,17 @@ module.exports = class LevelGuideView extends CocoView
       @$el.find('.nav-tabs li:first').addClass('active')
       @$el.find('.tab-content .tab-pane:first').addClass('active')
       @$el.find('.nav-tabs a').click(@clickTab)
+    @configureACEEditors()
     @playSound 'guide-open'
 
-  filterCodeLanguages: (text) ->
-    currentLanguage = me.get('aceConfig')?.language or 'python'
-    excludedLanguages = _.without ['javascript', 'python', 'coffeescript', 'clojure', 'lua', 'io'], currentLanguage
-    exclusionRegex = new RegExp "```(#{excludedLanguages.join('|')})\n[^`]+```\n?", 'gm'
-    text.replace exclusionRegex, ''
+  configureACEEditors: ->
+    oldEditor.destroy() for oldEditor in @aceEditors ? []
+    @aceEditors = []
+    aceEditors = @aceEditors
+    codeLanguage = me.get('aceConfig')?.language or 'python'
+    @$el.find('pre').each ->
+      aceEditor = utils.initializeACE @, codeLanguage
+      aceEditors.push aceEditor
 
   clickSubscribe: (e) ->
     level = @levelSlug # Save ref to level slug
diff --git a/scripts/analytics/mongodb/queries/insertPerDayAnalytics.js b/scripts/analytics/mongodb/queries/insertPerDayAnalytics.js
index 913709a17..f154e4805 100644
--- a/scripts/analytics/mongodb/queries/insertPerDayAnalytics.js
+++ b/scripts/analytics/mongodb/queries/insertPerDayAnalytics.js
@@ -642,7 +642,13 @@ function getRecurringRevenueCounts(startDay) {
   var cursor = db.payments.find({_id: {$gte: startObj}});
   while (cursor.hasNext()) {
     var doc = cursor.next();
-    var day = doc._id.getTimestamp().toISOString().substring(0, 10);
+    var day;
+    if (doc.created) {
+      day = doc.created.substring(0, 10);
+    }
+    else {
+      day = doc._id.getTimestamp().toISOString().substring(0, 10);
+    }
 
     if (doc.service === 'ios' || doc.service === 'bitcoin') continue;