Repository: deathbeds/jupyterlab-deck (language: python)

Issue:
Enable basic drag-and-drop organization of cells on slides
## Elevator Pitch

Allow positioning cells on a slide visually.

## Motivation

Using the property inspector is hard.

## Design Ideas

- from the property inspector, offer a _Position Cells_ mode
- Use an overlay built from [lumino drag-and-drop](https://lumino.readthedocs.io/en/latest/api/modules/dragdrop.html)
- offer `top`, `left`, `right`, `bottom`, `width`, `height`, `zoom` as percents 
  - also `z-index`
- offer some keyboard shortcuts
 - (<kbd>shift</kbd>)<kbd>tab</kbd> to cycle through visible cells
 - arrows to move around
 - <kbd>shift</kbd>+arrows to zoom  
- #21 is still a better long-term solution


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d20ba05..43352f7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ env:
   ROBOT_RETRIES: '3'
   CACHE_EPOCH: '0'
   DOIT_N_BUILD: '-n4'
-  DOIT_N_TEST: '-n4'
+  PABOT_PROCESSES: '3'
 
 jobs:
   build:
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index b730874..baac2bd 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -16,6 +16,7 @@ env:
   # our stuff
   ROBOT_RETRIES: '3'
   CACHE_EPOCH: '0'
+  PABOT_PROCESSES: '3'
 
 jobs:
   build:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f9dd1e4..071b081 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -2,7 +2,7 @@
 
 ### Setup
 
-- Start with [Mambaforge](https://github.com/conda-forge/miniforge)
+- Start with [Mambaforge](https://conda-forge.io/miniforge)
 
 ```bash
 mamba env update --prefix .venv --file .binder/environment.yml
diff --git a/README.md b/README.md
index 4f55d2d..01cb607 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,17 @@
 # `jupyterlab-deck`
 
-|        docs         |                      install                      |       extend        |                        demo                         |                    ci                     |
-| :-----------------: | :-----------------------------------------------: | :-----------------: | :-------------------------------------------------: | :---------------------------------------: |
-| [![rtd-badge]][rtd] | [![pypi-badge]][pypi]<br/>[![conda-badge]][conda] | [![npm-badge]][npm] | [![binder-badge]][binder]<br/>[![lite-badge]][lite] | [![ci-badge]][ci]<br/>[![cov-badge]][cov] |
+|        docs         |                      install                      |       extend        |                        demo                         |                                    ci                                     |
+| :-----------------: | :-----------------------------------------------: | :-----------------: | :-------------------------------------------------: | :-----------------------------------------------------------------------: |
+| [![rtd-badge]][rtd] | [![pypi-badge]][pypi]<br/>[![conda-badge]][conda] | [![npm-badge]][npm] | [![binder-badge]][binder]<br/>[![lite-badge]][lite] | [![ci-badge]][ci]<br/>[![reports-badge]][reports]<br/>[![cov-badge]][cov] |
 
 [binder-badge]: https://mybinder.org/badge_logo.svg
 [binder]:
   https://mybinder.org/v2/gh/deathbeds/jupyterlab-deck/HEAD?urlpath=lab/tree/examples/README.ipynb
 [ci-badge]: https://img.shields.io/github/workflow/status/deathbeds/jupyterlab-deck/CI
 [ci]: https://github.com/deathbeds/jupyterlab-deck/actions?query=branch%3Amain
+[reports-badge]:
+  https://img.shields.io/github/workflow/status/deathbeds/jupyterlab-deck/pages?label=reports
+[reports]: https://deathbeds.github.io/jupyterlab-deck/lab/index.html?path=README.ipynb
 [rtd-badge]: https://img.shields.io/readthedocs/jupyterlab-deck
 [rtd]: https://jupyterlab-deck.rtfd.io
 [lite-badge]:
@@ -62,6 +65,153 @@ or
 mamba remove jupyterlab-deck # or conda if you must
 ```
 
+## Usage
+
+### Get started
+
+After [installing](#installation), open or create a _Notebook_.
+
+> Other documents _work_ but aren't as much fun.
+
+### Deck Mode
+
+> Start _Deck Mode_ by
+>
+> - in the _Notebook Toolbar_, click ![deck-icon]
+> - open the [_Command Palette_][command-palette] and run _Start Deck_
+
+[command-palette]: https://jupyterlab.readthedocs.io/en/stable/user/commands.html
+
+In _Deck Mode_, until you configure any [slide types](#slides), all of your content
+should appear in a vertically-scrollable stack.
+
+#### Remote
+
+> In _Deck Mode_, navigate with:
+>
+> - the onscreen _remote_
+>   - if available, up, down, left, right will be available
+> - these correspond to the standard keyboard shortcuts,
+>   - <kbd>→</kbd>, <kbd>↓</kbd>, <kbd>←</kbd>, <kbd>↑</kbd>
+>   - <kbd>shift+enter</kbd> executes and advances
+> - the spacebar tries two directions:
+>   - <kbd>space</kbd> = <kbd>↓</kbd>, _or_ <kbd>→</kbd>
+>   - <kbd>shift+space</kbd> = <kbd>↑</kbd>, _or_ <kbd>←</kbd>
+
+#### Revealing JupyterLab UX Features
+
+Many of the core JupyterLab UI elements are still available, but hidden by default.
+Hover over their usual places to reveal them. These include:
+
+- the right and left sidebar
+- the _Notebook Toolbar_
+
+#### Hidden JupyterLab UX Features
+
+Some elements are _not_ visible, and cannot be revealed:
+
+- the _Main Menu_
+- the _Status Bar_
+- the _Cell Toolbar_
+
+> Next Steps:
+>
+> - use [slideshow types](#slides) to customize how much of your content appears
+>   on-screen.
+> - use [layer types](#layers) to customize foreground/background behavior
+> - use the [design tools](#design-tools) to customize the appearance of cells
+> - use the [slide layout tools](#slide-layout) to customize the position and size of
+>   cells
+
+#### Exiting Deck Mode
+
+> To exit _Deck Mode_:
+>
+> - from the remote, click the ![deck-icon]
+> - open the [_Command Palette_][command-palette] and run _Stop Deck_
+
+### Slides
+
+Build a slideshow by changing the _slideshow type_ per cell using the [_Property
+Inspector_ sidebar][property-inspector] or the [design tools][design-tools].
+
+[design-tools]: #design-tools
+[property-inspector]:
+  https://jupyterlab.readthedocs.io/en/stable/user/interface.html#left-and-right-sidebar
+
+| type       | purpose                                             |
+| ---------- | --------------------------------------------------- |
+| `-`        | (default) stack underneath the previous cell        |
+| `slide`    | start a new stack                                   |
+| `fragment` | reveal when activated                               |
+| `subslide` | start a new cell stack in the optional Y axis       |
+| `skip`     | hide the cell entirely                              |
+| `notes`    | _TBD: moves this cell to the offscreen note viewer_ |
+
+[deck-icon]:
+  https://raw.githubusercontent.com/deathbeds/jupyterlab-deck/main/js/jupyterlab-deck/style/img/deck.svg
+
+### Layers
+
+> Pick a layer type from:
+>
+> - the [property inspector][property-inspector]
+> - or the [design tools].
+
+_Layers_ either temporarily or permanently show content, and won't be reached by.
+Specifying a layer scope will override the _slideshow type_. Layers have one of the
+following _scopes_:
+
+| scope      | relationship to [slides](#slides)                       |
+| ---------- | ------------------------------------------------------- |
+| `deck`     | show on _all_ current and future `slide` or `subslides` |
+| `stack`    | show until the next `slide`                             |
+| `slide`    | show until the next `slide` _or_ `subslide`             |
+| `fragment` | only show until the next `fragment`                     |
+
+### Design Tools
+
+> In [Deck mode](#deck-mode), click the _ellipsis_ icon in the bottom right corner
+
+The design tools offer lightweight buttons to:
+
+- show/hide the [slide layout](#slide-layout) overlay
+- set the [slideshow type](#slides)
+- set the [layer type](#layers)
+- change a few key appearance properties:
+  - use the sliders to customize
+    - `z-index` controls the vertical stacking of elements:
+      - higher is "closer" to the user
+    - `opacity` cotnrols how vibrant the fonts and colors appear
+      - higher is more full
+    - `zoom` controls how big the contents of the cell appear
+      - higher is bigger
+  - uncheck the checkbox to restore to the defaults
+
+### Slide Layout
+
+> After opening the [design tools](#design-tools), click the _Show Layout_ button
+
+In _slide layout_ mode, each part of the slide receives an overlay.
+
+Moving a part manually will remove it from the default layout, and allow you to place it
+anywhere on the screen, but it will keep the same navigation index.
+
+The keyboard shortcuts and remote should still function as normal.
+
+#### Moving Parts
+
+Click and drag a part overlay to move the part underneath.
+
+#### Resizing Parts
+
+Click one of the _handles_ in the corners of the part overlay to resize a part.
+
+#### Reverting Part Move/Resize
+
+After moving a part to a fixed position, click the **↺** button on a part overlay to
+restore the part to the default layout.
+
 ## Configuration
 
 ### Enabling Deck Mode at startup
@@ -78,3 +228,38 @@ mamba remove jupyterlab-deck # or conda if you must
 
 [overrides]:
   https://jupyterlab.readthedocs.io/en/stable/user/directories.html#overrides-json
+
+## Frequently Asked Questions
+
+### Does it work with `notebook 6` aka classic?
+
+**No.** Use [RISE](https://github.com/damianavila/RISE/).
+
+### Does it work with `notebook 7`?
+
+**Not yet.** Navigating multiple documents during the same presentation will probably
+never work, as this is incompatible with the one-document-at-a-time design constraint of
+the Notebook UX.
+
+### Will it generate PowerPoint?
+
+**No.** This would be a fine third-party extension which could consume notebook metadata
+created by this extension, [jupyterlab-fonts], and `nbconvert`-compatible
+[slides](#slides).
+
+### Will it generate single-document static HTML presentations?
+
+**No.** Use [nbconvert], but no [layers](#layers) or style customizations will work.
+
+[nbconvert]:
+  https://nbconvert.readthedocs.io/en/latest/usage.html#reveal-js-html-slideshow
+
+For a full static viewing experience, try something like [JupyterLite].
+
+[jupyterlite]: https://github.com/jupyterlite/jupyterlite
+
+### Will it generate PDF?
+
+**Not yet.**
+
+[jupyterlab-fonts]: https://github.com/jupyterlab-fonts
diff --git a/dodo.py b/dodo.py
index f048492..e20de6b 100644
--- a/dodo.py
+++ b/dodo.py
@@ -80,7 +80,7 @@ class P:
     PAGES_LITE_CONFIG = PAGES_LITE / "jupyter_lite_config.json"
     PAGES_LITE_JSON = PAGES_LITE / "jupyter-lite.json"
     ESLINTRC = JS / ".eslintrc.js"
-    ALL_PLUGIN_SCHEMA = [*JS.glob("*/schmea/*.json")]
+    ALL_PLUGIN_SCHEMA = [*JS.glob("*/schema/*.json")]
     ATEST = ROOT / "atest"
     ROBOT_SUITES = ATEST / "suites"
 
@@ -95,6 +95,7 @@ class E:
     ROBOT_RETRIES = json.loads(os.environ.get("ROBOT_RETRIES", "0"))
     ROBOT_ARGS = json.loads(os.environ.get("ROBOT_ARGS", "[]"))
     WITH_JS_COV = bool(json.loads(os.environ.get("WITH_JS_COV", "0")))
+    PABOT_PROCESSES = int(json.loads(os.environ.get("PABOT_PROCESSES", "4")))
 
 
 class B:
@@ -150,6 +151,7 @@ class L:
         *P.DOCS.rglob("*.md"),
         *P.CI.rglob("*.md"),
         *P.EXAMPLES.glob("*.md"),
+        *P.EXT_JS_PKG.glob("*.md"),
     ]
     ALL_TS = [*P.JS.glob("*/src/**/*.ts"), *P.JS.glob("*/src/**/*.tsx")]
     ALL_YML = [*P.BINDER.glob("*.yml"), *P.CI.rglob("*.yml"), *P.ROOT.glob("*.yml")]
@@ -378,7 +380,7 @@ def run_robot(attempt=0, extra_args=None):
             if previous.exists():
                 extra_args += ["--rerunfailed", str(previous)]
 
-        runner = ["pabot", *C.PABOT_DEFAULTS]
+        runner = ["pabot", *C.PABOT_DEFAULTS, "--processes", E.PABOT_PROCESSES]
 
         if C.ROBOT_DRYRUN in extra_args:
             runner = ["robot"]
@@ -483,7 +485,7 @@ def task_setup():
             ],
         )
 
-    if not (E.IN_CI and B.YARN_INTEGRITY.exists()):
+    if E.LOCAL or not B.YARN_INTEGRITY.exists():
         yield dict(
             name="yarn",
             file_dep=[
diff --git a/js/_meta/src/tsconfig.json b/js/_meta/src/tsconfig.json
index 0f947f7..5e64d13 100644
--- a/js/_meta/src/tsconfig.json
+++ b/js/_meta/src/tsconfig.json
@@ -1,10 +1,10 @@
 {
-  "extends": "../../tsconfigbase",
   "compilerOptions": {
     "outDir": "../lib",
     "rootDir": ".",
     "tsBuildInfoFile": "../.src.tsbuildinfo"
   },
+  "extends": "../../tsconfigbase",
   "include": ["./**/*"],
   "references": [{ "path": "../../jupyterlab-deck/src" }]
 }
diff --git a/js/jupyterlab-deck/README.md b/js/jupyterlab-deck/README.md
index 4f55d2d..01cb607 100644
--- a/js/jupyterlab-deck/README.md
+++ b/js/jupyterlab-deck/README.md
@@ -1,14 +1,17 @@
 # `jupyterlab-deck`
 
-|        docs         |                      install                      |       extend        |                        demo                         |                    ci                     |
-| :-----------------: | :-----------------------------------------------: | :-----------------: | :-------------------------------------------------: | :---------------------------------------: |
-| [![rtd-badge]][rtd] | [![pypi-badge]][pypi]<br/>[![conda-badge]][conda] | [![npm-badge]][npm] | [![binder-badge]][binder]<br/>[![lite-badge]][lite] | [![ci-badge]][ci]<br/>[![cov-badge]][cov] |
+|        docs         |                      install                      |       extend        |                        demo                         |                                    ci                                     |
+| :-----------------: | :-----------------------------------------------: | :-----------------: | :-------------------------------------------------: | :-----------------------------------------------------------------------: |
+| [![rtd-badge]][rtd] | [![pypi-badge]][pypi]<br/>[![conda-badge]][conda] | [![npm-badge]][npm] | [![binder-badge]][binder]<br/>[![lite-badge]][lite] | [![ci-badge]][ci]<br/>[![reports-badge]][reports]<br/>[![cov-badge]][cov] |
 
 [binder-badge]: https://mybinder.org/badge_logo.svg
 [binder]:
   https://mybinder.org/v2/gh/deathbeds/jupyterlab-deck/HEAD?urlpath=lab/tree/examples/README.ipynb
 [ci-badge]: https://img.shields.io/github/workflow/status/deathbeds/jupyterlab-deck/CI
 [ci]: https://github.com/deathbeds/jupyterlab-deck/actions?query=branch%3Amain
+[reports-badge]:
+  https://img.shields.io/github/workflow/status/deathbeds/jupyterlab-deck/pages?label=reports
+[reports]: https://deathbeds.github.io/jupyterlab-deck/lab/index.html?path=README.ipynb
 [rtd-badge]: https://img.shields.io/readthedocs/jupyterlab-deck
 [rtd]: https://jupyterlab-deck.rtfd.io
 [lite-badge]:
@@ -62,6 +65,153 @@ or
 mamba remove jupyterlab-deck # or conda if you must
 ```
 
+## Usage
+
+### Get started
+
+After [installing](#installation), open or create a _Notebook_.
+
+> Other documents _work_ but aren't as much fun.
+
+### Deck Mode
+
+> Start _Deck Mode_ by
+>
+> - in the _Notebook Toolbar_, click ![deck-icon]
+> - open the [_Command Palette_][command-palette] and run _Start Deck_
+
+[command-palette]: https://jupyterlab.readthedocs.io/en/stable/user/commands.html
+
+In _Deck Mode_, until you configure any [slide types](#slides), all of your content
+should appear in a vertically-scrollable stack.
+
+#### Remote
+
+> In _Deck Mode_, navigate with:
+>
+> - the onscreen _remote_
+>   - if available, up, down, left, right will be available
+> - these correspond to the standard keyboard shortcuts,
+>   - <kbd>→</kbd>, <kbd>↓</kbd>, <kbd>←</kbd>, <kbd>↑</kbd>
+>   - <kbd>shift+enter</kbd> executes and advances
+> - the spacebar tries two directions:
+>   - <kbd>space</kbd> = <kbd>↓</kbd>, _or_ <kbd>→</kbd>
+>   - <kbd>shift+space</kbd> = <kbd>↑</kbd>, _or_ <kbd>←</kbd>
+
+#### Revealing JupyterLab UX Features
+
+Many of the core JupyterLab UI elements are still available, but hidden by default.
+Hover over their usual places to reveal them. These include:
+
+- the right and left sidebar
+- the _Notebook Toolbar_
+
+#### Hidden JupyterLab UX Features
+
+Some elements are _not_ visible, and cannot be revealed:
+
+- the _Main Menu_
+- the _Status Bar_
+- the _Cell Toolbar_
+
+> Next Steps:
+>
+> - use [slideshow types](#slides) to customize how much of your content appears
+>   on-screen.
+> - use [layer types](#layers) to customize foreground/background behavior
+> - use the [design tools](#design-tools) to customize the appearance of cells
+> - use the [slide layout tools](#slide-layout) to customize the position and size of
+>   cells
+
+#### Exiting Deck Mode
+
+> To exit _Deck Mode_:
+>
+> - from the remote, click the ![deck-icon]
+> - open the [_Command Palette_][command-palette] and run _Stop Deck_
+
+### Slides
+
+Build a slideshow by changing the _slideshow type_ per cell using the [_Property
+Inspector_ sidebar][property-inspector] or the [design tools][design-tools].
+
+[design-tools]: #design-tools
+[property-inspector]:
+  https://jupyterlab.readthedocs.io/en/stable/user/interface.html#left-and-right-sidebar
+
+| type       | purpose                                             |
+| ---------- | --------------------------------------------------- |
+| `-`        | (default) stack underneath the previous cell        |
+| `slide`    | start a new stack                                   |
+| `fragment` | reveal when activated                               |
+| `subslide` | start a new cell stack in the optional Y axis       |
+| `skip`     | hide the cell entirely                              |
+| `notes`    | _TBD: moves this cell to the offscreen note viewer_ |
+
+[deck-icon]:
+  https://raw.githubusercontent.com/deathbeds/jupyterlab-deck/main/js/jupyterlab-deck/style/img/deck.svg
+
+### Layers
+
+> Pick a layer type from:
+>
+> - the [property inspector][property-inspector]
+> - or the [design tools].
+
+_Layers_ either temporarily or permanently show content, and won't be reached by.
+Specifying a layer scope will override the _slideshow type_. Layers have one of the
+following _scopes_:
+
+| scope      | relationship to [slides](#slides)                       |
+| ---------- | ------------------------------------------------------- |
+| `deck`     | show on _all_ current and future `slide` or `subslides` |
+| `stack`    | show until the next `slide`                             |
+| `slide`    | show until the next `slide` _or_ `subslide`             |
+| `fragment` | only show until the next `fragment`                     |
+
+### Design Tools
+
+> In [Deck mode](#deck-mode), click the _ellipsis_ icon in the bottom right corner
+
+The design tools offer lightweight buttons to:
+
+- show/hide the [slide layout](#slide-layout) overlay
+- set the [slideshow type](#slides)
+- set the [layer type](#layers)
+- change a few key appearance properties:
+  - use the sliders to customize
+    - `z-index` controls the vertical stacking of elements:
+      - higher is "closer" to the user
+    - `opacity` cotnrols how vibrant the fonts and colors appear
+      - higher is more full
+    - `zoom` controls how big the contents of the cell appear
+      - higher is bigger
+  - uncheck the checkbox to restore to the defaults
+
+### Slide Layout
+
+> After opening the [design tools](#design-tools), click the _Show Layout_ button
+
+In _slide layout_ mode, each part of the slide receives an overlay.
+
+Moving a part manually will remove it from the default layout, and allow you to place it
+anywhere on the screen, but it will keep the same navigation index.
+
+The keyboard shortcuts and remote should still function as normal.
+
+#### Moving Parts
+
+Click and drag a part overlay to move the part underneath.
+
+#### Resizing Parts
+
+Click one of the _handles_ in the corners of the part overlay to resize a part.
+
+#### Reverting Part Move/Resize
+
+After moving a part to a fixed position, click the **↺** button on a part overlay to
+restore the part to the default layout.
+
 ## Configuration
 
 ### Enabling Deck Mode at startup
@@ -78,3 +228,38 @@ mamba remove jupyterlab-deck # or conda if you must
 
 [overrides]:
   https://jupyterlab.readthedocs.io/en/stable/user/directories.html#overrides-json
+
+## Frequently Asked Questions
+
+### Does it work with `notebook 6` aka classic?
+
+**No.** Use [RISE](https://github.com/damianavila/RISE/).
+
+### Does it work with `notebook 7`?
+
+**Not yet.** Navigating multiple documents during the same presentation will probably
+never work, as this is incompatible with the one-document-at-a-time design constraint of
+the Notebook UX.
+
+### Will it generate PowerPoint?
+
+**No.** This would be a fine third-party extension which could consume notebook metadata
+created by this extension, [jupyterlab-fonts], and `nbconvert`-compatible
+[slides](#slides).
+
+### Will it generate single-document static HTML presentations?
+
+**No.** Use [nbconvert], but no [layers](#layers) or style customizations will work.
+
+[nbconvert]:
+  https://nbconvert.readthedocs.io/en/latest/usage.html#reveal-js-html-slideshow
+
+For a full static viewing experience, try something like [JupyterLite].
+
+[jupyterlite]: https://github.com/jupyterlite/jupyterlite
+
+### Will it generate PDF?
+
+**Not yet.**
+
+[jupyterlab-fonts]: https://github.com/jupyterlab-fonts
diff --git a/js/jupyterlab-deck/package.json b/js/jupyterlab-deck/package.json
index 256667c..041cfea 100644
--- a/js/jupyterlab-deck/package.json
+++ b/js/jupyterlab-deck/package.json
@@ -24,11 +24,13 @@
     "@jupyterlab/apputils": "3",
     "@jupyterlab/notebook": "3",
     "@jupyterlab/statusbar": "3",
-    "@jupyterlab/ui-components": "3"
+    "@jupyterlab/ui-components": "3",
+    "d3-drag": "3"
   },
   "devDependencies": {
     "@deathbeds/jupyterlab-fonts": "^2.1.1",
-    "@jupyterlab/builder": "^3.4.8"
+    "@jupyterlab/builder": "^3.4.8",
+    "@types/d3-drag": "3"
   },
   "jupyterlab": {
     "extension": "lib/plugin.js",
@@ -39,6 +41,9 @@
       "@deathbeds/jupyterlab-fonts": {
         "bundled": false,
         "singleton": true
+      },
+      "d3-drag": {
+        "bundled": true
       }
     }
   }
diff --git a/js/jupyterlab-deck/schema/plugin.json b/js/jupyterlab-deck/schema/plugin.json
index 6dc8834..2923bb6 100644
--- a/js/jupyterlab-deck/schema/plugin.json
+++ b/js/jupyterlab-deck/schema/plugin.json
@@ -1,5 +1,35 @@
 {
   "$schema": "http://json-schema.org/draft-07/schema",
+  "definitions": {
+    "style-presets": {
+      "type": "object",
+      "description": "Style presets to apply to @deathbeds/jupyterlab-fonts",
+      "additionalItems": {
+        "$ref": "#/definitions/style-preset"
+      }
+    },
+    "style-preset": {
+      "type": "object",
+      "required": ["label", "styles"],
+      "properties": {
+        "label": {
+          "label": "string",
+          "description": "the human-readable label"
+        },
+        "scope": {
+          "type": "string",
+          "enum": ["layer", "slide", "deck", "any"]
+        },
+        "styles": {
+          "type": "object",
+          "description": "a JSS-compatible description of a stylesheet: will be nested correctly"
+        }
+      }
+    }
+  },
+  "description": "Configure Deck Settings",
+  "jupyter.lab.setting-icon": "deck:start",
+  "jupyter.lab.setting-icon-label": "Decks",
   "properties": {
     "active": {
       "title": "Activate Deck",
@@ -63,44 +93,6 @@
       }
     }
   },
-  "definitions": {
-    "style-presets": {
-      "type": "object",
-      "description": "Style presets to apply to @deathbeds/jupyterlab-fonts",
-      "additionalItems": {
-        "$ref": "#/definitions/style-preset"
-      }
-    },
-    "style-preset": {
-      "type": "object",
-      "required": [
-        "label",
-        "styles"
-      ],
-      "properties": {
-        "label": {
-          "label": "string",
-          "description": "the human-readable label"
-        },
-        "scope": {
-          "type": "string",
-          "enum": [
-            "layer",
-            "slide",
-            "deck",
-            "any"
-          ]
-        },
-        "styles": {
-          "type": "object",
-          "description": "a JSS-compatible description of a stylesheet: will be nested correctly"
-        }
-      }
-    }
-  },
-  "description": "Configure Deck Settings",
-  "jupyter.lab.setting-icon": "deck:start",
-  "jupyter.lab.setting-icon-label": "Decks",
   "title": "Decks",
   "type": "object"
 }
diff --git a/js/jupyterlab-deck/src/icons.ts b/js/jupyterlab-deck/src/icons.ts
index 1fb3694..716bd46 100644
--- a/js/jupyterlab-deck/src/icons.ts
+++ b/js/jupyterlab-deck/src/icons.ts
@@ -1,17 +1,61 @@
 import { LabIcon, caretUpEmptyThinIcon } from '@jupyterlab/ui-components';
 
-import DECK_START_SVG from '../style/img/deck.svg';
+import NOTE from '../style/img/chat-remove-outline.svg';
+import NULL from '../style/img/checkbox-blank-outline.svg';
+import FRAGMENT from '../style/img/checkbox-intermediate-variant.svg';
+import SKIP from '../style/img/close-box-outline.svg';
+import DECK_START from '../style/img/deck.svg';
+import LAYER_DECK from '../style/img/image-filter-hdr.svg';
+import LAYER_NULL from '../style/img/image-off-outline.svg';
+import LAYER_STACK from '../style/img/image-outline-multiple.svg';
+import LAYER_SLIDE from '../style/img/image-outline.svg';
+import ZOOM from '../style/img/loupe.svg';
+import LAYER_FRAGMENT from '../style/img/message-image-outline.svg';
+import Z_INDEX from '../style/img/order-numeric-descending.svg';
+import SUBSLIDE from '../style/img/plus-box-multiple.svg';
+import SLIDE from '../style/img/plus-box.svg';
+import OPACITY from '../style/img/square-opacity.svg';
+import TRANSFORM_STOP from '../style/img/transform-stop.svg';
+import TRANSFORM from '../style/img/transform.svg';
 
-import { CSS } from './tokens';
+import { CSS, TLayerScope, TSlideType } from './tokens';
 
-const DECK_STOP_SVG = DECK_START_SVG.replace(CSS.icon, CSS.iconWarn);
+const DECK_STOP = DECK_START.replace(CSS.icon, CSS.iconWarn);
 
 export namespace ICONS {
-  export const deckStart = new LabIcon({ name: 'deck:start', svgstr: DECK_START_SVG });
-  export const deckStop = new LabIcon({ name: 'deck:stop', svgstr: DECK_STOP_SVG });
+  export const deckStart = new LabIcon({ name: 'deck:start', svgstr: DECK_START });
+  export const deckStop = new LabIcon({ name: 'deck:stop', svgstr: DECK_STOP });
   export const goEnabled = new LabIcon({
     name: 'deck:go',
     svgstr: caretUpEmptyThinIcon.svgstr.replace(CSS.icon, CSS.iconContrast),
   });
   export const goDisabled = caretUpEmptyThinIcon;
+  // layover
+  export const transformStart = new LabIcon({
+    name: 'deck:layover-start',
+    svgstr: TRANSFORM,
+  });
+  export const transformStop = new LabIcon({
+    name: 'deck:layover-stop',
+    svgstr: TRANSFORM_STOP,
+  });
+  // design
+  export const slideshow: Record<'null' | Exclude<TSlideType, null>, LabIcon> = {
+    slide: new LabIcon({ name: 'deck:slide-slide', svgstr: SLIDE }),
+    subslide: new LabIcon({ name: 'deck:slide-subslide', svgstr: SUBSLIDE }),
+    null: new LabIcon({ name: 'deck:slide-null', svgstr: NULL }),
+    fragment: new LabIcon({ name: 'deck:slide-fragment', svgstr: FRAGMENT }),
+    skip: new LabIcon({ name: 'deck:slide-skip', svgstr: SKIP }),
+    notes: new LabIcon({ name: 'deck:slide-note', svgstr: NOTE }),
+  };
+  export const layer: Record<TLayerScope | 'null', LabIcon> = {
+    deck: new LabIcon({ name: 'deck:layer-deck', svgstr: LAYER_DECK }),
+    stack: new LabIcon({ name: 'deck:layer-stack', svgstr: LAYER_STACK }),
+    slide: new LabIcon({ name: 'deck:layer-slide', svgstr: LAYER_SLIDE }),
+    fragment: new LabIcon({ name: 'deck:layer-fragment', svgstr: LAYER_FRAGMENT }),
+    null: new LabIcon({ name: 'deck:layer-null', svgstr: LAYER_NULL }),
+  };
+  export const zoom = new LabIcon({ name: 'deck:zoom', svgstr: ZOOM });
+  export const zIndex = new LabIcon({ name: 'deck:z-index', svgstr: Z_INDEX });
+  export const opacity = new LabIcon({ name: 'deck:opacity', svgstr: OPACITY });
 }
diff --git a/js/jupyterlab-deck/src/manager.ts b/js/jupyterlab-deck/src/manager.ts
index 8d7fcfb..b9f1cb4 100644
--- a/js/jupyterlab-deck/src/manager.ts
+++ b/js/jupyterlab-deck/src/manager.ts
@@ -1,4 +1,5 @@
 import { IFontManager } from '@deathbeds/jupyterlab-fonts';
+import { GlobalStyles } from '@deathbeds/jupyterlab-fonts/lib/_schema';
 import { LabShell } from '@jupyterlab/application';
 import { ISettingRegistry } from '@jupyterlab/settingregistry';
 import { StatusBar } from '@jupyterlab/statusbar';
@@ -6,10 +7,9 @@ import { TranslationBundle } from '@jupyterlab/translation';
 import { each } from '@lumino/algorithm';
 import { CommandRegistry } from '@lumino/commands';
 import { Signal, ISignal } from '@lumino/signaling';
-import { Widget, DockPanel, BoxLayout } from '@lumino/widgets';
+import { Widget, DockPanel } from '@lumino/widgets';
 
 import { ICONS } from './icons';
-import { DeckRemote } from './remote';
 import {
   IDeckManager,
   DATA,
@@ -25,9 +25,35 @@ import {
   COMPOUND_KEYS,
   IStylePreset,
   IDeckSettings,
+  TSlideType,
+  TLayerScope,
 } from './tokens';
+import { DesignTools } from './tools/design';
+import type { Layover } from './tools/layover';
+import { DeckRemote } from './tools/remote';
 
 export class DeckManager implements IDeckManager {
+  protected _active = false;
+  protected _activeChanged = new Signal<IDeckManager, void>(this);
+  protected _activeWidget: Widget | null = null;
+  protected _presenters: IPresenter<any>[] = [];
+  protected _appStarted: Promise<void>;
+  protected _commands: CommandRegistry;
+  protected _remote: DeckRemote | null = null;
+  protected _designTools: DesignTools | null = null;
+  protected _settings: Promise<ISettingRegistry.ISettings>;
+  protected _shell: LabShell;
+  protected _statusbar: StatusBar | null;
+  protected _statusBarWasEnabled = false;
+  protected _styleCache = new Map<HTMLElement, string>();
+  protected _trans: TranslationBundle;
+  protected _stylePresets = new Map<string, IStylePreset>();
+  protected _stylePresetsChanged = new Signal<IDeckManager, void>(this);
+  protected _layoverChanged = new Signal<IDeckManager, void>(this);
+  protected _fonts: IFontManager;
+  protected _layover: Layover | null = null;
+  protected _activePresenter: IPresenter<Widget> | null = null;
+
   constructor(options: DeckManager.IOptions) {
     this._appStarted = options.appStarted;
     this._commands = options.commands;
@@ -39,8 +65,8 @@ export class DeckManager implements IDeckManager {
 
     this._shell.activeChanged.connect(this._onActiveWidgetChanged, this);
     this._shell.layoutModified.connect(this._addDeckStylesLater, this);
-    this._registerCommands();
-    this._registerKeyBindings();
+    this._addCommands();
+    this._addKeyBindings();
     this._settings
       .then(async (settings) => {
         settings.changed.connect(this._onSettingsChanged, this);
@@ -49,6 +75,14 @@ export class DeckManager implements IDeckManager {
       .catch(console.warn);
   }
 
+  public get activePresenter() {
+    return this._activePresenter;
+  }
+
+  public get layover() {
+    return this._layover;
+  }
+
   public get fonts() {
     return this._fonts;
   }
@@ -61,6 +95,10 @@ export class DeckManager implements IDeckManager {
     return this._stylePresetsChanged;
   }
 
+  public get layoverChanged(): ISignal<IDeckManager, void> {
+    return this._layoverChanged;
+  }
+
   /**
    * translate a string by message id (usually the en-US string), potentially
    * with positional arguments, starting with %1.
@@ -86,11 +124,14 @@ export class DeckManager implements IDeckManager {
   }
 
   /** enable deck mode */
-  public start = async (): Promise<void> => {
+  public start = async (force: boolean = false): Promise<void> => {
     await this._appStarted;
-    if (this._active) {
+    const wasActive = this._active;
+
+    if (wasActive && !force) {
       return;
     }
+
     if (!this._activeWidget) {
       const { _shellActiveWidget } = this;
       if (_shellActiveWidget) {
@@ -100,27 +141,35 @@ export class DeckManager implements IDeckManager {
         return;
       }
     }
+
     const { _shell, _activeWidget } = this;
-    this._active = true;
-    void this._settings.then((settings) => settings.set('active', true));
-    if (this._statusbar) {
-      this._statusBarWasEnabled = this._statusbar.isVisible;
-      this._statusbar.hide();
+
+    if (!wasActive) {
+      this._active = true;
+      void this._settings.then((settings) => settings.set('active', true));
+      if (this._statusbar) {
+        this._statusBarWasEnabled = this._statusbar.isVisible;
+        this._statusbar.hide();
+      }
+      _shell.presentationMode = false;
+      document.body.dataset[DATA.deckMode] = DATA.presenting;
+      each(this._dockpanel.tabBars(), (bar) => bar.hide());
+      _shell.mode = 'single-document';
+      this._remote = new DeckRemote({ manager: this });
+      this._designTools = new DesignTools({ manager: this });
+      window.addEventListener('resize', this._addDeckStylesLater);
     }
-    _shell.presentationMode = false;
-    document.body.dataset[DATA.deckMode] = DATA.presenting;
-    each(this._dockpanel.tabBars(), (bar) => bar.hide());
-    _shell.mode = 'single-document';
     _shell.update();
-    this._remote = new DeckRemote({ manager: this });
-    (_shell.layout as BoxLayout).addWidget(this._remote);
-    window.addEventListener('resize', this._addDeckStylesLater);
+
     await this._onActiveWidgetChanged();
 
     if (_activeWidget) {
       const presenter = this._getPresenter(_activeWidget);
       if (presenter) {
+        this._activePresenter = presenter;
         await presenter.start(_activeWidget);
+      } else {
+        this._activePresenter = null;
       }
     }
 
@@ -132,8 +181,11 @@ export class DeckManager implements IDeckManager {
       _shell.collapseLeft();
       _shell.collapseRight();
     }, 1000);
+
+    if (!wasActive) {
+      this._addDeckStyles();
+    }
     this._activeChanged.emit(void 0);
-    this._addDeckStyles();
   };
 
   public get activeWidget(): Widget | null {
@@ -147,7 +199,11 @@ export class DeckManager implements IDeckManager {
       return;
     }
 
-    const { _activeWidget, _shell, _statusbar, _remote } = this;
+    const { _activeWidget, _shell, _statusbar, _remote, _layover, _designTools } = this;
+
+    if (_layover) {
+      await this.hideLayover();
+    }
 
     if (_activeWidget) {
       const presenter = this._getPresenter(_activeWidget);
@@ -166,6 +222,10 @@ export class DeckManager implements IDeckManager {
       _remote.dispose();
       this._remote = null;
     }
+    if (_designTools) {
+      _designTools.dispose();
+      this._designTools = null;
+    }
     _shell.presentationMode = false;
     _shell.mode = 'multiple-document';
     window.removeEventListener('resize', this._addDeckStylesLater);
@@ -217,7 +277,7 @@ export class DeckManager implements IDeckManager {
   }
 
   /** overload the stock notebook keyboard shortcuts */
-  protected _registerKeyBindings() {
+  protected _addKeyBindings() {
     for (const direction of Object.values(DIRECTION)) {
       this._commands.addKeyBinding({
         command: CommandIds[direction],
@@ -237,12 +297,73 @@ export class DeckManager implements IDeckManager {
     }
   }
 
-  protected _registerCommands() {
+  public async showLayover() {
+    if (!this._layover) {
+      this._layover = new (await import('./tools/layover')).Layover({ manager: this });
+      this._layoverChanged.emit(void 0);
+    }
+    await this.start(true);
+  }
+
+  public async hideLayover() {
+    if (this._layover) {
+      this._layover.dispose();
+      this._layover = null;
+      this._layoverChanged.emit(void 0);
+    }
+  }
+
+  public getSlideType(): TSlideType {
+    let { _activeWidget, _activePresenter } = this;
+    if (_activeWidget && _activePresenter?.capabilities.slideType) {
+      return _activePresenter.getSlideType(_activeWidget) || null;
+    }
+    return null;
+  }
+
+  public setSlideType(slideType: TSlideType): void {
+    let { _activeWidget, _activePresenter } = this;
+    if (_activeWidget && _activePresenter?.capabilities.slideType) {
+      _activePresenter.setSlideType(_activeWidget, slideType);
+    }
+  }
+
+  public getLayerScope(): TLayerScope | null {
+    let { _activeWidget, _activePresenter } = this;
+    if (_activeWidget && _activePresenter?.capabilities.layerScope) {
+      return _activePresenter.getLayerScope(_activeWidget) || null;
+    }
+    return null;
+  }
+
+  public setLayerScope(layerScope: TLayerScope | null): void {
+    let { _activeWidget, _activePresenter } = this;
+    if (_activeWidget && _activePresenter?.capabilities.layerScope) {
+      _activePresenter.setLayerScope(_activeWidget, layerScope);
+    }
+  }
+
+  public getPartStyles(): GlobalStyles | null {
+    let { _activeWidget, _activePresenter } = this;
+    if (_activeWidget && _activePresenter?.capabilities.stylePart) {
+      const styles = _activePresenter.getPartStyles(_activeWidget) || null;
+      return styles;
+    }
+    return null;
+  }
+  public setPartStyles(styles: GlobalStyles | null): void {
+    let { _activeWidget, _activePresenter } = this;
+    if (_activeWidget && _activePresenter?.capabilities.stylePart) {
+      _activePresenter.setPartStyles(_activeWidget, styles);
+    }
+  }
+
+  protected _addCommands() {
     let { _commands, __, go } = this;
     _commands.addCommand(CommandIds.start, {
       label: __('Start Deck'),
       icon: ICONS.deckStart,
-      execute: this.start,
+      execute: () => this.start(),
     });
     _commands.addCommand(CommandIds.stop, {
       label: __('Stop Deck'),
@@ -256,6 +377,19 @@ export class DeckManager implements IDeckManager {
         await (this._active ? this.stop() : this.start());
       },
     });
+
+    this._commands.addCommand(CommandIds.showLayover, {
+      icon: ICONS.transformStart,
+      label: this.__('Show Slide Layout'),
+      execute: () => this.showLayover(),
+    });
+
+    this._commands.addCommand(CommandIds.hideLayover, {
+      icon: ICONS.transformStop,
+      label: this.__('Hide Slide Layout'),
+      execute: () => this.hideLayover(),
+    });
+
     _commands.addCommand(CommandIds.go, {
       label: __('Go direction in Deck'),
       execute: async (args: any) => {
@@ -289,11 +423,7 @@ export class DeckManager implements IDeckManager {
 
     const { _activeWidget, _shellActiveWidget } = this;
 
-    if (
-      !_shellActiveWidget ||
-      _shellActiveWidget === _activeWidget ||
-      _shellActiveWidget === this._remote
-    ) {
+    if (!_shellActiveWidget || _shellActiveWidget === _activeWidget) {
       /* modals and stuff? */
       return;
     }
@@ -310,8 +440,13 @@ export class DeckManager implements IDeckManager {
     if (_shellActiveWidget) {
       const presenter = this._getPresenter(_shellActiveWidget);
       if (presenter) {
+        this._activePresenter = presenter;
         await presenter.start(_shellActiveWidget);
+      } else {
+        this._activePresenter = null;
       }
+    } else {
+      this._activePresenter = null;
     }
 
     this._addDeckStyles();
@@ -399,23 +534,6 @@ export class DeckManager implements IDeckManager {
     this._shell.fit();
     setTimeout(this._addDeckStyles, 10);
   };
-
-  protected _active = false;
-  protected _activeChanged = new Signal<IDeckManager, void>(this);
-  protected _activeWidget: Widget | null = null;
-  protected _presenters: IPresenter<any>[] = [];
-  protected _appStarted: Promise<void>;
-  protected _commands: CommandRegistry;
-  protected _remote: DeckRemote | null = null;
-  protected _settings: Promise<ISettingRegistry.ISettings>;
-  protected _shell: LabShell;
-  protected _statusbar: StatusBar | null;
-  protected _statusBarWasEnabled = false;
-  protected _styleCache = new Map<HTMLElement, string>();
-  protected _trans: TranslationBundle;
-  protected _stylePresets = new Map<string, IStylePreset>();
-  protected _stylePresetsChanged = new Signal<IDeckManager, void>(this);
-  protected _fonts: IFontManager;
 }
 
 export namespace DeckManager {
diff --git a/js/jupyterlab-deck/src/notebook/decktools.tsx b/js/jupyterlab-deck/src/notebook/metadata.tsx
similarity index 84%
rename from js/jupyterlab-deck/src/notebook/decktools.tsx
rename to js/jupyterlab-deck/src/notebook/metadata.tsx
index 5edacb3..1dcc4fe 100644
--- a/js/jupyterlab-deck/src/notebook/decktools.tsx
+++ b/js/jupyterlab-deck/src/notebook/metadata.tsx
@@ -1,8 +1,4 @@
-import {
-  ISettings,
-  PACKAGE_NAME as FONTS_PACKAGE_NAME,
-  Stylist,
-} from '@deathbeds/jupyterlab-fonts';
+import { ISettings, Stylist } from '@deathbeds/jupyterlab-fonts';
 import { VDomModel, VDomRenderer } from '@jupyterlab/apputils';
 import { Cell, ICellModel } from '@jupyterlab/cells';
 import { INotebookTools, NotebookTools } from '@jupyterlab/notebook';
@@ -23,13 +19,10 @@ import {
   TLayerScope,
 } from '../tokens';
 
-const NULL_SELECTOR = '';
-const PRESENTING_CELL = `body[data-jp-deck-mode='presenting'] &`;
-
 type __ = IDeckManager['__'];
 
-export class NotebookDeckTools extends NotebookTools.Tool {
-  constructor(options: NotebookDeckTools.IOptions) {
+export class NotebookMetaTools extends NotebookTools.Tool {
+  constructor(options: NotebookMetaTools.IOptions) {
     super();
     const layout = new PanelLayout();
     this._model = new DeckCellEditor.Model(options);
@@ -50,7 +43,7 @@ export class NotebookDeckTools extends NotebookTools.Tool {
   protected _notebookTools: INotebookTools;
 }
 
-export namespace NotebookDeckTools {
+export namespace NotebookMetaTools {
   export interface IOptions {
     manager: IDeckManager;
     notebookTools: INotebookTools;
@@ -162,7 +155,7 @@ export class DeckCellEditor extends VDomRenderer<DeckCellEditor.Model> {
 
 export namespace DeckCellEditor {
   export class Model extends VDomModel {
-    constructor(options: NotebookDeckTools.IOptions) {
+    constructor(options: NotebookMetaTools.IOptions) {
       super();
       this._manager = options.manager;
       this._notebookTools = options.notebookTools;
@@ -170,7 +163,7 @@ export namespace DeckCellEditor {
 
     update() {
       this._activeMeta =
-        (this._activeCell?.model.metadata.get(META) as any as ICellDeckMetadata) ||
+        (this._activeCell?.model.metadata.get(META.deck) as any as ICellDeckMetadata) ||
         JSONExt.emptyObject;
       this.stateChanged.emit(void 0);
     }
@@ -197,7 +190,7 @@ export namespace DeckCellEditor {
         return;
       }
       let meta = {
-        ...((this._activeCell.model.metadata.get(FONTS_PACKAGE_NAME) ||
+        ...((this._activeCell.model.metadata.get(META.fonts) ||
           JSONExt.emptyObject) as ISettings),
       };
       for (const preset of this._manager.stylePresets) {
@@ -209,22 +202,22 @@ export namespace DeckCellEditor {
         }
 
         let metaStyles = meta['styles'] as any;
-        if (!metaStyles[NULL_SELECTOR]) {
-          metaStyles[NULL_SELECTOR] = {};
+        if (!metaStyles[META.nullSelector]) {
+          metaStyles[META.nullSelector] = {};
         }
 
-        let metaNull = metaStyles[NULL_SELECTOR] as any;
+        let metaNull = metaStyles[META.nullSelector] as any;
 
-        if (!metaNull[PRESENTING_CELL]) {
-          metaNull[PRESENTING_CELL] = {};
+        if (!metaNull[META.presentingCell]) {
+          metaNull[META.presentingCell] = {};
         }
 
-        let presenting = metaNull[PRESENTING_CELL];
+        let presenting = metaNull[META.presentingCell];
         for (let [key, value] of Object.entries(preset.styles)) {
           presenting[key] = value;
         }
-        this._activeCell.model.metadata.delete(FONTS_PACKAGE_NAME);
-        this._activeCell.model.metadata.set(FONTS_PACKAGE_NAME, meta as any);
+        this._activeCell.model.metadata.delete(META.fonts);
+        this._activeCell.model.metadata.set(META.fonts, meta as any);
         this.forceStyle();
         this._notebookTools.update();
         return;
@@ -237,7 +230,7 @@ export namespace DeckCellEditor {
         return;
       }
       let stylist = (this._manager.fonts as any)._stylist as Stylist;
-      let meta = panel.model?.metadata.get(FONTS_PACKAGE_NAME) || JSONExt.emptyObject;
+      let meta = panel.model?.metadata.get(META.fonts) || JSONExt.emptyObject;
       stylist.stylesheet(meta as ISettings, panel);
     }
 
@@ -256,9 +249,9 @@ export namespace DeckCellEditor {
 
     protected _setDeckMetadata(newMeta: ICellDeckMetadata, cell: Cell<ICellModel>) {
       if (Object.keys(newMeta).length) {
-        cell.model.metadata.set(META, newMeta as ReadonlyPartialJSONObject);
+        cell.model.metadata.set(META.deck, newMeta as ReadonlyPartialJSONObject);
       } else {
-        cell.model.metadata.delete(META);
+        cell.model.metadata.delete(META.deck);
       }
     }
 
diff --git a/js/jupyterlab-deck/src/notebook/presenter.ts b/js/jupyterlab-deck/src/notebook/presenter.ts
index 8c277d5..ba0f978 100644
--- a/js/jupyterlab-deck/src/notebook/presenter.ts
+++ b/js/jupyterlab-deck/src/notebook/presenter.ts
@@ -1,4 +1,6 @@
-import { ICellModel } from '@jupyterlab/cells';
+import { ISettings, Stylist } from '@deathbeds/jupyterlab-fonts';
+import type { GlobalStyles } from '@deathbeds/jupyterlab-fonts/lib/_schema';
+import { Cell, ICellModel } from '@jupyterlab/cells';
 import {
   INotebookModel,
   INotebookTools,
@@ -26,9 +28,11 @@ import {
   COMPOUND_KEYS,
   META,
   ICellDeckMetadata,
+  TLayerScope,
 } from '../tokens';
+import type { Layover } from '../tools/layover';
 
-import { NotebookDeckTools } from './decktools';
+import { NotebookMetaTools } from './metadata';
 
 const emptyMap = Object.freeze(new Map());
 
@@ -36,6 +40,14 @@ const emptyMap = Object.freeze(new Map());
 export class NotebookPresenter implements IPresenter<NotebookPanel> {
   public readonly id = 'notebooks';
   public readonly rank = 100;
+  public readonly capabilities = {
+    layout: true,
+    slideType: true,
+    layerScope: true,
+    stylePart: true,
+    styleDeck: true,
+  };
+
   protected _manager: IDeckManager;
   protected _previousActiveCellIndex: number = -1;
   protected _commands: CommandRegistry;
@@ -105,14 +117,111 @@ export class NotebookPresenter implements IPresenter<NotebookPanel> {
 
     panel.content.activeCellChanged.connect(this._onActiveCellChanged, this);
     await this._onActiveCellChanged(panel.content);
+    this._forceStyle();
   }
 
   public get activeChanged(): ISignal<IPresenter<NotebookPanel>, void> {
     return this._activeChanged;
   }
 
+  public getSlideType(panel: NotebookPanel): TSlideType {
+    let { activeCell } = panel.content;
+    if (activeCell) {
+      const meta = (activeCell.model.metadata.get(META.slideshow) || {}) as any;
+      return (meta[META.slideType] || null) as TSlideType;
+    }
+    return null;
+  }
+
+  public setSlideType(panel: NotebookPanel, slideType: TSlideType): void {
+    let { activeCell } = panel.content;
+    if (!activeCell) {
+      return;
+    }
+    let oldMeta =
+      (activeCell.model.metadata.get(META.slideshow) as Record<string, any>) || null;
+    if (slideType == null) {
+      if (oldMeta == null) {
+        activeCell.model.metadata.delete(META.slideshow);
+      } else {
+        activeCell.model.metadata.set(META.slideshow, {
+          ...oldMeta,
+          [META.slideType]: slideType,
+        });
+      }
+    } else {
+      if (oldMeta == null) {
+        oldMeta = {};
+      }
+      activeCell.model.metadata.set(META.slideshow, {
+        ...oldMeta,
+        [META.slideType]: slideType,
+      });
+    }
+    if (panel.content.model) {
+      this._onNotebookContentChanged(panel.content.model);
+    }
+    void this._onActiveCellChanged(panel.content);
+  }
+
+  public getLayerScope(panel: NotebookPanel): string | null {
+    let { activeCell } = panel.content;
+    if (activeCell) {
+      const meta = (activeCell.model.metadata.get(META.deck) || {}) as any;
+      return (meta[META.layer] || null) as TLayerScope;
+    }
+    return null;
+  }
+
+  public setLayerScope(panel: NotebookPanel, layerScope: TLayerScope): void {
+    let { activeCell } = panel.content;
+    if (!activeCell) {
+      return;
+    }
+    let oldMeta =
+      (activeCell.model.metadata.get(META.deck) as Record<string, any>) || null;
+    if (layerScope == null) {
+      if (oldMeta == null) {
+        activeCell.model.metadata.delete(META.layer);
+      } else {
+        activeCell.model.metadata.set(META.deck, {
+          ...oldMeta,
+          [META.layer]: layerScope,
+        });
+      }
+    } else {
+      if (oldMeta == null) {
+        oldMeta = {};
+      }
+      activeCell.model.metadata.set(META.deck, {
+        ...oldMeta,
+        [META.layer]: layerScope,
+      });
+    }
+    if (panel.content.model) {
+      this._onNotebookContentChanged(panel.content.model);
+    }
+    void this._onActiveCellChanged(panel.content);
+  }
+
+  public getPartStyles(panel: NotebookPanel): GlobalStyles | null {
+    let { activeCell } = panel.content;
+    if (!activeCell) {
+      return null;
+    }
+    return this._getCellStyles(activeCell);
+  }
+
+  public setPartStyles(panel: NotebookPanel, styles: GlobalStyles | null): void {
+    let { activeCell } = panel.content;
+    if (!activeCell) {
+      return;
+    }
+    return this._setCellStyles(activeCell, styles);
+  }
+
   protected _makeDeckTools(notebookTools: INotebookTools) {
-    const tool = new NotebookDeckTools({ manager: this._manager, notebookTools });
+    const tool = new NotebookMetaTools({ manager: this._manager, notebookTools });
     notebookTools.addItem({ tool, section: 'common', rank: 3 });
   }
 
@@ -286,6 +395,10 @@ export class NotebookPresenter implements IPresenter<NotebookPanel> {
 
     idx = -1;
 
+    let { layover } = this._manager;
+
+    let onScreen: Layover.BasePart[] = [];
+
     for (const cell of notebook.widgets) {
       idx++;
 
@@ -293,6 +406,9 @@ export class NotebookPresenter implements IPresenter<NotebookPanel> {
         cell.addClass(CSS.layer);
         cell.addClass(CSS.onScreen);
         cell.addClass(CSS.visible);
+        if (layover) {
+          onScreen.push(this._toLayoutPart(cell));
+        }
       } else {
         cell.removeClass(CSS.layer);
         if (activeExtent.visible.includes(idx)) {
@@ -303,6 +419,9 @@ export class NotebookPresenter implements IPresenter<NotebookPanel> {
         }
         if (activeExtent.onScreen.includes(idx)) {
           cell.addClass(CSS.onScreen);
+          if (layover) {
+            onScreen.push(this._toLayoutPart(cell));
+          }
         } else {
           cell.removeClass(CSS.onScreen);
         }
@@ -314,6 +433,53 @@ export class NotebookPresenter implements IPresenter<NotebookPanel> {
       notebook.widgets[activeCellIndex].node
     );
     this._activeChanged.emit(void 0);
+    if (this._manager.layover) {
+      this._manager.layover.model.parts = onScreen;
+    }
+  }
+
+  protected _forceStyle() {
+    let panel = this._manager.activeWidget;
+    if (!panel || !(panel instanceof NotebookPanel)) {
+      return;
+    }
+    let stylist = (this._manager.fonts as any)._stylist as Stylist;
+    let meta = panel.model?.metadata.get(META.fonts) || JSONExt.emptyObject;
+    stylist.stylesheet(meta as ISettings, panel);
+    this._manager.layover?.render();
+  }
+
+  protected _toLayoutPart(cell: Cell<ICellModel>): Layover.BasePart {
+    return {
+      key: cell.model.id,
+      node: cell.node,
+      getStyles: () => this._getCellStyles(cell),
+      setStyles: (styles: GlobalStyles | null) => this._setCellStyles(cell, styles),
+    };
+  }
+
+  protected _getCellStyles(cell: Cell<ICellModel>) {
+    try {
+      const meta = cell.model.metadata.get(META.fonts) as any;
+      const styles = meta.styles[META.nullSelector][META.presentingCell];
+      return styles;
+    } catch {
+      return JSONExt.emptyObject;
+    }
+  }
+
+  protected _setCellStyles(cell: Cell<ICellModel>, styles: GlobalStyles | null) {
+    let meta = (cell.model.metadata.get(META.fonts) || {}) as ISettings;
+    if (!meta.styles) {
+      meta.styles = {};
+    }
+    if (!meta.styles[META.nullSelector]) {
+      meta.styles[META.nullSelector] = {};
+    }
+    (meta.styles[META.nullSelector] as any)[META.presentingCell] = styles;
+    cell.model.metadata.set(META.fonts, JSONExt.emptyObject as any);
+    cell.model.metadata.set(META.fonts, { ...meta } as any);
+    this._forceStyle();
   }
 
   /** Get the nbconvert-compatible `slide_type` from metadata. */
@@ -358,7 +524,8 @@ export class NotebookPresenter implements IPresenter<NotebookPanel> {
 
   /** Get layer metadata from `jupyterlab-deck` namespace */
   protected _getCellDeckMetadata(cell: ICellModel): ICellDeckMetadata {
-    return (cell.metadata.get(META) || JSONExt.emptyObject) as any as ICellDeckMetadata;
+    return (cell.metadata.get(META.deck) ||
+      JSONExt.emptyObject) as any as ICellDeckMetadata;
   }
 
   _numSort(a: number, b: number): number {
diff --git a/js/jupyterlab-deck/src/plugin.ts b/js/jupyterlab-deck/src/plugin.ts
index b834c2d..176f5e8 100644
--- a/js/jupyterlab-deck/src/plugin.ts
+++ b/js/jupyterlab-deck/src/plugin.ts
@@ -51,9 +51,13 @@ const plugin: JupyterFrontEndPlugin<IDeckManager> = {
 
     const { __ } = manager;
 
+    let category = __(CATEGORY);
+
     if (palette) {
-      palette.addItem({ command: CommandIds.start, category: __(CATEGORY) });
-      palette.addItem({ command: CommandIds.stop, category: __(CATEGORY) });
+      palette.addItem({ command: CommandIds.start, category });
+      palette.addItem({ command: CommandIds.stop, category });
+      palette.addItem({ command: CommandIds.showLayover, category });
+      palette.addItem({ command: CommandIds.hideLayover, category });
     }
 
     return manager;
diff --git a/js/jupyterlab-deck/src/tokens.ts b/js/jupyterlab-deck/src/tokens.ts
index 99cb509..5ef9adc 100644
--- a/js/jupyterlab-deck/src/tokens.ts
+++ b/js/jupyterlab-deck/src/tokens.ts
@@ -1,10 +1,17 @@
-import { IFontManager, IStyles } from '@deathbeds/jupyterlab-fonts';
+import {
+  PACKAGE_NAME as FONTS_PACKAGE_NAME,
+  IFontManager,
+  IStyles,
+} from '@deathbeds/jupyterlab-fonts';
+import type { GlobalStyles } from '@deathbeds/jupyterlab-fonts/lib/_schema';
 import { Token } from '@lumino/coreutils';
 import { ISignal } from '@lumino/signaling';
 import { Widget } from '@lumino/widgets';
 
 import * as _PACKAGE from '../package.json';
 
+import type { Layover } from './tools/layover';
+
 export const PACKAGE = _PACKAGE;
 
 export const NS = PACKAGE.name;
@@ -12,10 +19,9 @@ export const VERSION = PACKAGE.version;
 export const PLUGIN_ID = `${NS}:plugin`;
 export const CATEGORY = 'Decks';
 /** The cell/notebook metadata. */
-export const META = NS.split('/')[1];
 
 export interface IDeckManager {
-  start(): Promise<void>;
+  start(force: boolean): Promise<void>;
   stop(): Promise<void>;
   __: (msgid: string, ...args: string[]) => string;
   go(direction: TDirection, alternate?: TDirection): void;
@@ -26,18 +32,44 @@ export interface IDeckManager {
   addStylePreset(preset: IStylePreset): void;
   stylePresets: IStylePreset[];
   activeWidget: Widget | null;
+  layover: Layover | null;
   // signals
   activeChanged: ISignal<IDeckManager, void>;
   stylePresetsChanged: ISignal<IDeckManager, void>;
   // re-hosted
   fonts: IFontManager;
+  showLayover(): void;
+  hideLayover(): void;
+  layoverChanged: ISignal<IDeckManager, void>;
+  activePresenter: IPresenter<any> | null;
+  setSlideType(slideType: TSlideType): void;
+  getSlideType(): TSlideType;
+  getLayerScope(): TLayerScope | null;
+  setLayerScope(layerScope: TLayerScope | null): void;
+  getPartStyles(): GlobalStyles | null;
+  setPartStyles(styles: GlobalStyles | null): void;
 }
 
 export const IDeckManager = new Token<IDeckManager>(PLUGIN_ID);
 
+export interface IPresenterCapbilities {
+  layout: boolean;
+  slideType: boolean;
+  layerScope: boolean;
+  stylePart: boolean;
+}
+
+export const INCAPABLE: IPresenterCapbilities = Object.freeze({
+  layout: false,
+  slideType: false,
+  layerScope: false,
+  stylePart: false,
+});
+
 export interface IPresenter<T extends Widget> {
   id: string;
   rank: number;
+  capabilities: IPresenterCapbilities;
   accepts(widget: Widget): T | null;
   stop(widget: Widget): Promise<void>;
   start(widget: T): Promise<void>;
@@ -45,27 +77,23 @@ export interface IPresenter<T extends Widget> {
   canGo(widget: T): Partial<TCanGoDirection>;
   style(widget: T): void;
   activeChanged: ISignal<IPresenter<T>, void>;
+
+  setSlideType(widget: T, slideType: TSlideType): void;
+  getSlideType(widget: T): TSlideType;
+  setLayerScope(widget: T, layerType: TLayerScope | null): void;
+  getLayerScope(widget: T): TLayerScope | null;
+  getPartStyles(widget: T): GlobalStyles | null;
+  setPartStyles(widget: T, styles: GlobalStyles | null): void;
 }
 
 export namespace DATA {
   export const deckMode = 'jpDeckMode';
   export const presenting = 'presenting';
+  export const layoutMode = 'jpDeckLayoutMode';
+  export const designing = 'designing';
 }
 
 export namespace CSS {
-  export const deck = 'jp-Deck';
-  export const remote = 'jp-Deck-Remote';
-  export const directions = 'jp-Deck-Remote-directions';
-  export const direction = 'jp-deck-mod-direction';
-  export const onScreen = 'jp-deck-mod-onscreen';
-  export const visible = 'jp-deck-mod-visible';
-  export const layer = 'jp-deck-mod-layer';
-  export const mainContent = 'jp-main-content-panel';
-  export const presenting = `[data-jp-deck-mode='${DATA.presenting}']`;
-  export const stop = 'jp-deck-mod-stop';
-  export const metaTool = 'jp-Deck-Metadata';
-  export const selectSplit = 'jp-Deck-SelectSplit';
-  export const apply = 'jp-deck-mod-apply';
   // lab
   export const disabled = 'jp-mod-disabled';
   export const icon = 'jp-icon3';
@@ -75,9 +103,42 @@ export namespace CSS {
   export const selectWrapper = 'jp-select-wrapper';
   export const styled = 'jp-mod-styled';
   export const accept = 'jp-mod-accept';
-  // tools
+  export const active = 'jp-mod-active';
+  export const mainContent = 'jp-main-content-panel';
+  // deck
+  export const deck = 'jp-Deck';
+  export const presenting = `[data-jp-deck-mode='${DATA.presenting}']`;
+  // remote
+  export const remote = 'jp-Deck-Remote';
+  export const directions = 'jp-Deck-Remote-directions';
+  export const stop = 'jp-deck-mod-stop';
+  // notebook
+  export const direction = 'jp-deck-mod-direction';
+  export const onScreen = 'jp-deck-mod-onscreen';
+  export const visible = 'jp-deck-mod-visible';
+  export const layer = 'jp-deck-mod-layer';
+  // metadata
+  export const metaTool = 'jp-Deck-Metadata';
+  export const selectSplit = 'jp-Deck-SelectSplit';
   export const toolLayer = 'jp-Deck-Tool-layer';
   export const toolPreset = 'jp-Deck-Tool-preset';
+  export const apply = 'jp-deck-mod-apply';
+  // layover
+  export const layover = 'jp-Deck-Layover';
+  export const layoverPart = 'jp-Deck-LayoverPart';
+  export const layoverPartLabel = 'jp-Deck-LayoverLabel';
+  export const layoverHandle = 'jp-Deck-LayoverHandle';
+  export const dragging = 'jp-deck-mod-dragging';
+  export const layoverUnstyle = 'jp-Deck-LayoverUnstyle';
+  // design tools
+  export const designTools = 'jp-Deck-DesignTools';
+  export const selector = 'jp-Deck-DesignTools-Selector';
+  export const slider = 'jp-Deck-DesignTools-Slider';
+  export const slideType = 'jp-deck-mod-slidetype';
+  export const layerScope = 'jp-deck-mod-layerscope';
+  export const zoom = 'jp-deck-mod-zoom';
+  export const opacity = 'jp-deck-mod-opacity';
+  export const zIndex = 'jp-deck-mod-z-index';
 }
 
 export namespace ID {
@@ -134,18 +195,35 @@ export namespace CommandIds {
   export const back = 'deck:back';
   export const down = 'deck:down';
   export const up = 'deck:up';
+  /* layover */
+  export const showLayover = 'deck:show-layover';
+  export const hideLayover = 'deck:hide-layover';
+}
+
+export namespace META {
+  // nbconvert
+  export const slideshow = 'slideshow';
+  export const slideType = 'slide_type';
+  // fonts
+  export const fonts = FONTS_PACKAGE_NAME;
+  export const nullSelector = '';
+  export const presentingCell = `body[data-jp-deck-mode='presenting'] &`;
+  // deck
+  export const deck = NS.split('/')[1];
+  export const layer = 'layer';
 }
 
 /**
  * mutually-exclusive `cells/{i}/metadata/slideshow` values supported by
  * nbconvert, notebook, and lab UI
  **/
-export type TSlideType = 'fragment' | 'slide' | 'subslide' | 'skip' | 'notes' | null;
+export const SLIDE_TYPES = ['slide', 'subslide', null, 'fragment', 'notes', 'skip'];
+export type TSlideType = typeof SLIDE_TYPES[number];
 
 /** The scope of extents that will have this layer */
-export type TLayerScope = 'deck' | 'stack' | 'slide' | 'fragment';
 
-export const LAYER_SCOPES: TLayerScope[] = ['deck', 'stack', 'slide', 'fragment'];
+export const LAYER_SCOPES = ['deck', 'stack', 'slide', 'fragment'];
+export type TLayerScope = typeof LAYER_SCOPES[number];
 
 export type TSelectLabels<T extends string> = Record<T, string>;
 
diff --git a/js/jupyterlab-deck/src/tools/design.tsx b/js/jupyterlab-deck/src/tools/design.tsx
new file mode 100644
index 0000000..9f75648
--- /dev/null
+++ b/js/jupyterlab-deck/src/tools/design.tsx
@@ -0,0 +1,368 @@
+import type { GlobalStyles } from '@deathbeds/jupyterlab-fonts/lib/_schema';
+import { VDomRenderer, VDomModel } from '@jupyterlab/apputils';
+import { LabIcon, ellipsesIcon, caretLeftIcon } from '@jupyterlab/ui-components';
+import { JSONExt } from '@lumino/coreutils';
+import React from 'react';
+
+import { ICONS } from '../icons';
+import {
+  CSS,
+  IDeckManager,
+  INCAPABLE,
+  IPresenterCapbilities,
+  LAYER_SCOPES,
+  SLIDE_TYPES,
+  TLayerScope,
+  TSlideType,
+} from '../tokens';
+
+export class DesignTools extends VDomRenderer<DesignTools.Model> {
+  constructor(options: DesignTools.IOptions) {
+    super(new DesignTools.Model(options));
+    this.addClass(CSS.designTools);
+    document.body.appendChild(this.node);
+  }
+
+  dispose() {
+    super.dispose();
+    if (this.model) {
+      this.model.dispose();
+    }
+    document.body.removeChild(this.node);
+  }
+
+  protected render(): JSX.Element {
+    return <div>{this.more()}</div>;
+  }
+
+  more(): JSX.Element[] {
+    const { model } = this;
+    const { __ } = model.manager;
+    const { capabilities, showMore } = model;
+
+    if (!capabilities.layout && !capabilities.slideType && !capabilities.layerScope) {
+      return [];
+    }
+
+    if (!showMore) {
+      return [
+        this.makeButton(
+          ellipsesIcon,
+          __('Show Design Tools'),
+          () => (model.showMore = true)
+        ),
+      ];
+    }
+
+    let items: JSX.Element[] = [
+      this.makeButton(
+        caretLeftIcon,
+        __('Hide Design Tools'),
+        () => (model.showMore = false)
+      ),
+    ];
+
+    const { layover } = this.model.manager;
+
+    if (capabilities.layout) {
+      items.push(
+        this.makeButton(
+          layover ? ICONS.transformStop : ICONS.transformStart,
+          layover ? __('Hide Layout') : __('Show Layout'),
+          () => {
+            let { manager } = this.model;
+            manager.layover ? manager.hideLayover() : manager.showLayover();
+          }
+        )
+      );
+    }
+
+    if (capabilities.slideType) {
+      const { currentSlideType } = model;
+      let slideTypes: JSX.Element[] = [];
+      let slideType: TSlideType;
+      let activeItem: JSX.Element | null = null;
+      for (slideType of SLIDE_TYPES) {
+        let item = this.makeSlideTypeItem(slideType, currentSlideType);
+        if (currentSlideType === slideType) {
+          activeItem = item;
+        } else {
+          slideTypes.push(item);
+        }
+      }
+      if (activeItem) {
+        slideTypes.push(activeItem);
+      }
+      items.push(
+        <ul key="slide-type" className={`${CSS.selector} ${CSS.slideType}`}>
+          {slideTypes}
+        </ul>
+      );
+    }
+
+    if (capabilities.layerScope) {
+      const { currentLayerScope } = model;
+      let layerScopes: JSX.Element[] = [];
+      let layerScope: TLayerScope | null;
+      let activeItem: JSX.Element | null = null;
+      for (layerScope of [...LAYER_SCOPES, null]) {
+        let item = this.makeLayerScopeItem(layerScope, currentLayerScope);
+        if (currentLayerScope === layerScope) {
+          activeItem = item;
+        } else {
+          layerScopes.push(item);
+        }
+      }
+      if (activeItem) {
+        layerScopes.push(activeItem);
+      }
+      items.push(
+        <ul key="layer" className={`${CSS.selector} ${CSS.layerScope}`}>
+          {layerScopes}
+        </ul>
+      );
+    }
+
+    if (capabilities.stylePart) {
+      let { currentPartStyles } = model;
+      items.push(
+        this.makeSlider('z-index', currentPartStyles),
+        this.makeSlider('zoom', currentPartStyles),
+        this.makeSlider('opacity', currentPartStyles)
+      );
+    }
+
+    return items;
+  }
+
+  makeSlideTypeItem = (
+    slideType: TSlideType,
+    currentSlideType: TSlideType
+  ): JSX.Element => {
+    let { __ } = this.model.manager;
+    let slideTypeKey = slideType == null ? 'null' : slideType;
+    let icon = ICONS.slideshow[slideTypeKey];
+    let label = __(slideTypeKey);
+    let button = this.makeButton(
+      icon,
+      label,
+      () => this.model.manager.setSlideType(slideType),
+      '',
+      [<label key="label">{label}</label>]
+    );
+    return (
+      <li key={slideType} className={currentSlideType === slideType ? CSS.active : ''}>
+        {button}
+      </li>
+    );
+  };
+
+  makeLayerScopeItem = (
+    layerScope: TLayerScope | null,
+    currentLayerScope: TLayerScope | null
+  ): JSX.Element => {
+    let { __ } = this.model.manager;
+    let layerScopeKey = layerScope == null ? 'null' : layerScope;
+    let icon = ICONS.layer[layerScopeKey];
+    let label = __(layerScopeKey);
+    let button = this.makeButton(
+      icon,
+      label,
+      () => this.model.manager.setLayerScope(layerScope),
+      '',
+      [<label key="label">{label}</label>]
+    );
+    return (
+      <li className={currentLayerScope === layerScope ? CSS.active : ''}>{button}</li>
+    );
+  };
+
+  makeSlider = (
+    attr: DesignTools.TSliderAttr,
+    styles: GlobalStyles | null
+  ): JSX.Element => {
+    const config = DesignTools.SLIDER_CONFIG[attr];
+    const { suffix, className, icon } = config;
+
+    let value = styles ? (styles[attr] as any) : null;
+
+    let checkbox: JSX.Element | null = null;
+    if (value != null) {
+      if (suffix) {
+        value = value.replace(suffix, '');
+      }
+      value = parseFloat(value);
+      checkbox = (
+        <input
+          type="checkbox"
+          name={`${attr}-enabled`}
+          checked
+          onChange={this.onSliderChange}
+        ></input>
+      );
+    }
+
+    let valueAttr = { value: value == null ? config.defaultValue : value };
+    let finalClassName = value == null ? className : `${className} ${CSS.active}`;
+
+    return (
+      <div className={`${CSS.slider} ${finalClassName}`} title={attr} key={attr}>
+        {checkbox ? [checkbox] : []}
+        <input
+          name={attr}
+          type="range"
+          {...config.attrs}
+          {...valueAttr}
+          onChange={this.onSliderChange}
+        ></input>
+        <label>
+          <icon.react width={32} />
+          <span>{attr}</span>
+        </label>
+      </div>
+    );
+  };
+
+  onSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+    const { value, name, checked } = event.currentTarget;
+    if (name.endsWith('-enabled')) {
+      if (!checked) {
+        this.model.setPartStyles(name.replace('-enabled', ''), null);
+      }
+    } else {
+      const config = DesignTools.SLIDER_CONFIG[name as DesignTools.TSliderAttr];
+      const suffix = config.suffix || '';
+      this.model.setPartStyles(name, `${value}${suffix}`);
+    }
+  };
+
+  makeButton(
+    icon: LabIcon,
+    title: string,
+    onClick: () => void,
+    className: string = '',
+    children: JSX.Element[] = []
+  ) {
+    return (
+      <button
+        className={className}
+        onClick={onClick}
+        title={this.model.manager.__(title)}
+        key={title}
+      >
+        <icon.react width={32} />
+        {children}
+      </button>
+    );
+  }
+}
+
+export namespace DesignTools {
+  export class Model extends VDomModel {
+    private _manager: IDeckManager;
+    private _showMore = false;
+
+    constructor(options: IOptions) {
+      super();
+      this._manager = options.manager;
+      this._manager.activeChanged.connect(this._onActiveChanged, this);
+      this._manager.layoverChanged.connect(this._emit);
+    }
+
+    dispose() {
+      this._manager.activeChanged.disconnect(this._onActiveChanged, this);
+      this._manager.layoverChanged.disconnect(this._emit);
+      super.dispose();
+    }
+
+    get manager(): IDeckManager {
+      return this._manager;
+    }
+
+    get showMore() {
+      return this._showMore;
+    }
+
+    set showMore(showMore: boolean) {
+      if (this._showMore !== showMore) {
+        this._showMore = showMore;
+        this._emit();
+      }
+    }
+
+    get capabilities(): IPresenterCapbilities {
+      return this._manager.activePresenter?.capabilities || INCAPABLE;
+    }
+
+    get currentSlideType(): TSlideType | null {
+      return this._manager.getSlideType();
+    }
+
+    get currentLayerScope(): TLayerScope | null {
+      return this._manager.getLayerScope();
+    }
+
+    get currentPartStyles(): GlobalStyles | null {
+      let styles = this._manager.getPartStyles();
+      return styles;
+    }
+
+    setPartStyles(attr: string, value: any) {
+      let styles = { ...(this.currentPartStyles || JSONExt.emptyObject) };
+      styles[attr] = value;
+      this._manager.setPartStyles(styles as GlobalStyles);
+      this._emit();
+    }
+
+    private _emit = () => {
+      this.stateChanged.emit(void 0);
+    };
+
+    private _onActiveChanged() {
+      this.stateChanged.emit(void 0);
+    }
+  }
+  export interface IOptions {
+    manager: IDeckManager;
+  }
+
+  export type TSliderAttr = 'z-index' | 'zoom' | 'opacity';
+
+  export interface ISliderBounds {
+    attrs: {
+      min: number;
+      max: number;
+      step: number;
+    };
+    defaultValue: number;
+    suffix?: string;
+    icon: LabIcon;
+    className: string;
+  }
+  export type TSliders = {
+    [key in TSliderAttr]: ISliderBounds;
+  };
+
+  export const SLIDER_CONFIG: TSliders = {
+    zoom: {
+      attrs: { min: 50, max: 500, step: 1 },
+      defaultValue: 100,
+      suffix: '%',
+      icon: ICONS.zoom,
+      className: CSS.zoom,
+    },
+    'z-index': {
+      attrs: { min: -10, max: 10, step: 1 },
+      defaultValue: 0,
+      icon: ICONS.zIndex,
+      className: CSS.zIndex,
+    },
+    opacity: {
+      attrs: { min: 0, max: 100, step: 1 },
+      suffix: '%',
+      defaultValue: 100,
+      icon: ICONS.opacity,
+      className: CSS.opacity,
+    },
+  };
+}
diff --git a/js/jupyterlab-deck/src/tools/layover.ts b/js/jupyterlab-deck/src/tools/layover.ts
new file mode 100644
index 0000000..628929b
--- /dev/null
+++ b/js/jupyterlab-deck/src/tools/layover.ts
@@ -0,0 +1,260 @@
+import type { GlobalStyles } from '@deathbeds/jupyterlab-fonts/lib/_schema';
+import { VDomModel } from '@jupyterlab/apputils';
+import { JSONExt } from '@lumino/coreutils';
+import { Widget } from '@lumino/widgets';
+import { drag, D3DragEvent } from 'd3-drag';
+import * as d3 from 'd3-selection';
+
+import { IDeckManager, CSS, DATA } from '../tokens';
+
+/** An interactive layer positioner. */
+export class Layover extends Widget {
+  protected _model: Layover.Model;
+  protected _d3: typeof d3 | null = null;
+  protected _drag: typeof drag | null = null;
+
+  constructor(options: Layover.IOptions) {
+    super(options);
+    this.addClass(CSS.layover);
+    this._model = new Layover.Model();
+    document.body.appendChild(this.node);
+    this.model.stateChanged.connect(this.render, this);
+    window.addEventListener('resize', this.render);
+    document.body.dataset[DATA.layoutMode] = DATA.designing;
+    this.render();
+  }
+
+  dispose(): void {
+    /* istanbul ignore if */
+    if (this.isDisposed) {
+      return;
+    }
+    this.model.dispose();
+    const { node } = this;
+    super.dispose();
+    document.body.removeChild(node);
+    window.removeEventListener('resize', this.render);
+    delete document.body.dataset[DATA.layoutMode];
+  }
+
+  render = () => {
+    const boxes = d3
+      .select(this.node)
+      .selectAll(`.${CSS.layoverPart}`)
+      .data(this.model.partData, Layover.getPartKey as any)
+      .join('div')
+      .classed(CSS.layoverPart, true)
+      .style('left', ({ bounds }) => `${bounds.left}px`)
+      .style('top', ({ bounds }) => `${bounds.top}px`)
+      .style('height', ({ bounds }) => `${bounds.height}px`)
+      .style('width', ({ bounds }) => `${bounds.width}px`)
+      .call(Layover.boxDrag as any);
+
+    boxes
+      .selectAll(`.${CSS.layoverPartLabel}`)
+      .data((d, i) => [i])
+      .join('div')
+      .classed(CSS.layoverPartLabel, true)
+      .text((d) => d + 1);
+
+    boxes
+      .selectAll(`.${CSS.layoverHandle}`)
+      .data(Layover.handleData)
+      .join('div')
+      .attr('class', Layover.getHandleClass)
+      .classed(CSS.layoverHandle, true)
+      .call(Layover.handleDrag.container(this.node) as any);
+
+    boxes
+      .selectAll(`.${CSS.layoverUnstyle}`)
+      .data(Layover.resetData)
+      .join('button')
+      .classed(CSS.layoverUnstyle, true)
+      .text('↺')
+      .on('click', Layover.onReset as any);
+  };
+
+  get model() {
+    return this._model;
+  }
+}
+
+export namespace Layover {
+  export interface IOptions extends Widget.IOptions {
+    manager: IDeckManager;
+  }
+  export class Model extends VDomModel {
+    private _parts: BasePart[] = [];
+
+    get partData() {
+      return this._parts.map(this._partDatum);
+    }
+
+    get parts() {
+      return this._parts;
+    }
+
+    set parts(parts: BasePart[]) {
+      this._parts = parts;
+      this.stateChanged.emit(void 0);
+    }
+
+    protected _partDatum = (part: BasePart) => {
+      const { left, top, width, height } = part.node.getBoundingClientRect();
+      let zoom = parseFloat(
+        window.getComputedStyle(part.node).getPropertyValue('zoom') || '1.0'
+      );
+      return {
+        ...part,
+        bounds: {
+          left: left * zoom,
+          top: top * zoom,
+          width: width * zoom,
+          height: height * zoom,
+        },
+      };
+    };
+  }
+  export interface DOMRectLike {
+    top: number;
+    left: number;
+    width: number;
+    height: number;
+  }
+  export interface BasePart {
+    key: string;
+    node: HTMLElement;
+    getStyles(): GlobalStyles | null;
+    setStyles(styles: GlobalStyles | null): void;
+  }
+  export interface Part extends BasePart {
+    bounds: DOMRectLike;
+  }
+
+  function setStyles(d: Layover.Part) {
+    const { bounds } = d;
+
+    const { innerWidth, innerHeight } = window;
+
+    d.setStyles({
+      ...(d.getStyles() || JSONExt.emptyObject),
+      position: 'fixed',
+      left: `${100 * (bounds.left / innerWidth)}%`,
+      top: `${100 * (bounds.top / innerHeight)}%`,
+      width: `${100 * (bounds.width / innerWidth)}%`,
+      height: `${100 * (bounds.height / innerHeight)}%`,
+    });
+  }
+
+  export function resetData(d: Part) {
+    let style = d.getStyles();
+    return style && style.position === 'fixed' ? [d] : [];
+  }
+
+  export function onReset(event: PointerEvent, d: Layover.Part) {
+    let styles = d.getStyles() || JSONExt.emptyObject;
+    d.setStyles({
+      ...styles,
+      position: null as any,
+      left: null as any,
+      top: null as any,
+      width: null as any,
+      height: null as any,
+    });
+  }
+
+  export function getPartKey(d: Part) {
+    return d.key;
+  }
+
+  export function getHandleClass(h: PartHandle) {
+    return `${CSS.layoverHandle}-${h.handle}`;
+  }
+
+  function onBoxDragStart(this: HTMLDivElement, event: TPartDrag, d: Layover.Part) {
+    d3.select(this).classed(CSS.dragging, true);
+  }
+
+  function onBoxDrag(this: HTMLDivElement, event: TPartDrag, d: Layover.Part) {
+    d.bounds.left += event.dx;
+    d.bounds.top += event.dy;
+    d3.select(this)
+      .style('left', `${d.bounds.left}px`)
+      .style('top', `${d.bounds.top}px`);
+  }
+
+  function onBoxDragEnd(this: HTMLDivElement, event: TPartDrag, d: Layover.Part) {
+    d3.select(this).classed(CSS.dragging, false);
+    setStyles(d);
+  }
+
+  export const boxDrag = drag<HTMLDivElement, Layover.Part>()
+    .on('start', onBoxDragStart)
+    .on('drag', onBoxDrag)
+    .on('end', onBoxDragEnd);
+
+  function onHandleDragStart(
+    this: HTMLDivElement,
+    event: TPartDrag,
+    d: Layover.PartHandle
+  ) {
+    d3.select(this.parentElement).classed(CSS.dragging, true);
+  }
+
+  function onHandleDrag(this: HTMLDivElement, event: TPartDrag, d: Layover.PartHandle) {
+    let { dx, dy } = event;
+    let { bounds } = d.part;
+    let h = d.handle;
+
+    if (h.includes('n')) {
+      bounds.top += dy;
+      bounds.height -= dy;
+    } else if (h.includes('s')) {
+      bounds.height += dy;
+    }
+    if (h.includes('w')) {
+      bounds.left += dx;
+      bounds.width -= dx;
+    } else if (h.includes('e')) {
+      bounds.width += dx;
+    }
+
+    d3.select(this.parentElement)
+      .style('left', `${bounds.left}px`)
+      .style('top', `${bounds.top}px`)
+      .style('height', `${bounds.height}px`)
+      .style('width', `${bounds.width}px`);
+  }
+
+  function onHandleDragEnd(
+    this: HTMLDivElement,
+    event: TPartDrag,
+    d: Layover.PartHandle
+  ) {
+    d3.select(this.parentElement).classed(CSS.dragging, false);
+    setStyles(d.part);
+  }
+
+  export const handleDrag = drag<HTMLDivElement, Layover.PartHandle>()
+    .on('start', onHandleDragStart)
+    .on('drag', onHandleDrag)
+    .on('end', onHandleDragEnd);
+
+  export interface PartHandle {
+    part: Part;
+    handle: THandle;
+  }
+  export function handleData(part: Part) {
+    let handleData: PartHandle[] = [];
+    for (const handle of HANDLES) {
+      handleData.push({ part, handle });
+    }
+    return handleData;
+  }
+
+  const HANDLES = ['nw', 'n', 'ne', 'w', 'e', 'sw', 's', 'se'] as const;
+
+  type THandle = typeof HANDLES[number];
+
+  type TPartDrag = D3DragEvent<HTMLDivElement, Layover.Part, Layover.Part>;
+}
diff --git a/js/jupyterlab-deck/src/remote.tsx b/js/jupyterlab-deck/src/tools/remote.tsx
similarity index 93%
rename from js/jupyterlab-deck/src/remote.tsx
rename to js/jupyterlab-deck/src/tools/remote.tsx
index fb9232f..046c97c 100644
--- a/js/jupyterlab-deck/src/remote.tsx
+++ b/js/jupyterlab-deck/src/tools/remote.tsx
@@ -3,19 +3,20 @@ import { LabIcon } from '@jupyterlab/ui-components';
 import { JSONExt } from '@lumino/coreutils';
 import React from 'react';
 
-import { ICONS } from './icons';
+import { ICONS } from '../icons';
 import {
   CSS,
   IDeckManager,
   DIRECTION,
   DIRECTION_LABEL,
   TCanGoDirection,
-} from './tokens';
+} from '../tokens';
 
 export class DeckRemote extends VDomRenderer<DeckRemote.Model> {
   constructor(options: DeckRemote.IOptions) {
     super(new DeckRemote.Model(options));
     this.addClass(CSS.remote);
+    document.body.appendChild(this.node);
   }
 
   dispose() {
@@ -23,10 +24,12 @@ export class DeckRemote extends VDomRenderer<DeckRemote.Model> {
     if (this.model) {
       this.model.dispose();
     }
+    document.body.removeChild(this.node);
   }
 
   protected render(): JSX.Element {
     const { manager, canGo } = this.model;
+    const { __ } = manager;
 
     const directions: Record<string, JSX.Element> = {};
 
@@ -42,7 +45,7 @@ export class DeckRemote extends VDomRenderer<DeckRemote.Model> {
 
     const exit = this.makeButton(
       ICONS.deckStop,
-      'Exit Deck',
+      __('Exit Deck'),
       () => void this.model.manager.stop(),
       CSS.stop
     );
@@ -80,6 +83,9 @@ export class DeckRemote extends VDomRenderer<DeckRemote.Model> {
 
 export namespace DeckRemote {
   export class Model extends VDomModel {
+    private _manager: IDeckManager;
+    private _canGo: Partial<TCanGoDirection> = {};
+
     constructor(options: IOptions) {
       super();
       this._manager = options.manager;
@@ -106,9 +112,6 @@ export namespace DeckRemote {
         this.stateChanged.emit(void 0);
       }
     }
-
-    private _manager: IDeckManager;
-    private _canGo: Partial<TCanGoDirection> = {};
   }
   export interface IOptions {
     manager: IDeckManager;
diff --git a/js/jupyterlab-deck/src/tsconfig.cov.json b/js/jupyterlab-deck/src/tsconfig.cov.json
index fb0e7f7..b3dc2a2 100644
--- a/js/jupyterlab-deck/src/tsconfig.cov.json
+++ b/js/jupyterlab-deck/src/tsconfig.cov.json
@@ -1,5 +1,4 @@
 {
-  "extends": "./tsconfig",
   "compilerOptions": {
     "rootDir": ".",
     "outDir": "../lib",
@@ -7,6 +6,7 @@
     "tsBuildInfoFile": "../.src.tsbuildinfo",
     "sourceMap": false
   },
+  "extends": "./tsconfig",
   "include": ["./**/*"],
   "references": [{ "path": "../" }]
 }
diff --git a/js/jupyterlab-deck/src/tsconfig.json b/js/jupyterlab-deck/src/tsconfig.json
index ac3fca6..2cfd52a 100644
--- a/js/jupyterlab-deck/src/tsconfig.json
+++ b/js/jupyterlab-deck/src/tsconfig.json
@@ -1,10 +1,10 @@
 {
-  "extends": "../../tsconfigbase",
   "compilerOptions": {
     "rootDir": ".",
     "outDir": "../lib",
     "tsBuildInfoFile": "../.src.tsbuildinfo"
   },
+  "extends": "../../tsconfigbase",
   "include": ["./**/*"],
   "references": [{ "path": "../" }]
 }
diff --git a/js/jupyterlab-deck/style/design.css b/js/jupyterlab-deck/style/design.css
new file mode 100644
index 0000000..112a1d4
--- /dev/null
+++ b/js/jupyterlab-deck/style/design.css
@@ -0,0 +1,124 @@
+.jp-Deck-DesignTools {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  display: flex;
+  flex-direction: column;
+  z-index: var(--jp-deck-z-remote);
+}
+
+.jp-Deck-DesignTools label {
+  color: var(--jp-ui-font-color1);
+}
+
+.jp-Deck-DesignTools > div {
+  display: flex;
+  flex: 1;
+  flex-direction: row;
+  align-items: end;
+}
+
+.jp-Deck-DesignTools button {
+  border: 0;
+  background-color: transparent;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector {
+  display: flex;
+  flex: 1;
+  flex-direction: column;
+  margin: 0;
+  padding: 0;
+  list-style: none;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector li {
+  align-items: center;
+  display: flex;
+  flex-direction: row;
+  flex: 1;
+  margin: 0;
+  opacity: 0.25;
+  padding: 0;
+  transition: opacity 0.1s;
+}
+
+/* stlyelint wants these here... */
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider label {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider label span {
+  display: none;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider:hover label span {
+  display: block;
+}
+
+/* and now back ... */
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector li label {
+  display: none;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector li:hover,
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector li:active {
+  opacity: 0.8;
+  transition: opacity 0.1s;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector li:hover label,
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector li:active label {
+  display: block;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector li.jp-mod-active {
+  opacity: 1;
+  transition: opacity 0.1s;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector li:not(.jp-mod-active) {
+  display: none;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Selector:hover li:not(.jp-mod-active) {
+  display: block;
+}
+
+/* slider */
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider {
+  display: flex;
+  flex-direction: column;
+  align-items: start;
+  opacity: 0.5;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider.jp-mod-active {
+  opacity: 1;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider input {
+  display: none;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider input[type='checkbox'] {
+  width: var(--jp-deck-checkbox-width);
+  flex: 0;
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider input[type='range'] {
+  flex: 1;
+  appearance: slider-vertical;
+  width: var(--jp-deck-vslider-width);
+}
+
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider:hover input,
+.jp-Deck-DesignTools .jp-Deck-DesignTools-Slider:active input {
+  display: block;
+}
diff --git a/js/jupyterlab-deck/style/img/chat-remove-outline.svg b/js/jupyterlab-deck/style/img/chat-remove-outline.svg
new file mode 100644
index 0000000..8e11693
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/chat-remove-outline.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M15.46 15.88L16.88 14.46L19 16.59L21.12 14.47L22.54 15.88L20.41 18L22.54 20.12L21.12 21.54L19 19.41L16.88 21.54L15.46 20.12L17.59 18L15.47 15.88M12 3C17.5 3 22 6.58 22 11C22 11.58 21.92 12.14 21.78 12.68C21.19 12.38 20.55 12.16 19.88 12.06C19.96 11.72 20 11.36 20 11C20 7.69 16.42 5 12 5C7.58 5 4 7.69 4 11C4 14.31 7.58 17 12 17L13.09 16.95L13 18L13.08 18.95L12 19C10.81 19 9.62 18.83 8.47 18.5C6.64 20 4.37 20.89 2 21C4.33 18.67 4.75 17.1 4.75 16.5C3.06 15.17 2.05 13.15 2 11C2 6.58 6.5 3 12 3Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/checkbox-blank-outline.svg b/js/jupyterlab-deck/style/img/checkbox-blank-outline.svg
new file mode 100644
index 0000000..c3df74b
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/checkbox-blank-outline.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M19,5V19H5V5H19Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/checkbox-intermediate-variant.svg b/js/jupyterlab-deck/style/img/checkbox-intermediate-variant.svg
new file mode 100644
index 0000000..dc787e5
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/checkbox-intermediate-variant.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M19 19H5V5H19V19M7 17V7H17" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/close-box-outline.svg b/js/jupyterlab-deck/style/img/close-box-outline.svg
new file mode 100644
index 0000000..2d90beb
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/close-box-outline.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M19,19H5V5H19V19M17,8.4L13.4,12L17,15.6L15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/image-filter-hdr.svg b/js/jupyterlab-deck/style/img/image-filter-hdr.svg
new file mode 100644
index 0000000..301dc24
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/image-filter-hdr.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M14,6L10.25,11L13.1,14.8L11.5,16C9.81,13.75 7,10 7,10L1,18H23L14,6Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/image-off-outline.svg b/js/jupyterlab-deck/style/img/image-off-outline.svg
new file mode 100644
index 0000000..cb0add5
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/image-off-outline.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M22 20.7L3.3 2L2 3.3L3 4.3V19C3 20.1 3.9 21 5 21H19.7L20.7 22L22 20.7M5 19V6.3L12.6 13.9L11.1 15.8L9 13.1L6 17H15.7L17.7 19H5M8.8 5L6.8 3H19C20.1 3 21 3.9 21 5V17.2L19 15.2V5H8.8" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/image-outline-multiple.svg b/js/jupyterlab-deck/style/img/image-outline-multiple.svg
new file mode 100644
index 0000000..9ecb6ac
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/image-outline-multiple.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M21,17H7V3H21M21,1H7A2,2 0 0,0 5,3V17A2,2 0 0,0 7,19H21A2,2 0 0,0 23,17V3A2,2 0 0,0 21,1M3,5H1V21A2,2 0 0,0 3,23H19V21H3M15.96,10.29L13.21,13.83L11.25,11.47L8.5,15H19.5L15.96,10.29Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/image-outline.svg b/js/jupyterlab-deck/style/img/image-outline.svg
new file mode 100644
index 0000000..a9dc8da
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/image-outline.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M19,19H5V5H19M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3M13.96,12.29L11.21,15.83L9.25,13.47L6.5,17H17.5L13.96,12.29Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/loupe.svg b/js/jupyterlab-deck/style/img/loupe.svg
new file mode 100644
index 0000000..9a0107e
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/loupe.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22H20A2,2 0 0,0 22,20V12A10,10 0 0,0 12,2M13,7H11V11H7V13H11V17H13V13H17V11H13V7Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/message-image-outline.svg b/js/jupyterlab-deck/style/img/message-image-outline.svg
new file mode 100644
index 0000000..00b03ea
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/message-image-outline.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M20 2H4C2.9 2 2 2.9 2 4V22L6 18H20C21.1 18 22 17.1 22 16V4C22 2.9 21.1 2 20 2M20 16H5.2L4 17.2V4H20V16M5 14L8.5 9.5L11 12.5L14.5 8L19 14" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/order-numeric-descending.svg b/js/jupyterlab-deck/style/img/order-numeric-descending.svg
new file mode 100644
index 0000000..29c5e31
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/order-numeric-descending.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M7 11H3V9H7V8H5C3.9 8 3 7.11 3 6V5C3 3.9 3.9 3 5 3H7C8.11 3 9 3.9 9 5V9C9 10.11 8.11 11 7 11M7 5H5V6H7M5 13H7C8.11 13 9 13.9 9 15V19C9 20.11 8.11 21 7 21H5C3.9 21 3 20.11 3 19V15C3 13.9 3.9 13 5 13M5 19H7V15H5M12 5H22V7H12M12 19V17H22V19M12 11H22V13H12Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/plus-box-multiple.svg b/js/jupyterlab-deck/style/img/plus-box-multiple.svg
new file mode 100644
index 0000000..3589312
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/plus-box-multiple.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M19,11H15V15H13V11H9V9H13V5H15V9H19M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M4,6H2V20A2,2 0 0,0 4,22H18V20H4V6Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/plus-box.svg b/js/jupyterlab-deck/style/img/plus-box.svg
new file mode 100644
index 0000000..bdb2cff
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/plus-box.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M17,13H13V17H11V13H7V11H11V7H13V11H17M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/square-opacity.svg b/js/jupyterlab-deck/style/img/square-opacity.svg
new file mode 100644
index 0000000..803c3fe
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/square-opacity.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon3 jp-icon-selectable" fill="#616161">
+    <path d="M19 13V11H21V13H19M21 9V7H19V9H21M21 3H19V5H21V3M17 15H19V13H17V15M21 17V15H19V17H21M13 17V15H15V13H13V11H15V9H13V7H15V5H13V3H3V21H13V19H15V17H13M15 21H17V19H15V21M17 3H15V5H17V3M17 11H19V9H17V11M15 17H17V15H15V17M17 19H19V17H17V19M21 21V19H19V21H21M15 9H17V7H15V9M15 13H17V11H15V13M17 5V7H19V5H17Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/transform-stop.svg b/js/jupyterlab-deck/style/img/transform-stop.svg
new file mode 100644
index 0000000..6239e57
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/transform-stop.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon-warn0 jp-icon-selectable" fill="#616161">
+    <path d="M20.12 14.46L21.54 15.88L19.41 18L21.54 20.12L20.12 21.54L18 19.41L15.88 21.54L14.47 20.12L16.59 18L14.47 15.88L15.88 14.47L18 16.59L20.12 14.46M20.33 4.67L18.67 3L17 4.67L15.33 3L13.67 4.67L12 3L10.33 4.67L8.67 3L7 4.67L5.33 3L3.67 4.67L2 3V19C2 20.11 2.9 21 4 21H12.8C12.29 20.12 12 19.09 12 18C12 16.77 12.37 15.63 13 14.68V13H14.68C15.63 12.37 16.77 12 18 12C19.53 12 20.93 12.58 22 13.5V3L20.33 4.67M11 19H4V13H11V19M20 11H4V8H20V11Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/img/transform.svg b/js/jupyterlab-deck/style/img/transform.svg
new file mode 100644
index 0000000..c6ce116
--- /dev/null
+++ b/js/jupyterlab-deck/style/img/transform.svg
@@ -0,0 +1,5 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <g class="jp-icon-brand1 jp-icon-selectable" fill="#616161">
+    <path d="M20,11H4V8H20M20,15H13V13H20M20,19H13V17H20M11,19H4V13H11M20.33,4.67L18.67,3L17,4.67L15.33,3L13.67,4.67L12,3L10.33,4.67L8.67,3L7,4.67L5.33,3L3.67,4.67L2,3V19A2,2 0 0,0 4,21H20A2,2 0 0,0 22,19V3L20.33,4.67Z" />
+  </g>
+</svg>
diff --git a/js/jupyterlab-deck/style/index.css b/js/jupyterlab-deck/style/index.css
index 19faca8..be56ceb 100644
--- a/js/jupyterlab-deck/style/index.css
+++ b/js/jupyterlab-deck/style/index.css
@@ -3,3 +3,5 @@
 @import './shell.css';
 @import './remote.css';
 @import './decktools.css';
+@import './layover.css';
+@import './design.css';
diff --git a/js/jupyterlab-deck/style/layover.css b/js/jupyterlab-deck/style/layover.css
new file mode 100644
index 0000000..a1fdc42
--- /dev/null
+++ b/js/jupyterlab-deck/style/layover.css
@@ -0,0 +1,139 @@
+.jp-Deck-Layover {
+  position: absolute;
+  right: 0;
+  top: 0;
+  bottom: 0;
+  left: 0;
+  background-color: rgba(0, 0, 0, 0.06);
+  pointer-events: none;
+}
+
+.jp-Deck-Layover .jp-deck-mod-dragging {
+  z-index: var(--jp-deck-z-layover);
+  cursor: grabbing;
+}
+
+.jp-Deck-Layover .jp-Deck-LayoverPart {
+  position: absolute;
+  pointer-events: all;
+  border: solid 2px var(--jp-deck-layover-color);
+  opacity: 0.25;
+  cursor: grab;
+}
+
+.jp-Deck-Layover .jp-Deck-LayoverPart:hover,
+.jp-Deck-Layover .jp-Deck-LayoverPart:active {
+  opacity: 0.8;
+  transition: opacity 0.1s;
+}
+
+.jp-Deck-LayoverHandle {
+  border: solid 1px var(--jp-deck-layover-color);
+}
+
+.jp-Deck-Layover .jp-Deck-LayoverPart .jp-Deck-LayoverHandle {
+  position: absolute;
+  width: var(--jp-deck-handle-size);
+  height: var(--jp-deck-handle-size);
+  opacity: 0.1;
+  transition: opacity 0.1s;
+}
+
+.jp-Deck-Layover .jp-Deck-LayoverHandle:hover {
+  opacity: 1;
+  background-color: var(--jp-deck-layover-color);
+  transition: opacity 0.1s;
+}
+
+.jp-Deck-Layover .jp-Deck-LayoverPart:hover .jp-Deck-LayoverHandle,
+.jp-Deck-Layover .jp-Deck-LayoverPart:active .jp-Deck-LayoverHandle {
+  opacity: 0.8;
+  transition: opacity 0.1s;
+}
+
+/* centering */
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-n,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-s {
+  left: 50%;
+  margin-left: calc(-0.5 * var(--jp-deck-handle-size));
+  cursor: ns-resize;
+}
+
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-e,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-w {
+  top: 50%;
+  margin-top: calc(-0.5 * var(--jp-deck-handle-size));
+  cursor: ew-resize;
+}
+
+/* border and position */
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-n,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-nw,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-ne {
+  top: 0;
+  border-top: 0;
+}
+
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-s,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-sw,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-se {
+  bottom: 0;
+  border-bottom: 0;
+}
+
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-e,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-ne,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-se {
+  right: 0;
+  border-right: 0;
+}
+
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-w,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-nw,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-sw {
+  left: 0;
+  border-left: 0;
+}
+
+/** extra cursors */
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-ne,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-sw {
+  cursor: nesw-resize;
+}
+
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-nw,
+.jp-Deck-LayoverHandle.jp-Deck-LayoverHandle-se {
+  cursor: nwse-resize;
+}
+
+/* index */
+
+.jp-Deck-Layover .jp-Deck-LayoverUnstyle,
+.jp-Deck-Layover .jp-Deck-LayoverLabel {
+  line-height: 1.75;
+  font-weight: bold;
+  position: absolute;
+  margin-top: calc(-0.5 * var(--jp-deck-handle-size));
+  margin-left: calc(-0.5 * var(--jp-deck-handle-size));
+  text-align: center;
+  height: var(--jp-deck-handle-size);
+  width: var(--jp-deck-handle-size);
+  color: var(--jp-layout-color0);
+}
+
+.jp-Deck-Layover .jp-Deck-LayoverLabel {
+  background-color: var(--jp-deck-layover-color);
+  left: 50%;
+  top: 50%;
+  border-radius: var(--jp-deck-handle-size);
+  border: solid 1px var(--jp-deck-layover-color);
+}
+
+.jp-Deck-Layover .jp-Deck-LayoverUnstyle {
+  background-color: var(--jp-error-color1);
+  border: none;
+  left: 75%;
+  margin-top: calc(-0.5 * var(--jp-deck-handle-size));
+  top: 50%;
+  cursor: no-drop;
+}
diff --git a/js/jupyterlab-deck/style/remote.css b/js/jupyterlab-deck/style/remote.css
index 6e8ea3d..cee1126 100644
--- a/js/jupyterlab-deck/style/remote.css
+++ b/js/jupyterlab-deck/style/remote.css
@@ -4,7 +4,7 @@
   bottom: 0;
   display: flex;
   flex-direction: column;
-  z-index: 999;
+  z-index: var(--jp-deck-z-remote);
 }
 
 .jp-Deck-Remote-directions {
diff --git a/js/jupyterlab-deck/style/shell.css b/js/jupyterlab-deck/style/shell.css
index a098c25..1b89c73 100644
--- a/js/jupyterlab-deck/style/shell.css
+++ b/js/jupyterlab-deck/style/shell.css
@@ -1,8 +1,8 @@
-[data-jp-deck-mode='presenting'] {
+body[data-jp-deck-mode='presenting'] {
   background: var(--jp-layout-color0);
 }
 
-[data-jp-deck-mode='presenting'] #jp-main-content-panel {
+body[data-jp-deck-mode='presenting'] #jp-main-content-panel {
   width: 100vw !important;
   height: 100vh !important;
   border: 0 !important;
@@ -11,43 +11,45 @@
   top: 0 !important;
 }
 
-[data-jp-deck-mode='presenting'] #jp-menu-panel,
-[data-jp-deck-mode='presenting'] #jp-top-panel {
+body[data-jp-deck-mode='presenting'] #jp-menu-panel,
+body[data-jp-deck-mode='presenting'] #jp-top-panel {
   display: none;
 }
 
-[data-jp-deck-mode='presenting'] .jp-SideBar {
+body[data-jp-deck-mode='presenting'] .jp-SideBar {
   opacity: 0;
   transition: opacity 0.1s;
-  z-index: 1;
+  z-index: var(--jp-deck-z-lab-chrome);
   background: none;
   border-color: transparent;
+  visibility: var(--jp-deck-chrome-visibility);
 }
 
-[data-jp-deck-mode='presenting'] .jp-SideBar.lm-TabBar.jp-mod-left,
-[data-jp-deck-mode='presenting'] .jp-SideBar.lm-TabBar.jp-mod-right {
+body[data-jp-deck-mode='presenting'] .jp-SideBar.lm-TabBar.jp-mod-left,
+body[data-jp-deck-mode='presenting'] .jp-SideBar.lm-TabBar.jp-mod-right {
   border-color: transparent;
 }
 
-[data-jp-deck-mode='presenting'] .jp-SideBar .lm-TabBar-tab:not(.lm-mod-current) {
+body[data-jp-deck-mode='presenting'] .jp-SideBar .lm-TabBar-tab:not(.lm-mod-current) {
   background: unset;
   border-color: transparent;
 }
 
-[data-jp-deck-mode='presenting'] #jp-right-stack,
-[data-jp-deck-mode='presenting'] #jp-left-stack {
+body[data-jp-deck-mode='presenting'] #jp-right-stack,
+body[data-jp-deck-mode='presenting'] #jp-left-stack {
   opacity: 0.25;
-  z-index: 1;
+  z-index: var(--jp-deck-z-lab-chrome);
   transition: opacity 0.1s;
+  visibility: var(--jp-deck-chrome-visibility);
 }
 
-[data-jp-deck-mode='presenting'] .jp-SideBar:hover,
-[data-jp-deck-mode='presenting'] #jp-right-stack:hover,
-[data-jp-deck-mode='presenting'] #jp-left-stack:hover {
+body[data-jp-deck-mode='presenting'] .jp-SideBar:hover,
+body[data-jp-deck-mode='presenting'] #jp-right-stack:hover,
+body[data-jp-deck-mode='presenting'] #jp-left-stack:hover {
   opacity: 0.9;
   transition: opacity 0.1s;
 }
 
-[data-jp-deck-mode='presenting'] #jp-main-dock-panel {
+body[data-jp-deck-mode='presenting'] #jp-main-dock-panel {
   padding: 0;
 }
diff --git a/js/jupyterlab-deck/style/variables.css b/js/jupyterlab-deck/style/variables.css
index 62bbc41..8fe1bf1 100644
--- a/js/jupyterlab-deck/style/variables.css
+++ b/js/jupyterlab-deck/style/variables.css
@@ -1,5 +1,23 @@
-[data-jp-deck-mode='presenting'] {
+body[data-jp-deck-mode='presenting'] {
   --jp-cell-padding: 0;
   --jp-statusbar-height: 0;
   --jp-private-menubar-height: 0;
+
+  /* z-stack */
+  --jp-deck-z-remote: 9999;
+  --jp-deck-z-lab-chrome: 9998;
+  --jp-deck-z-layover: 9997;
+
+  /* layover */
+  --jp-deck-layover-color: var(--jp-warn-color1);
+  --jp-deck-handle-size: var(--jp-content-font-size4);
+  --jp-deck-chrome-visibility: visible;
+
+  /* design */
+  --jp-deck-vslider-width: calc(1.2 * var(--jp-content-font-size4));
+  --jp-deck-checkbox-width: calc(1.1 * var(--jp-content-font-size4));
+}
+
+body[data-jp-deck-mode='presenting'][data-jp-deck-layout-mode='designing'] {
+  --jp-deck-chrome-visibility: hidden;
 }
diff --git a/package.json b/package.json
index f154ee5..76a5263 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
     "nyc": "^15.1.0",
     "prettier": "^2.7.1",
     "prettier-package-json": "^2.7.0",
+    "prettier-plugin-sort-json": "^0.0.3",
     "source-map-loader": "^4.0.1",
     "stylelint": "^14.13.0",
     "stylelint-config-prettier": "^9.0.3",
diff --git a/yarn.lock b/yarn.lock
index 50298bd..f3674b8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -249,13 +249,14 @@
   integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==
 
 "@deathbeds/jupyterlab-deck@file:js/jupyterlab-deck":
-  version "0.1.2"
+  version "0.1.3"
   dependencies:
     "@jupyterlab/application" "3"
     "@jupyterlab/apputils" "3"
     "@jupyterlab/notebook" "3"
     "@jupyterlab/statusbar" "3"
     "@jupyterlab/ui-components" "3"
+    d3-drag "3"
 
 "@deathbeds/jupyterlab-fonts@^2.1.1":
   version "2.1.1"
@@ -2122,6 +2123,18 @@
   resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
   integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
 
+"@types/d3-drag@3":
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz#fb1e3d5cceeee4d913caa59dedf55c94cb66e80f"
+  integrity sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==
+  dependencies:
+    "@types/d3-selection" "*"
+
+"@types/d3-selection@*":
+  version "3.0.3"
+  resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.3.tgz#57be7da68e7d9c9b29efefd8ea5a9ef1171e42ba"
+  integrity sha512-Mw5cf6nlW1MlefpD9zrshZ+DAWL4IQ5LnWfRheW6xwsdaWOb6IRRu2H7XPAQcyXEx1D7XQWgdoKR83ui1/HlEA==
+
 "@types/dom4@^2.0.1":
   version "2.0.2"
   resolved "https://registry.npmjs.org/@types/dom4/-/dom4-2.0.2.tgz#6495303f049689ce936ed328a3e5ede9c51408ee"
@@ -2196,6 +2209,11 @@
   resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
   integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
 
+"@types/prettier@^2.3.2":
+  version "2.7.1"
+  resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz#dfd20e2dc35f027cdd6c1908e80a5ddc7499670e"
+  integrity sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==
+
 "@types/prop-types@*":
   version "15.7.5"
   resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
@@ -3696,6 +3714,24 @@ csstype@3.0.10, csstype@^3.0.2, csstype@~3.0.3:
   resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
   integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
 
+"d3-dispatch@1 - 3":
+  version "3.0.1"
+  resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+  integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+d3-drag@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+  integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+  dependencies:
+    d3-dispatch "1 - 3"
+    d3-selection "3"
+
+d3-selection@3:
+  version "3.0.0"
+  resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+  integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
 d@1, d@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a"
@@ -7975,6 +8011,13 @@ prettier-package-json@^2.7.0:
     sort-object-keys "^1.1.3"
     sort-order "^1.0.1"
 
+prettier-plugin-sort-json@^0.0.3:
+  version "0.0.3"
+  resolved "https://registry.npmjs.org/prettier-plugin-sort-json/-/prettier-plugin-sort-json-0.0.3.tgz#724a4c377e236f6689f3a7e99ec4d666d30536f7"
+  integrity sha512-1WLOaeaRJRyVzDmuPif32xRn77+K6J00XIin1O/XuXpD5wy3XX8XyoQH/BSlzwdZ3GpnTOJRyIGXhxDCUb4L9Q==
+  dependencies:
+    "@types/prettier" "^2.3.2"
+
 prettier@^2.7.1:
   version "2.7.1"
   resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64"


Test patch:
diff --git a/atest/resources/Deck.resource b/atest/resources/Deck.resource
index 9adb6ac..5980947 100644
--- a/atest/resources/Deck.resource
+++ b/atest/resources/Deck.resource
@@ -88,28 +88,3 @@ Really Back Up Deck With Keyboard
     [Documentation]    REALLY go to the up/back slide with space, wait a bit, then screenshot.
     [Arguments]    ${screenshot}=${EMPTY}    ${expect}=${EMPTY}
     Really Advance Deck With Keyboard    ${screenshot}    ${expect}    backup=${TRUE}
-
-Make Cell Layer
-    [Documentation]    Use the Property Inspector to make a cell a layer
-    [Arguments]    ${idx}    ${layer}    ${screenshot}=${EMPTY}
-    Click Element    css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${idx})
-    Maybe Open JupyterLab Sidebar    Property Inspector
-    Maybe Open Cell Metadata JSON
-    Select From List By Value    css:${CSS_DECK_LAYER_SELECT}    ${layer}
-    IF    '${layer}' != '-'
-        Wait Until Cell Metadata Contains    "layer": "${layer}"
-    ELSE
-        Wait Until Cell Metadata Does Not Contain    jupyterlab-deck
-    END
-    IF    ${screenshot.__len__()}    Capture Page Screenshot    ${screenshot}
-
-Use Cell Style Preset
-    [Documentation]    Use the Property Inspector to use a cell preset
-    [Arguments]    ${idx}    ${preset}    ${expect}=${EMPTY}    ${screenshot}=${EMPTY}
-    Click Element    css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${idx})
-    Maybe Open JupyterLab Sidebar    Property Inspector
-    Maybe Open Cell Metadata JSON
-    Select From List By Value    css:${CSS_DECK_PRESET_SELECT}    ${preset}
-    Click Element    css:${CSS_DECK_TOOL_PRESET} button
-    IF    ${expect.__len__()}    Wait Until Cell Metadata Contains    ${expect}
-    IF    ${screenshot.__len__()}    Capture Page Screenshot    ${screenshot}
diff --git a/atest/resources/DeckSelectors.resource b/atest/resources/DeckSelectors.resource
index 5b3238a..60c77b7 100644
--- a/atest/resources/DeckSelectors.resource
+++ b/atest/resources/DeckSelectors.resource
@@ -4,24 +4,38 @@ Documentation       Selectors defined in this repo... could be loaded from JSON?
 
 *** Variables ***
 # body
-${CSS_DECK_PRESENTING}          [data-jp-deck-mode="presenting"]
+${CSS_DECK_PRESENTING}              [data-jp-deck-mode="presenting"]
 # notebook
-${CSS_DECK_NOTEBOOK_BUTTON}     .jp-ToolbarButtonComponent\[data-command="deck:toggle"]
+${CSS_DECK_NOTEBOOK_BUTTON}         .jp-ToolbarButtonComponent\[data-command="deck:toggle"]
 # cells
-${CSS_DECK_VISIBLE}             .jp-deck-mod-visible
-${CSS_DECK_ONSCREEN}            .jp-deck-mod-onscreen
+${CSS_DECK_VISIBLE}                 .jp-deck-mod-visible
+${CSS_DECK_ONSCREEN}                .jp-deck-mod-onscreen
 # remote
-${CSS_DECK_STOP}                .jp-Deck-Remote .jp-deck-mod-stop
-${CSS_DECK_FORWARD}             .jp-Deck-Remote .jp-deck-mod-direction-forward
-${CSS_DECK_BACK}                .jp-Deck-Remote .jp-deck-mod-direction-back
-${CSS_DECK_UP}                  .jp-Deck-Remote .jp-deck-mod-direction-up
-${CSS_DECK_DOWN}                .jp-Deck-Remote .jp-deck-mod-direction-down
-${CSS_DECK_DIR_STEM}            .jp-Deck-Remote .jp-deck-mod-direction
+${CSS_DECK_STOP}                    .jp-Deck-Remote .jp-deck-mod-stop
+${CSS_DECK_FORWARD}                 .jp-Deck-Remote .jp-deck-mod-direction-forward
+${CSS_DECK_BACK}                    .jp-Deck-Remote .jp-deck-mod-direction-back
+${CSS_DECK_UP}                      .jp-Deck-Remote .jp-deck-mod-direction-up
+${CSS_DECK_DOWN}                    .jp-Deck-Remote .jp-deck-mod-direction-down
+${CSS_DECK_DIR_STEM}                .jp-Deck-Remote .jp-deck-mod-direction
 
-@{CSS_DECK_NEXT}                down    forward
-@{CSS_DECK_PREV}                up    back
+@{CSS_DECK_NEXT}                    down    forward
+@{CSS_DECK_PREV}                    up    back
 
 # metadata
-${CSS_DECK_LAYER_SELECT}        \#id-jp-decktools-select-layer
-${CSS_DECK_PRESET_SELECT}       \#id-jp-decktools-select-preset
-${CSS_DECK_TOOL_PRESET}         .jp-Deck-Tool-preset
+${CSS_DECK_LAYER_SELECT}            \#id-jp-decktools-select-layer
+${CSS_DECK_PRESET_SELECT}           \#id-jp-decktools-select-preset
+${CSS_DECK_TOOL_PRESET}             .jp-Deck-Tool-preset
+
+# design
+${CSS_DECK_DESIGN_TOOLS}            .jp-Deck-DesignTools
+${CSS_DECK_ICON_LAYOVER_START}      [data-icon="deck:layover-start"]
+${CSS_DECK_ICON_LAYOVER_STOP}       [data-icon="deck:layover-stop"]
+${CSS_DECK_LAYOVER_PART}            .jp-Deck-LayoverPart
+${CSS_DECK_LAYOVER_UNSTYLE}         .jp-Deck-LayoverUnstyle
+${CSS_DECK_PART_HANDLE}             .jp-Deck-LayoverHandle
+${CSS_DECK_MOD_SLIDE_TYPE}          .jp-deck-mod-slidetype
+${CSS_DECK_MOD_LAYER_SCOPE}         .jp-deck-mod-layerscope
+${CSS_DECK_ICON_SLIDE_FMT}          [data-icon="deck:slide-{}"]
+${CSS_DECK_ICON_LAYER_FMT}          [data-icon="deck:layer-{}"]
+${CSS_DECK_SLIDER}                  .jp-Deck-DesignTools-Slider
+${CSS_DECK_SLIDER_FMT}              ${CSS_DECK_SLIDER}.jp-deck-mod-{}
diff --git a/atest/resources/Design.resource b/atest/resources/Design.resource
new file mode 100644
index 0000000..15deabb
--- /dev/null
+++ b/atest/resources/Design.resource
@@ -0,0 +1,131 @@
+*** Settings ***
+Documentation       Keywords for working with decks.
+
+Resource            ./LabSelectors.resource
+Resource            ./DeckSelectors.resource
+Resource            ./Lab.resource
+
+
+*** Variables ***
+@{PART_HANDLES}     nw    n    ne    w    e    sw    s    se
+@{SLIDE_TYPES}      slide    subslide    null    fragment
+@{LAYER_SCOPES}     deck    stack    slide    fragment    null
+@{SLIDERS}          z-index    zoom    opacity
+
+
+*** Keywords ***
+Maybe Open Design Tools
+    [Documentation]    Ensure the design tools are open.
+    ${sel} =    Set Variable    css:${CSS_DECK_DESIGN_TOOLS} ${CSS_LAB_ICON_ELLIPSES}
+    ${el} =    Get WebElements    ${sel}
+    IF    ${el.__len__()}    Click Element    ${sel}
+    Wait Until Element Is Visible    css:${CSS_DECK_DESIGN_TOOLS} ${CSS_LAB_ICON_CARET_LEFT}
+
+Maybe Open Slide Layout
+    [Documentation]    Ensure the slide layout overlay is open.
+    Maybe Open Design Tools
+    ${sel} =    Set Variable    css:${CSS_DECK_DESIGN_TOOLS} ${CSS_DECK_ICON_LAYOVER_START}
+    ${el} =    Get WebElements    ${sel}
+    IF    ${el.__len__()}    Click Element    ${sel}
+    Wait Until Element Is Visible    css:${CSS_DECK_LAYOVER_PART}
+
+Maybe Close Slide Layout
+    [Documentation]    Ensure the slide layout overlay is closed.
+    ${sel} =    Set Variable    css:${CSS_DECK_DESIGN_TOOLS} ${CSS_DECK_ICON_LAYOVER_STOP}
+    ${el} =    Get WebElements    ${sel}
+    IF    ${el.__len__()}    Click Element    ${sel}
+    Wait Until Element Is Not Visible    css:${CSS_DECK_LAYOVER_PART}
+
+Maybe Close Design Tools
+    [Documentation]    Ensure the design tools are closed.
+    Maybe Close Slide Layout
+    ${sel} =    Set Variable    css:${CSS_DECK_DESIGN_TOOLS} ${CSS_LAB_ICON_CARET_LEFT}
+    ${el} =    Get WebElements    ${sel}
+    IF    ${el.__len__()}    Click Element    ${sel}
+    Wait Until Element Is Visible    css:${CSS_DECK_DESIGN_TOOLS} ${CSS_LAB_ICON_ELLIPSES}
+
+Move A Part
+    [Documentation]    Move a part by dragging and dropping
+    [Arguments]    ${index}    ${x}    ${y}    ${screenshot}=${EMPTY}
+    ${sel} =    Set Variable    ${CSS_DECK_LAYOVER_PART}:nth-child(${index})
+    Drag And Drop By Offset    css:${sel}    ${x}    ${y}
+    Sleep    0.1s
+    IF    not ${screenshot.__len__()}    RETURN
+    Capture Page Screenshot    ${screenshot}
+
+Resize A Part
+    [Documentation]    Resize a part by dragging and dropping the handle
+    [Arguments]    ${index}    ${handle}    ${x}    ${y}    ${screenshot}=${EMPTY}
+    ${sel} =    Set Variable    ${CSS_DECK_LAYOVER_PART}:nth-child(${index}) ${CSS_DECK_PART_HANDLE}-${handle}
+    Drag And Drop By Offset    css:${sel}    ${x}    ${y}
+    Sleep    0.1s
+    IF    not ${screenshot.__len__()}    RETURN
+    Capture Page Screenshot    ${screenshot}
+
+Unstyle A Part
+    [Documentation]    Use the reset button to unstyle a part.
+    [Arguments]    ${index}    ${screenshot}=${EMPTY}
+    ${part} =    Set Variable    ${CSS_DECK_LAYOVER_PART}:nth-child(${index})
+    ${unstyle} =    Set Variable    ${part} ${CSS_DECK_LAYOVER_UNSTYLE}
+    Mouse Over    css:${part}
+    Click Element    css:${unstyle}
+    Wait Until Page Does Not Contain Element    css:${unstyle}
+    IF    not ${screenshot.__len__()}    RETURN
+    Capture Page Screenshot    ${screenshot}
+
+Select A Slide Type
+    [Documentation]    Make a cell a slide type with the design tools.
+    [Arguments]    ${index}    ${type}    ${screenshot}=${EMPTY}
+    Select From Selector Tool    ${index}    ${type}    ${screenshot}
+    ...    ${CSS_DECK_MOD_SLIDE_TYPE}    ${CSS_DECK_ICON_SLIDE_FMT}
+
+Select A Layer Scope
+    [Documentation]    Make a cell a layer with the design tools.
+    [Arguments]    ${index}    ${scope}    ${screenshot}=${EMPTY}
+    Select From Selector Tool    ${index}    ${scope}    ${screenshot}
+    ...    ${CSS_DECK_MOD_LAYER_SCOPE}    ${CSS_DECK_ICON_LAYER_FMT}
+
+Select From Selector Tool
+    [Documentation]    Select an option from a selector tool.
+    [Arguments]    ${index}    ${kind}    ${screenshot}    ${tool}    ${fmt}
+    Maybe Open Design Tools
+    Click Element    css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${index})
+    Really Mouse Over    css:${tool}
+    ${icon} =    Set Variable    ${fmt.format("${kind}")}
+    Click Element    css:${tool} ${icon}
+    Mouse Over    css:${CSS_DECK_DESIGN_TOOLS} ${CSS_LAB_ICON_CARET_LEFT}
+    Wait Until Element Is Visible    css:${tool} ${CSS_LAB_MOD_ACTIVE} ${icon}
+    IF    not ${screenshot.__len__()}    RETURN
+    Capture Page Screenshot    ${screenshot}
+
+Really Mouse Over
+    [Documentation]    Mouse over something twice, waiting for it to react.
+    [Arguments]    ${selector}
+    Mouse Over    ${selector}
+    Sleep    0.1s
+    Mouse Over    ${selector}
+
+Configure A Style With Slider
+    [Documentation]    Use a design tool slider to configure a part
+    [Arguments]    ${index}    ${attr}    ${screenshot}    ${inverse}=${FALSE}
+    ${tool} =    Set Variable    ${CSS_DECK_SLIDER_FMT.format("${attr}")}
+    Really Mouse Over    css:${tool}
+    IF    ${inverse}
+        Click Element    css:${tool} input\[type\="checkbox"]
+        Wait Until Page Does Not Contain Element    css:${tool}.jp-mod-active
+    ELSE
+        Click Element    css:${tool} input\[type\="range"]
+        Wait Until Page Contains Element    css:${tool}.jp-mod-active
+    END
+    IF    not ${screenshot.__len__()}    RETURN
+    Capture Page Screenshot    ${screenshot}
+
+Unconfigure A Style With Slider
+    [Documentation]    Use a design tool slider to unconfigure a part
+    [Arguments]    ${index}    ${attr}    ${screenshot}
+    Configure A Style With Slider    ${index}    ${attr}    ${screenshot}    ${TRUE}
+
+Reset Design Tools Test
+    [Documentation]    Reset the test, ensuring the design tools are closed first.
+    Maybe Close Design Tools
+    Reset Interactive Test
diff --git a/atest/resources/Lab.resource b/atest/resources/Lab.resource
index 06e368d..d68db05 100644
--- a/atest/resources/Lab.resource
+++ b/atest/resources/Lab.resource
@@ -25,10 +25,10 @@ Enter Command Mode
 
 Really Set Cell Type With Keyboard
     [Documentation]    Use the keyboard to change the cell type.
-    [Arguments]    ${index}    ${type}    ${limit}=5
+    [Arguments]    ${index}    ${type}    ${limit}=5    ${timeout}=1s
     ${ok} =    Set Variable    ${FALSE}
     WHILE    not ${ok}    limit=${limit}
-        ${ok} =    Run Keyword And Return Status    Set Cell Type With Keyboard    ${index}    ${type}
+        ${ok} =    Run Keyword And Return Status    Set Cell Type With Keyboard    ${index}    ${type}    ${timeout}
         IF    ${ok}
             RETURN
         ELSE IF    ${index} > 1
@@ -42,7 +42,7 @@ Really Set Cell Type With Keyboard
 
 Set Cell Type With Keyboard
     [Documentation]    Use the keyboard to change the cell type.
-    [Arguments]    ${index}    ${type}
+    [Arguments]    ${index}    ${type}    ${timeout}=5s
     Click Element    css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${index})
     Enter Command Mode
     IF    '${type}' == 'code'
@@ -55,6 +55,7 @@ Set Cell Type With Keyboard
     Press Keys    css:body    ${keys}
     Wait Until Page Contains Element
     ...    css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${index})${sel}
+    ...    timeout=${timeout}
 
 Make Markdown Cell
     [Documentation]    Turn the current cell into markdown.
diff --git a/atest/resources/LabSelectors.resource b/atest/resources/LabSelectors.resource
index b053801..d1147fc 100644
--- a/atest/resources/LabSelectors.resource
+++ b/atest/resources/LabSelectors.resource
@@ -8,6 +8,7 @@ ${CSS_LAB_FILES_DIR_ITEM}               .jp-DirListing-item
 ${CSS_LAB_SPINNER}                      .jp-Spinner
 ${CSS_LAB_MOD_DISABLED}                 .jp-mod-disabled
 ${CSS_LAB_MOD_CMD}                      .jp-mod-commandMode
+${CSS_LAB_MOD_ACTIVE}                   .jp-mod-active
 ${CSS_LAB_MOD_EDIT}                     .jp-mod-editMode
 ${CSS_LAB_MOD_RENDERED}                 .jp-mod-rendered
 ${CSS_LAB_MOD_HIDDEN}                   .lm-mod-hidden
@@ -18,3 +19,7 @@ ${CSS_LAB_NB_TOOLBAR_CELLTYPE}          .jp-Notebook-toolbarCellType
 ${CSS_LAB_CELL_META_JSON_CM}            .jp-MetadataEditorTool .CodeMirror
 ${CSS_LAB_CELL_META_JSON_CM_HIDDEN}     ${CSS_LAB_MOD_HIDDEN} ${CSS_LAB_CELL_META_JSON_CM}
 ${CSS_LAB_ADVANCED_COLLAPSE}            .jp-NotebookTools .jp-Collapse-header
+
+# icons
+${CSS_LAB_ICON_ELLIPSES}                [data-icon="ui-components:ellipses"]
+${CSS_LAB_ICON_CARET_LEFT}              [data-icon="ui-components:caret-left"]
diff --git a/atest/resources/Sidebar.resource b/atest/resources/Sidebar.resource
new file mode 100644
index 0000000..53bb515
--- /dev/null
+++ b/atest/resources/Sidebar.resource
@@ -0,0 +1,33 @@
+*** Settings ***
+Documentation       Keywords for working with decks.
+
+Resource            ./LabSelectors.resource
+Resource            ./DeckSelectors.resource
+Resource            ./Lab.resource
+
+
+*** Keywords ***
+Make Cell Layer With Sidebar
+    [Documentation]    Use the Property Inspector to make a cell a layer
+    [Arguments]    ${idx}    ${layer}    ${screenshot}=${EMPTY}
+    Click Element    css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${idx})
+    Maybe Open JupyterLab Sidebar    Property Inspector
+    Maybe Open Cell Metadata JSON
+    Select From List By Value    css:${CSS_DECK_LAYER_SELECT}    ${layer}
+    IF    '${layer}' != '-'
+        Wait Until Cell Metadata Contains    "layer": "${layer}"
+    ELSE
+        Wait Until Cell Metadata Does Not Contain    jupyterlab-deck
+    END
+    IF    ${screenshot.__len__()}    Capture Page Screenshot    ${screenshot}
+
+Use Cell Style Preset
+    [Documentation]    Use the Property Inspector to use a cell preset
+    [Arguments]    ${idx}    ${preset}    ${expect}=${EMPTY}    ${screenshot}=${EMPTY}
+    Click Element    css:${JLAB CSS ACTIVE DOC CELLS}:nth-child(${idx})
+    Maybe Open JupyterLab Sidebar    Property Inspector
+    Maybe Open Cell Metadata JSON
+    Select From List By Value    css:${CSS_DECK_PRESET_SELECT}    ${preset}
+    Click Element    css:${CSS_DECK_TOOL_PRESET} button
+    IF    ${expect.__len__()}    Wait Until Cell Metadata Contains    ${expect}
+    IF    ${screenshot.__len__()}    Capture Page Screenshot    ${screenshot}
diff --git a/atest/suites/lab/03-layers.robot b/atest/suites/lab/03-layers.robot
index 2b39fd7..4485241 100644
--- a/atest/suites/lab/03-layers.robot
+++ b/atest/suites/lab/03-layers.robot
@@ -8,6 +8,7 @@ Resource            ../../resources/Deck.resource
 Resource            ../../resources/Lab.resource
 Resource            ../../resources/Screenshots.resource
 Resource            ../../resources/Docs.resource
+Resource            ../../resources/Sidebar.resource
 
 Suite Setup         Set Up Interactive Suite    layers
 Suite Teardown      Tear Down Interactive Suite
@@ -27,17 +28,17 @@ Build A Slide With Layers
     Start Basic Notebook Deck
     Make Markdown Cell    - item91011    item91011    screenshot=s0-03-91011.png
     Make Markdown Cell    - item121314    item121314    screenshot=s0-04-121314.png
-    Make Cell Layer    2    deck    s0-05-deck.png
-    Make Cell Layer    3    stack    s0-06-stack.png
-    Make Cell Layer    4    slide    s0-07-slide.png
-    Make Cell Layer    4    -    s0-08-null.png
-    Make Cell Layer    5    fragment    s0-09-fragment.png
+    Make Cell Layer With Sidebar    2    deck    s0-05-deck.png
+    Make Cell Layer With Sidebar    3    stack    s0-06-stack.png
+    Make Cell Layer With Sidebar    4    slide    s0-07-slide.png
+    Make Cell Layer With Sidebar    4    -    s0-08-null.png
+    Make Cell Layer With Sidebar    5    fragment    s0-09-fragment.png
 
 Build A Slide With Style Presets
     [Documentation]    Use the sidebar to work with presets.
     Set Attempt Screenshot Directory    lab${/}interactive${/}presets
     Start Basic Notebook Deck
     Use Cell Style Preset    2    fade-out    ${FADE_JSON}    s0-03-fragment-fade.png
-    Make Cell Layer    2    deck    s0-04-deck-layer.png
+    Make Cell Layer With Sidebar    2    deck    s0-04-deck-layer.png
     Capture Page Screenshot    s0-05-deck.png
     [Teardown]    Stop Deck With Remote
diff --git a/atest/suites/lab/04-design-tools.robot b/atest/suites/lab/04-design-tools.robot
new file mode 100644
index 0000000..10e29db
--- /dev/null
+++ b/atest/suites/lab/04-design-tools.robot
@@ -0,0 +1,54 @@
+*** Settings ***
+Documentation       The design tools work.
+
+Library             OperatingSystem
+Library             JupyterLibrary
+Resource            ../../resources/Coverage.resource
+Resource            ../../resources/Deck.resource
+Resource            ../../resources/Lab.resource
+Resource            ../../resources/Screenshots.resource
+Resource            ../../resources/Docs.resource
+Resource            ../../resources/Design.resource
+
+Suite Setup         Set Up Interactive Suite    design
+Suite Teardown      Tear Down Interactive Suite
+Test Teardown       Reset Design Tools Test
+
+Force Tags          suite:design
+
+
+*** Test Cases ***
+Slide Types
+    [Documentation]    Use the slide type tool to work with parts.
+    Set Attempt Screenshot Directory    lab${/}design${/}slide-types
+    Start Basic Notebook Deck
+    Maybe Open Design Tools
+    FOR    ${i}    ${type}    IN ENUMERATE    @{SLIDE_TYPES}
+        Select A Slide Type    2    ${type}    01-${i}-${type}.png
+    END
+    Capture Page Screenshot    02-presenting.png
+
+Layer Scopes
+    [Documentation]    Use the layer scope tool to work with parts.
+    Set Attempt Screenshot Directory    lab${/}design${/}layer-scopes
+    Start Basic Notebook Deck
+    Make Markdown Cell    - itemA    itemA
+    Make Markdown Cell    - itemB    itemB
+    Make Markdown Cell    - itemC    itemC
+    Make Markdown Cell    - itemD    itemD
+    Maybe Open Design Tools
+    FOR    ${i}    ${scope}    IN ENUMERATE    @{LAYER_SCOPES}
+        Select A Layer Scope    ${i + 2}    ${scope}    01-${i}-${scope}.png
+    END
+    Capture Page Screenshot    02-presenting.png
+
+Sliders
+    [Documentation]    Use the slider tools to work with parts.
+    Set Attempt Screenshot Directory    lab${/}design${/}sliders
+    Start Basic Notebook Deck
+    Maybe Open Design Tools
+    FOR    ${i}    ${slider}    IN ENUMERATE    @{SLIDERS}
+        Configure A Style With Slider    1    ${slider}    01-${i}-0-${slider}.png
+        Unconfigure A Style With Slider    1    ${slider}    01-${i}-1-no-${slider}.png
+    END
+    Capture Page Screenshot    02-presenting.png
diff --git a/atest/suites/lab/05-slide-layout.robot b/atest/suites/lab/05-slide-layout.robot
new file mode 100644
index 0000000..bc05da5
--- /dev/null
+++ b/atest/suites/lab/05-slide-layout.robot
@@ -0,0 +1,33 @@
+*** Settings ***
+Documentation       The slide layout overlay works.
+
+Library             OperatingSystem
+Library             JupyterLibrary
+Resource            ../../resources/Coverage.resource
+Resource            ../../resources/Deck.resource
+Resource            ../../resources/Lab.resource
+Resource            ../../resources/Screenshots.resource
+Resource            ../../resources/Docs.resource
+Resource            ../../resources/Design.resource
+
+Suite Setup         Set Up Interactive Suite    layout
+Suite Teardown      Tear Down Interactive Suite
+Test Teardown       Reset Design Tools Test
+
+Force Tags          suite:design
+
+
+*** Test Cases ***
+Slide Layout
+    [Documentation]    Use the design tools to work with parts.
+    Set Attempt Screenshot Directory    lab${/}layout${/}layover
+    Start Basic Notebook Deck
+    Maybe Open Slide Layout
+    Capture Page Screenshot    00-layover.png
+    Move A Part    1    0    -100    01-moved.png
+    FOR    ${i}    ${handle}    IN ENUMERATE    @{PART_HANDLES}
+        Resize A Part    1    ${handle}    10    10    02-${i}-resized.png
+    END
+    Unstyle A Part    1    03-unstyled.png
+    Maybe Close Design Tools
+    Capture Page Screenshot    04-presenting.png