diff --git a/panels_ipe/css/panels_ipe-rtl.css b/panels_ipe/css/panels_ipe-rtl.css
deleted file mode 100644
index ef78afb..0000000
--- a/panels_ipe/css/panels_ipe-rtl.css
+++ /dev/null
@@ -1,67 +0,0 @@
-
-div.panels-ipe-handlebar-wrapper ul {
-  float: right;
-  text-align: left;
-}
-
-div.panels-ipe-handlebar-wrapper li {
-  margin: 0 0 0 .5em;
-  float: right;
-}
-
-div.panels-ipe-draghandle span.panels-ipe-draghandle-icon {
-  float: left;
-}
-
-div.panels-ipe-placeholder {
-  text-align: right;
-}
-
-div.panels-ipe-newblock {
-  left: 30px;
-  right: auto;
-}
-
-div.panels-ipe-handlebar-wrapper li a span,
-div.panels-ipe-newblock a span {
-  text-align: right;
-}
-
-div.panels-ipe-newblock a.style {
-  margin-left: .5em;
-  margin-right: auto;
-}
-
-.panels-ipe-editing .panels-ipe-region {
-  float: right;
-}
-
-/** ============================================================================
- * Controller form markup
- */
-
-.ipe-throbber {
-  right: 49%;
-  right: auto;
-}
-
-div.panels-ipe-control .form-submit {
-  padding: 0 34px 2px 0.8em;
-}
-
-input#panels-ipe-save,
-input#panels-ipe-cancel {
-  background-position: 86% 0;
-}
-
-div.panels-ipe-pseudobutton-container a.panels-ipe-startedit {
-  padding-right: 34px;
-  padding-left: 10px;
-  background-position: 93% 9px;
-}
-
-div.panels-ipe-pseudobutton-container a.panels-ipe-change-layout {
-  padding-right: 34px;
-  padding-left: 10px;
-  background-position: 93% 9px;
-}
diff --git a/panels_ipe/css/panels_ipe.css b/panels_ipe/css/panels_ipe.css
index ec372c6..0f963ef 100644
--- a/panels_ipe/css/panels_ipe.css
+++ b/panels_ipe/css/panels_ipe.css
@@ -1,559 +1,221 @@
-body.panels-ipe {
-  margin-bottom: 60px !important;
-}
+/**
+ * @file
+ * Contains all CSS for the Panels In-Place Editor.
+ */
 
-/* Hide the IPE toolbar on print output. */
-@media print {
-  #panels-ipe-control-container {
-    display: none !important;
-  }
-  body.panels-ipe {
-    margin-top: 0 !important;
-  }
+/* Define our icon font, which is generated from the SVGs in /images. */
+@font-face {
+  font-family: PanelsIPEIcon;
+  src: url(../fonts/ipeicons.woff);
 }
 
-/* Hide the control container when the overlay is open. */
-html.overlay-open #panels-ipe-control-container {
-  display: none !important;
-}
-html.overlay-open body.panels-ipe {
-  margin-top: 0 !important;
+.ipe-icon {
+  display:inline-block;
+  vertical-align: middle;
+  font-family: PanelsIPEIcon;
+  font-size: 24px;
 }
 
-div.panels-ipe-handlebar-wrapper {
-  border-bottom: 1px solid #CCC;
+.ipe-icon.ipe-icon-warning:before {
+  content: "\e902";
 }
 
-.panels-ipe-editing div.panels-ipe-portlet-wrapper {
-  margin-top: 1em;
-  border: 1px solid #CCC;
-  width: 100%;
+.ipe-icon.ipe-icon-change_layout:before {
+  content: "\e903";
 }
 
-/* Hide empty panes when not editing them. */
-.panels-ipe-empty-pane {
-  display: none;
+.ipe-icon.ipe-icon-manage_content:before {
+  content: "\e905";
 }
 
-.panels-ipe-editing .panels-ipe-empty-pane {
-  display: block;
-}
-
-.panels-ipe-editing div.panels-ipe-portlet-wrapper:hover {
-  border: 1px dashed #CCC;
+.ipe-icon.ipe-icon-edit:before {
+  content: "\e904";
 }
 
-.panels-ipe-editing .panels-ipe-sort-container {
-  min-height: 40px;
+.ipe-icon.ipe-icon-save:before {
+  content: "\e906";
 }
 
-.panels-ipe-editing .panels-ipe-sort-container .ui-sortable-helper {
-  background: white;
+.ipe-icon.ipe-icon-loading:before {
+  content: "\e907";
+  animation: spin 1s infinite linear;
 }
 
-.panels-ipe-editing div.panel-pane div.admin-links {
-  display: none !important;
+@keyframes spin {
+  from {transform:rotate(360deg);}
+  to {transform:rotate(0deg);}
 }
 
-.panels-ipe-editing .panels-ipe-sort-container .ui-sortable-placeholder {
-  border: 2px dashed #999;
-  background-color: #FFFF99;
-  margin: 1em 0;
-  -moz-border-radius: 0;
-	-khtml-border-radius: 0;
-	-webkit-border-radius: 0;
-	border-radius: 0;
+/* Fix the output of the AppView to the bottom of the screen. */
+#panels-ipe-tray {
+  position: fixed;
+  width: 100%;
+  bottom: 0;
+  left: 0;
+  text-align: center;
 }
 
-div.panels-ipe-handlebar-wrapper ul {
-  float: left;
+/* Force text to render as a sans-serif web-safe font. */
+#panels-ipe-tray, #panels-ipe-tray a, #panels-ipe-tray p {
+  font-family: Arial, Helvetica, sans-serif;
   margin: 0;
-  padding: 0;
-  text-align: right;
 }
 
-div.panels-ipe-handlebar-wrapper li {
-  background: none;
-  list-style-type: none;
-  list-style-image: none;
-  margin: 0 .5em 0 0;
+/* Remove list styling from the output of the TabsView. */
+.ipe-tabs {
+  list-style: none;
+  margin: 0;
   padding: 0;
-  float: left;
 }
 
-div.panels-ipe-draghandle,
-div.panels-ipe-nodraghandle {
-  background: #E9E9E9;
-  background-image: linear-gradient(bottom, #D5D5D5 0%, #FCFCFC 100%);
-  background-image: -o-linear-gradient(bottom, #D5D5D5 0%, #FCFCFC 100%);
-  background-image: -moz-linear-gradient(bottom, #D5D5D5 0%, #FCFCFC 100%);
-  background-image: -webkit-linear-gradient(bottom, #D5D5D5 0%, #FCFCFC 100%);
-  background-image: -ms-linear-gradient(bottom, #D5D5D5 0%, #FCFCFC 100%);
-  background-image: -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #D5D5D5),
-    color-stop(1, #FCFCFC)
-  );
-
-  padding: 8px 7px;
+/* Display tabs inline and slightly on top of .ipe-tabs-content. */
+.ipe-tab {
+  display: inline-block;
+  vertical-align: bottom;
+  padding: 10px 5px 10px 5px;
+  background-color: white;
+  border-top: 1px solid darkgray;
+  margin-bottom: -1px;
 }
 
-div.panels-ipe-draghandle span.panels-ipe-draghandle-icon {
-  display: block;
-  float: right;
-  cursor: move;
-  width: 34px;
-  height: 34px;
-  padding: 0 !important; /* override button defaults */
+.ipe-tab:first-child {
+  border-left: 1px solid darkgray;
+  border-top-left-radius: 5px;
 }
 
-span.panels-ipe-draghandle-icon-inner {
-  display: block;
-  width: 34px;
-  height: 34px;
-  background: url(../images/icon-draggable.png) no-repeat 3px 3px;
-}
-
-div.panels-ipe-placeholder {
-  border: 2px dashed #999;
-  padding: .5em;
-  position: relative;
-  margin-top: .5em;
-  background-color: #ECFAFF;
-  color: #999;
-  font: 15px/1.3em "Open Sans", "Lucida Grande", Tahoma, Verdana, sans-serif;
-  text-transform: none;
-  letter-spacing: 0;
-  text-align: left;
-  word-spacing: 0;
-}
-
-div.panels-ipe-placeholder h3 {
-  font-weight: normal;
-  font-size: 15px;
-  width: 75px; /* In order to prevent the region title from running into the button, set a width. Initital width only--this will be changed by jQuery */
-  margin: 1.154em 0;
-}
-
-/* Hide editor-state-on elements initially */
-.panels-ipe-on {
-  display: none;
+.ipe-tab:last-child {
+  border-right: 1px solid darkgray;
+  border-top-right-radius: 5px;
 }
 
-.panels-ipe-editing .panels-ipe-on {
-  display: block;
+.ipe-tab.active a {
+  color: darkgray;
 }
 
-/* Show editor-state-off elements initially */
-.panels-ipe-off {
+.ipe-tab a {
+  color: black;
+  height: 30px;
   display: block;
+  text-transform: uppercase;
+  vertical-align: top;
+  border: none;
+  cursor: pointer;
+  transition: .2s;
 }
 
-div.panels-ipe-newblock {
-  -webkit-box-shadow: 0px 0 5px 5px #ECFAFF;
-  -moz-box-shadow: 0px 0 5px 5px #ECFAFF;
-  box-shadow: 0px 0 5px 5px #ECFAFF;
-  position: absolute;
-  right: 10px;
-  top: 50%;
-  margin-top: -18px; /* some initial guesses to help center the add button
-    panels_ipe.js will evaluate the width and get this pixel-perfect */
-  margin-left: -30px;
-  z-index: 99;
+.ipe-tab a:hover {
+  color: darkgray;
+  border: none;
 }
 
-div.panels-ipe-newblock li {
-  padding: 0;
+/* Provide default styles and a minimum height for tab content. */
+.ipe-tab-content {
+  display: none;
+  min-height: 100px;
+  padding: 5px 5px 10px 5px;
+  background-color: white;
+  border-top: 1px solid darkgray;
 }
 
-div.panels-ipe-handlebar-wrapper li a,
-div.panels-ipe-dragtitle span,
-div.panels-ipe-newblock a,
-span.panels-ipe-draghandle-icon {
-  display: inline-block;
-  border: 1px solid #ccc;
-	padding: 0 8px 1px;
-	font: bold 12px/32px 'Open Sans', 'Lucida Sans', 'Lucida Grande', verdana sans-serif;
-	text-decoration: none;
-	height: 33px;
-	color: #666;
-	cursor: pointer;
-	outline: none;
-	-moz-border-radius: 3px;
-	-khtml-border-radius: 3px;
-	-webkit-border-radius: 3px;
-	border-radius: 3px;
-
-	background: #FAFAFA;
-  background-image: linear-gradient(bottom, #E9EAEC 0%, #FAFAFA 100%);
-  background-image: -o-linear-gradient(bottom, #E9EAEC 0%, #FAFAFA 100%);
-  background-image: -moz-linear-gradient(bottom, #E9EAEC 0%, #FAFAFA 100%);
-  background-image: -webkit-linear-gradient(bottom, #E9EAEC 0%, #FAFAFA 100%);
-  background-image: -ms-linear-gradient(bottom, #E9EAEC 0%, #FAFAFA 100%);
-  background-image: -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #E9EAEC),
-    color-stop(1, #FAFAFA)
-  );
-
-  -webkit-box-shadow: 0px 3px 3px 0px #d2d2d2;
-  -moz-box-shadow: 0px 3px 3px 0px #d2d2d2;
-  box-shadow: 0px 3px 3px 0px #d2d2d2;
-}
-
-div.panels-ipe-handlebar-wrapper li a span,
-div.panels-ipe-newblock a span {
+.ipe-tab-content.active {
   display: block;
-  height: 32px;
-  width: 18px;
-  background-position: center center;
-  background-repeat: no-repeat;
-  text-align: left;
-  text-indent: -9999em;
 }
 
-div.panels-ipe-handlebar-wrapper li.edit a span {
-  background-image: url(../images/icon-settings.png);
+/* Don't show text for these tabs */
+[data-tab-id="save"], [data-tab-id="edit"] {
+  overflow-x: hidden;
+  width: 24px;
 }
 
-div.panels-ipe-handlebar-wrapper li.style a span,
-div.panels-ipe-newblock a.style span {
-  background-image: url(../images/icon-style.png);
+/* Styles for the Layout selector. */
+.ipe-current-layout, .ipe-all-layouts {
+  display: inline-block;
 }
 
-div.panels-ipe-newblock a.style {
-  margin-right: .5em;
+.ipe-current-layout {
+  vertical-align: top;
+  padding-right: 10px;
 }
 
-div.panels-ipe-newblock a.add span {
-  background-image: url(../images/icon-add.png);
+/* Remove <ul> list styling and make list scrollable. */
+.ipe-layouts {
+  vertical-align: top;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  max-width: 600px;
+  white-space: nowrap;
+  overflow-x: auto;
 }
 
-div.panels-ipe-handlebar-wrapper li.delete a span {
-  background-image: url(../images/icon-delete.png);
+/* Show layouts as clickable things. */
+.ipe-layout {
+  cursor: pointer;
+  display: inline-block;
+  margin-right: 5px;
 }
 
-
-div.panels-ipe-handlebar-wrapper li a:hover,
-div.panels-ipe-dragtitle span:hover,
-div.panels-ipe-newblock a:hover,
-span.panels-ipe-draghandle-icon:hover {
-  background: #E6E6E6;
-  background-image: linear-gradient(bottom, #C5C5C5 0%, #FAFAFA 100%);
-  background-image: -o-linear-gradient(bottom, #C5C5C5 0%, #FAFAFA 100%);
-  background-image: -moz-linear-gradient(bottom, #C5C5C5 0%, #FAFAFA 100%);
-  background-image: -webkit-linear-gradient(bottom, #C5C5C5 0%, #FAFAFA 100%);
-  background-image: -ms-linear-gradient(bottom, #C5C5C5 0%, #FAFAFA 100%);
-  background-image: -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #C5C5C5),
-    color-stop(1, #FAFAFA)
-  );
+.ipe-layout-title {
+  font-weight: bold;
+  text-transform: uppercase;
 }
 
-div.panels-ipe-handlebar-wrapper li a:active,
-div.panels-ipe-dragtitle span:active,
-div.panels-ipe-newblock a:active,
-span.panels-ipe-draghandle-icon:active {
-  outline: none;
-  background-image: linear-gradient(bottom, #FFFFFF 0%, #E9EAEC 100%);
-  background-image: -o-linear-gradient(bottom, #FFFFFF 0%, #E9EAEC 100%);
-  background-image: -moz-linear-gradient(bottom, #FFFFFF 0%, #E9EAEC 100%);
-  background-image: -webkit-linear-gradient(bottom, #FFFFFF 0%, #E9EAEC 100%);
-  background-image: -ms-linear-gradient(bottom, #FFFFFF 0%, #E9EAEC 100%);
-  background-image: -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #FFFFFF),
-    color-stop(1, #E9EAEC)
-  );
+.ipe-layout-title a {
 
-  -webkit-box-shadow: 0px 0px 0px 0px #fff;
-  -moz-box-shadow: 0px 0px 0px 0px #fff;
-  box-shadow: 0px 0px 0px 0px #fff;
 }
 
-.panels-ipe-editing .panels-ipe-portlet-content {
-  margin: 10px 3px;
-  overflow: hidden;
-}
-
-.panels-ipe-editing .panels-ipe-region {
-  border: transparent dotted 1px;
-  float: left;
-  width: 100%;
-  margin-bottom: 5px;
+/* Hide the tray for the edit and save tabs. */
+[data-tab-content-id="edit"].active, [data-tab-content-id="save"].active {
+  display: none;
 }
 
-div.panels-ipe-draghandle {
-  border: none;
+/* Style the block/region headers. */
+div.ipe-actions {
+  display: block;
+  height: 20px;
+  background-color: rgb(222, 222, 222);
+  padding: 5px;
+  margin-top: 1px;
+  clear: both;
 }
 
-.ui-sortable-placeholder {
-  margin: 1em;
-  border: 1px dotted black;
-  visibility: visible !important;
-  height: 50px !important;
-}
-.ui-sortable-placeholder * {
-  visibility: hidden;
+.ipe-actions ul.ipe-action-list {
+  float: right;
+  list-style: none;
+  margin: 0;
+  padding: 0;
 }
 
-/** ============================================================================
- * Controller form markup
- */
-
-div#panels-ipe-control-container {
-  z-index: 99999;
-  position: fixed;
-  bottom: 0;
-  display: none;
-  background-color: #000;
-  padding: 0.5em 0;
-  width: 100%;
-  overflow: hidden;
-  -moz-box-shadow: 0 3px 20px #000;
-  -webkit-box-shadow: 0 3px 20px #000;
-  box-shadow: 0 3px 20px #000;
-}
-
-.ipe-throbber {
-  background-color: #232323;
-  background-image: url("../images/loading-small.gif");
-  background-position: center center;
-  background-repeat: no-repeat;
-  -moz-border-radius: 7px;
-  -webkit-border-radius: 7px;
-  border-radius: 7px;
-  height: 24px;
-  opacity: .9;
-  padding: 4px;
-  width: 24px;
-  /* Can't do center:50% middle: 50%, so approximate it for a typical window size. */
-  left: 49%;
-  position: fixed;
-  top: 48.5%;
-  z-index: 1001;
+.ipe-actions h5, .ipe-actions li {
+  font-family: Arial, Helvetica, sans-serif;
+  font-size: 12px;
+  text-transform: uppercase;
+  font-weight: bold;
+  margin: 0;
 }
 
-/* Hide the drupal system throbber image */
-.ipe-throbber .throbber {
-  display: none;
+.ipe-actions h5 {
+  float: left;
 }
 
-div.panels-ipe-pseudobutton-container,
-div.panels-ipe-control .form-submit {
+.ipe-actions a {
+  color: black;
+  text-transform: uppercase;
+  border: none;
   cursor: pointer;
-  background: #666666;
-  background-image: linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: -o-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: -moz-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: -webkit-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: -ms-linear-gradient(bottom, #383838 0%, #666666 100%);
-
-  background-image: -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #383838),
-    color-stop(1, #666666)
-  );
-  border: 0;
-  -moz-border-radius: 3px;
-  -webkit-border-radius: 3px;
-  border-radius: 3px;
-  color: #CCC;
-  display: inline-block;
-  font: bold 12px/33px "Open Sans", "Lucida Grande", Tahoma, Verdana, sans-serif;
-  height: 33px;
-  margin: 0 10px;
 }
 
-div.panels-ipe-control .form-submit {
-  padding: 0 0.8em 2px 34px;
-}
-
-div.panels-ipe-control input.panels-ipe-save, div.panels-ipe-control input.panels-ipe-cancel,
-div.panels-ipe-control input.panels-ipe-save:hover, div.panels-ipe-control input.panels-ipe-cancel:hover,
-div.panels-ipe-control input.panels-ipe-save:active, div.panels-ipe-control input.panels-ipe-cancel:active {
-  background-repeat: no-repeat;
+.ipe-actions a:hover {
+  color: #393939;
+  border: none;
 }
 
-div.panels-ipe-pseudobutton-container a {
-  height: 33px;
-  padding: 0 0.8em;
+.ipe-action-list li {
   display: inline-block;
-  color: #CCC;
-  text-decoration: none;
-}
-
-div.panels-ipe-control input.panels-ipe-save {
-  background-image: url(../images/icon-save.png);
-  background-image: url(../images/icon-save.png), linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: url(../images/icon-save.png), -o-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: url(../images/icon-save.png), -moz-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: url(../images/icon-save.png), -webkit-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: url(../images/icon-save.png), -ms-linear-gradient(bottom, #383838 0%, #666666 100%);
-
-  background-image: url(../images/icon-save.png), -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #383838),
-    color-stop(1, #666666)
-  );
-}
-
-div.panels-ipe-control input.panels-ipe-cancel {
-  background-image: url(../images/icon-close.png);
-  background-image: url(../images/icon-close.png), linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: url(../images/icon-close.png), -o-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: url(../images/icon-close.png), -moz-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: url(../images/icon-close.png), -webkit-linear-gradient(bottom, #383838 0%, #666666 100%);
-  background-image: url(../images/icon-close.png), -ms-linear-gradient(bottom, #383838 0%, #666666 100%);
-
-  background-image: url(../images/icon-close.png), -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #383838),
-    color-stop(1, #666666)
-  );
-}
-
-div.panels-ipe-pseudobutton-container:hover,
-div.panels-ipe-control .form-submit:hover {
-  background: #999999;
-  background-image: linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: -o-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: -moz-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: -webkit-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: -ms-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #3D3D3D),
-    color-stop(1, #999999)
-  );
-
-  color: #FFF;
-}
-
-div.panels-ipe-pseudobutton-container a:hover {
-  color: #FFF;
-}
-
-div.panels-ipe-control input.panels-ipe-cancel:hover {
-  background-image: url(../images/icon-close.png), linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: url(../images/icon-close.png), -o-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: url(../images/icon-close.png), -moz-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: url(../images/icon-close.png), -webkit-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: url(../images/icon-close.png), -ms-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-
-  background-image: url(../images/icon-close.png), -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #3D3D3D),
-    color-stop(1, #999999)
-  );
-}
-
-div.panels-ipe-control input.panels-ipe-save:hover {
-  background-image: url(../images/icon-save.png), linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: url(../images/icon-save.png), -o-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: url(../images/icon-save.png), -moz-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: url(../images/icon-save.png), -webkit-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-  background-image: url(../images/icon-save.png), -ms-linear-gradient(bottom, #3D3D3D 0%, #999999 100%);
-
-  background-image: url(../images/icon-save.png), -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #3D3D3D),
-    color-stop(1, #999999)
-  );
-}
-
-div.panels-ipe-pseudobutton-container:active,
-div.panels-ipe-control .form-submit:active {
-  background: #333;
-  background-image: linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: -o-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: -moz-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: -webkit-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: -ms-linear-gradient(bottom, #616161 0%, #333333 100%);
-
-  background-image: -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #616161),
-    color-stop(1, #333333)
-  );
-
-  color: #CCC;
-}
-
-div.panels-ipe-pseudobutton-container a:active {
-  color: #CCC;
-}
-
-div.panels-ipe-control input.panels-ipe-cancel:active {
-  background-image: url(../images/icon-close.png), linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: url(../images/icon-close.png), -o-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: url(../images/icon-close.png), -moz-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: url(../images/icon-close.png), -webkit-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: url(../images/icon-close.png), -ms-linear-gradient(bottom, #616161 0%, #333333 100%);
-
-  background-image: url(../images/icon-close.png), -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #616161),
-    color-stop(1, #333333)
-  );
-}
-
-div.panels-ipe-control input.panels-ipe-save:active {
-  background-image: url(../images/icon-save.png), linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: url(../images/icon-save.png), -o-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: url(../images/icon-save.png), -moz-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: url(../images/icon-save.png), -webkit-linear-gradient(bottom, #616161 0%, #333333 100%);
-  background-image: url(../images/icon-save.png), -ms-linear-gradient(bottom, #616161 0%, #333333 100%);
-
-  background-image: url(../images/icon-save.png), -webkit-gradient(
-    linear,
-    left bottom,
-    left top,
-    color-stop(0, #616161),
-    color-stop(1, #333333)
-  );
-}
-
-div.panels-ipe-pseudobutton-container a.panels-ipe-startedit {
-  padding-left: 34px;
-  background: url(../images/icon-configure.png) no-repeat 10px 9px;
-}
-
-div.panels-ipe-pseudobutton-container a.panels-ipe-change-layout {
-  padding-left: 34px;
-  background: url(../images/icon-change-layout.png) no-repeat 10px 9px;
-}
-
-div.panels-ipe-button-container {
-  margin: 0.3em 0.5em;
-  text-align: center;
 }
 
-form#panels-ipe-edit-control-form {
-  text-align: center;
+.ipe-action-list [data-action-id="move"] select {
+  background: transparent;
+  border: none;
+  text-transform: uppercase;
 }
diff --git a/panels_ipe/fonts/ipeicons.woff b/panels_ipe/fonts/ipeicons.woff
new file mode 100755
index 0000000000000000000000000000000000000000..13fea03cc5b713ad4ec4691846a09acdc990155d
GIT binary patch
literal 1768
zcmah}O>7%Q6n-<aw(HcfYm$v=h=Lu5B-F@B*WMVUwD^Erm8h)<tPem$-NZN{v0WvO
zXrzFA;?M(d38)f>9Jt_-5QiQ?s?<p308&-e6-NXYqFzBl0^58uyP*X|Fv)xKz3;tw
zKjYnf_fnxijIjK_&~d$gru0$k(yP-J5jzIoWhId(e7`SMOIN|Iz<y0hta$IkN@-JZ
zU&B_hN={VPx8DZ0PsDF3dGC!o@2`|g%S6UC_-B;_?tUUyK!W=U_PCP3r{epSY6CIG
zeb_TfCdiQM^(Dml4A{d;gvH;fmad_$FAm$4D5YzqY8hM}wLegD<K^HVSL>S%%=_)%
zaRNw)JM3ffp|>0SJJ=t&J9|hO7KHuyVkaKA6wVVJ1jMc0Ey@cB7=5SM)rxDas{oC_
zr|<~0iW(t<p2yyYo8tIm6h8>`I=vn^oIN0oS?}kV;hoS9C`JrX5uL^5I8p8eUHVb|
z|KX5iS`c4Tvj<r$7t3+#(cJ$29ACqJ`}Q0$#F&VAMa8hdWFW}+J_104L`2VRK~M_w
zx5XOzJVyQKB@txLB<+l8E0q$hZmOH&RK&`Q)SvC9<H%ME^EYv>De^!;3ey15Kq8um
z;y)36mO-6lfKBFfr50B!YnREc+R?H?Lhbr0xX#%SS@evbD`#fdBxAGLX*Mwy!I;fG
z_QPIxivJo4E%zrw7xymy=rZhPDA~UpQjAI@$mu;qZ@cJhn4ZS`i7~$!%|sJ+Oryb}
zOv0MAquGh1?w89hjax<3@19CpR+2qXx_gQ}5W7~gBe~<WtnrRiEO*AOj>DafM4iJs
z2-8y(Kdf_@eNv1~*xA{PHDnI@lLHAmVe)pDbdqAyl5Kl_-d57=I!uZ@Dz&teWXEwm
z_B@kPs+l-R%;aDu6TcWSI}o$2QI5=k*jZ+0(>xg+OLNrAO@^5{ILb3-ZqmdtX)hM0
zB9XHvN9@I&bN?`;6lSv}n^T2FJAP*P#i=)~b32Q6NB)aYG3dd2J{P{(T74@r_ZuBI
zRA+Rr_C@T`jxO8iQhh`46zhd^`aQ#=qLyeMX40loA=l&D>X?BLS11Kv(-X(r^_Af5
z8>CTuUtYqu?K*u)Kd~p->o~0X4AFB)pw?j*+VE5u(e5;i0^nTCfiHVl0MB~Z0N(Pj
z4_{o<!+!Ad9`3`Fc3r*eMi5LG9v5@qiiZX884nx4J0AAY5XC+02fyIqKFZSPt4sB2
zy<VeLTB15tA$6)1%Nv`k^;-ONdWMQrrVZLe03673n$k3L<f$Xcg>q$Uy|jU%Wi+})
z>r|qRBO?p-S|eU5*UB5EMtM1YWjlU;seS>yrKzB6H&DHTG&S8(i5l3K!Cj$k=<|Aj
O3+@2uQN5@Cx&8uZ?<_$8

literal 0
HcmV?d00001

diff --git a/panels_ipe/fonts/selection.json b/panels_ipe/fonts/selection.json
new file mode 100755
index 0000000..10abee7
--- /dev/null
+++ b/panels_ipe/fonts/selection.json
@@ -0,0 +1,228 @@
+{
+	"IcoMoonType": "selection",
+	"icons": [
+		{
+			"icon": {
+				"paths": [
+					"M512 170.667v-128l-170.667 170.667 170.667 170.667v-128c141.227 0 256 114.773 256 256 0 43.093-10.667 84.053-29.867 119.467l62.293 62.293c33.28-52.48 52.907-114.773 52.907-181.76 0-188.587-152.747-341.333-341.333-341.333zM512 768c-141.227 0-256-114.773-256-256 0-43.093 10.667-84.053 29.867-119.467l-62.293-62.293c-33.28 52.48-52.907 114.773-52.907 181.76 0 188.587 152.747 341.333 341.333 341.333v128l170.667-170.667-170.667-170.667v128z"
+				],
+				"attrs": [],
+				"isMulticolor": false,
+				"grid": 0,
+				"tags": [
+					"ic_sync_black_24px"
+				]
+			},
+			"attrs": [],
+			"properties": {
+				"order": 10,
+				"id": 7,
+				"prevSize": 32,
+				"code": 59655,
+				"name": "ic_sync_black_24px"
+			},
+			"setIdx": 0,
+			"setId": 2,
+			"iconIdx": 0
+		},
+		{
+			"icon": {
+				"paths": [
+					"M512 341.333l-256 256 60.16 60.16 195.84-195.413 195.84 195.413 60.16-60.16z"
+				],
+				"attrs": [],
+				"isMulticolor": false,
+				"grid": 0,
+				"tags": [
+					"ic_expand_less_black_24px"
+				]
+			},
+			"attrs": [],
+			"properties": {
+				"order": 9,
+				"id": 6,
+				"prevSize": 32,
+				"code": 59648,
+				"name": "ic_expand_less_black_24px"
+			},
+			"setIdx": 0,
+			"setId": 2,
+			"iconIdx": 1
+		},
+		{
+			"icon": {
+				"paths": [
+					"M707.84 366.507l-195.84 195.413-195.84-195.413-60.16 60.16 256 256 256-256z"
+				],
+				"attrs": [],
+				"isMulticolor": false,
+				"grid": 0,
+				"tags": [
+					"ic_expand_more_black_24px"
+				]
+			},
+			"attrs": [],
+			"properties": {
+				"order": 6,
+				"id": 5,
+				"prevSize": 32,
+				"code": 59649,
+				"name": "ic_expand_more_black_24px"
+			},
+			"setIdx": 0,
+			"setId": 2,
+			"iconIdx": 2
+		},
+		{
+			"icon": {
+				"paths": [
+					"M42.667 896h938.667l-469.333-810.667-469.333 810.667zM554.667 768h-85.333v-85.333h85.333v85.333zM554.667 597.333h-85.333v-170.667h85.333v170.667z"
+				],
+				"attrs": [],
+				"isMulticolor": false,
+				"grid": 0,
+				"tags": [
+					"ic_warning_black_24px"
+				]
+			},
+			"attrs": [],
+			"properties": {
+				"order": 5,
+				"id": 4,
+				"prevSize": 32,
+				"code": 59650,
+				"name": "ic_warning_black_24px"
+			},
+			"setIdx": 0,
+			"setId": 2,
+			"iconIdx": 3
+		},
+		{
+			"icon": {
+				"paths": [
+					"M128 554.667h341.333v-426.667h-341.333v426.667zM128 896h341.333v-256h-341.333v256zM554.667 896h341.333v-426.667h-341.333v426.667zM554.667 128v256h341.333v-256h-341.333z"
+				],
+				"attrs": [],
+				"isMulticolor": false,
+				"grid": 0,
+				"tags": [
+					"tab_change_layout"
+				]
+			},
+			"attrs": [],
+			"properties": {
+				"order": 4,
+				"id": 3,
+				"prevSize": 32,
+				"code": 59651,
+				"name": "tab_change_layout"
+			},
+			"setIdx": 0,
+			"setId": 2,
+			"iconIdx": 4
+		},
+		{
+			"icon": {
+				"paths": [
+					"M128 736v160h160l471.893-471.893-160-160-471.893 471.893zM883.627 300.373c16.64-16.64 16.64-43.52 0-60.16l-99.84-99.84c-16.64-16.64-43.52-16.64-60.16 0l-78.080 78.080 160 160 78.080-78.080z"
+				],
+				"attrs": [],
+				"isMulticolor": false,
+				"grid": 0,
+				"tags": [
+					"tab_edit"
+				]
+			},
+			"attrs": [],
+			"properties": {
+				"order": 3,
+				"id": 2,
+				"prevSize": 32,
+				"code": 59652,
+				"name": "tab_edit"
+			},
+			"setIdx": 0,
+			"setId": 2,
+			"iconIdx": 5
+		},
+		{
+			"icon": {
+				"paths": [
+					"M682.667 42.667h-512c-46.933 0-85.333 38.4-85.333 85.333v597.333h85.333v-597.333h512v-85.333zM810.667 213.333h-469.333c-46.933 0-85.333 38.4-85.333 85.333v597.333c0 46.933 38.4 85.333 85.333 85.333h469.333c46.933 0 85.333-38.4 85.333-85.333v-597.333c0-46.933-38.4-85.333-85.333-85.333zM810.667 896h-469.333v-597.333h469.333v597.333z"
+				],
+				"attrs": [],
+				"isMulticolor": false,
+				"grid": 0,
+				"tags": [
+					"tab_manage_content"
+				]
+			},
+			"attrs": [],
+			"properties": {
+				"order": 2,
+				"id": 1,
+				"prevSize": 32,
+				"code": 59653,
+				"name": "tab_manage_content"
+			},
+			"setIdx": 0,
+			"setId": 2,
+			"iconIdx": 6
+		},
+		{
+			"icon": {
+				"paths": [
+					"M725.333 128h-512c-47.36 0-85.333 38.4-85.333 85.333v597.333c0 46.933 37.973 85.333 85.333 85.333h597.333c46.933 0 85.333-38.4 85.333-85.333v-512l-170.667-170.667zM512 810.667c-70.827 0-128-57.173-128-128s57.173-128 128-128 128 57.173 128 128-57.173 128-128 128zM640 384h-426.667v-170.667h426.667v170.667z"
+				],
+				"attrs": [],
+				"isMulticolor": false,
+				"grid": 0,
+				"tags": [
+					"tab_save"
+				]
+			},
+			"attrs": [],
+			"properties": {
+				"order": 1,
+				"id": 0,
+				"prevSize": 32,
+				"code": 59654,
+				"name": "tab_save"
+			},
+			"setIdx": 0,
+			"setId": 2,
+			"iconIdx": 7
+		}
+	],
+	"height": 1024,
+	"metadata": {
+		"name": "icomoon"
+	},
+	"preferences": {
+		"showGlyphs": true,
+		"showQuickUse": true,
+		"showQuickUse2": true,
+		"showSVGs": true,
+		"fontPref": {
+			"prefix": "icon-",
+			"metadata": {
+				"fontFamily": "icomoon"
+			},
+			"metrics": {
+				"emSize": 1024,
+				"baseline": 6.25,
+				"whitespace": 50
+			},
+			"embed": false
+		},
+		"imagePref": {
+			"prefix": "icon-",
+			"png": true,
+			"useClassSelector": true,
+			"color": 4473924,
+			"bgColor": 16777215
+		},
+		"historySize": 100,
+		"showCodes": true
+	}
+}
\ No newline at end of file
diff --git a/panels_ipe/images/block_down.svg b/panels_ipe/images/block_down.svg
new file mode 100644
index 0000000..2e5c41c
--- /dev/null
+++ b/panels_ipe/images/block_down.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z"/>
+    <path d="M0 0h24v24H0z" fill="none"/>
+</svg>
\ No newline at end of file
diff --git a/panels_ipe/images/block_up.svg b/panels_ipe/images/block_up.svg
new file mode 100644
index 0000000..32f02d9
--- /dev/null
+++ b/panels_ipe/images/block_up.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"/>
+    <path d="M0 0h24v24H0z" fill="none"/>
+</svg>
\ No newline at end of file
diff --git a/panels_ipe/images/dragger.png b/panels_ipe/images/dragger.png
deleted file mode 100644
index bb3b57b2611981fb82453d1dd77ab6b34688826f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 946
zcmV;j15NyiP)<h;3K|Lk000e1NJLTq000^Q000#T0ssI2*M4bC00003b3#c}2nYz<
z;ZNWI000SaNLh0L01FZT01FZU(%pXi0000LbVXQnLvm$dbZKvHAXI5>WdJcRGB__W
zFd27aFaQ7m9CSrkbW?9;ba!ELWdK2BZ(?O2Mrm?ocW-iQb09-gGzPNMpa1{^3`s;m
zR5*>Tl1pe4K^Vv9vD4j>oefDX0j0DSr17XCh>){h6nvogDrlpgw6|gnMQQP<^dzmI
zUVR~=9&EHHy*Hk0z4$0rwB4XhHr<_X9v*_}DosT6&m3l$|M%mY`F~hSIY0m2_m|MW
zsiR|ES67eqUswpCI>h=Ti&Dy18$t*`D5Y8x$)8x3)+V1no6n!s+E8k>_P4@OYH6*9
zhK7CL_kDk8Xjp6An%ESFF%d!(3M1umIh9JKQmJyeTqukPAuw)Q)dGiRW=gqS&hxxv
zGMP*!J<rSKax*g}jGK06{r0;UV@fH;L~Ff$`?f^FEf$|!mW2=!LL4}-F9@ny8$zft
zS}A3W`2{DXinADF^Yfo>-n>0JdN~YNw62^vbDA-R5CVWor6Q$toc58COZoh%4I9==
z32`EkKc$o+ge)w4zHs4U5Cn5`Z++hffZpDo?(VMc?ylb69spQc@@Hq~YPH(%aA9HL
zGa;)4DJAXiKW2<JMpH_^eEB*yb_F3+tJS8br`y}x`}#J8VF(}qgp@!jEf!x)PTq*3
zh!FDf<*Th*x3smfI>8uCq*V2V5V>}3oDkx=uIsv-bKADHHVuQ6N^4`=Hs`$VA%tAL
zIvzKywi-p@;NUsVx#KvF<1oemKnRGxMF;=@##r6MIUgK6A2;B~j~_L1ux<O>x9_)Z
z-R|t{+`W63Wm%rbjWHNwgiw96yj+n|K6^H`xVShla4MPf!Z3^{D;ozXCFkym6UV2f
zikVC%2&%@IckkXelHItm$8qdLq61^x-+$b76G0Hflg0P%-}!Cs7-NJ`r6k5Uo$lDb
z|6qOYq?8XIJ}8%cV+^I1)(QYx&0Spp5+P(+)|)r8)oL}JPB(B*o;-D3ck|}#%1VgV
z(l1jJ7XW0leWlWC&bjA#p67ASOQqM@Y##tLtD<}N?$K*WDTOiFxpT*d4<D6MjIm56
zGca&6ii8l+nk8E6H69yFrlyoSdi2QMyY~=6hYugBRH{)FHS;5c@WjOR7T15PADb;g
U2`bPFa{vGU07*qoM6N<$f>}|jp#T5?

diff --git a/panels_ipe/images/icon-add.png b/panels_ipe/images/icon-add.png
deleted file mode 100644
index 521bec0aabb39786772eaffe4618b759c11ee74e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1052
zcmaJ=J8aWH7`AAI+A3Wr14XDh+?1hh{0Mnq(@;{|A*teE5=(@saqMejHTD_%O5Bdn
z1&I+<!33&U8DQ+d#86ZT=EA_l3Of>TPLseuILZ3&etqBfKmAav+?bp=KS5E{WT_}u
ziKU}&Y$WnL{qdccX{^+74Xxvj;X^8Kp(X?+$Jl^XXjt2K58)C;jl^xej_Yb!G?Bv?
z5rzpJkFY6fX*u)^a}#3Fgd4V-qrV-zrGahb=-X+PQ#}c`?Bb3OYdaO)+}Sh*i(bA4
zmO_yTI1n2kblPqphB>;cE0TNUW@*rc;LRL;l2l#QfP{Pq(hQd{xeO07DTYsRLP}T!
zNsi}OKFM;~1kZ`-tjO`8cW4sLx7Nj~T<FCjuN>XN*b`Z{)9Ems%M9{2SY8kWmP@k9
zWP%_P!M2NyFyRKXeFYf?rf+-LMlOgHjV9W{Ihtg8Qi9_RWZj_GCekoAG(47PxTvH)
zP*wjAb({e@z*YDs-+u}R`nCtzDh$w;Z<4{S&qkp<QSzaIk*_1v?srjZA&i0+@_<xJ
zGdxfY({>|Im!Ya+$qlgKny@72XrjT`wk0YG&#x$%f-Fli&ntXhO67(06)CAC(?UKg
z^|>-Kw;br=KGzy>v)oW_v<!|%BFoUX??J2JBL{Sg7VY7=$V2sdTx)198D%J!CCRYS
zX#W~@-$nK(x(*hXJO--|U9#^!S?tmG)=%<N%xGm@(RJOjta`m(EEXG$Mm!$ZG;Mx<
zer;`yKvoZnZ-|+|)k^JG|4^@w7C#Vgr6lL|@VNOUK6b9~<m0XJg{un}=h_$AyN@|8
zspZF~W~XN6j$*M`dGXO=`TDczgEOZKqYd!sEd6;!xjFS>clzDSeS7cgDs?JG#r7`k
TAK!WKoG4Qzr6RvtY25z}{+dVZ

diff --git a/panels_ipe/images/icon-change-layout.png b/panels_ipe/images/icon-change-layout.png
deleted file mode 100644
index 49fa5f26598cd4aa06ca10aaa283ad6dcf76dc49..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 319
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`EX7WqAsj$Z!;#Vf<Z~8yL>4nJ
za0`Jj<E6WGe}IA_C9V-A&PAz-C7Jno3L%-fsSL*YX8MLkO<V5x0Oe&s^1=COCFO}l
zsSJ)O`AMk?Zka`?<@rU~#R|^BriEJ{n*r6$^mK6yvFJ^nV_*NTp6$(niH(g18yg!J
z%K5GVQU~SK4jejpveVhVu%WfPyE}=Q@6m+c4pSKQ^z`_4sPxNPi#c3q`276*V{Vs2
zdJ3jISFT=_4d78Q6_c~AG65OTkR$G3s(gu2Sy_1{+X4nLrcdk_e(+03NQfzP8}F<A
zEf%k1laP=Qu;2(QC(nvXMrH#+g9ZhO3TB2cqB3?R8^z889m(M7>gTe~DWM4f5o%`S

diff --git a/panels_ipe/images/icon-close.png b/panels_ipe/images/icon-close.png
deleted file mode 100644
index 7e3f4812d76d54599f933940929e1eb6bedb05f4..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 285
zcmV+&0pk9NP)<h;3K|Lk000e1NJLTq000^Q000;W1^@s6<bv(R0000PbVXQnQ*UN;
zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUyzez+vRCwC#l+g*qAPhj$PS8zmhT9|?
zT{mb)=?1z<yFo62w0MVF&lEq73J*kskC4QUh@i#lq0zcUx9E>WAOl;%5f6aO$PCFh
z`Hq<-wo0TDQhZFN5+B6jPoghLR2GpOGHZ<<%%TDjE_!CGQ6j57GoQFaOEXX*SlxAj
zY|RMP4@s@4ATu=2Q6SkeqRpI9lNx0tPO*30iDXccGV8#4=Gk#g1ljA^r333Sf1SH(
jU|$8l_Y^k6TYv!o?gon=DmvD200000NkvXXu0mjfP;hTI

diff --git a/panels_ipe/images/icon-configure.png b/panels_ipe/images/icon-configure.png
deleted file mode 100644
index 348a8f944bc8d612fa50bfeb5bfde1290530d151..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 546
zcmV+-0^R+IP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV00004b3#c}2nYxW
zd<bNS00009a7bBm000XT000XT0n*)m`~Uy|6m&&cbVG7wVRUJ4ZXi@?ZDjy6FETGN
zG7Z*C8vp<R9CSrkbW?9;ba!ELWdK2BZ(?O2Mrm?ocW-iQb09-gHF34$HUIztd`Uz>
zR5*=|lCh53Kp2ESN9?rc&J9vbMR^Vl4F#b`A~ci{Pmu5imp9-I;+BRa;sL@*G;W+w
zy5b_y(1Em-8?IoT?45k!R$A@O@0tIfSvlZ^wOA}t;G=`@CX>nUI$oo?ESJj=2*=~`
z1As9m_IRPk`3$5LF92ArRtc~MZh$H9-Qio;;Df_e?F7Wb;qcf1kTh~)fUm&Z!vNo$
zsH)vKSC<CHm^m==Ci-G(YSY1B@Gk<uX0u6vUp2z#>qoEG8&x34vMh0SM!+YJIO}%1
z^DN6^E^l({eY<z(Ly+hBxn|0?)9EA?$n(4p{H(LnkaX4a|2RQW6e$pMiQ(IvwcG7^
zQ51C%-n;Yo_YrryT@385+JQen(rUGi;QfilnEoro0B?cye!uTGo6U=Z!{P9n>06%0
z_!33Y-5UT&M@c71NZLwjNt!u~r28m}5=m2sPm<Cn?X9)3wf4?h8#>&#)}Gy7KjVd0
kgO+8Plx2B#JAH}w2-y0DCEr0vw*UYD07*qoM6N<$g6QG&djJ3c

diff --git a/panels_ipe/images/icon-delete.png b/panels_ipe/images/icon-delete.png
deleted file mode 100644
index 4b3bc18feb8127ef311d4fce2c8a151dc04cf5ea..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 658
zcmV;D0&V??P)<h;3K|Lk000e1NJLTq000mG000sQ1^@s6QafI100004b3#c}2nYxW
zd<bNS00009a7bBm000XT000XT0n*)m`~Uy|9CSrkbW?9;ba!ELWdK2BZ(?O2Mrm?o
zcW-iQb09-gHF34$HUIzs6?8>dbVG7wVRUJ4ZXi@?ZDjy6FEKPPF*0qC)?EMq0qaRb
zK~y-6-IFm+8$lF?zc)K$b{$)BB-V*i`2aXcLy;4pEi56Qz?a|-NL(fzqCtqtWiEgO
zVvD4)K-hS9c6MJ1SOmlcY4b~?8U1?mY3AvF!ZWFA0(=5K0$aRP3H%0riO8BwQq@tG
zWoK!c>To!GvC=fvEX&SRb@cp>eZ|<$^L)44?V_qQO#^^21`#38^Ic#YxO@Vr>U&@W
z41h0#!Js=F4%yq=yF55J_yKTqbhLkRax(JXcY*J!`VP1OE=5(pW?A;L*Xv=eC5~f^
zF?jFs-UHC-bm(+CG)+TM6oe31E|(l1AAhqz<ea15?-NB4s)~pZ$1z)54}BDor@hzf
zQP(wPSpq}`xSq{shb=itlHk23gn)=NBGQBq@ZOUoNeCfO6a}-{>=3v%z+yU`p0-7k
z$>cmu)7z@57>~yn<MH^Ssw&bny`4-Z=Pi6Xot^><D<ZY3`nKz<)oM|eWmVTT_xJZJ
zpnY<+TCEmM)1ayVUPNlMF%bWt{{ZkJ$|8buE{>weJf2<aM^R*)b8(xAjR0#cd7kfh
z@B7wT?(XhBx4!qjpXd3GwU(-?HUpe<%;)owF$U)xS65dbTE8d?H=oZ(&N<fW^<RLS
s<#PGve?QjV00_XViF}J5CQ;<}4-MA!KdlwK&Hw-a07*qoM6N<$f-Jcl3IG5A

diff --git a/panels_ipe/images/icon-draggable.png b/panels_ipe/images/icon-draggable.png
deleted file mode 100644
index 1a4e7deb0f125d4edce740390b80c8a801bea627..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1454
zcmV;f1yTBmP)<h;3K|Lk000e1NJLTq000~S000~a1^@s6at+^<00004b3#c}2nYxW
zd<bNS00009a7bBm000XT000XT0n*)m`~Uy|6m&&cbVG7wVRUJ4ZXi@?ZDjy6FE%eR
zGJ>?+OaK4?9CSrkbW?9;ba!ELWdK2BZ(?O2Mrm?ocW-iQb09-gHF34$HUIzx2uVai
zR7i=nR!wMASrq<m-^**7-zFF`FU3Nk(uP2_V58JQBDjeb7o~~_l4=A~L0Vkssvvad
zXmk|BfocW@Tnr<M{X@FYMY}K}q9BfnLn&ihnbJwxw3A8R>o<$O)HK$Kj?RG#@7{Or
z_nn`6&$$AhDI_fbHW<@cA;i+>Vtfu+CCTPcD3lC^LP?U%BrE?fGMVJ2SS&W^^?K>`
zdKrtw21#xrDSvG;iR8wM7cXA$dcE}deDwKz^m@I#bm`Isk{d}%UxiH5G(pnS-`{^G
z5D4TO8yo5O`|0=l+1S{~Kp>F6e*O9xk{(^xMLE2Rate|pneN}e?@uHWrxZnz9S#Sa
zPA8mBCmaq36h)B}iNvV~4<7g{Dk@Cn@XA_5vg+8eV~29N+&-Vrr@ngiYV+daq5=R`
zt2Niq(D3x_+qct-qTKE8@4qR8`0GmvWdJr~G#x#9^vC)6c@F?M9F9M3+_=#R;2D4n
zfQ%4A`!~D~PM4->LXspIE8*4*fE0if$qlkB>vp?+WwR{n0H%ZxzZV%O8Mw>3u4}R^
zvlxF(4oOx>lH?v89SwGOceeqsmW#kTV(sqkZW|pP4N8*aCb@=Vbz+209T*rm(9_d%
zYhq&JFaUcASSFQ9Ev3_Gq|<4nQmG}ZgtA!d6B84MdwP0q4Gat%0HA(wPmKz)DISlX
zudA!etE$RaES4m>hvXJx)RXj*Y#_PA7!Af+eUaXtSS*%QRh4yhb@_Nae%>e!l>i`&
z+SqXN<jKB~k&(S7)5^(qyWKgP&6d`6T?c@Lg@vVu4<GIkLcA<7wqe`0ZNqlE-2woT
zBuSY}#yK}PrvN}cpGP<x9=dYnN-uzyf@E!5TiY*_lanoqqCf~y00Q|w-`Q*y&!0d4
zRtPa(LbxLk2#lG{=G8SJ#2TDjE{EFM+DC(fgB>yem`o;EEEZH&R(_PQu)dyDRaL=a
zv3!(JmSq$oCj*$hef#$D)2C1OC6md$X0sVW2w1JwoY`zn=ks|T0OsfCmjKMJ!#C^k
zcz$y@92UcPDVxnY7Z(>5lE`MW*tv7((7AKxdI8Le!l40Bd-dwo<0B&@-6ZAa=H^Fz
zeSK#EOc}r#01E)BQPdBya!2i0DGUJM>+S75`{c=!79oTd4u`)#apJ@e049YHe;4OM
zCRsB)JlxgZ-aZqJM(>cUDaluTQ&ZC$kH<rg$HS(krZ*((OLS_Y(deD__V$_K;o&Zl
zHO1Fr^+O1uk(>^P!#|r$rf2u=-TTJys90<^o6RE2a$#aA@~j{WE|=@4g9i_Ow|n>Q
zUjh6ngjilD6{{9jkhG9=kW@<kZE0<7omN$qs;aWJwRM{0mJ%Js*lQ`RGHX06@^l%%
z>N};Rc>5$tT5Yt(w~yp3-WTs2fZWF%`lR;M+1Yt0lgaE;RaJfb`0?h#rFOgBxvg8b
zKAoDHnzq~RcN2+3+4fYn0Rzw@k;v<rnVIe5<Kr!wrYY6c)u^tnhNfxC*w|RhyLa!l
zM<S8e0Q7QTWeIg%&+po`>qRsgjk{bft<dcY!{u^m(P%Ut3WZ)QFE8i6n9%5Fb4QLG
zx!KXtaaoci*lae~Y&J-eg#G*XU+(Peya`}Vk|dVH`;?GE2%!U*?&|8g77PZ5WLbtR
z%LoR8L$O%w8h~j-=j%%0AsmTBl95Q{KRtxYp3-$lzv!v|56otU`N{MSYXATM07*qo
IM6N<$g6!z4_5c6?

diff --git a/panels_ipe/images/icon-save.png b/panels_ipe/images/icon-save.png
deleted file mode 100644
index b4568137601a41c9de044d0c476cc74522763ac4..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 247
zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)p!3HGt@7X1QI3=zTCBgY=CFO}lsSJ)O`AMk?
zp1FzXsX?iUDV2pMQ*D5XW_r3fhE&{2`t$$4J+m#t;}x!p#d@P$?7PF(M!9x&OGS2s
zo;-V&_xh8^k1soT{GFy7?U3~<v1XC(qbE;PPIA9^b@HQN<SX}DmumUK!orCug1f2~
zeZIIpe*cTf{Wosh(9n{cA@PDmW8;d6g4_(9!FSyc{K);m&&b=X_QGMElGx&AHl}?o
ujr<Nd4qmk`exLqv9N{~#VmE_80z*baXLaA9KO#U!GI+ZBxvX<aXaWEpv|;c7

diff --git a/panels_ipe/images/icon-settings.png b/panels_ipe/images/icon-settings.png
deleted file mode 100644
index 19ee4ddcdefdb1bac06a7b1b22eb0317e9ac6788..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 567
zcmV-70?7S|P)<h;3K|Lk000e1NJLTq000sI000sQ1^@s6R?d!B00004b3#c}2nYxW
zd<bNS00009a7bBm000XT000XT0n*)m`~Uy|9CSrkbW?9;ba!ELWdK2BZ(?O2Mrm?o
zcW-iQb09-gHF34$HUIzs6?8>dbVG7wVRUJ4ZXi@?ZDjy6FEKPPF*0qC)?EMq0gy>V
zK~y-6jgvu6!ax*;zcD1;Rb)9V9Fy*?;R4(M64oS|%mMHOmJ`69NhIL_mJ^5vAkq`4
zn_1%taM9N|LNR^Gq|^V+`}*d+S9LfXXktn!wARLZ|Eaa!h&%+0_kO3Eg1X&q3cLer
zpfB<#a0}ct21!OaFgS9pz4yb$pwVcQOD31V9bkZ$%HL{40Xm&dJ9te61W*9EsteLu
zPXN64^H8_e)?KXt`WH2jJ}?7jvPWy3YOULpS1V)zOeu?PS^!CsWWYq$KH#>KB)JEk
zB{&uT1TfCI49EZ!K(E)^3Hd)Ow%cubt~o7$IF9du4P{5_?2F&osWmki3=CxlUPb$s
z)B)xJd@9d8&-2|$MsXZ}9BIqdYPAFykH-dh68$MSa|(*0=vXt=CW;~x=mlk=Papuw
zT5BYD30wz3@V5b1&bj@$Vk-b^ZJV;#|8=ea`isS4U+1g_G7BI@;Qylh+pFx%xvx~9
zr2rnBb1Cpv>CU+!Wyc<!bG7vWnIk!n*7*10&&~lV`vG*uc|5l!0IUE2002ovPDHLk
FV1f@M=IH<c

diff --git a/panels_ipe/images/icon-style.png b/panels_ipe/images/icon-style.png
deleted file mode 100644
index 7f376e6385c873a7658d55223b1d87979b78f3a7..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 488
zcmV<E0T=#>P)<h;3K|Lk000e1NJLTq000pH000mO1^@s6@G+EM00004b3#c}2nYxW
zd<bNS00009a7bBm000XT000XT0n*)m`~Uy|9CSrkbW?9;ba!ELWdK2BZ(?O2Mrm?o
zcW-iQb09-gHF34$HUIzs6?8>dbVG7wVRUJ4ZXi@?ZDjy6FEKPPF*0qC)?EMq0YOPb
zK~y-6ozuT<!ax`W@Q3C1Jvy(D5lF{B!!`775M?q#MsPOB04Iesh{96H1|ESC7$Kg(
zX%5GPm_Sal<l_AE_nz*0_~#l9hXazOnIZZQFdB^pl1fR5B=l|tCX>m)%u14pnN=j+
zO-P!i#|R0@%FJ9*6#MvEU_PIp03=;e6ng;Ib>f`MS{+o98UO$;PLihS9m$;JgPGwN
zVY}V#+xt2@naySy$#*kD0AObKo6Y6~sO$PX&Uie|oO3%#hkn2RCJEtr{t5gVXF8oe
zl6(z+c4p?ndq(o5Yw8n3Qr;RpAz21&?Y;l(LYht%i$&8xKtK|49DjK4vo4^n>+eY<
zW$lm63{eyTK-mSH#gUY?fv_q7>#LCEa+!y_fH-d43c%B)E*fODTCI<VDU4qM*a67B
e_lLg#0QdrRzEC6$$403D0000<MNUMnLSTZhE5wul

diff --git a/panels_ipe/images/loading-small.gif b/panels_ipe/images/loading-small.gif
deleted file mode 100644
index 5cbf6e7b75523144e46e36aba2d58263b767b93b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2112
zcmZvddstKF9mmhf<&cwelAN4Na&i)mazZFccpS;VK|}`#B;e&CXuPo2a`RGSt&CQw
znQ{vvBta7}fKn8J&PyqUskPJ#Vp+i<qE?*GI;Tw8tliM9n{D^(amVhzKK}PU?;r2;
ze%|lr{r>FPSxL!-G(ZEN0l@R;&!0VeHaj~zGc$Ad?%k=Wsfme+k&%(}=g*%$eY&@|
zx2LD4y}iA;x!Ldcd%a$d$5U2T=61V_i;MH~^YilZa&mIg)6;D>o5f-=8jVp=Q5uZ~
z!!Z72005$%Bg->au2`Iwn`xaFF9cs~eJ2u20Tzhk*Zd`D01*H8UQFYI`ODUyXk3ED
zxvyN++QFpM(*8|9Q7p5Egbv#wpF!Ux5mbup#Y36|oiD>$7)tUFrKS#Tvj!a-b{=%Z
z8772J&V66bNmu_e`wf){qRI^#lGs)nsiW6e7{)}AjSKZOtyhRdNHEM07)EI+NGui+
zUb9-!rKEp-T=+q_ufOfkdxsZxcJ**g2g2EH`k!tb*&XcV7zK=|u5mObCoU|)p$MpB
z$0UZz(BUx!B#D6C3`MPB<T5!L`?~5Lq*$RKV<iJ=0%6&%yp`p1L7!NRsA7+)-bzo`
zB!nc*<=693SZSbrrT3>Bu$R5agCnmrcH5o&i(HLQaHl}knE%xex4MY>rlFzvPfK(S
z@GGvUvjuF%+)B&<tXY5D6-!&$fWr?LfDHztH5SE9vio1>HZgm=nIoN;2re=#DX=V|
zZP6;q8ttuRv51J=088P7M<r>3Vgqy=L1@}L!tBxRo?z2~Kr8Oye8=_lexufMj=f+|
zqH^_+0aqR|5HKQ@>xZ<s0#lrFOA!RU$f`W&l-Sf2POF}PmD4Et^BBb~+N4ruvTE6X
zQ1xl@o9$cf+@5>n<6n@vq@f+=+DlZ_OI7&d1^8+g55BK3#T~aQ!-*Xm&tdJie%4_B
zXF=}#6E(=pS`<Q@33IV<{@%q!32lB!by{UGL^nGRRDD&rj(Ed00xXLP4m#@=53c!#
zp21hKxKz5;s(}E&sYZg*WJ@F#06=1YsM#EDJy=^;Z}A06cULZ~_Od&ea+_zp9HY&=
zKB@suOYt#d0|O*Z;A(@GK>3vWS@k&g+La39Wi(|%p&%S8d`x3f6Unp!o24DQ|7t0x
zXP=xjP_rH1b7$Z7z7%x&1?a}LJSbM3>K+VE70%g|lCLzZg1o)MPcs<m&iYPA#s;*e
zDrz$d)s`5Ku8BLwHC$`5kkXp0&7lI@hT|eI(wRvIkaH#3^cOadNn=*q+|i^=_D&rq
z0l`YS4^6h{l%P-`T+}QJ*YR>MYVbF*3>>WF_Eg)56Gk0FXPQVQ?ma__1u$b&@uA`)
z21W!-$_Mr2C>GGtv{pxhlYsOlM~^9rDICw%!S{$iKOkeZ8k`_5WXh2zX3xK$e1~7^
zEa*kPop*~1-e2v1sn~ybfxY839{a|C<MCj9%e3jX?&b<Z{+ZfgE|gLvgNnJG(WL#_
z<$|Q{`9xON{mF6DBIk5ve1`o{Lm0e%Oyv^BC6=bf_h(L@dt8>o-_Ev+B@0;!!<N=E
zh)86oeV8J77a<gjA*$Ke;x!*^@2FQO0=4@?`l_y7b%y@h)hdFtP>>KYsrg_@o34U`
zKtY~SHB2nkX_T0T6A6SYYSdiDGZ@|*XzRt_4c=8HN8%43;*oji;{`hMDL{^%(IW!U
zxBuEVapFflgM6{6`D*(U&l~+Pi9G({q2JHn)^+A&oikiMa@DXr_@z|%Zc1K<q(*sX
z3kQ!3r)!-o<|rvq?xcOSMJERe=7daRQOlNYZr<BHbLZ>U)8VXzT^_4ucOtb;jpNj>
z-j+o~U=>6Vr>i*<3P@C%AklPyZD~v1v$r7;4eaBpSltoMYtrdxrf+xE*=Pm^MdYU@
z7_$CG4vdMIL|7yimgsfTF#v4lN;Hso^0zmMDY#F<f-|g4C=^j<9U+yE6QDo<zLhPj
zPdxSM-yhbvivI{~6D^thln}hEynax-u8_}|B3<sdqFh<$-S_+2-g(q&|6A3`rSndV
zXK?)wV)lKWkx&@VjD|9;j~eHV20p*ODL$s?My<Kizdd>MfHS)(j>y@NS?0*oMOveo
zqU|h+GrAi6B0a(f;t43E(h33$0a&$_k|Nz|vORRT=4dzK;OncVrA=S&3vm1NRi8xH
zafk?tH>nU<VY(s!Av3}^c!dGw|AolIyvlHJgao=`Ag{8>C86*uRsr2GX-VWEDdOh?
z>Bkkz8Q18?eXV`UC!>)|otN(Yzq8>VO|c(3yJbC(eLG<-KgEh-KEWB^m#31LMp?_9
z@;K7JMX)I*siPqyPw8Y8dqbZ&6F?B%SaXR^R7xJC8B2AqSI6`OOXhsEGClW5stZ?=
zSynpD;x!QqqO;V|G7yxM`B|k*_9n@HX;4{VQK4;e?!CjE{_gj4cx->`!DwGN&TKP#
z>kTppe%GK?%TT3BDHID~vs{Y6Z(bZ1LwfQSiz0}0%s^cg%D5ruFD>v5)dh}{VXTgd
gkVHhdSYkS?`sR^#23&_qB-rHTo?$`S3&$7y4~a^U-v9sr

diff --git a/panels_ipe/images/loading.svg b/panels_ipe/images/loading.svg
new file mode 100644
index 0000000..294f735
--- /dev/null
+++ b/panels_ipe/images/loading.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/>
+    <path d="M0 0h24v24H0z" fill="none"/>
+</svg>
\ No newline at end of file
diff --git a/panels_ipe/images/tab_change_layout.svg b/panels_ipe/images/tab_change_layout.svg
new file mode 100644
index 0000000..cbae5ae
--- /dev/null
+++ b/panels_ipe/images/tab_change_layout.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>
+</svg>
\ No newline at end of file
diff --git a/panels_ipe/images/tab_edit.svg b/panels_ipe/images/tab_edit.svg
new file mode 100644
index 0000000..4488460
--- /dev/null
+++ b/panels_ipe/images/tab_edit.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>
+    <path d="M0 0h24v24H0z" fill="none"/>
+</svg>
\ No newline at end of file
diff --git a/panels_ipe/images/tab_manage_content.svg b/panels_ipe/images/tab_manage_content.svg
new file mode 100644
index 0000000..7ee7f4c
--- /dev/null
+++ b/panels_ipe/images/tab_manage_content.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
+</svg>
\ No newline at end of file
diff --git a/panels_ipe/images/tab_save.svg b/panels_ipe/images/tab_save.svg
new file mode 100644
index 0000000..f473722
--- /dev/null
+++ b/panels_ipe/images/tab_save.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
+</svg>
\ No newline at end of file
diff --git a/panels_ipe/images/warning.svg b/panels_ipe/images/warning.svg
new file mode 100644
index 0000000..576964c
--- /dev/null
+++ b/panels_ipe/images/warning.svg
@@ -0,0 +1,4 @@
+<svg fill="#000000" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
+    <path d="M0 0h24v24H0z" fill="none"/>
+    <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
+</svg>
\ No newline at end of file
diff --git a/panels_ipe/includes/panels_ipe.pipelines.inc b/panels_ipe/includes/panels_ipe.pipelines.inc
deleted file mode 100644
index 5bc3b03..0000000
--- a/panels_ipe/includes/panels_ipe.pipelines.inc
+++ /dev/null
@@ -1,44 +0,0 @@
-<?php
-
-/**
- * @file
- * Bulk export of panels_layouts objects generated by Bulk export module.
- */
-
-/**
- * Implements hook_default_panels_renderer_pipeline().
- */
-function panels_ipe_default_panels_renderer_pipeline() {
-  $pipelines = array();
-
-  $pipeline = new stdClass;
-  $pipeline->disabled = FALSE; /* Edit this to true to make a default pipeline disabled initially */
-  $pipeline->api_version = 1;
-  $pipeline->name = 'ipe';
-  $pipeline->admin_title = t('In-Place Editor');
-  $pipeline->admin_description = t('Allows privileged users to update and rearrange the content while viewing this panel.');
-  $pipeline->weight = 0;
-  $pipeline->settings = array(
-    'renderers' => array(
-      0 => array(
-        'access' => array(
-          'plugins' => array(
-            0 => array(
-              'name' => 'perm',
-              'settings' => array(
-                'perm' => 'use panels in place editing',
-              ),
-              'context' => 'logged-in-user',
-            ),
-          ),
-          'logic' => 'and',
-        ),
-        'renderer' => 'ipe',
-        'options' => array(),
-      ),
-    ),
-  );
-  $pipelines[$pipeline->name] = $pipeline;
-
-  return $pipelines;
-}
diff --git a/panels_ipe/js/models/AppModel.js b/panels_ipe/js/models/AppModel.js
new file mode 100644
index 0000000..8910fdf
--- /dev/null
+++ b/panels_ipe/js/models/AppModel.js
@@ -0,0 +1,67 @@
+/**
+ * @file
+ * The primary Backbone model for Panels IPE.
+ *
+ * @see Drupal.panels_ipe.AppView
+ */
+
+(function (Backbone, Drupal) {
+
+  'use strict';
+
+  /**
+   * @constructor
+   *
+   * @augments Backbone.Model
+   */
+  Drupal.panels_ipe.AppModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.AppModel# */{
+
+    /**
+     * @type {object}
+     *
+     * @prop {bool} active
+     * @prop {Drupal.panels_ipe.TabModel} activeTab
+     * @prop {Drupal.panels_ipe.BlockModel} activeBlock
+     * @prop {Drupal.panels_ipe.RegionModel} activeRegion
+     */
+    defaults: /** @lends Drupal.panels_ipe.AppModel# */{
+
+      /**
+       * Whether or not the editing part of the application is active.
+       *
+       * @type {bool}
+       */
+      active: false,
+
+      /**
+       * The current Layout.
+       *
+       * @type {Drupal.panels_ipe.LayoutModel}
+       */
+      layout: null,
+
+      /**
+       * A collection of all tabs on screen.
+       *
+       * @type {Drupal.panels_ipe.TabCollection}
+       */
+      tabCollection: null,
+
+      /**
+       * The "Edit" tab.
+       *
+       * @type {Drupal.panels_ipe.TabModel}
+       */
+      editTab: null,
+
+      /**
+       * The "Save" tab.
+       *
+       * @type {Drupal.panels_ipe.TabModel}
+       */
+      saveTab: null
+    }
+
+  });
+
+}(Backbone, Drupal));
diff --git a/panels_ipe/js/models/BlockModel.js b/panels_ipe/js/models/BlockModel.js
new file mode 100644
index 0000000..4f990cb
--- /dev/null
+++ b/panels_ipe/js/models/BlockModel.js
@@ -0,0 +1,115 @@
+/**
+ * @file
+ * Base Backbone model for a Block.
+ */
+
+(function (_, $, Backbone, Drupal, drupalSettings) {
+
+  'use strict';
+
+  Drupal.panels_ipe.BlockModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.BlockModel# */{
+
+    /**
+     * @type {object}
+     */
+    defaults: /** @lends Drupal.panels_ipe.BlockModel# */{
+
+      /**
+       * The block state.
+       *
+       * @type {bool}
+       */
+      active: false,
+
+      /**
+       * The ID of the block.
+       *
+       * @type {string}
+       */
+      id: null,
+
+      /**
+       * The unique ID of the block.
+       *
+       * @type {string}
+       */
+      uuid: null,
+
+      /**
+       * The label of the block.
+       *
+       * @type {string}
+       */
+      label: null,
+
+      /**
+       * The provider for the block (usually the module name).
+       *
+       * @type {string}
+       */
+      provider: null,
+
+      /**
+       * The HTML content of the block. This is stored in the model as the
+       * IPE doesn't actually care what the block's content is, the functional
+       * elements of the model are the metadata. The BlockView renders this
+       * wrapped inside IPE elements.
+       *
+       * @type {string}
+       */
+      html: null
+
+    },
+
+    /**
+     * @type {function}
+     */
+    url: function() {
+      return Drupal.panels_ipe.urlRoot(drupalSettings) + '/block/' + this.get('uuid');
+    }
+
+  });
+
+  /**
+   * @constructor
+   *
+   * @augments Backbone.Collection
+   */
+  Drupal.panels_ipe.BlockCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.BlockCollection# */{
+
+    /**
+     * @type {Drupal.panels_ipe.BlockModel}
+     */
+    model: Drupal.panels_ipe.BlockModel,
+
+    /**
+     * For Blocks, our identifier is the UUID, not the ID.
+     *
+     * @type {function}
+     */
+    modelId: function(attrs) {
+      return attrs.uuid;
+    },
+
+    /**
+     * Moves a Block up or down in this collection.
+     *
+     * @type {function}
+     *
+     * @param {Drupal.panels_ipe.BlockModel} block
+     *  The BlockModel you want to move.
+     * @param {string} direction
+     *  The string name of the direction (either "up" or "down").
+     */
+    shift: function(block, direction) {
+      var index = this.indexOf(block);
+      if ((direction == 'up' && index > 0) || (direction == 'down' && index < this.models.length)) {
+        this.remove(block, {'silent': true});
+        var new_index = direction == 'up' ? index-1 : index+1;
+        this.add(block, {'at': new_index, 'silent': true});
+      }
+    }
+
+  });
+
+}(_, jQuery, Backbone, Drupal, drupalSettings));
diff --git a/panels_ipe/js/models/LayoutModel.js b/panels_ipe/js/models/LayoutModel.js
new file mode 100644
index 0000000..f7691d6
--- /dev/null
+++ b/panels_ipe/js/models/LayoutModel.js
@@ -0,0 +1,117 @@
+/**
+ * @file
+ * Base Backbone model for a Layout.
+ */
+
+(function (_, $, Backbone, Drupal) {
+
+  'use strict';
+
+  Drupal.panels_ipe.LayoutModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.LayoutModel# */{
+
+    /**
+     * @type {object}
+     */
+    defaults: /** @lends Drupal.panels_ipe.LayoutModel# */{
+
+      /**
+       * The layout machine name.
+       *
+       * @type {string}
+       */
+      id: null,
+
+      /**
+       * Whether or not this was the original layout for the variant.
+       *
+       * @type {bool}
+       */
+      original: false,
+
+      /**
+       * The layout label.
+       *
+       * @type {string}
+       */
+      label: null,
+
+      /**
+       * Whether or not this is the current layout.
+       *
+       * @type {bool}
+       */
+      current: false,
+
+      /**
+       * The wrapping HTML for this layout. Only used for initial rendering.
+       *
+       * @type {string}
+       */
+      html: null,
+
+      /**
+       * A collection of regions contained in this Layout.
+       *
+       * @type {Drupal.panels_ipe.RegionCollection}
+       */
+      regionCollection: null
+
+    },
+
+    /**
+     * Overrides the isNew method to mark if this is the initial layout or not.
+     */
+    isNew: function () {
+      return !this.get('original');
+    },
+
+    /**
+     * Overrides the parse method to set our regionCollection dynamically.
+     *
+     * @param {Object} resp
+     * @param {Object} options
+     */
+    parse: function(resp, options) {
+      // If possible, initialize our region collection.
+      if (typeof resp.regions != 'undefined') {
+        resp.regionCollection = new Drupal.panels_ipe.RegionCollection();
+        for (var i in resp.regions) {
+          var region = new Drupal.panels_ipe.RegionModel(resp.regions[i]);
+          region.set({'blockCollection': new Drupal.panels_ipe.BlockCollection()});
+          resp.regionCollection.add(region);
+        }
+      }
+      return resp;
+    },
+
+    /**
+     * @type {function}
+     */
+    url: function() {
+      return Drupal.panels_ipe.urlRoot(drupalSettings) + '/layouts/' + this.get('id')
+    }
+
+  });
+
+  /**
+   * @constructor
+   *
+   * @augments Backbone.Collection
+   */
+  Drupal.panels_ipe.LayoutCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.LayoutCollection# */{
+
+    /**
+     * @type {Drupal.panels_ipe.LayoutModel}
+     */
+    model: Drupal.panels_ipe.LayoutModel,
+
+    /**
+     * @type {function}
+     */
+    url: function() {
+      return Drupal.panels_ipe.urlRoot(drupalSettings) + '/layouts'
+    }
+
+  });
+
+}(_, jQuery, Backbone, Drupal));
diff --git a/panels_ipe/js/models/RegionModel.js b/panels_ipe/js/models/RegionModel.js
new file mode 100644
index 0000000..c4f7620
--- /dev/null
+++ b/panels_ipe/js/models/RegionModel.js
@@ -0,0 +1,69 @@
+/**
+ * @file
+ * Base Backbone model for a Region.
+ *
+ * @todo Support sync operations to refresh a region, even if we don't have
+ * a use case for that yet.
+ */
+
+(function (_, $, Backbone, Drupal) {
+
+  'use strict';
+
+  Drupal.panels_ipe.RegionModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.RegionModel# */{
+
+    /**
+     * @type {object}
+     */
+    defaults: /** @lends Drupal.panels_ipe.RegionModel# */{
+
+      /**
+       * The machine name of the region.
+       *
+       * @type {string}
+       */
+      name: null,
+
+      /**
+       * The label of the region.
+       *
+       * @type {string}
+       */
+      label: null,
+
+      /**
+       * A BlockCollection for all blocks in this region.
+       *
+       * @type {Drupal.panels_ipe.BlockCollection}
+       *
+       * @see Drupal.panels_ipe.BlockCollection
+       */
+      blockCollection: null
+    }
+
+  });
+
+  /**
+   * @constructor
+   *
+   * @augments Backbone.Collection
+   */
+  Drupal.panels_ipe.RegionCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.RegionCollection# */{
+
+    /**
+     * @type {Drupal.panels_ipe.RegionModel}
+     */
+    model: Drupal.panels_ipe.RegionModel,
+
+    /**
+     * For Regions, our identifier is the region name.
+     *
+     * @type {function}
+     */
+    modelId: function(attrs) {
+      return attrs.name;
+    }
+
+  });
+
+}(_, jQuery, Backbone, Drupal));
diff --git a/panels_ipe/js/models/TabModel.js b/panels_ipe/js/models/TabModel.js
new file mode 100644
index 0000000..ae8bd9b
--- /dev/null
+++ b/panels_ipe/js/models/TabModel.js
@@ -0,0 +1,71 @@
+/**
+ * @file
+ * A .
+ *
+ * @see Drupal.panels_ipe.TabView
+ */
+
+(function (Backbone, Drupal) {
+
+  'use strict';
+
+  /**
+   * @constructor
+   *
+   * @augments Backbone.Model
+   */
+  Drupal.panels_ipe.TabModel = Backbone.Model.extend(/** @lends Drupal.panels_ipe.TabModel# */{
+
+    /**
+     * @type {object}
+     *
+     * @prop {bool} active
+     * @prop {string} title
+     */
+    defaults: /** @lends Drupal.panels_ipe.TabModel# */{
+
+      /**
+       * The ID of the tab.
+       *
+       * @type {int}
+       */
+      id: null,
+
+      /**
+       * Whether or not the tab is active.
+       *
+       * @type {bool}
+       */
+      active: false,
+
+      /**
+       * Whether or not the tab is loading.
+       *
+       * @type {bool}
+       */
+      loading: false,
+
+      /**
+       * The title of the tab.
+       *
+       * @type {string}
+       */
+      title: null
+    }
+
+  });
+
+  /**
+   * @constructor
+   *
+   * @augments Backbone.Collection
+   */
+  Drupal.panels_ipe.TabCollection = Backbone.Collection.extend(/** @lends Drupal.panels_ipe.TabCollection# */{
+
+    /**
+     * @type {Drupal.panels_ipe.TabModel}
+     */
+    model: Drupal.panels_ipe.TabModel
+  });
+
+}(Backbone, Drupal));
diff --git a/panels_ipe/js/panels_ipe.js b/panels_ipe/js/panels_ipe.js
index aad076d..9261bf0 100644
--- a/panels_ipe/js/panels_ipe.js
+++ b/panels_ipe/js/panels_ipe.js
@@ -1,480 +1,107 @@
-
-// Ensure the $ alias is owned by jQuery.
-(function($) {
-
-// randomly lock a pane.
-// @debug only
-Drupal.settings.Panels = Drupal.settings.Panels || {};
-Drupal.settings.Panels.RegionLock = {
-  10: { 'top': false, 'left': true, 'middle': true }
-}
-
-Drupal.PanelsIPE = {
-  editors: {},
-  bindClickDelete: function(context) {
-    $('a.pane-delete:not(.pane-delete-processed)', context)
-      .addClass('pane-delete-processed')
-      .click(function() {
-        if (confirm(Drupal.t('Remove this pane?'))) {
-          $(this).parents('div.panels-ipe-portlet-wrapper').fadeOut('medium', function() {
-            var $sortable = $(this).closest('.ui-sortable');
-            $(this).empty().remove();
-            $sortable.trigger('sortremove');
-          });
-          $(this).parents('div.panels-ipe-display-container').addClass('changed');
-        }
-        return false;
-      });
-  }
-}
-
-
-Drupal.behaviors.PanelsIPE = {
-  attach: function(context) {
-    for (var i in Drupal.settings.PanelsIPECacheKeys) {
-      var key = Drupal.settings.PanelsIPECacheKeys[i];
-      $('div#panels-ipe-display-' + key + ':not(.panels-ipe-processed)')
-        .addClass('panels-ipe-processed')
-        .each(function() {
-          // If we're replacing an old IPE, clean it up a little.
-          if (Drupal.PanelsIPE.editors[key]) {
-            Drupal.PanelsIPE.editors[key].editing = false;
-          }
-          Drupal.PanelsIPE.editors[key] = new DrupalPanelsIPE(key);
-          Drupal.PanelsIPE.editors[key].showContainer();
-        });
-    }
-    $('.panels-ipe-hide-bar').once('panels-ipe-hide-bar-processed').click(function() {
-      Drupal.PanelsIPE.editors[key].hideContainer();
-    });
-    Drupal.PanelsIPE.bindClickDelete(context);
-  }
-};
-
 /**
- * Base object (class) definition for the Panels In-Place Editor.
+ * @file
+ * Attaches behavior for the Panels IPE module.
  *
- * A new instance of this object is instanciated for every unique IPE on a given
- * page.
- *
- * Note that this form is provisional, and we hope to replace it with a more
- * flexible, loosely-coupled model that utilizes separate controllers for the
- * discrete IPE elements. This will result in greater IPE flexibility.
  */
-function DrupalPanelsIPE(cache_key, cfg) {
-  cfg = cfg || {};
-  var ipe = this;
-  this.key = cache_key;
-  this.lockPath = null;
-  this.state = {};
-  this.container = $('#panels-ipe-control-container');
-  this.control = $('div#panels-ipe-control-' + cache_key);
-  this.initButton = $('div.panels-ipe-startedit', this.control);
-  this.cfg = cfg;
-  this.changed = false;
-  this.sortableOptions = $.extend({
-    opacity: 0.75, // opacity of sortable while sorting
-    items: 'div.panels-ipe-portlet-wrapper',
-    handle: 'div.panels-ipe-draghandle',
-    cancel: '.panels-ipe-nodrag',
-    dropOnEmpty: true
-  }, cfg.sortableOptions || {});
-
-  this.regions = [];
-  this.sortables = {};
-
-  $(document).bind('CToolsDetachBehaviors', function() {
-    // If the IPE is off and the container is not visible, then we need
-    // to reshow the container on modal close.
-    if (!$('.panels-ipe-form-container', ipe.control).html() && !ipe.container.is(':visible')) {
-      ipe.showContainer();
-      ipe.cancelLock();
-    }
-
-    // If the IPE is on and we've hidden the bar for a modal, we need to
-    // re-display it.
-    if (ipe.topParent && ipe.topParent.hasClass('panels-ipe-editing') && ipe.container.is(':not(visible)')) {
-      ipe.showContainer();
-    }
-  });
 
+(function ($, _, Backbone, Drupal) {
 
-  // If a user navigates away from a locked IPE, cancel the lock in the background.
-  $(window).bind('beforeunload', function() {
-    if (!ipe.editing) {
-      return;
-    }
-
-    if (ipe.topParent && ipe.topParent.hasClass('changed')) {
-      ipe.changed = true;
-    }
-
-    if (ipe.changed) {
-      return Drupal.t('This will discard all unsaved changes. Are you sure?');
-    }
-  });
-
-  // If a user navigates away from a locked IPE, cancel the lock in the background.
-  $(window).bind('unload', function() {
-    ipe.cancelLock(true);
-  });
+  'use strict';
 
   /**
-   * If something caused us to abort what we were doing, send a background
-   * cancel lock request to the server so that we do not leave stale locks
-   * hanging around.
+   * Contains initial Backbone initialization for the IPE.
+   *
+   * @type {Drupal~behavior}
    */
-  this.cancelLock = function(sync) {
-    // If there's a lockpath and an ajax available, inform server to clear lock.
-    // We borrow the ajax options from the customize this page link.
-    if (ipe.lockPath && Drupal.ajax['panels-ipe-customize-page']) {
-      var ajaxOptions = {
-        type: 'POST',
-        url: ipe.lockPath
-      }
-
-      if (sync) {
-        ajaxOptions.async = false;
-      }
-
-      // Make sure we don't somehow get another one:
-      ipe.lockPath = null;
+  Drupal.behaviors.panels_ipe = {
+    attach: function (context, settings) {
+      // Perform initial setup of our app.
+      $('body').once('panels-ipe-init').each(Drupal.panels_ipe.init, [settings]);
+    },
+    detach: function (context, settings) {
 
-      // Send the request. This is synchronous to prevent being cancelled.
-      $.ajax(ajaxOptions);
-    }
-  }
-
-  this.activateSortable = function(event, ui) {
-    if (!Drupal.settings.Panels || !Drupal.settings.Panels.RegionLock) {
-      // don't bother if there are no region locks in play.
-      return;
-    }
-
-    var region = event.data.region;
-    var paneId = ui.item.attr('id').replace('panels-ipe-paneid-', '');
-
-    var disabledRegions = false;
-
-    // Determined if this pane is locked out of this region.
-    if (!Drupal.settings.Panels.RegionLock[paneId] || Drupal.settings.Panels.RegionLock[paneId][region]) {
-      ipe.sortables[region].sortable('enable');
-      ipe.sortables[region].sortable('refresh');
-    }
-    else {
-      disabledRegions = true;
-      ipe.sortables[region].sortable('disable');
-      ipe.sortables[region].sortable('refresh');
     }
-
-    // If we disabled regions, we need to
-    if (disabledRegions) {
-      $(event.srcElement).bind('dragstop', function(event, ui) {
-        // Go through
-      });
-    }
-  };
-
-  // When dragging is stopped, we need to ensure all sortable regions are enabled.
-  this.enableRegions = function(event, ui) {
-    for (var i in ipe.regions) {
-      ipe.sortables[ipe.regions[i]].sortable('enable');
-      ipe.sortables[ipe.regions[i]].sortable('refresh');
-    }
-  }
-
-  this.initSorting = function() {
-    var $region = $(this).parents('.panels-ipe-region');
-    var region = $region.attr('id').replace('panels-ipe-regionid-', '');
-    ipe.sortables[region] = $(this).sortable(ipe.sortableOptions);
-    ipe.regions.push(region);
-    $(this).bind('sortactivate', {region: region}, ipe.activateSortable);
-  };
-
-  this.initEditing = function(formdata) {
-    ipe.editing = true;
-    ipe.topParent = $('div#panels-ipe-display-' + cache_key);
-    ipe.backup = this.topParent.clone();
-
-    // See http://jqueryui.com/demos/sortable/ for details on the configuration
-    // parameters used here.
-    ipe.changed = false;
-
-    $('div.panels-ipe-sort-container', ipe.topParent).each(ipe.initSorting);
-
-    // Since the connectWith option only does a one-way hookup, iterate over
-    // all sortable regions to connect them with one another.
-    $('div.panels-ipe-sort-container', ipe.topParent)
-      .sortable('option', 'connectWith', ['div.panels-ipe-sort-container']);
-
-    $('div.panels-ipe-sort-container', ipe.topParent).bind('sortupdate', function() {
-      ipe.changed = true;
-    });
-
-    $('div.panels-ipe-sort-container', ipe.topParent).bind('sortstop', this.enableRegions);
-
-    $('.panels-ipe-form-container', ipe.control).append(formdata);
-
-    $('input:submit:not(.ajax-processed)', ipe.control).addClass('ajax-processed').each(function() {
-      var element_settings = {};
-
-      element_settings.url = $(this.form).attr('action');
-      element_settings.setClick = true;
-      element_settings.event = 'click';
-      element_settings.progress = { 'type': 'throbber' };
-      element_settings.ipe_cache_key = cache_key;
-
-      var base = $(this).attr('id');
-      Drupal.ajax[ipe.base] = new Drupal.ajax(base, this, element_settings);
-    });
-
-    // Perform visual effects in a particular sequence.
-    // .show() + .hide() cannot have speeds associated with them, otherwise
-    // it clears out inline styles.
-    $('.panels-ipe-on').show();
-    ipe.showForm();
-    ipe.topParent.addClass('panels-ipe-editing');
-
-  };
-
-  this.hideContainer = function() {
-    ipe.container.slideUp('fast');
   };
 
-  this.showContainer = function() {
-    ipe.container.slideDown('normal');
-  };
-
-  this.showButtons = function() {
-    $('.panels-ipe-form-container').hide();
-    $('.panels-ipe-button-container').show();
-    ipe.showContainer();
-  };
-
-  this.showForm = function() {
-    $('.panels-ipe-button-container').hide();
-    $('.panels-ipe-form-container').show();
-    ipe.showContainer();
-  };
-
-  this.endEditing = function() {
-    ipe.editing = false;
-    ipe.lockPath = null;
-    $('.panels-ipe-form-container').empty();
-    // Re-show all the IPE non-editing meta-elements
-    $('div.panels-ipe-off').show('fast');
-
-    ipe.showButtons();
-    // Re-hide all the IPE meta-elements
-    $('div.panels-ipe-on').hide();
-
-    $('.panels-ipe-editing').removeClass('panels-ipe-editing');
-    $('div.panels-ipe-sort-container.ui-sortable', ipe.topParent).sortable("destroy");
-  };
+  /**
+   * @namespace
+   */
+  Drupal.panels_ipe = {};
 
-  this.saveEditing = function() {
-    $('div.panels-ipe-region', ipe.topParent).each(function() {
-      var val = '';
-      var region = $(this).attr('id').split('panels-ipe-regionid-')[1];
-      $(this).find('div.panels-ipe-portlet-wrapper').each(function() {
-        var id = $(this).attr('id').split('panels-ipe-paneid-')[1];
-        if (id) {
-          if (val) {
-            val += ',';
-          }
-          val += id;
-        }
-      });
-      $('input[name="panel[pane][' +  region + ']"]', ipe.control).val(val);
+  /**
+   * Setups up our initial Collection and Views based on the current settings.
+   */
+  Drupal.panels_ipe.init = function(settings) {
+    // Set up our initial tabs.
+    var tab_collection = new Drupal.panels_ipe.TabCollection();
+    tab_collection.add({title: 'Change Layout', id: 'change_layout'});
+    tab_collection.add({title: 'Manage Content', id: 'manage_content'});
+
+    // The edit and save tabs are special, and are tracked by our app.
+    var edit_tab = new Drupal.panels_ipe.TabModel({title: 'Edit', id: 'edit'});
+    var save_tab = new Drupal.panels_ipe.TabModel({title: 'Save', id: 'save'});
+    tab_collection.add(edit_tab);
+    tab_collection.add(save_tab);
+
+    // Create a global(ish) AppModel.
+    Drupal.panels_ipe.app = new Drupal.panels_ipe.AppModel({
+      'tabCollection': tab_collection,
+      'editTab': edit_tab,
+      'saveTab': save_tab
     });
-  }
 
-  this.cancelIPE = function() {
-    ipe.hideContainer();
-    ipe.topParent.fadeOut('medium', function() {
-      ipe.topParent.replaceWith(ipe.backup.clone());
-      ipe.topParent = $('div#panels-ipe-display-' + ipe.key);
+    // Set up our initial tab views.
+    var tab_views = {
+      'change_layout': new Drupal.panels_ipe.LayoutPicker()
+    };
 
-      // Processing of these things got lost in the cloning, but the classes remained behind.
-      // @todo this isn't ideal but I can't seem to figure out how to keep an unprocessed backup
-      // that will later get processed.
-      $('.ctools-use-modal-processed', ipe.topParent).removeClass('ctools-use-modal-processed');
-      $('.pane-delete-processed', ipe.topParent).removeClass('pane-delete-processed');
-      ipe.topParent.fadeIn('medium');
-      Drupal.attachBehaviors();
+    // Create an AppView instance.
+    var app_view = new Drupal.panels_ipe.AppView({
+      model: Drupal.panels_ipe.app,
+      'el': '#panels-ipe-tray',
+      'tabContentViews': tab_views
     });
-  };
+    $('body').append(app_view.render().$el);
+
+    // Assemble the initial region and block collections.
+    // This logic is a little messy, as traditionally we would never initialize
+    // Backbone with existing HTML content.
+    var region_collection = new Drupal.panels_ipe.RegionCollection();
+    for (var i in settings.panels_ipe.regions) {
+      var region = new Drupal.panels_ipe.RegionModel();
+      region.set(settings.panels_ipe.regions[i]);
+
+      var block_collection = new Drupal.panels_ipe.BlockCollection();
+      for (var j in settings.panels_ipe.regions[i].blocks) {
+        // Add a new block model.
+        var block = new Drupal.panels_ipe.BlockModel();
+        block.set(settings.panels_ipe.regions[i].blocks[j]);
+        block_collection.add(block);
+      }
 
-  this.cancelEditing = function() {
-    if (ipe.topParent.hasClass('changed')) {
-      ipe.changed = true;
-    }
+      region.set({'blockCollection': block_collection});
 
-    if (!ipe.changed || confirm(Drupal.t('This will discard all unsaved changes. Are you sure?'))) {
-      this.cancelIPE();
-      return true;
+      region_collection.add(region);
     }
-    else {
-      // Cancel the submission.
-      return false;
-    }
-  };
 
-  this.createSortContainers = function() {
-    $('div.panels-ipe-region', this.topParent).each(function() {
-      $(this).children('div.panels-ipe-portlet-marker').parent()
-        .wrapInner('<div class="panels-ipe-sort-container" />');
-
-      // Move our gadgets outside of the sort container so that sortables
-      // cannot be placed after them.
-      $('div.panels-ipe-portlet-static', this).each(function() {
-        $(this).prependTo($(this).parent().parent());
-      });
+    // Create the Layout model/view.
+    var layout = new Drupal.panels_ipe.LayoutModel(settings.panels_ipe.layout);
+    layout.set({'regionCollection': region_collection});
+    var layout_view = new Drupal.panels_ipe.LayoutView({
+      'model': layout,
+      'el': "#panels-ipe-content"
     });
-  }
-
-  this.createSortContainers();
-
-};
-
-$(function() {
-  Drupal.ajax.prototype.commands.initIPE = function(ajax, data, status) {
-    if (Drupal.PanelsIPE.editors[data.key]) {
-      Drupal.PanelsIPE.editors[data.key].initEditing(data.data);
-      Drupal.PanelsIPE.editors[data.key].lockPath = data.lockPath;
-    }
-    Drupal.attachBehaviors();
-
-  };
-
-  Drupal.ajax.prototype.commands.IPEsetLockState = function(ajax, data, status) {
-    if (Drupal.PanelsIPE.editors[data.key]) {
-      Drupal.PanelsIPE.editors[data.key].lockPath = data.lockPath;
-    }
-  };
-
-  Drupal.ajax.prototype.commands.addNewPane = function(ajax, data, status) {
-    if (Drupal.PanelsIPE.editors[data.key]) {
-      Drupal.PanelsIPE.editors[data.key].changed = true;
-    }
-  };
-
-  Drupal.ajax.prototype.commands.cancelIPE = function(ajax, data, status) {
-    if (Drupal.PanelsIPE.editors[data.key]) {
-      Drupal.PanelsIPE.editors[data.key].cancelIPE();
-      Drupal.PanelsIPE.editors[data.key].endEditing();
-    }
-  };
-
-  Drupal.ajax.prototype.commands.unlockIPE = function(ajax, data, status) {
-    if (confirm(data.message)) {
-      var ajaxOptions = ajax.options;
-      ajaxOptions.url = data.break_path;
-      $.ajax(ajaxOptions);
-    }
-    else {
-      Drupal.PanelsIPE.editors[data.key].endEditing();
-    }
-  };
-
-  Drupal.ajax.prototype.commands.endIPE = function(ajax, data, status) {
-    if (Drupal.PanelsIPE.editors[data.key]) {
-      Drupal.PanelsIPE.editors[data.key].endEditing();
-    }
-  };
-
-  Drupal.ajax.prototype.commands.insertNewPane = function(ajax, data, status) {
-    IPEContainerSelector = '#panels-ipe-regionid-' + data.regionId + ' div.panels-ipe-sort-container';
-    firstPaneSelector = IPEContainerSelector + ' div.panels-ipe-portlet-wrapper:first';
-    // Insert the new pane before the first existing pane in the region, if
-    // any.
-    if ($(firstPaneSelector).length) {
-      insertData = {
-        'method': 'before',
-        'selector': firstPaneSelector,
-        'data': data.renderedPane,
-        'settings': null
-      }
-      Drupal.ajax.prototype.commands.insert(ajax, insertData, status);
-    }
-    // Else, insert it as a first child of the container. Doing so might fall
-    // outside of the wrapping markup for the style, but it's the best we can
-    // do.
-    else {
-      insertData = {
-        'method': 'prepend',
-        'selector': IPEContainerSelector,
-        'data': data.renderedPane,
-        'settings': null
-      }
-      Drupal.ajax.prototype.commands.insert(ajax, insertData, status);
-    }
-  };
-
-  /**
-   * Override the eventResponse on ajax.js so we can add a little extra
-   * behavior.
-   */
-  Drupal.ajax.prototype.ipeReplacedEventResponse = Drupal.ajax.prototype.eventResponse;
-  Drupal.ajax.prototype.eventResponse = function (element, event) {
-    if (element.ipeCancelThis) {
-      element.ipeCancelThis = null;
-      return false;
-    }
+    layout_view.render();
 
-    if ($(this.element).attr('id') == 'panels-ipe-cancel') {
-      if (!Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].cancelEditing()) {
-        return false;
-      }
-    }
-
-    var retval = this.ipeReplacedEventResponse(element, event);
-    if (this.ajaxing && this.element_settings.ipe_cache_key) {
-      // Move the throbber so that it appears outside our container.
-      if (this.progress.element) {
-        $(this.progress.element).addClass('ipe-throbber').appendTo($('body'));
-      }
-      Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].hideContainer();
-    }
-    // @TODO $('#panels-ipe-throbber-backdrop').remove();
-    return retval;
+    Drupal.panels_ipe.app.set({'layout': layout});
+    app_view.layoutView = layout_view;
   };
 
   /**
-   * Override the eventResponse on ajax.js so we can add a little extra
-   * behavior.
+   * Returns the urlRoot for all callbacks
    */
-  Drupal.ajax.prototype.ipeReplacedError = Drupal.ajax.prototype.error;
-  Drupal.ajax.prototype.error = function (response, uri) {
-    var retval = this.ipeReplacedError(response, uri);
-    if (this.element_settings.ipe_cache_key) {
-      Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].showContainer();
-    }
+  Drupal.panels_ipe.urlRoot = function(settings) {
+    return '/admin/panels_ipe/variant/' + settings.panels_ipe.display_variant.id;
   };
 
-  Drupal.ajax.prototype.ipeReplacedBeforeSerialize = Drupal.ajax.prototype.beforeSerialize;
-  Drupal.ajax.prototype.beforeSerialize = function (element_settings, options) {
-    if ($(this.element).hasClass('panels-ipe-save')) {
-      Drupal.PanelsIPE.editors[this.element_settings.ipe_cache_key].saveEditing();
-    };
-    return this.ipeReplacedBeforeSerialize(element_settings, options);
-  };
-
-});
-
-/**
- * Apply margin to bottom of the page.
- *
- * Note that directly applying marginBottom does not work in IE. To prevent
- * flickering/jumping page content with client-side caching, this is a regular
- * Drupal behavior.
- *
- * @see admin_menu.js via https://drupal.org/project/admin_menu
- */
-Drupal.behaviors.panelsIpeMarginBottom = {
-  attach: function () {
-    $('body:not(.panels-ipe)').addClass('panels-ipe');
-  }
-};
-
-})(jQuery);
+}(jQuery, _, Backbone, Drupal));
diff --git a/panels_ipe/js/views/AppView.js b/panels_ipe/js/views/AppView.js
new file mode 100644
index 0000000..f25c0d3
--- /dev/null
+++ b/panels_ipe/js/views/AppView.js
@@ -0,0 +1,180 @@
+/**
+ * @file
+ * The primary Backbone view for Panels IPE. For now this only controls the
+ * bottom tray, but in the future could have a larger scope.
+ *
+ * see Drupal.panels_ipe.AppModel
+ */
+
+(function ($, _, Backbone, Drupal) {
+
+  'use strict';
+
+  Drupal.panels_ipe.AppView = Backbone.View.extend(/** @lends Drupal.panels_ipe.AppView# */{
+
+    /**
+     * @type {function}
+     */
+    template: _.template('<div class="ipe-tab-wrapper"></div>'),
+
+    /**
+     * @type {Drupal.panels_ipe.TabsView}
+     */
+    tabsView: null,
+
+    /**
+     * @type {Drupal.panels_ipe.LayoutView}
+     */
+    layoutView: null,
+
+    /**
+     * @type {Drupal.panels_ipe.AppModel}
+     */
+    model: null,
+
+    /**
+     * @constructs
+     *
+     * @augments Backbone.View
+     *
+     * @param {object} options
+     *   An object with the following keys:
+     * @param {Drupal.panels_ipe.AppModel} options.model
+     *   The application state model.
+     * @param {Object} options.tabContentViews
+     *   An object mapping TabModel ids to arbitrary Backbone views.
+     */
+    initialize: function (options) {
+      this.model = options.model;
+
+      // Create a TabsView instance.
+      this.tabsView = new Drupal.panels_ipe.TabsView({
+        'collection': this.model.get('tabCollection'),
+        'tabViews': options.tabContentViews
+      });
+
+      // Listen to important global events throughout the app.
+      this.listenTo(this.model, 'changeLayout', this.changeLayout);
+
+      // Listen to tabs that don't have associated BackboneViews.
+      this.listenTo(this.model.get('editTab'), 'change:active', this.clickEditTab);
+      this.listenTo(this.model.get('saveTab'), 'change:active', this.clickSaveTab);
+    },
+
+    /**
+     * Appends the IPE tray to the bottom of the screen.
+     */
+    render: function() {
+      // Empty our list.
+      this.$el.html(this.template(this.model.toJSON()));
+      // Add our tab collection to the App.
+      this.tabsView.setElement(this.$('.ipe-tab-wrapper')).render();
+      // Re-render our layout.
+      if (this.layoutView) {
+        this.layoutView.render();
+      }
+      return this;
+    },
+
+    /**
+     * Actives all regions and blocks for editing.
+     */
+    openIPE: function() {
+      var active = this.model.get('active');
+      if (active) {
+        return;
+      }
+
+      // Set our active state correctly.
+      this.model.set({'active': true});
+
+      // Set the layout's active state correctly.
+      this.model.get('layout').set({'active': true});
+
+      this.$el.addClass('active');
+    },
+
+    /**
+     * Deactivate all regions and blocks for editing.
+     */
+    closeIPE: function() {
+      var active = this.model.get('active');
+      if (!active) {
+        return;
+      }
+
+      // Set our active state correctly.
+      this.model.set({'active': false});
+
+      // Set the layout's active state correctly.
+      this.model.get('layout').set({'active': false});
+
+      this.$el.removeClass('active');
+    },
+
+    /**
+     * Event callback for when a new layout has been selected.
+     */
+    changeLayout: function (args) {
+      // Grab the layout from the argument list.
+      var layout = args[0];
+
+      // Sync the layout from Drupal.
+      var self = this;
+      layout.fetch().done(function() {
+        // Grab all the blocks from the current layout.
+        var regions = self.model.get('layout').get('regionCollection');
+        var block_collection = new Drupal.panels_ipe.BlockCollection();
+        regions.each(function(region) {
+          block_collection.add(region.get('blockCollection').toJSON());
+        });
+
+        // Get the first region in the layout.
+        // @todo Be smarter about re-adding blocks.
+        var first_region = layout.get('regionCollection').at(0);
+
+        // Append all blocks from previous layout.
+        first_region.set({'blockCollection': block_collection});
+
+        // Change the default layout in our AppModel.
+        self.model.set({'layout': layout});
+
+        // Change the LayoutView's layout.
+        self.layoutView.changeLayout(layout);
+
+        // Re-render the app.
+        self.render();
+      });
+    },
+
+    /**
+     * Sets the IPE active state based on the "Edit" TabModel.
+     */
+    clickEditTab: function(){
+      var active = this.model.get('editTab').get('active');
+      if (active) {
+        this.openIPE();
+      }
+      else {
+        this.closeIPE();
+      }
+    },
+
+    /**
+     * Saves our layout to the server.
+     */
+    clickSaveTab: function(){
+      if (this.model.get('saveTab').get('active')) {
+        // Save the Layout and disable the tab.
+        var self = this;
+        self.model.get('saveTab').set({'loading': true});
+        this.model.get('layout').save().done(function() {
+          self.model.get('saveTab').set({'loading': false, 'active': false});
+          self.tabsView.render();
+        });
+      }
+    }
+
+  });
+
+}(jQuery, _, Backbone, Drupal));
diff --git a/panels_ipe/js/views/BlockView.js b/panels_ipe/js/views/BlockView.js
new file mode 100644
index 0000000..f7f9dff
--- /dev/null
+++ b/panels_ipe/js/views/BlockView.js
@@ -0,0 +1,77 @@
+/**
+ * @file
+ * The primary Backbone view for a Block.
+ *
+ * see Drupal.panels_ipe.BlockModel
+ */
+
+(function ($, _, Backbone, Drupal) {
+
+  'use strict';
+
+  Drupal.panels_ipe.BlockView = Backbone.View.extend(/** @lends Drupal.panels_ipe.BlockView# */{
+
+    /**
+     * @type {function}
+     */
+    template_actions: _.template(
+      '<div class="ipe-actions" data-block-action-id="<%= uuid %>">' +
+      '  <h5>Block: <%= label %></h5>' +
+      '  <ul class="ipe-action-list">' +
+      '    <li data-action-id="up">' +
+      '      <a>▲</a>' +
+      '    </li>' +
+      '    <li data-action-id="down">' +
+      '      <a>▼</a>' +
+      '    </li>' +
+      '    <li data-action-id="move">' +
+      '      <select><option>Move</option></select>' +
+      '    </li>' +
+      '  </ul>' +
+      '</div>'
+    ),
+
+    /**
+     * @type {Drupal.panels_ipe.BlockModel}
+     */
+    model: null,
+
+    /**
+     * @constructs
+     *
+     * @augments Backbone.View
+     *
+     * @param {object} options
+     *   An object with the following keys:
+     * @param {Drupal.panels_ipe.BlockModel} options.model
+     *   The block state model.
+     * @param {string} options.el
+     *   An optional selector if an existing element is already on screen.
+     */
+    initialize: function (options) {
+      this.model = options.model;
+      // An element already exists and our HTML properly isn't set.
+      // This only occurs on initial page load for performance reasons.
+      if (options.el && !this.model.get('html')) {
+        this.model.set({'html': this.$el.prop('outerHTML')});
+      }
+      this.listenTo(this.model, 'reset', this.render);
+      this.listenTo(this.model, 'change:active', this.render);
+    },
+
+    /**
+     * Renders the wrapping elements and refreshes a block model.
+     */
+    render: function() {
+      // Replace our current HTML.
+      this.$el.replaceWith(this.model.get('html'));
+      this.setElement("[data-block-id='" + this.model.get('uuid') + "']");
+      if (this.model.get('active')) {
+        this.$el.prepend(this.template_actions(this.model.toJSON()));
+      }
+      return this;
+    }
+
+  });
+
+}(jQuery, _, Backbone, Drupal));
diff --git a/panels_ipe/js/views/LayoutPicker.js b/panels_ipe/js/views/LayoutPicker.js
new file mode 100644
index 0000000..0dc594c
--- /dev/null
+++ b/panels_ipe/js/views/LayoutPicker.js
@@ -0,0 +1,112 @@
+/**
+ * @file
+ * Renders a collection of Layouts for selection.
+ *
+ * see Drupal.panels_ipe.LayoutCollection
+ */
+
+(function ($, _, Backbone, Drupal) {
+
+  'use strict';
+
+  Drupal.panels_ipe.LayoutPicker = Backbone.View.extend(/** @lends Drupal.panels_ipe.LayoutPicker# */{
+
+    /**
+     * @type {function}
+     */
+    template_layout: _.template('<li class="ipe-layout" data-layout-id="<%= id %>"><h5 class="ipe-layout-title"><a><%= label %></a></h5></li>'),
+
+    /**
+     * @type {function}
+     */
+    template_current: _.template('<p>Current Layout: </p><h5 class="ipe-layout-title"><%= label %></h5>'),
+
+    /**
+     * @type {function}
+     */
+    template_loading: _.template(
+      '<span class="ipe-icon ipe-icon-loading"></span>'
+    ),
+
+    /**
+     * @type {function}
+     */
+    template: _.template(
+      '<div class="ipe-current-layout"></div><div class="ipe-all-layouts"><p>Available Layouts:</p><ul class="ipe-layouts"></ul></div>'
+    ),
+
+    /**
+     * @type {Drupal.panels_ipe.LayoutCollection}
+     */
+    collection: null,
+
+    /**
+     * @type {object}
+     */
+    events: {
+      'click .ipe-layout': 'selectLayout'
+    },
+
+    /**
+     * Renders the selection menu for picking Layouts.
+     */
+    render: function() {
+      // If we don't have layouts yet, pull some from the server.
+      if (!this.collection) {
+        // Indicate an AJAX request.
+        this.$el.html(this.template_loading());
+
+        // Fetch a list of layouts from the server.
+        this.collection = new Drupal.panels_ipe.LayoutCollection;
+        var self = this;
+        this.collection.fetch().done(function(){
+          // We have a collection now, re-render ourselves.
+          self.render();
+        });
+      }
+      // Render our LayoutCollection.
+      else {
+        this.$el.empty();
+
+        // Setup the empty list.
+        this.$el.html(this.template());
+
+        // Append each layout option.
+        this.collection.each(function(layout) {
+          if (!layout.get('current')) {
+            this.$('.ipe-layouts').append(this.template_layout(layout.toJSON()));
+          }
+          else {
+            this.$('.ipe-current-layout').append(this.template_current(layout.toJSON()));
+          }
+        }, this);
+      }
+    },
+
+    /**
+     * Fires a global Backbone event that the App watches to switch layouts.
+     */
+    selectLayout: function(e) {
+      e.preventDefault();
+      var id = $(e.currentTarget).data('layout-id');
+
+      // Unset the current tab.
+      this.collection.each(function(layout) {
+        if (id == layout.id) {
+          layout.set('current', true);
+          // Indicate an AJAX request.
+          this.$el.html(this.template_loading());
+
+          // Only the AppView is aware of the rendered Layout.
+          // @todo Investigate using non-global events.
+          Drupal.panels_ipe.app.trigger('changeLayout', [layout]);
+        }
+        else {
+          layout.set('current', false);
+        }
+      }, this);
+    }
+
+  });
+
+}(jQuery, _, Backbone, Drupal));
diff --git a/panels_ipe/js/views/LayoutView.js b/panels_ipe/js/views/LayoutView.js
new file mode 100644
index 0000000..ae512e4
--- /dev/null
+++ b/panels_ipe/js/views/LayoutView.js
@@ -0,0 +1,233 @@
+/**
+ * @file
+ * The primary Backbone view for a Layout.
+ *
+ * see Drupal.panels_ipe.LayoutModel
+ */
+
+(function ($, _, Backbone, Drupal) {
+
+  'use strict';
+
+  Drupal.panels_ipe.LayoutView = Backbone.View.extend(/** @lends Drupal.panels_ipe.LayoutView# */{
+
+    /**
+     * @type {function}
+     */
+    template_region_actions: _.template(
+      '<div class="ipe-actions" data-region-action-id="<%= name %>">' +
+      '  <h5>Region: <%= name %></h5>' +
+      '  <ul class="ipe-action-list"></ul>' +
+      '</div>'
+    ),
+
+    /**
+     * @type {function}
+     */
+    template_region_option: _.template(
+      '<option data-region-option-name="<%= name %>"><%= name %></option>'
+    ),
+
+    /**
+     * @type {Drupal.panels_ipe.LayoutModel}
+     */
+    model: null,
+
+    /**
+     * @type {Array}
+     *   An array of child Drupal.panels_ipe.BlockView objects.
+     */
+    blockViews: [],
+
+    /**
+     * @type {object}
+     */
+    events: {
+      'mousedown [data-action-id="move"] > select': 'showBlockRegionList',
+      'blur [data-action-id="move"] > select': 'hideBlockRegionList',
+      'change [data-action-id="move"] > select': 'selectBlockRegionList',
+      'click [data-action-id="up"]': 'moveBlock',
+      'click [data-action-id="down"]': 'moveBlock'
+    },
+
+    /**
+     * @constructs
+     *
+     * @augments Backbone.View
+     *
+     * @param {object} options
+     *   An object with the following keys:
+     * @param {Drupal.panels_ipe.LayoutModel} options.model
+     *   The layout state model.
+     */
+    initialize: function (options) {
+      this.model = options.model;
+      // Initialize our html, this never changes.
+      if (this.model.get('html')) {
+        this.$el.html(this.model.get('html'));
+      }
+      this.listenTo(this.model, 'change:active', this.changeState);
+    },
+
+    /**
+     * Re-renders our blocks, we have no HTML to be re-rendered.
+     */
+    render: function() {
+      // Remove all existing BlockViews.
+      for (var i in this.blockViews) {
+        this.blockViews[i].remove();
+      }
+      this.blockViews = [];
+
+      // Re-attach all BlockViews to appropriate regions.
+      this.model.get('regionCollection').each(function (region) {
+        region.get('blockCollection').each(function (block) {
+          // Attach an empty element for our View to attach itself to.
+          if (this.$('[data-block-id="' + block.get('uuid') + '"]').length == 0) {
+            var empty_elem = $('<div data-block-id="' + block.get('uuid') + '">');
+            this.$('[data-region-name="' + region.get('name') + '"]').append(empty_elem);
+          }
+
+          // Attach a View to this empty element.
+          var block_view = new Drupal.panels_ipe.BlockView({
+            'model': block,
+            'el': '[data-block-id="' + block.get('uuid') + '"]'
+          });
+          this.blockViews.push(block_view);
+
+          // Fetch the Block's content from the server, if needed.
+          if (!block.get('html')) {
+            block.fetch();
+          }
+          else {
+            block_view.render();
+          }
+
+        }, this);
+      }, this);
+
+      return this;
+    },
+
+    /**
+     * Prepends Regions and Blocks with action items.
+     */
+    changeState: function(model, value, options) {
+      // Toggles the action headers on each RegionView and BlockView.
+      this.model.get('regionCollection').each(function (region) {
+        // Prepend or remove the action header for this region.
+        if (value) {
+          var selector = '[data-region-name="' + region.get('name') + '"]';
+          this.$(selector).prepend(this.template_region_actions(region.toJSON()));
+        }
+        else {
+          this.$('.ipe-actions').remove();
+        }
+
+        // BlockViews handle their own rendering, so just set the active value here.
+        region.get('blockCollection').each(function (block) {
+          block.set({'active': value});
+        }, this);
+      }, this);
+      this.render();
+    },
+
+    /**
+     * Replaces the "Move" button with a select list of regions.
+     */
+    showBlockRegionList: function(e) {
+      // Get the BlockModel id (uuid).
+      var id = $(e.currentTarget).closest('[data-block-action-id]').data('block-action-id');
+
+      $(e.currentTarget).empty();
+
+      // Add other regions to select list.
+      this.model.get('regionCollection').each(function (region) {
+        // If this is the current region, place it first in the list.
+        if (region.get('blockCollection').get(id)) {
+          $(e.currentTarget).prepend(this.template_region_option(region.toJSON()));
+        }
+        else {
+          $(e.currentTarget).append(this.template_region_option(region.toJSON()));
+        }
+      }, this);
+    },
+
+    /**
+     * Hides the region selector.
+     */
+    hideBlockRegionList: function(e) {
+      $(e.currentTarget).html('<option>Move</option>');
+    },
+
+    /**
+     * React to a new region being selected.
+     */
+    selectBlockRegionList: function(e) {
+      // Get the BlockModel id (uuid).
+      var id = $(e.currentTarget).closest('[data-block-action-id]').data('block-action-id');
+
+      // Grab the value of this region.
+      var region_name = $(e.currentTarget).children(':selected').data('region-option-name');
+
+      // First, remove the Block from the current region.
+      var block;
+      var region_collection = this.model.get('regionCollection');
+      region_collection.each(function (region) {
+        var block_collection = region.get('blockCollection');
+        if (block_collection.get(id)) {
+          block = block_collection.get(id);
+          block_collection.remove(block);
+        }
+      });
+
+      // Next, add the Block to the new region.
+      if (block) {
+        var region = this.model.get('regionCollection').get(region_name);
+        region.get('blockCollection').add(block);
+      }
+
+      // Hide the select list.
+      this.hideBlockRegionList(e);
+
+      // Re-render.
+      this.render();
+    },
+
+    /**
+     * Changes the LayoutModel for this view.
+     *
+     * @param {Drupal.panels_ipe.LayoutModel} layout
+     */
+    changeLayout: function (layout) {
+      // Stop listening to the current model.
+      this.stopListening(this.model);
+      // Initialize with the new model.
+      this.initialize({'model': layout});
+    },
+
+    /**
+     * Moves a block up or down in its RegionModel's BlockCollection.
+     */
+    moveBlock: function(e) {
+      // Get the BlockModel id (uuid).
+      var id = $(e.currentTarget).closest('[data-block-action-id]').data('block-action-id');
+
+      // Get the direction the block is moving.
+      var dir = $(e.currentTarget).data('action-id');
+
+      // Grab the model for this region.
+      var region_name = $(e.currentTarget).closest('[data-region-name]').data('region-name');
+      var region = this.model.get('regionCollection').get(region_name);
+      var block = region.get('blockCollection').get(id);
+
+      // Shift the Block.
+      region.get('blockCollection').shift(block, dir);
+
+      // Re-render ourselves.
+      this.render();
+    }
+
+  });
+
+}(jQuery, _, Backbone, Drupal));
diff --git a/panels_ipe/js/views/TabsView.js b/panels_ipe/js/views/TabsView.js
new file mode 100644
index 0000000..d892cce
--- /dev/null
+++ b/panels_ipe/js/views/TabsView.js
@@ -0,0 +1,147 @@
+/**
+ * @file
+ * The primary Backbone view for a tab collection.
+ *
+ * see Drupal.panels_ipe.TabCollection
+ */
+
+(function ($, _, Backbone, Drupal, drupalSettings) {
+
+  Drupal.panels_ipe.TabsView = Backbone.View.extend(/** @lends Drupal.panels_ipe.TabsView# */{
+
+    /**
+     * @type {function}
+     */
+    template_tab: _.template(
+      '<li class="ipe-tab<% if (active) { %> active<% } %>" data-tab-id="<%= id %>">' +
+      '  <a title="<%= title %>"><span class="ipe-icon ipe-icon-<% if (loading) { %>loading<% } else { print(id) } %>"></span><%= title %></a>' +
+      '</li>'
+    ),
+
+    /**
+     * @type {function}
+     */
+    template_content: _.template('<div class="ipe-tab-content<% if (active) { %> active<% } %>" data-tab-content-id="<%= id %>"></div>'),
+
+    /**
+     * @type {object}
+     */
+    events: {
+      'click .ipe-tab > a': 'switchTab'
+    },
+
+    /**
+     * @type {Drupal.panels_ipe.TabCollection}
+     */
+    collection: null,
+
+    /**
+     * @type {Object}
+     *
+     * An object mapping tab IDs to Backbone views.
+     */
+    tabViews: {},
+
+    /**
+     * @constructs
+     *
+     * @augments Backbone.TabsView
+     *
+     * @param {object} options
+     *   An object with the following keys:
+     * @param {object} options.tabViews
+     *   An object mapping tab IDs to Backbone views.
+     */
+    initialize: function (options) {
+      this.tabViews = options.tabViews;
+    },
+
+    /**
+     * Renders our tab collection.
+     */
+    render: function() {
+      // Empty our list.
+      this.$el.empty();
+
+      // Setup the initial wrapping elements.
+      this.$el.append('<ul class="ipe-tabs"></ul>');
+      this.$el.append('<div class="ipe-tabs-content"></div>');
+
+      // Append each of our tabs and their tab content view.
+      this.collection.each(function(tab) {
+        // Append the tab.
+        var id = tab.get('id');
+
+        this.$('.ipe-tabs').append(this.template_tab(tab.toJSON()));
+
+        // Render the tab content.
+        this.$('.ipe-tabs-content').append(this.template_content(tab.toJSON()));
+        // Check to see if this tab has content.
+        if (tab.get('active') && this.tabViews[id]) {
+          this.tabViews[id].setElement('[data-tab-content-id="' + id + '"]').render();
+        }
+      }, this);
+
+      return this;
+    },
+
+    /**
+     * Switches the current tab.
+     */
+    switchTab: function(e) {
+      e.preventDefault();
+      var id = $(e.currentTarget).parent().data('tab-id');
+
+      // Disable all existing tabs.
+      var animation = null;
+      var already_open = false;
+      this.collection.each(function(tab) {
+        // If the tab is loading, do nothing.
+        if (tab.get('loading')) {
+          return;
+        }
+
+        // Don't repeat comparisons, if possible.
+        var clicked = tab.get('id') == id;
+        var active = tab.get('active');
+
+        // If the user is clicking the same tab twice, close it.
+        if (clicked && active) {
+          tab.set('active', false);
+          animation = 'slideUp';
+        }
+        // If this is the first click, open the tab.
+        else if (clicked) {
+          tab.set('active', true);
+          animation = 'slideDown';
+        }
+        // The tab wasn't clicked, make sure it's closed.
+        else {
+          // Mark that the View was already open.
+          if (active) {
+            already_open = true;
+          }
+          tab.set('active', false);
+        }
+      });
+
+      // Trigger a re-render, with animation if needed.
+      if (animation == 'slideUp') {
+        // Close the tab, then re-render.
+        var self = this;
+        this.$('.ipe-tabs-content')[animation]('fast', function() { self.render(); });
+      }
+      else if (animation == 'slideDown' && !already_open) {
+        // We need to render first as hypothetically nothing is open.
+        this.render();
+        this.$('.ipe-tabs-content').hide();
+        this.$('.ipe-tabs-content')[animation]('fast');
+      }
+      else {
+        this.render();
+      }
+    }
+
+  });
+
+}(jQuery, _, Backbone, Drupal, drupalSettings));
diff --git a/panels_ipe/panels_ipe.info b/panels_ipe/panels_ipe.info
deleted file mode 100644
index b0172a7..0000000
--- a/panels_ipe/panels_ipe.info
+++ /dev/null
@@ -1,7 +0,0 @@
-name = Panels In-Place Editor
-description = Provide a UI for managing some Panels directly on the frontend, instead of having to use the backend.
-package = "Panels"
-dependencies[] = panels
-core = 7.x
-configure = admin/structure/panels
-files[] = panels_ipe.module
diff --git a/panels_ipe/panels_ipe.info.yml b/panels_ipe/panels_ipe.info.yml
new file mode 100644
index 0000000..9786c02
--- /dev/null
+++ b/panels_ipe/panels_ipe.info.yml
@@ -0,0 +1,8 @@
+name: Panels IPE
+type: module
+description: Panels In-place editor.
+core: 8.x
+package: Panels
+dependencies:
+  - panels
+  - page_manager
\ No newline at end of file
diff --git a/panels_ipe/panels_ipe.libraries.yml b/panels_ipe/panels_ipe.libraries.yml
new file mode 100644
index 0000000..32cf78d
--- /dev/null
+++ b/panels_ipe/panels_ipe.libraries.yml
@@ -0,0 +1,30 @@
+panels_ipe:
+  version: VERSION
+  js:
+    # Core.
+    js/panels_ipe.js: {}
+    # Models.
+    js/models/AppModel.js: {}
+    js/models/BlockModel.js: {}
+    js/models/RegionModel.js: {}
+    js/models/TabModel.js: {}
+    js/models/LayoutModel.js: {}
+    # Views.
+    js/views/AppView.js: {}
+    js/views/BlockView.js: {}
+    js/views/LayoutPicker.js: {}
+    js/views/LayoutView.js: {}
+    js/views/TabsView.js: {}
+  css:
+    component:
+      css/panels_ipe.css: {}
+  dependencies:
+    - core/jquery
+    - core/jquery.once
+    - core/jquery.ui
+    - core/jquery.ui.draggable
+    - core/underscore
+    - core/backbone
+    - core/drupal
+    - core/drupal.form
+    - core/drupalSettings
diff --git a/panels_ipe/panels_ipe.module b/panels_ipe/panels_ipe.module
index c309684..18d7d94 100644
--- a/panels_ipe/panels_ipe.module
+++ b/panels_ipe/panels_ipe.module
@@ -1,266 +1,26 @@
 <?php
 
 /**
- * Implementation of hook_ctools_plugin_directory().
+ * @file
+ * Contains panels_ipe.module.
  */
-function panels_ipe_ctools_plugin_directory($module, $plugin) {
-  if ($module == 'panels' && $plugin == 'display_renderers') {
-    return 'plugins/' . $plugin;
-  }
-}
-
-/**
- * Implementation of hook_ctools_plugin_api().
- *
- * Inform CTools about version information for various plugins implemented by
- * Panels.
- *
- * @param string $owner
- *   The system name of the module owning the API about which information is
- *   being requested.
- * @param string $api
- *   The name of the API about which information is being requested.
- */
-function panels_ipe_ctools_plugin_api($owner, $api) {
-  if ($owner == 'panels' && $api == 'pipelines') {
-    return array(
-      'version' => 1,
-      'path' => drupal_get_path('module', 'panels_ipe') . '/includes',
-    );
-  }
-}
 
-/**
- * Implementation of hook_theme().
- */
-function panels_ipe_theme() {
-  return array(
-    'panels_ipe_pane_wrapper' => array(
-      'variables' => array('output' => NULL, 'pane' => NULL, 'display' => NULL, 'renderer' => NULL),
-    ),
-    'panels_ipe_region_wrapper' => array(
-      'variables' => array('output' => NULL, 'region_id' => NULL, 'display' => NULL, 'controls' => NULL, 'renderer' => NULL),
-    ),
-    'panels_ipe_add_pane_button' => array(
-      'variables' => array('region_id' => NULL, 'display' => NULL, 'renderer' => NULL),
-    ),
-    'panels_ipe_placeholder_pane' => array(
-      'variables' => array('region_id' => NULL, 'region_title' => NULL),
-    ),
-    'panels_ipe_dnd_form_container' => array(
-      'variables' => array('link' => NULL, 'cache_key' => NULL, 'display' => NULL),
-    ),
-    'panels_ipe_toolbar' => array(
-      'variables' => array('buttons' => NULL),
-    ),
-  );
-}
+use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
- * Theme the 'placeholder' pane, which is shown on an active IPE when no panes
- * live in that region.
- *
- * @param string $region_id
- * @param string $region_title
+ * Implements hook_help().
  */
-function theme_panels_ipe_placeholder_pane($vars) {
-  $region_id = $vars['region_id'];
-  $region_title = $vars['region_title'];
-
-  $output = '<div class="panels-ipe-placeholder-content">';
-  $output .= "<h3>$region_title</h3>";
-  $output .= '</div>';
-  return $output;
-}
-
-function template_preprocess_panels_ipe_pane_wrapper(&$vars) {
-  $pane = $vars['pane'];
-  $display = $vars['display'];
-  $renderer = $vars['renderer'];
-
-  $content_type = ctools_get_content_type($pane->type);
-  $subtype = ctools_content_get_subtype($content_type, $pane->subtype);
-  $vars['links'] = array();
-
-  if (ctools_content_editable($content_type, $subtype, $pane->configuration)) {
-    $vars['links']['edit'] = array(
-      'title' => isset($content_type['edit text']) ? '<span>' . $content_type['edit text'] . '</span>' : '<span>' . t('Settings') . '</span>',
-      'href' => $renderer->get_url('edit-pane', $pane->pid),
-      'html' => TRUE,
-      'attributes' => array(
-        'class' => array('ctools-use-modal', 'panels-ipe-hide-bar'),
-        'title' => isset($content_type['edit text']) ? $content_type['edit text'] : t('Settings'),
-        // 'id' => "pane-edit-panel-pane-$pane->pid",
-      ),
-    );
-  }
+function panels_ipe_help($route_name, RouteMatchInterface $route_match) {
+  switch ($route_name) {
+    // Main module help for the panels_ipe module.
+    case 'help.page.panels_ipe':
+      $output = '';
+      $output .= '<h3>' . t('About') . '</h3>';
+      $output .= '<p>' . t('Panels In-place editor.') . '</p>';
+      // @todo: Add useful help text for Panels In-place editor.
+      return $output;
 
-  // Add option to configure style in IPE
-  if (user_access('administer panels styles')) {
-    $vars['links']['style'] = array(
-      'title' => '<span>' . t('Style') . '</span>',
-      'href' => $renderer->get_url('style-type', 'pane', $pane->pid),
-      'html' => TRUE,
-      'attributes' => array(
-        'class' => array('ctools-use-modal', 'panels-ipe-hide-bar'),
-        'title' => t('Style'),
-      ),
-    );
+    default:
   }
-
-  // Deleting is managed entirely in the js; this is just an attachment point
-  // for it
-  $vars['links']['delete'] = array(
-    'title' => '<span>' . t('Delete') . '</span>',
-    'href' => '#',
-    'html' => TRUE,
-    'attributes' => array(
-      'class' => 'pane-delete',
-      'id' => "pane-delete-panel-pane-$pane->pid",
-      'title' => t('Delete'),
-    ),
-  );
-
-  $context = array(
-    'pane' => $pane,
-    'display' => $display,
-    'renderer' => $renderer
-  );
-  drupal_alter('panels_ipe_pane_links', $vars['links'], $context);
-
-}
-
-function theme_panels_ipe_pane_wrapper($vars) {
-  $output = $vars['output'];
-  $pane = $vars['pane'];
-
-  $attributes = array(
-    'class' => 'panels-ipe-linkbar',
-  );
-
-  $links = theme('links', array('links' => $vars['links'], 'attributes' => $attributes));
-
-  if (!empty($pane->locks['type']) && $pane->locks['type'] == 'immovable') {
-    $links = '<div class="panels-ipe-dragbar panels-ipe-nodraghandle clearfix">' . $links . '</div>';
-  }
-  else {
-    $links = '<div class="panels-ipe-dragbar panels-ipe-draghandle clearfix">' . $links . '<span class="panels-ipe-draghandle-icon"><span class="panels-ipe-draghandle-icon-inner"></span></span></div>';
-  }
-
-  $handlebar = '<div class="panels-ipe-handlebar-wrapper panels-ipe-on">' . $links . '</div>';
-
-  return $handlebar . $output;
 }
 
-function theme_panels_ipe_region_wrapper($vars) {
-  return $vars['controls'] . $vars['output'];
-}
-
-function template_preprocess_panels_ipe_add_pane_button(&$vars) {
-  $region_id = $vars['region_id'];
-  $display = $vars['display'];
-  $renderer = $vars['renderer'];
-  $vars['links'] = '';
-
-  // Add option to configure style in IPE
-  if (user_access('administer panels styles')) {
-    $vars['links']['style'] = array(
-      'title' => '<span>' . t('Region style') . '</span>',
-      'href' => $renderer->get_url('style-type', 'region', $region_id),
-      'html' => TRUE,
-      'attributes' => array(
-        'class' => array('ctools-use-modal', 'panels-ipe-hide-bar', 'style'),
-        'title' => t('Region style'),
-      ),
-    );
-  }
-
-  // Add option to add items in the IPE
-  $vars['links']['add-pane'] = array(
-    'title' => '<span>' . t('Add new pane') . '</span>',
-    'href' => $renderer->get_url('select-content', $region_id),
-    'attributes' => array(
-      'class' => array('ctools-use-modal', 'add', 'panels-ipe-hide-bar'),
-      'title' => t('Add new pane'),
-    ),
-    'html' => TRUE,
-  );
-
-  $context = array(
-    'region_id' => $region_id,
-    'display' => $display,
-    'renderer' => $renderer,
-  );
-  drupal_alter('panels_ipe_region_links', $vars['links'], $context);
-
-}
-
-function theme_panels_ipe_add_pane_button($vars) {
-  $attributes = array(
-    'class' => array('panels-ipe-linkbar', 'inline'),
-  );
-
-  $links = theme('links', array('links' => $vars['links'], 'attributes' => $attributes));
-
-  return '<div class="panels-ipe-newblock panels-ipe-on">' . $links . '</div>';
-}
-
-/**
- * @deprecated
- */
-function panels_ipe_get_cache_key($key = NULL) {
-  return array();
-}
-
-/**
- * Add a button to the IPE toolbar.
- */
-function panels_ipe_toolbar_add_button($cache_key, $id, $button) {
-  $buttons = &drupal_static('panels_ipe_toolbar_buttons', array());
-  $buttons[$cache_key][$id] = $button;
-}
-
-/**
- * Implementation of hook_footer()
- *
- * Adds the IPE control container.
- *
- * @param unknown_type $main
- */
-function panels_ipe_page_alter(&$page) {
-  $buttons = &drupal_static('panels_ipe_toolbar_buttons', array());
-  if (empty($buttons)) {
-    return;
-  }
-
-  $output = theme('panels_ipe_toolbar', array('buttons' => $buttons));
-
-  $page['page_bottom']['panels_ipe'] = array(
-    '#markup' => $output,
-  );
-}
-
-function theme_panels_ipe_toolbar($vars) {
-  $buttons = $vars['buttons'];
-
-  $output = "<div id='panels-ipe-control-container' class='clearfix'>";
-  foreach ($buttons as $key => $ipe_buttons) {
-    $output .= "<div id='panels-ipe-control-$key' class='panels-ipe-control'>";
-
-    // Controls in this container will appear when the IPE is not on.
-    $output .= '<div class="panels-ipe-button-container clearfix">';
-    foreach ($ipe_buttons as $button) {
-      $output .= is_string($button) ? $button : drupal_render($button);
-    }
-    $output .= '</div>';
-
-    // Controls in this container will appear when the IPE is on. It is usually
-    // filled via AJAX.
-    $output .= '<div class="panels-ipe-form-container clearfix"></div>';
-    $output .= '</div>';
-  }
-
-  $output .= "</div>";
-
-  return $output;
-}
diff --git a/panels_ipe/panels_ipe.permissions.yml b/panels_ipe/panels_ipe.permissions.yml
new file mode 100644
index 0000000..6350b38
--- /dev/null
+++ b/panels_ipe/panels_ipe.permissions.yml
@@ -0,0 +1,2 @@
+access panels in-place editing:
+  title: 'Access panels in-place editing'
\ No newline at end of file
diff --git a/panels_ipe/panels_ipe.routing.yml b/panels_ipe/panels_ipe.routing.yml
new file mode 100644
index 0000000..6bdba2e
--- /dev/null
+++ b/panels_ipe/panels_ipe.routing.yml
@@ -0,0 +1,39 @@
+panels_ipe.block:
+  path: '/admin/panels_ipe/variant/{variant_id}/block/{block_id}'
+  defaults:
+      _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getBlock'
+  requirements:
+    _permission: 'access panels in-place editing'
+    _method: 'GET'
+
+panels_ipe.layouts:
+  path: '/admin/panels_ipe/variant/{variant_id}/layouts'
+  defaults:
+      _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getLayouts'
+  requirements:
+    _permission: 'access panels in-place editing'
+    _method: 'GET'
+
+panels_ipe.layout:
+  path: '/admin/panels_ipe/variant/{variant_id}/layouts/{layout_id}'
+  defaults:
+      _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::getLayout'
+  requirements:
+    _permission: 'access panels in-place editing'
+    _method: 'GET'
+
+panels_ipe.layout.update:
+  path: '/admin/panels_ipe/variant/{variant_id}/layouts/{layout_id}'
+  defaults:
+      _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::updateLayout'
+  requirements:
+    _permission: 'access panels in-place editing'
+    _method: 'PUT'
+
+panels_ipe.layout.save:
+  path: '/admin/panels_ipe/variant/{variant_id}/layouts/{layout_id}'
+  defaults:
+      _controller: '\Drupal\panels_ipe\Controller\PanelsIPEPageController::createLayout'
+  requirements:
+    _permission: 'access panels in-place editing'
+    _method: 'POST'
\ No newline at end of file
diff --git a/panels_ipe/plugins/display_renderers/ipe.inc b/panels_ipe/plugins/display_renderers/ipe.inc
deleted file mode 100644
index ab63f9e..0000000
--- a/panels_ipe/plugins/display_renderers/ipe.inc
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php
-
-$plugin = array(
-  'renderer' => 'panels_renderer_ipe',
-);
diff --git a/panels_ipe/plugins/display_renderers/panels_renderer_ipe.class.php b/panels_ipe/plugins/display_renderers/panels_renderer_ipe.class.php
deleted file mode 100644
index f1b32c5..0000000
--- a/panels_ipe/plugins/display_renderers/panels_renderer_ipe.class.php
+++ /dev/null
@@ -1,466 +0,0 @@
-<?php
-
-/**
- * Renderer class for all In-Place Editor (IPE) behavior.
- */
-class panels_renderer_ipe extends panels_renderer_editor {
-  // The IPE operates in normal render mode, not admin mode.
-  var $admin = FALSE;
-
-  function render() {
-    $output = parent::render();
-    return "<div id='panels-ipe-display-{$this->clean_key}' class='panels-ipe-display-container'>$output</div>";
-  }
-
-  function add_meta() {
-    ctools_include('display-edit', 'panels');
-    ctools_include('content');
-
-    if (empty($this->display->cache_key)) {
-      $this->cache = panels_edit_cache_get_default($this->display);
-    }
-    // @todo we may need an else to load the cache, but I am not sure we
-    // actually need to load it if we already have our cache key, and doing
-    // so is a waste of resources.
-
-    ctools_include('cleanstring');
-    $this->clean_key = ctools_cleanstring($this->display->cache_key);
-    $button = array(
-      '#type' => 'link',
-      '#title' => t('Customize this page'),
-      '#href' => $this->get_url('save_form'),
-      '#id' => 'panels-ipe-customize-page',
-      '#attributes' => array(
-        'class' => array('panels-ipe-startedit', 'panels-ipe-pseudobutton'),
-      ),
-      '#ajax' => array(
-        'progress' => 'throbber',
-        'ipe_cache_key' => $this->clean_key,
-      ),
-      '#prefix' => '<div class="panels-ipe-pseudobutton-container">',
-      '#suffix' => '</div>',
-    );
-
-    panels_ipe_toolbar_add_button($this->clean_key, 'panels-ipe-startedit', $button);
-
-    // @todo this actually should be an IPE setting instead.
-    if (user_access('change layouts in place editing')) {
-      $button = array(
-        '#type' => 'link',
-        '#title' => t('Change layout'),
-        '#href' => $this->get_url('change_layout'),
-        '#attributes' => array(
-          'class' => array('panels-ipe-change-layout', 'panels-ipe-pseudobutton', 'ctools-modal-layout'),
-        ),
-        '#ajax' => array(
-          'progress' => 'throbber',
-          'ipe_cache_key' => $this->clean_key,
-        ),
-
-      '#prefix' => '<div class="panels-ipe-pseudobutton-container">',
-      '#suffix' => '</div>',
-      );
-
-      panels_ipe_toolbar_add_button($this->clean_key, 'panels-ipe-change-layout', $button);
-    }
-
-    ctools_include('ajax');
-    ctools_include('modal');
-    ctools_modal_add_js();
-
-    ctools_add_css('panels_dnd', 'panels');
-    ctools_add_css('panels_admin', 'panels');
-    ctools_add_js('panels_ipe', 'panels_ipe');
-    ctools_add_css('panels_ipe', 'panels_ipe');
-
-    drupal_add_js(array('PanelsIPECacheKeys' => array($this->clean_key)), 'setting');
-
-    drupal_add_library('system', 'ui.draggable');
-    drupal_add_library('system', 'ui.droppable');
-    drupal_add_library('system', 'ui.sortable');
-
-    parent::add_meta();
-  }
-
-  /**
-   * Override & call the parent, then pass output through to the dnd wrapper
-   * theme function.
-   *
-   * @param $pane
-   */
-  function render_pane(&$pane) {
-    $output = parent::render_pane($pane);
-    if (empty($output)) {
-      return;
-    }
-
-    // If there are region locks, add them.
-    if (!empty($pane->locks['type']) && $pane->locks['type'] == 'regions') {
-      static $key = NULL;
-      $javascript = &drupal_static('drupal_add_js', array());
-
-      // drupal_add_js breaks as we add these, but we can't just lump them
-      // together because panes can be rendered independently. So game the system:
-      if (empty($key)) {
-        $settings['Panels']['RegionLock'][$pane->pid] = $pane->locks['regions'];
-        drupal_add_js($settings, 'setting');
-
-        // These are just added via [] so we have to grab the last one
-        // and reference it.
-        $keys = array_keys($javascript['settings']['data']);
-        $key = end($keys);
-      }
-      else {
-        $javascript['settings']['data'][$key]['Panels']['RegionLock'][$pane->pid] = $pane->locks['regions'];
-      }
-
-    }
-
-    if (empty($pane->IPE_empty)) {
-      // Add an inner layer wrapper to the pane content before placing it into
-      // draggable portlet
-      $output = "<div class=\"panels-ipe-portlet-content\">$output</div>";
-    }
-    else {
-      $output = "<div class=\"panels-ipe-portlet-content panels-ipe-empty-pane\">$output</div>";
-    }
-    // Hand it off to the plugin/theme for placing draggers/buttons
-    $output = theme('panels_ipe_pane_wrapper', array('output' => $output, 'pane' => $pane, 'display' => $this->display, 'renderer' => $this));
-
-    if (!empty($pane->locks['type']) && $pane->locks['type'] == 'immovable') {
-      return "<div id=\"panels-ipe-paneid-{$pane->pid}\" class=\"panels-ipe-nodrag panels-ipe-portlet-wrapper panels-ipe-portlet-marker\">" . $output . "</div>";
-    }
-
-    return "<div id=\"panels-ipe-paneid-{$pane->pid}\" class=\"panels-ipe-portlet-wrapper panels-ipe-portlet-marker\">" . $output . "</div>";
-  }
-
-  function prepare_panes($panes) {
-    // Set to admin mode just for this to ensure all panes are represented.
-    $this->admin = TRUE;
-    $panes = parent::prepare_panes($panes);
-    $this->admin = FALSE;
-  }
-
-  function render_pane_content(&$pane) {
-    if (!empty($pane->shown) && panels_pane_access($pane, $this->display)) {
-      $content = parent::render_pane_content($pane);
-    }
-    // Ensure that empty panes have some content.
-    if (empty($content) || empty($content->content)) {
-      if (empty($content)) {
-        $content = new stdClass();
-      }
-
-      // Get the administrative title.
-      $content_type = ctools_get_content_type($pane->type);
-      $title = ctools_content_admin_title($content_type, $pane->subtype, $pane->configuration, $this->display->context);
-
-      $content->content = t('Placeholder for empty or inaccessible "@title"', array('@title' => html_entity_decode($title, ENT_QUOTES)));
-      // Add these to prevent notices.
-      $content->type = 'panels_ipe';
-      $content->subtype = 'panels_ipe';
-      $pane->IPE_empty = TRUE;
-    }
-
-    return $content;
-  }
-
-  /**
-   * Add an 'empty' pane placeholder above all the normal panes.
-   *
-   * @param $region_id
-   * @param $panes
-   */
-  function render_region($region_id, $panes) {
-    // Generate this region's 'empty' placeholder pane from the IPE plugin.
-    $empty_ph = theme('panels_ipe_placeholder_pane', array('region_id' => $region_id, 'region_title' => $this->plugins['layout']['regions'][$region_id]));
-
-    // Wrap the placeholder in some guaranteed markup.
-    $control = '<div class="panels-ipe-placeholder panels-ipe-on panels-ipe-portlet-marker panels-ipe-portlet-static">' . $empty_ph . theme('panels_ipe_add_pane_button', array('region_id' => $region_id, 'display' => $this->display, 'renderer' => $this)) . "</div>";
-
-    $output = parent::render_region($region_id, $panes);
-    $output = theme('panels_ipe_region_wrapper', array('output' => $output, 'region_id' => $region_id, 'display' => $this->display, 'controls' => $control, 'renderer' => $this));
-    $classes = 'panels-ipe-region';
-
-    return "<div id='panels-ipe-regionid-$region_id' class='panels-ipe-region'>$output</div>";
-  }
-
-  /**
-   * This is a generic lock test.
-   */
-  function ipe_test_lock($url, $break) {
-    if (!empty($this->cache->locked)) {
-      if ($break != 'break') {
-        $account  = user_load($this->cache->locked->uid);
-        $name     = format_username($account);
-        $lock_age = format_interval(time() - $this->cache->locked->updated);
-
-        $message = t("This panel is being edited by user !user, and is therefore locked from editing by others. This lock is !age old.\n\nClick OK to break this lock and discard any changes made by !user.", array('!user' => $name, '!age' => $lock_age));
-
-        $this->commands[] = array(
-          'command' => 'unlockIPE',
-          'message' => $message,
-          'break_path' => url($this->get_url($url, 'break')),
-          'key' => $this->clean_key,
-        );
-        return TRUE;
-      }
-
-      // Break the lock.
-      panels_edit_cache_break_lock($this->cache);
-    }
-  }
-
-  /**
-   * AJAX callback to unlock the IPE.
-   *
-   * This is called whenever something server side determines that editing
-   * has stopped and cleans up no longer needed locks.
-   *
-   * It has no visible return value as this is considered a background task
-   * and the client side has already given all indications that things are
-   * now in a 'normal' state.
-   */
-  function ajax_unlock_ipe() {
-    panels_edit_cache_clear($this->cache);
-    $this->commands[] = array();
-  }
-
-  /**
-   * AJAX entry point to create the controller form for an IPE.
-   */
-  function ajax_save_form($break = NULL) {
-    if ($this->ipe_test_lock('save-form', $break)) {
-      return;
-    }
-
-    // Reset the $_POST['ajax_html_ids'] values to preserve
-    // proper IDs on form elements when they are rebuilt
-    // by the Panels IPE without refreshing the page
-    $_POST['ajax_html_ids'] = array();
-
-    $form_state = array(
-      'renderer' => $this,
-      'display' => &$this->display,
-      'content_types' => $this->cache->content_types,
-      'rerender' => FALSE,
-      'no_redirect' => TRUE,
-      // Panels needs this to make sure that the layout gets callbacks
-      'layout' => $this->plugins['layout'],
-    );
-
-    $output = drupal_build_form('panels_ipe_edit_control_form', $form_state);
-    if (empty($form_state['executed'])) {
-      // At this point, we want to save the cache to ensure that we have a lock.
-      $this->cache->ipe_locked = TRUE;
-      panels_edit_cache_set($this->cache);
-      $this->commands[] = array(
-        'command' => 'initIPE',
-        'key' => $this->clean_key,
-        'data' => drupal_render($output),
-        'lockPath' => url($this->get_url('unlock_ipe')),
-      );
-      return;
-    }
-
-    // Check to see if we have a lock that was broken. If so we need to
-    // inform the user and abort.
-    if (empty($this->cache->ipe_locked)) {
-      $this->commands[] = ajax_command_alert(t('A lock you had has been externally broken, and all your changes have been reverted.'));
-      $this->commands[] = array(
-        'command' => 'cancelIPE',
-        'key' => $this->clean_key,
-      );
-      return;
-    }
-
-    // Otherwise it was submitted.
-    if (!empty($form_state['clicked_button']['#save-display'])) {
-      // Saved. Save the cache.
-      panels_edit_cache_save($this->cache);
-      // A rerender should fix IDs on added panes as well as ensure style changes are
-      // rendered.
-      $this->meta_location = 'inline';
-      $this->commands[] = ajax_command_replace("#panels-ipe-display-{$this->clean_key}", panels_render_display($this->display, $this));
-    }
-    else {
-      // Cancelled. Clear the cache.
-      panels_edit_cache_clear($this->cache);
-    }
-
-    $this->commands[] = array(
-      'command' => 'endIPE',
-      'key' => $this->clean_key,
-    );
-  }
-
-  /**
-   * AJAX entry point to create the controller form for an IPE.
-   */
-  function ajax_change_layout($break = NULL) {
-    if ($this->ipe_test_lock('change_layout', $break)) {
-      return;
-    }
-
-    // At this point, we want to save the cache to ensure that we have a lock.
-    $this->cache->ipe_locked = TRUE;
-    panels_edit_cache_set($this->cache);
-
-    ctools_include('plugins', 'panels');
-    ctools_include('common', 'panels');
-
-    // @todo figure out a solution for this, it's critical
-    if (isset($this->display->allowed_layouts)) {
-      $layouts = $this->display->allowed_layouts;
-    }
-    else {
-      $layouts = panels_common_get_allowed_layouts('panels_page');
-    }
-
-    // Filter out builders
-    $layouts = array_filter($layouts, '_panels_builder_filter');
-
-    // Define the current layout
-    $current_layout = $this->plugins['layout']['name'];
-
-    $output = panels_common_print_layout_links($layouts, $this->get_url('set_layout'), array('attributes' => array('class' => array('use-ajax'))), $current_layout);
-
-    $this->commands[] = ctools_modal_command_display(t('Change layout'), $output);
-    $this->commands[] = array(
-      'command' => 'IPEsetLockState',
-      'key' => $this->clean_key,
-      'lockPath' => url($this->get_url('unlock_ipe')),
-    );
-  }
-
-  function ajax_set_layout($layout) {
-    ctools_include('context');
-    ctools_include('display-layout', 'panels');
-    $form_state = array(
-      'layout' => $layout,
-      'display' => $this->display,
-      'finish' => t('Save'),
-      'no_redirect' => TRUE,
-    );
-
-    // Reset the $_POST['ajax_html_ids'] values to preserve
-    // proper IDs on form elements when they are rebuilt
-    // by the Panels IPE without refreshing the page
-    $_POST['ajax_html_ids'] = array();
-
-    $output = drupal_build_form('panels_change_layout', $form_state);
-    $output = drupal_render($output);
-    if (!empty($form_state['executed'])) {
-      if (isset($form_state['back'])) {
-        return $this->ajax_change_layout();
-      }
-
-      if (!empty($form_state['clicked_button']['#save-display'])) {
-        // Saved. Save the cache.
-        panels_edit_cache_save($this->cache);
-        $this->display->skip_cache = TRUE;
-
-        // Since the layout changed, we have to update these things in the
-        // renderer in order to get the right settings.
-        $layout = panels_get_layout($this->display->layout);
-        $this->plugins['layout'] = $layout;
-        if (!isset($layout['regions'])) {
-          $this->plugins['layout']['regions'] = panels_get_regions($layout, $this->display);
-        }
-
-        $this->meta_location = 'inline';
-
-        $this->commands[] = ajax_command_replace("#panels-ipe-display-{$this->clean_key}", panels_render_display($this->display, $this));
-        $this->commands[] = ctools_modal_command_dismiss();
-        return;
-      }
-    }
-
-    $this->commands[] = ctools_modal_command_display(t('Change layout'), $output);
-  }
-
-  /**
-   * Create a command array to redraw a pane.
-   */
-  function command_update_pane($pid) {
-    if (is_object($pid)) {
-      $pane = $pid;
-    }
-    else {
-      $pane = $this->display->content[$pid];
-    }
-
-    $this->commands[] = ajax_command_replace("#panels-ipe-paneid-$pane->pid", $this->render_pane($pane));
-    $this->commands[] = ajax_command_changed("#panels-ipe-display-{$this->clean_key}");
-  }
-
-  /**
-   * Create a command array to add a new pane.
-   */
-  function command_add_pane($pid) {
-    if (is_object($pid)) {
-      $pane = $pid;
-    }
-    else {
-      $pane = $this->display->content[$pid];
-    }
-
-    $this->commands[] = array(
-      'command' => 'insertNewPane',
-      'regionId' => $pane->panel,
-      'renderedPane' => $this->render_pane($pane),
-    );
-    $this->commands[] = ajax_command_changed("#panels-ipe-display-{$this->clean_key}");
-    $this->commands[] = array(
-      'command' => 'addNewPane',
-      'key' => $this->clean_key,
-    );
-  }
-}
-
-/**
- * FAPI callback to create the Save/Cancel form for the IPE.
- */
-function panels_ipe_edit_control_form($form, &$form_state) {
-  $display = &$form_state['display'];
-  // @todo -- this should be unnecessary as we ensure cache_key is set in add_meta()
-//  $display->cache_key = isset($display->cache_key) ? $display->cache_key : $display->did;
-
-  // Annoyingly, theme doesn't have access to form_state so we have to do this.
-  $form['#display'] = $display;
-
-  $layout = panels_get_layout($display->layout);
-  $layout_panels = panels_get_regions($layout, $display);
-
-  $form['panel'] = array('#tree' => TRUE);
-  $form['panel']['pane'] = array('#tree' => TRUE);
-
-  foreach ($layout_panels as $panel_id => $title) {
-    // Make sure we at least have an empty array for all possible locations.
-    if (!isset($display->panels[$panel_id])) {
-      $display->panels[$panel_id] = array();
-    }
-
-    $form['panel']['pane'][$panel_id] = array(
-      // Use 'hidden' instead of 'value' so the js can access it.
-      '#type' => 'hidden',
-      '#default_value' => implode(',', (array) $display->panels[$panel_id]),
-    );
-  }
-
-  $form['buttons']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save'),
-    '#id' => 'panels-ipe-save',
-    '#attributes' => array('class' => array('panels-ipe-save')),
-    '#submit' => array('panels_edit_display_form_submit'),
-    '#save-display' => TRUE,
-  );
-  $form['buttons']['cancel'] = array(
-    '#type' => 'submit',
-    '#id' => 'panels-ipe-cancel',
-    '#attributes' => array('class' => array('panels-ipe-cancel')),
-    '#value' => t('Cancel'),
-  );
-  return $form;
-}
diff --git a/panels_ipe/src/Controller/PanelsIPEPageController.php b/panels_ipe/src/Controller/PanelsIPEPageController.php
new file mode 100644
index 0000000..0d9e22c
--- /dev/null
+++ b/panels_ipe/src/Controller/PanelsIPEPageController.php
@@ -0,0 +1,339 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels_ipe\Controller\PanelsIPEPageController.
+ */
+
+namespace Drupal\panels_ipe\Controller;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Render\Element;
+use Drupal\layout_plugin\Layout;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\page_manager\Entity\PageVariant;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Zend\Diactoros\Response\JsonResponse;
+use Zend\Feed\PubSubHubbub\HttpResponse;
+
+/**
+ * Contains all JSON endpoints required for Panels IPE + Page Manager.
+ */
+class PanelsIPEPageController extends ControllerBase {
+
+  /**
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new PanelsIPEController.
+   *
+   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
+   *   The block manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(BlockManagerInterface $block_manager, RendererInterface $renderer) {
+    $this->blockManager = $block_manager;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.block'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * Gets a block's metadata and rendered HTML.
+   *
+   * @param string $variant_id
+   *   The machine name of the current display variant.
+   * @param string $block_id
+   *   The UUID of the requested block.
+   *
+   * @return JsonResponse
+   *
+   * @throws AccessDeniedHttpException|NotFoundHttpException
+   */
+  public function getBlock($variant_id, $block_id) {
+    // Check if the variant exists.
+    /** @var \Drupal\page_manager\PageVariantInterface $variant */
+    if (!$variant = PageVariant::load($variant_id)) {
+      throw new NotFoundHttpException();
+    }
+
+    /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant_plugin */
+    $variant_plugin = $variant->getVariantPlugin();
+
+    // Check if the block exists.
+    if (!$block = $variant_plugin->getBlock($block_id)) {
+      throw new NotFoundHttpException();
+    }
+
+    // Check entity access before continuing.
+    $user = $this->currentUser();
+    if (!$block->access($user)) {
+      throw new AccessDeniedHttpException();
+    }
+
+    // Assemble a render array for the block.
+    // @todo Support contexts and revisions for all entities.
+    $configuration = $block->getConfiguration();
+    $elements = [
+      '#theme' => 'block',
+      '#attributes' => [
+        'data-block-id' => $block->getConfiguration()['uuid']
+      ],
+      '#configuration' => $configuration,
+      '#plugin_id' => $block->getPluginId(),
+      '#base_plugin_id' => $block->getBaseId(),
+      '#derivative_plugin_id' => $block->getDerivativeId(),
+      'content' => $block->build()
+    ];
+
+    // Return a structured JSON response for our Backbone App.
+    $data = [
+      'html' => $this->renderer->render($elements),
+      'uuid' => $configuration['uuid'],
+      'label' => $block->label(),
+      'id' => $block->getPluginId()
+    ];
+
+    return new JsonResponse($data);
+  }
+
+  /**
+   * Gets a list of available Layouts, without wrapping HTML.
+   *
+   * @param string $variant_id
+   *   The machine name of the current display variant.
+   *
+   * @return JsonResponse
+   *
+   * @throws AccessDeniedHttpException|NotFoundHttpException
+   */
+  public function getLayouts($variant_id) {
+    // Check if the variant exists.
+    /** @var \Drupal\page_manager\PageVariantInterface $variant */
+    if (!$variant = PageVariant::load($variant_id)) {
+      throw new NotFoundHttpException();
+    }
+
+    // Check variant access.
+    if (!$variant->access('read')) {
+      throw new AccessDeniedHttpException();
+    }
+
+    /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant_plugin */
+    $variant_plugin = $variant->getVariantPlugin();
+
+    // Get the current layout.
+    $layout = $variant_plugin->getConfiguration()['layout'];
+
+    // Get a list of all available layouts.
+    $layouts = Layout::getLayoutOptions();
+    $data = [];
+    foreach ($layouts as $id => $label) {
+      $data[] = [
+        'id' => $id,
+        'label' => $label,
+        'current' => $layout == $id
+      ];
+    }
+
+    // Return a structured JSON response for our Backbone App.
+    return new JsonResponse($data);
+  }
+
+  /**
+   * Gets a given layout with empty regions and relevant metadata.
+   *
+   * @param string $variant_id
+   *   The machine name of the current display variant.
+   * @param string $layout_id
+   *   The machine name of the requested layout.
+   *
+   * @return JsonResponse
+   *
+   * @throws AccessDeniedHttpException|NotFoundHttpException
+   */
+  public function getLayout($variant_id, $layout_id) {
+    // Check if the variant exists.
+    /** @var \Drupal\page_manager\PageVariantInterface $variant */
+    if (!$variant = PageVariant::load($variant_id)) {
+      throw new NotFoundHttpException();
+    }
+
+    // Check variant access.
+    if (!$variant->access('read')) {
+      throw new AccessDeniedHttpException();
+    }
+
+    /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant_plugin */
+    $variant_plugin = $variant->getVariantPlugin();
+
+    // Build the requested layout.
+    $configuration = $variant_plugin->getConfiguration();
+    $configuration['layout'] = $layout_id;
+    $variant_plugin->setConfiguration($configuration);
+    $build = $variant_plugin->build();
+
+    // Remove all blocks from the build.
+    $regions = $variant_plugin->getRegionNames();
+    $region_data = [];
+    foreach ($regions as $id => $label) {
+      // Get all block/random keys.
+      $children = Element::getVisibleChildren($build[$id]);
+      // Unset those keys, retaining the theme variables for the region.
+      $build[$id] = array_diff_key($build[$id], array_flip($children));
+
+      // Format region metadata.
+      $region_data[] = [
+        'name' => $id,
+        'label' => $label
+      ];
+    }
+
+    // Remove the wrapping elements, which our builder adds to every build.
+    unset($build['#suffix']);
+    unset($build['#prefix']);
+
+    // Get the current layout.
+    $current_layout = $variant_plugin->getConfiguration()['layout'];
+
+    // Get a list of all available layouts.
+    $layouts = Layout::getLayoutOptions();
+
+    $data = [
+      'id' => $layout_id,
+      'label' => $layouts[$layout_id],
+      'current' => $current_layout == $layout_id,
+      'html' => $this->renderer->render($build),
+      'regions' => $region_data
+    ];
+
+    // Return a structured JSON response for our Backbone App.
+    return new JsonResponse($data);
+  }
+
+  /**
+   * Updates the current PageVariant based on the changes done in our app.
+   *
+   * @param \Drupal\page_manager\PageVariantInterface $variant
+   *   The current variant.
+   * @param array $layout
+   *   The decoded LayoutModel from our App.
+   */
+  protected function updateVariant($variant, $layout) {
+    // Load the current variant plugin.
+    /** @var \Drupal\panels\Plugin\DisplayVariant\PanelsDisplayVariant $variant_plugin */
+    $variant_plugin = $variant->getVariantPlugin();
+
+    // Change the layout.
+    $configuration = $variant_plugin->getConfiguration();
+    $configuration['layout'] = $layout['id'];
+    $variant_plugin->setConfiguration($configuration);
+    $variant_plugin->getLayout();
+
+    // Edit our blocks.
+    foreach ($layout['regionCollection'] as $region) {
+      $weight = 0;
+      foreach ($region['blockCollection'] as $block) {
+        // Our Backbone app models Blocks to abstract their region.
+        $block['region'] = $region['name'];
+
+        // Weight is based by order in the collection.
+        $block['weight'] = ++$weight;
+
+        // @todo This should be removed in Backbone.
+        unset($block['html']);
+        unset($block['active']);
+
+        // If the block already exists, update it. Otherwise add it.
+        if (isset($configuration['blocks'][$block['uuid']])) {
+          $variant_plugin->updateBlock($block['uuid'], array_merge($configuration['blocks'][$block['uuid']], $block));
+        }
+        else {
+          $variant_plugin->addBlock($block);
+        }
+      }
+    }
+
+    // Save the plugin.
+    $variant->save();
+
+    return new JsonResponse(['success' => true]);
+  }
+
+  /**
+   * Updates (PUT) an existing Layout in this Variant.
+   *
+   * @param string $variant_id
+   *   The machine name of the current display variant.
+   * @param string $layout_id
+   *   The machine name of the requested layout.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return JsonResponse
+   *
+   * @throws AccessDeniedHttpException|NotFoundHttpException
+   */
+  public function updateLayout($variant_id, $layout_id, Request $request) {
+    // Check if the variant exists.
+    /** @var \Drupal\page_manager\PageVariantInterface $variant */
+    if (!$variant = PageVariant::load($variant_id)) {
+      throw new NotFoundHttpException();
+    }
+
+    // Check variant access.
+    if (!$variant->access('update')) {
+      throw new AccessDeniedHttpException();
+    }
+
+    // Decode the request.
+    $content = $request->getContent();
+    if (!empty($content) && $layout = Json::decode($content)) {
+      return $this->updateVariant($variant, $layout);
+    }
+    else {
+      return new JsonResponse(['success' => false], 400);
+    }
+  }
+
+  /**
+   * Creates (POST) a new Layout for this Variant.
+   *
+   * @param string $variant_id
+   *   The machine name of the current display variant.
+   * @param string $layout_id
+   *   The machine name of the new layout.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   *
+   * @return JsonResponse
+   *
+   * @throws AccessDeniedHttpException|NotFoundHttpException
+   */
+  public function createLayout($variant_id, $layout_id, Request $request) {
+    // For now, creating and updating a layout is the same thing.
+    return $this->updateLayout($variant_id, $layout_id, $request);
+  }
+
+}
diff --git a/panels_ipe/src/Plugin/DisplayBuilder/InPlaceEditorDisplayBuilder.php b/panels_ipe/src/Plugin/DisplayBuilder/InPlaceEditorDisplayBuilder.php
new file mode 100644
index 0000000..4af2b35
--- /dev/null
+++ b/panels_ipe/src/Plugin/DisplayBuilder/InPlaceEditorDisplayBuilder.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\panels_ipe\Plugin\DisplayBuilder\InPlaceEditorDisplayBuilder.
+ */
+
+namespace Drupal\panels_ipe\Plugin\DisplayBuilder;
+
+use Drupal\Component\Utility\Html;
+use Drupal\layout_plugin\Plugin\Layout\LayoutInterface;
+use Drupal\panels\Plugin\DisplayBuilder\StandardDisplayBuilder;
+
+/**
+ * The In-place editor display builder for viewing and editing a 
+ * PanelsDisplayVariant in the same place.
+ *
+ * @DisplayBuilder(
+ *   id = "in_place_editor",
+ *   label = @Translation("In-place editor")
+ * )
+ */
+class InPlaceEditorDisplayBuilder extends StandardDisplayBuilder {
+
+  /**
+   * Compiles settings needed for the IPE to function.
+   *
+   * @param array $regions
+   *   The render array representing regions.
+   *
+   * @return array
+   *   An associative array representing the contents of drupalSettings.
+   */
+  protected function getDrupalSettings(array $regions, array $contexts, LayoutInterface $layout = NULL) {
+    $settings = [
+      'regions' => [],
+    ];
+
+    // Add current block IDs to settings sorted by region.
+    foreach ($regions as $region => $blocks) {
+      $settings['regions'][$region]  = [
+        'name' => $region,
+        'label' => '',
+        'blocks' => []
+      ];
+
+      if (!$blocks) {
+        continue;
+      }
+
+      /** @var \Drupal\Core\Block\BlockPluginInterface[] $blocks */
+      foreach ($blocks as $block_uuid => $block) {
+        $block_config = $block->getConfiguration();
+        $settings['regions'][$region]['blocks'][$block_uuid] = [
+          'uuid' => $block_uuid,
+          'label' => $block->label(),
+          'id' => $block->getPluginId(),
+          'provider' => $block_config['provider']
+        ];
+      }
+    }
+
+    // Add the layout information.
+    if ($layout) {
+      $layout_definition = $layout->getPluginDefinition();
+      $settings['layout'] = [
+        'id' => $layout->getPluginId(),
+        'label' => $layout_definition['label'],
+        'original' => true
+      ];
+    }
+
+    // Explicitly support Page Manger, as we need to have a reference for where
+    // to save the display.
+    $variant = \Drupal::request()->attributes->get('page_manager_page_variant');
+    if ($variant) {
+      // Add the display variant's config.
+      $settings['display_variant'] = [
+        'label' => $variant->label(),
+        'id' => $variant->id(),
+        'uuid' => $variant->uuid(),
+      ];
+    }
+
+    // Add the module path so that our Javascript can dynamically load images.
+    $settings['base_path'] = drupal_get_path('module', 'panels_ipe');
+
+    return ['panels_ipe' => $settings];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function build(array $regions, array $contexts, LayoutInterface $layout = NULL) {
+    $build = parent::build($regions, $contexts, $layout);
+    // Attach the Panels In-place editor library based on permissions.
+    if ($this->account->hasPermission('access panels in-place editing')) {
+      foreach ($regions as $region => $blocks) {
+        // Wrap each region with a unique class and data attribute.
+        $region_name = Html::getClass("block-region-$region");
+        $build[$region]['#prefix'] = '<div class="' . $region_name . '" data-region-name="' . $region . '">';
+        $build[$region]['#suffix'] = '</div>';
+
+        if ($blocks) {
+          foreach ($blocks as $block_id => $block) {
+            $build[$region][$block_id]['#attributes']['data-block-id'] = $block_id;
+          }
+        }
+      }
+
+      // Attach the required settings and IPE.
+      $build['#attached'] = [
+        'library' => ['panels_ipe/panels_ipe'],
+        'drupalSettings' => $this->getDrupalSettings($regions, $contexts, $layout)
+      ];
+
+      // Add our custom elements to the build.
+      $build['#prefix'] = '<div id="panels-ipe-content">';
+      $build['#suffix'] = '</div><div id="panels-ipe-tray"></div>';
+    }
+    return $build;
+  }
+
+}
