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

Issue:
Add markdown presenter
## Elevator Pitch

Add a basic presenter for markdown slides viewed in the Markdown Preview activity.

## Motivation

`.md` files should make fine slides. 

## Design Ideas

The specs for [markdown](https://daringfireball.net/projects/markdown/syntax#hr) and [commonmark](https://spec.commonmark.org/0.29/#thematic-break) allow for several syntaxes for horizontal rules, and these are commonly used for tools such as [marp](https://marp.app/) and others.

Without supporting _all_ the features, we can probably fiat `hr`s (written with `---`) as slides. The `~` operator can be used to select stuff between multiple as-rendered `hr` elements. 




Discussion / hints:
To expand on the above, given the id of the markdown viewer widget:

Or, once the presenter gets to work
```css
body[data-jp-deck-mode="presenting"] :not(.lm-mod-hidden) .jp-MarkdownViewer > * {
  display: none;  // or equivalent
}
body[data-jp-deck-mode="presenting"] :not(.lm-mod-hidden) .jp-MarkdownViewer hr:nth-of-type(1) ~ :not(hr:nth-of-type(2) ~ *):not(hr)') {
  display: block; // or equivalent
}
```

This could be tightened up by annotating the leading `hr` element with .e.g `jp-deck-mod-onscreen`, so that the CSS isn't changing as frequently, though it will be a full repaint _anyway_ when changing the classes.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1a9dcc2..25ea3cf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,13 @@
 
 ### `0.1.3` (unreleased)
 
+- [#19] adds a basic Markdown presenter, with slides delimited by `---`
 - [#22] adds a stack of previously-viewed documents when navigating between documents
 - [#27] adds a drag-and-drop _slide layout_ overlay and design tools to support
   customization
 - [#29] adds support for using `#<header>` anchors while presenting
 
+[#19]: https://github.com/deathbeds/jupyterlab-deck/issues/19
 [#22]: https://github.com/deathbeds/jupyterlab-deck/issues/22
 [#27]: https://github.com/deathbeds/jupyterlab-deck/issues/27
 [#29]: https://github.com/deathbeds/jupyterlab-deck/issues/29
diff --git a/examples/README.ipynb b/examples/README.ipynb
index 7a9b715..229519e 100644
--- a/examples/README.ipynb
+++ b/examples/README.ipynb
@@ -489,7 +489,20 @@
     "tags": []
    },
    "source": [
-    "  - images (_no link here, yet, as it's hard to get back!_)..."
+    "  - [images](./deck.svg) ..."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d139b228-9fbe-494d-b238-46c08b0709a0",
+   "metadata": {
+    "slideshow": {
+     "slide_type": "fragment"
+    },
+    "tags": []
+   },
+   "source": [
+    "  - [code](./hello_world.py) ..."
    ]
   },
   {
diff --git a/examples/README.md b/examples/README.md
index a0a5c90..e4df63d 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -10,8 +10,34 @@ The contents of this folder are copied into the JupyterLite site.
 If you got here from the main presentation, you can [go back](./README.ipynb#Multiple-Documents).
 ```
 
+> This file is **also** a [basic deck](#Simple-Markdown-Decks) that can be viewed with
+> the _Start Deck_ command.
+
+---
+
+## Simple Markdown Decks
+
+- each `---` starts a new slide
+  - including the first
+- not-yet-appearing
+  - no subslides
+  - no fragments
+  - no document styles
+  - no live editing: see [workaround](#Live-Updating-Workaround)
+
 ---
 
-## Markdown Presentations
+## Future Work
+
+- running code blocks
+- styling with front/back matter
+
+---
+
+## Live Updating Workaround
+
+- doesn't currently support this
+  - well, sorta: right click and select _Show Markdown Editor_
+  - <kbd>ctrl+shift+d</kbd>
 
-> coming soon
+> this may be improved in [future work](#Future-Work)
diff --git a/examples/hello_world.py b/examples/hello_world.py
new file mode 100644
index 0000000..a9ded1a
--- /dev/null
+++ b/examples/hello_world.py
@@ -0,0 +1,7 @@
+"""Just an example of a code file
+
+Click the link in the remote to go back!
+"""
+
+def hello_world(world="Jupyter") -> str:
+    return f"Hello {world}"
diff --git a/js/jupyterlab-deck/package.json b/js/jupyterlab-deck/package.json
index 041cfea..fbfb31d 100644
--- a/js/jupyterlab-deck/package.json
+++ b/js/jupyterlab-deck/package.json
@@ -22,6 +22,7 @@
   "dependencies": {
     "@jupyterlab/application": "3",
     "@jupyterlab/apputils": "3",
+    "@jupyterlab/markdownviewer": "3",
     "@jupyterlab/notebook": "3",
     "@jupyterlab/statusbar": "3",
     "@jupyterlab/ui-components": "3",
diff --git a/js/jupyterlab-deck/src/manager.ts b/js/jupyterlab-deck/src/manager.ts
index a18bd52..85c379c 100644
--- a/js/jupyterlab-deck/src/manager.ts
+++ b/js/jupyterlab-deck/src/manager.ts
@@ -320,7 +320,7 @@ export class DeckManager implements IDeckManager {
 
   public getSlideType(): TSlideType {
     let { _activeWidget, _activePresenter } = this;
-    if (_activeWidget && _activePresenter?.capabilities.slideType) {
+    if (_activeWidget && _activePresenter?.getSlideType) {
       return _activePresenter.getSlideType(_activeWidget) || null;
     }
     /* istanbul ignore next */
@@ -329,14 +329,14 @@ export class DeckManager implements IDeckManager {
 
   public setSlideType(slideType: TSlideType): void {
     let { _activeWidget, _activePresenter } = this;
-    if (_activeWidget && _activePresenter?.capabilities.slideType) {
+    if (_activeWidget && _activePresenter?.setSlideType) {
       _activePresenter.setSlideType(_activeWidget, slideType);
     }
   }
 
   public getLayerScope(): TLayerScope | null {
     let { _activeWidget, _activePresenter } = this;
-    if (_activeWidget && _activePresenter?.capabilities.layerScope) {
+    if (_activeWidget && _activePresenter?.getLayerScope) {
       return _activePresenter.getLayerScope(_activeWidget) || null;
     }
     /* istanbul ignore next */
@@ -345,14 +345,14 @@ export class DeckManager implements IDeckManager {
 
   public setLayerScope(layerScope: TLayerScope | null): void {
     let { _activeWidget, _activePresenter } = this;
-    if (_activeWidget && _activePresenter?.capabilities.layerScope) {
+    if (_activeWidget && _activePresenter?.setLayerScope) {
       _activePresenter.setLayerScope(_activeWidget, layerScope);
     }
   }
 
   public getPartStyles(): GlobalStyles | null {
     let { _activeWidget, _activePresenter } = this;
-    if (_activeWidget && _activePresenter?.capabilities.stylePart) {
+    if (_activeWidget && _activePresenter?.getPartStyles) {
       const styles = _activePresenter.getPartStyles(_activeWidget) || null;
       return styles;
     }
@@ -361,7 +361,7 @@ export class DeckManager implements IDeckManager {
   }
   public setPartStyles(styles: GlobalStyles | null): void {
     let { _activeWidget, _activePresenter } = this;
-    if (_activeWidget && _activePresenter?.capabilities.stylePart) {
+    if (_activeWidget && _activePresenter?.setPartStyles) {
       _activePresenter.setPartStyles(_activeWidget, styles);
     }
   }
@@ -517,18 +517,22 @@ export class DeckManager implements IDeckManager {
     }
   }
 
-  cacheStyle(node: HTMLElement) {
-    if (!this._styleCache.get(node)) {
-      this._styleCache.set(node, node.getAttribute('style') || '');
+  cacheStyle(...nodes: HTMLElement[]) {
+    for (const node of nodes) {
+      if (!this._styleCache.get(node)) {
+        this._styleCache.set(node, node.getAttribute('style') || '');
+      }
+      node.setAttribute('style', '');
     }
-    node.setAttribute('style', '');
   }
 
-  uncacheStyle(node: HTMLElement) {
-    const style = this._styleCache.get(node);
-    if (style) {
-      node.setAttribute('style', style);
-      this._styleCache.delete(node);
+  uncacheStyle(...nodes: HTMLElement[]) {
+    for (const node of nodes) {
+      const style = this._styleCache.get(node);
+      if (style) {
+        node.setAttribute('style', style);
+        this._styleCache.delete(node);
+      }
     }
   }
 
diff --git a/js/jupyterlab-deck/src/markdown/presenter.ts b/js/jupyterlab-deck/src/markdown/presenter.ts
new file mode 100644
index 0000000..282752e
--- /dev/null
+++ b/js/jupyterlab-deck/src/markdown/presenter.ts
@@ -0,0 +1,186 @@
+import { MarkdownDocument } from '@jupyterlab/markdownviewer';
+import { CommandRegistry } from '@lumino/commands';
+import { ISignal, Signal } from '@lumino/signaling';
+import { Widget } from '@lumino/widgets';
+
+import {
+  IDeckManager,
+  IPresenter,
+  TCanGoDirection,
+  TDirection,
+  CSS,
+  DIRECTION,
+  CommandIds,
+  DIRECTION_KEYS,
+  COMPOUND_KEYS,
+} from '../tokens';
+
+export class SimpleMarkdownPresenter implements IPresenter<MarkdownDocument> {
+  protected _activeChanged = new Signal<IPresenter<MarkdownDocument>, void>(this);
+
+  public readonly id = 'simple-markdown';
+  public readonly rank = 100;
+  public readonly capabilities = {};
+  protected _manager: IDeckManager;
+  protected _previousActiveCellIndex: number = -1;
+  protected _commands: CommandRegistry;
+  protected _activeSlide = new Map<MarkdownDocument, number>();
+  protected _lastSlide = new Map<MarkdownDocument, number>();
+  protected _stylesheets = new Map<MarkdownDocument, HTMLStyleElement>();
+
+  constructor(options: SimpleMarkdownPresenter.IOptions) {
+    this._manager = options.manager;
+    this._commands = options.commands;
+    this._addKeyBindings();
+    this._addWindowListeners();
+  }
+
+  public accepts(widget: Widget): MarkdownDocument | null {
+    if (widget instanceof MarkdownDocument) {
+      return widget;
+    }
+    return null;
+  }
+
+  public async stop(panel: MarkdownDocument): Promise<void> {
+    this._removeStyle(panel);
+    return;
+  }
+  public async start(panel: MarkdownDocument): Promise<void> {
+    const activeSlide = this._activeSlide.get(panel) || 1;
+    await panel.content.ready;
+    this._updateSheet(panel, activeSlide);
+    return;
+  }
+
+  public async go(
+    panel: MarkdownDocument,
+    direction: TDirection,
+    alternate?: TDirection
+  ): Promise<void> {
+    await panel.content.ready;
+    let index = this._activeSlide.get(panel) || 1;
+    let lastSlide = this._lastSlide.get(panel) || -1;
+    if (direction == 'forward' || alternate == 'forward') {
+      index++;
+    } else if (direction == 'back' || alternate == 'back') {
+      index--;
+    }
+    index = index < 1 ? 1 : index > lastSlide ? lastSlide : index;
+    this._updateSheet(panel, index);
+  }
+
+  public canGo(panel: MarkdownDocument): Partial<TCanGoDirection> {
+    let index = this._activeSlide.get(panel) || 1;
+    // TODO: someplace better
+    let hrCount = panel.content.renderer.node.querySelectorAll('hr').length;
+    this._lastSlide.set(panel, hrCount);
+    return {
+      forward: index < hrCount,
+      back: index > 1,
+    };
+  }
+
+  public style(panel: MarkdownDocument): void {
+    const { _manager } = this;
+    panel.addClass(CSS.deck);
+    _manager.cacheStyle(panel.node, panel.content.node, panel.content.renderer.node);
+  }
+
+  public get activeChanged(): ISignal<IPresenter<MarkdownDocument>, void> {
+    return this._activeChanged;
+  }
+
+  /** overload the stock notebook keyboard shortcuts */
+  protected _addKeyBindings() {
+    for (const direction of Object.values(DIRECTION)) {
+      this._commands.addKeyBinding({
+        command: CommandIds[direction],
+        keys: DIRECTION_KEYS[direction],
+        selector: `.${CSS.deck} .${CSS.markdownViewer}`,
+      });
+    }
+    for (const [directions, keys] of COMPOUND_KEYS.entries()) {
+      const [direction, alternate] = directions;
+      this._commands.addKeyBinding({
+        command: CommandIds.go,
+        args: { direction, alternate },
+        keys,
+        selector: `.${CSS.deck} .${CSS.markdownViewer}`,
+      });
+    }
+  }
+
+  protected _addWindowListeners() {
+    window.addEventListener('hashchange', this._onHashChange);
+  }
+
+  protected _onHashChange = (event: HashChangeEvent) => {
+    const { activeWidget } = this._manager;
+    const panel = activeWidget && this.accepts(activeWidget);
+    /* istanbul ignore if */
+    if (!panel) {
+      return;
+    }
+    const url = new URL(event.newURL);
+    const { hash } = url || '#';
+    /* istanbul ignore if */
+    if (hash === '#') {
+      return;
+    }
+    this._activateByAnchor(panel, hash);
+  };
+
+  protected _activateByAnchor(panel: MarkdownDocument, fragment: string) {
+    const anchored = document.getElementById(fragment.slice(1));
+    /* istanbul ignore if */
+    if (!anchored || !panel.node.contains(anchored)) {
+      return;
+    }
+    let index = 0;
+    for (const child of panel.content.renderer.node.children) {
+      if (child.tagName === 'HR') {
+        index += 1;
+        continue;
+      }
+      if (child === anchored) {
+        this._updateSheet(panel, index);
+        break;
+      }
+    }
+  }
+
+  protected _updateSheet(panel: MarkdownDocument, index: number) {
+    let sheet = this._stylesheets.get(panel);
+    if (sheet == null) {
+      sheet = document.createElement('style');
+      sheet.className = CSS.sheet;
+      this._stylesheets.set(panel, sheet);
+      document.body.appendChild(sheet);
+    }
+    sheet.textContent = `
+    #${panel.id} > .${CSS.markdownViewer} .${
+      CSS.renderedMarkdown
+    } > hr:nth-of-type(${index}) ~ :not(hr:nth-of-type(${index + 1}) ~ *):not(hr) {
+      display: block;
+    }`;
+    this._activeSlide.set(panel, index);
+  }
+
+  protected _removeStyle(panel: MarkdownDocument) {
+    /* istanbul ignore if */
+    if (panel.isDisposed) {
+      return;
+    }
+    const { _manager } = this;
+    panel.removeClass(CSS.deck);
+    _manager.uncacheStyle(panel.content.node, panel.node, panel.content.renderer.node);
+  }
+}
+
+export namespace SimpleMarkdownPresenter {
+  export interface IOptions {
+    manager: IDeckManager;
+    commands: CommandRegistry;
+  }
+}
diff --git a/js/jupyterlab-deck/src/notebook/presenter.ts b/js/jupyterlab-deck/src/notebook/presenter.ts
index 478d151..2c92129 100644
--- a/js/jupyterlab-deck/src/notebook/presenter.ts
+++ b/js/jupyterlab-deck/src/notebook/presenter.ts
@@ -72,8 +72,7 @@ export class NotebookPresenter implements IPresenter<NotebookPanel> {
 
   public style(panel: NotebookPanel): void {
     panel.addClass(CSS.deck);
-    this._manager.cacheStyle(panel.node);
-    this._manager.cacheStyle(panel.content.node);
+    this._manager.cacheStyle(panel.node, panel.content.node);
   }
 
   public async stop(panel: NotebookPanel): Promise<void> {
@@ -387,8 +386,7 @@ export class NotebookPresenter implements IPresenter<NotebookPanel> {
     }
     const { _manager } = this;
     panel.removeClass(CSS.deck);
-    _manager.uncacheStyle(panel.content.node);
-    _manager.uncacheStyle(panel.node);
+    _manager.uncacheStyle(panel.content.node, panel.node);
     for (const cell of panel.content.widgets) {
       cell.removeClass(CSS.layer);
       cell.removeClass(CSS.onScreen);
diff --git a/js/jupyterlab-deck/src/plugin.ts b/js/jupyterlab-deck/src/plugin.ts
index eb4e5f1..eee686a 100644
--- a/js/jupyterlab-deck/src/plugin.ts
+++ b/js/jupyterlab-deck/src/plugin.ts
@@ -12,6 +12,7 @@ import { IStatusBar, StatusBar } from '@jupyterlab/statusbar';
 import { ITranslator, nullTranslator } from '@jupyterlab/translation';
 
 import { DeckManager } from './manager';
+import { SimpleMarkdownPresenter } from './markdown/presenter';
 import { NotebookDeckExtension } from './notebook/extension';
 import { NotebookPresenter } from './notebook/presenter';
 import { NS, IDeckManager, CommandIds, CATEGORY, PLUGIN_ID } from './tokens';
@@ -88,4 +89,18 @@ const notebookPlugin: JupyterFrontEndPlugin<void> = {
   },
 };
 
-export default [plugin, notebookPlugin];
+const simpleMarkdownPlugin: JupyterFrontEndPlugin<void> = {
+  id: `${NS}:simple-markdown`,
+  requires: [IDeckManager],
+  autoStart: true,
+  activate: (app: JupyterFrontEnd, decks: IDeckManager) => {
+    const { commands } = app;
+    const presenter = new SimpleMarkdownPresenter({
+      manager: decks,
+      commands,
+    });
+    decks.addPresenter(presenter);
+  },
+};
+
+export default [plugin, notebookPlugin, simpleMarkdownPlugin];
diff --git a/js/jupyterlab-deck/src/tokens.ts b/js/jupyterlab-deck/src/tokens.ts
index 841a7a0..ca668ca 100644
--- a/js/jupyterlab-deck/src/tokens.ts
+++ b/js/jupyterlab-deck/src/tokens.ts
@@ -26,8 +26,8 @@ export interface IDeckManager {
   __: (msgid: string, ...args: string[]) => string;
   go(direction: TDirection, alternate?: TDirection): void;
   canGo(): Partial<TCanGoDirection>;
-  cacheStyle(node: HTMLElement): void;
-  uncacheStyle(node: HTMLElement): void;
+  cacheStyle(...nodes: HTMLElement[]): void;
+  uncacheStyle(...nodes: HTMLElement[]): void;
   addPresenter(presenter: IPresenter<any>): void;
   addStylePreset(preset: IStylePreset): void;
   stylePresets: IStylePreset[];
@@ -55,10 +55,11 @@ export interface IDeckManager {
 export const IDeckManager = new Token<IDeckManager>(PLUGIN_ID);
 
 export interface IPresenterCapbilities {
-  layout: boolean;
-  slideType: boolean;
-  layerScope: boolean;
-  stylePart: boolean;
+  layout?: boolean;
+  slideType?: boolean;
+  layerScope?: boolean;
+  stylePart?: boolean;
+  subslides?: boolean;
 }
 
 export const INCAPABLE: IPresenterCapbilities = Object.freeze({
@@ -66,9 +67,19 @@ export const INCAPABLE: IPresenterCapbilities = Object.freeze({
   slideType: false,
   layerScope: false,
   stylePart: false,
+  subslides: false,
 });
 
-export interface IPresenter<T extends Widget> {
+export interface IPresenterOptional<T> {
+  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 interface IPresenter<T extends Widget> extends Partial<IPresenterOptional<T>> {
   id: string;
   rank: number;
   capabilities: IPresenterCapbilities;
@@ -79,13 +90,6 @@ 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 {
@@ -107,6 +111,8 @@ export namespace CSS {
   export const accept = 'jp-mod-accept';
   export const active = 'jp-mod-active';
   export const mainContent = 'jp-main-content-panel';
+  export const renderedMarkdown = 'jp-RenderedMarkdown';
+  export const markdownViewer = 'jp-MarkdownViewer';
   // deck
   export const deck = 'jp-Deck';
   export const presenting = `[data-jp-deck-mode='${DATA.presenting}']`;
@@ -142,6 +148,8 @@ export namespace CSS {
   export const zoom = 'jp-deck-mod-zoom';
   export const opacity = 'jp-deck-mod-opacity';
   export const zIndex = 'jp-deck-mod-z-index';
+  // sheets
+  export const sheet = 'jp-Deck-Stylesheet';
 }
 
 export namespace ID {
diff --git a/js/jupyterlab-deck/src/tools/remote.tsx b/js/jupyterlab-deck/src/tools/remote.tsx
index 99da6c3..94f3244 100644
--- a/js/jupyterlab-deck/src/tools/remote.tsx
+++ b/js/jupyterlab-deck/src/tools/remote.tsx
@@ -2,6 +2,7 @@ import { VDomRenderer, VDomModel } from '@jupyterlab/apputils';
 import { PathExt } from '@jupyterlab/coreutils';
 import { LabIcon } from '@jupyterlab/ui-components';
 import { JSONExt } from '@lumino/coreutils';
+import type { Widget } from '@lumino/widgets';
 import React from 'react';
 
 import { ICONS } from '../icons';
@@ -11,6 +12,7 @@ import {
   DIRECTION,
   DIRECTION_LABEL,
   TCanGoDirection,
+  IPresenter,
 } from '../tokens';
 
 export class DeckRemote extends VDomRenderer<DeckRemote.Model> {
@@ -106,6 +108,7 @@ export namespace DeckRemote {
   export class Model extends VDomModel {
     private _manager: IDeckManager;
     private _canGo: Partial<TCanGoDirection> = {};
+    private _activePresenter: IPresenter<Widget> | null = null;
 
     constructor(options: IOptions) {
       super();
@@ -128,11 +131,24 @@ export namespace DeckRemote {
 
     private _onActiveChanged() {
       const canGo = this._manager.canGo();
+      let emit = false;
       if (!JSONExt.deepEqual(canGo, this._canGo)) {
         this._canGo = canGo;
-        this.stateChanged.emit(void 0);
+        emit = true;
+      }
+      let { activePresenter } = this._manager;
+      if (activePresenter !== this._activePresenter) {
+        this._activePresenter = activePresenter;
+        emit = true;
+      }
+      if (emit) {
+        this.emit();
       }
     }
+
+    private emit = () => {
+      this.stateChanged.emit(void 0);
+    };
   }
   export interface IOptions {
     manager: IDeckManager;
diff --git a/js/jupyterlab-deck/style/index.css b/js/jupyterlab-deck/style/index.css
index be56ceb..86643c5 100644
--- a/js/jupyterlab-deck/style/index.css
+++ b/js/jupyterlab-deck/style/index.css
@@ -1,7 +1,12 @@
 @import './variables.css';
-@import './notebook.css';
 @import './shell.css';
+
+/* tools */
 @import './remote.css';
 @import './decktools.css';
 @import './layover.css';
 @import './design.css';
+
+/* docs */
+@import './markdown.css';
+@import './notebook.css';
diff --git a/js/jupyterlab-deck/style/markdown.css b/js/jupyterlab-deck/style/markdown.css
new file mode 100644
index 0000000..08b88e2
--- /dev/null
+++ b/js/jupyterlab-deck/style/markdown.css
@@ -0,0 +1,26 @@
+.jp-Deck > .jp-MarkdownViewer {
+  display: flex;
+  flex-direction: column;
+  position: fixed;
+  left: var(--jp-private-sidebar-tab-width);
+  right: var(--jp-private-sidebar-tab-width);
+  justify-content: center;
+  align-content: center;
+  align-items: stretch;
+  height: 100vh;
+  overflow-y: hidden;
+}
+
+.jp-Deck > .jp-MarkdownViewer > .jp-RenderedMarkdown {
+  flex-shrink: 0;
+  gap: 0;
+  overflow-y: auto;
+  max-height: 100vh;
+  margin: 0;
+  padding: 0;
+  background: transparent;
+}
+
+.jp-Deck > .jp-MarkdownViewer > .jp-RenderedMarkdown > * {
+  display: none;
+}
diff --git a/yarn.lock b/yarn.lock
index f3674b8..2c0c357 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -253,6 +253,7 @@
   dependencies:
     "@jupyterlab/application" "3"
     "@jupyterlab/apputils" "3"
+    "@jupyterlab/markdownviewer" "3"
     "@jupyterlab/notebook" "3"
     "@jupyterlab/statusbar" "3"
     "@jupyterlab/ui-components" "3"
@@ -456,18 +457,18 @@
     "@lumino/signaling" "^1.10.0"
     "@lumino/widgets" "^1.33.0"
 
-"@jupyterlab/apputils@3", "@jupyterlab/apputils@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/apputils/-/apputils-3.4.8.tgz#09126a7628958ab2409379d3b691a00d144eee90"
-  integrity sha512-b7vvPQUdkXcrZnfPUXJVYw4vpKl5HmPqFtFTGmatY+F734FSLYkZeIrNxByinI2DJg35I1p1NfSQW/3DVVwpog==
-  dependencies:
-    "@jupyterlab/coreutils" "^5.4.8"
-    "@jupyterlab/observables" "^4.4.8"
-    "@jupyterlab/services" "^6.4.8"
-    "@jupyterlab/settingregistry" "^3.4.8"
-    "@jupyterlab/statedb" "^3.4.8"
-    "@jupyterlab/translation" "^3.4.8"
-    "@jupyterlab/ui-components" "^3.4.8"
+"@jupyterlab/apputils@3", "@jupyterlab/apputils@^3.4.8", "@jupyterlab/apputils@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/apputils/-/apputils-3.5.0.tgz#9ce715f6d56d3647798dc8d1108c638466459d3f"
+  integrity sha512-brL1CR0F2ocxt+YSWQGRh9OoJWxlqQb5BxQNJy+qJceCpwkMyZmZyf2gxHc9bu67HkL96Sa46wGIn6WKobARrA==
+  dependencies:
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/observables" "^4.5.0"
+    "@jupyterlab/services" "^6.5.0"
+    "@jupyterlab/settingregistry" "^3.5.0"
+    "@jupyterlab/statedb" "^3.5.0"
+    "@jupyterlab/translation" "^3.5.0"
+    "@jupyterlab/ui-components" "^3.5.0"
     "@lumino/algorithm" "^1.9.0"
     "@lumino/commands" "^1.19.0"
     "@lumino/coreutils" "^1.11.0"
@@ -592,17 +593,17 @@
     marked "^4.0.17"
     react "^17.0.1"
 
-"@jupyterlab/codeeditor@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/codeeditor/-/codeeditor-3.4.8.tgz#2684c9a6f40192f5144937951e1852d1b652dd9e"
-  integrity sha512-1ymmnK6zE40ivJRpVRd3XI/p7WydqTyD/qbgcYK4JHRzDUvfIJz0OQkei4Rq1oOD9DsMdJOIuyyEXgA8FPE4/g==
-  dependencies:
-    "@jupyterlab/coreutils" "^5.4.8"
-    "@jupyterlab/nbformat" "^3.4.8"
-    "@jupyterlab/observables" "^4.4.8"
-    "@jupyterlab/shared-models" "^3.4.8"
-    "@jupyterlab/translation" "^3.4.8"
-    "@jupyterlab/ui-components" "^3.4.8"
+"@jupyterlab/codeeditor@^3.4.8", "@jupyterlab/codeeditor@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/codeeditor/-/codeeditor-3.5.0.tgz#b5345f028196c68077424b4a9f33ca315ec49828"
+  integrity sha512-imdYuovxyIIQqZdoRnZAr0VQHqiIVPPFwk8hAgDYtfl8VxFOPMTh203Z6y+CLv5V62J03OU7HZutP/f5u1nZ1w==
+  dependencies:
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/nbformat" "^3.5.0"
+    "@jupyterlab/observables" "^4.5.0"
+    "@jupyterlab/shared-models" "^3.5.0"
+    "@jupyterlab/translation" "^3.5.0"
+    "@jupyterlab/ui-components" "^3.5.0"
     "@lumino/coreutils" "^1.11.0"
     "@lumino/disposable" "^1.10.0"
     "@lumino/dragdrop" "^1.13.0"
@@ -610,19 +611,19 @@
     "@lumino/signaling" "^1.10.0"
     "@lumino/widgets" "^1.33.0"
 
-"@jupyterlab/codemirror@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/codemirror/-/codemirror-3.4.8.tgz#fe72b58d5d01da83da9725f59393326a6bf8ca90"
-  integrity sha512-HojL7+vaUTBPIgg0gzCJKBbsN8TdVR+DxXfeGII1RoQf5uUCQdBz9/Ksyt96uHCS+EmYRWkLik2dU94LIEr4dw==
-  dependencies:
-    "@jupyterlab/apputils" "^3.4.8"
-    "@jupyterlab/codeeditor" "^3.4.8"
-    "@jupyterlab/coreutils" "^5.4.8"
-    "@jupyterlab/nbformat" "^3.4.8"
-    "@jupyterlab/observables" "^4.4.8"
-    "@jupyterlab/shared-models" "^3.4.8"
-    "@jupyterlab/statusbar" "^3.4.8"
-    "@jupyterlab/translation" "^3.4.8"
+"@jupyterlab/codemirror@^3.4.8", "@jupyterlab/codemirror@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/codemirror/-/codemirror-3.5.0.tgz#b16190e99584acfb0b18c44dd1c005366a829062"
+  integrity sha512-i6rGYLnWsBuL8zkCpPTCMeZc2lHI5pIgtEpO/CEfeigYhZI9NkaLSiF64Jwt8bgurS10O02bxl+3hIgU3mSSQA==
+  dependencies:
+    "@jupyterlab/apputils" "^3.5.0"
+    "@jupyterlab/codeeditor" "^3.5.0"
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/nbformat" "^3.5.0"
+    "@jupyterlab/observables" "^4.5.0"
+    "@jupyterlab/shared-models" "^3.5.0"
+    "@jupyterlab/statusbar" "^3.5.0"
+    "@jupyterlab/translation" "^3.5.0"
     "@lumino/algorithm" "^1.9.0"
     "@lumino/commands" "^1.19.0"
     "@lumino/coreutils" "^1.11.0"
@@ -634,10 +635,10 @@
     react "^17.0.1"
     y-codemirror "^3.0.1"
 
-"@jupyterlab/coreutils@^5.4.8":
-  version "5.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-5.4.8.tgz#e3a81a8edb51c9a8d40f9baf4149f86c5e5109d0"
-  integrity sha512-UICv9nBCL+thSSOFlLWGjPm+UTT1ioPq+pOMjgn0E/DPliUMAMKtrAU5viAbRhITGAU55uL2KH9ijMUIc6o3xA==
+"@jupyterlab/coreutils@^5.4.8", "@jupyterlab/coreutils@^5.5.0":
+  version "5.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-5.5.0.tgz#0cbceb74e75a96cc69c8dd14b61f37d8ea940d75"
+  integrity sha512-mVBuVDUA87hvtS5DfbjfLIE1EFdhAGEU8f19G33QfhD/w2vYDi7vE4ro4arNT47r17MzXW4XfaE4LwatR6uvPw==
   dependencies:
     "@lumino/coreutils" "^1.11.0"
     "@lumino/disposable" "^1.10.0"
@@ -668,34 +669,34 @@
     "@lumino/widgets" "^1.33.0"
     react "^17.0.1"
 
-"@jupyterlab/docprovider@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/docprovider/-/docprovider-3.4.8.tgz#d75e8ccfc9d3f08cf13c6b9b97cd987bdbdcd6d2"
-  integrity sha512-srOLhPzP+pfOjpQNtXmKuSyBsQzTxf087+MpUdTMPt177PCGG7eyMcCNcDTkBXo6QzuwQuJIj+uESQjVYmQ7Hw==
+"@jupyterlab/docprovider@^3.4.8", "@jupyterlab/docprovider@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/docprovider/-/docprovider-3.5.0.tgz#8593ada08c0e7ec014084e16e918d26aac14c441"
+  integrity sha512-F5VtIIDUpWEFKc0S/xDs8GIjEZC/xn6SVrdNY0+ixDPyC5VNJo+IU5JmqrcU25DlJ+jMbnKlPdRLYsRtJTDKrw==
   dependencies:
-    "@jupyterlab/shared-models" "^3.4.8"
+    "@jupyterlab/shared-models" "^3.5.0"
     "@lumino/coreutils" "^1.11.0"
     lib0 "^0.2.42"
     y-websocket "^1.3.15"
     yjs "^13.5.17"
 
-"@jupyterlab/docregistry@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/docregistry/-/docregistry-3.4.8.tgz#d9e4c3d7a69b718ce3e6a57f1c4a76bca7cb95b7"
-  integrity sha512-4DRy4zMrUIhdlEsStbKSOrUUe27EtJBMhwFqTIiKQdaQp4QHgMaHYTLqX/DomGJd4FPmXtcgNfkTr+nM2Tf3yw==
-  dependencies:
-    "@jupyterlab/apputils" "^3.4.8"
-    "@jupyterlab/codeeditor" "^3.4.8"
-    "@jupyterlab/codemirror" "^3.4.8"
-    "@jupyterlab/coreutils" "^5.4.8"
-    "@jupyterlab/docprovider" "^3.4.8"
-    "@jupyterlab/observables" "^4.4.8"
-    "@jupyterlab/rendermime" "^3.4.8"
-    "@jupyterlab/rendermime-interfaces" "^3.4.8"
-    "@jupyterlab/services" "^6.4.8"
-    "@jupyterlab/shared-models" "^3.4.8"
-    "@jupyterlab/translation" "^3.4.8"
-    "@jupyterlab/ui-components" "^3.4.8"
+"@jupyterlab/docregistry@^3.4.8", "@jupyterlab/docregistry@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/docregistry/-/docregistry-3.5.0.tgz#aa3ebc2cd676f7ff564dd055fdd629f0dd16b5b1"
+  integrity sha512-OdP+q4rvZARqJvZWCyae23K8IHN+TvSP0xPyTVHd1aXFXi6cWlNUOUGRHd9TlEUNqyJxKjkZNuhozMu8ANEBAQ==
+  dependencies:
+    "@jupyterlab/apputils" "^3.5.0"
+    "@jupyterlab/codeeditor" "^3.5.0"
+    "@jupyterlab/codemirror" "^3.5.0"
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/docprovider" "^3.5.0"
+    "@jupyterlab/observables" "^4.5.0"
+    "@jupyterlab/rendermime" "^3.5.0"
+    "@jupyterlab/rendermime-interfaces" "^3.5.0"
+    "@jupyterlab/services" "^6.5.0"
+    "@jupyterlab/shared-models" "^3.5.0"
+    "@jupyterlab/translation" "^3.5.0"
+    "@jupyterlab/ui-components" "^3.5.0"
     "@lumino/algorithm" "^1.9.0"
     "@lumino/coreutils" "^1.11.0"
     "@lumino/disposable" "^1.10.0"
@@ -744,10 +745,24 @@
     "@lumino/coreutils" "^1.11.0"
     "@lumino/widgets" "^1.33.0"
 
-"@jupyterlab/nbformat@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-3.4.8.tgz#8552c9d32e8f04bd3e9be468be57662f0c5307c2"
-  integrity sha512-RcyITAagwXMIWqehpctb43mVB1H3LrTfikGvykLICmA5AfT+byhooCDN4d+ipg4rkeioUmEgX+2uTfForCsJWQ==
+"@jupyterlab/markdownviewer@3":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/markdownviewer/-/markdownviewer-3.5.0.tgz#f7255d317c485ffd86d355d98f50579fd285d434"
+  integrity sha512-qYV7l4KgSJhzSxKajgXOUevsLuMmnC6MngkCfXfm6pcbnnaeGD6yYAngbmVDWIJpIcHPhGXaMapFSjhjWvS45Q==
+  dependencies:
+    "@jupyterlab/apputils" "^3.5.0"
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/docregistry" "^3.5.0"
+    "@jupyterlab/rendermime" "^3.5.0"
+    "@jupyterlab/translation" "^3.5.0"
+    "@lumino/coreutils" "^1.11.0"
+    "@lumino/messaging" "^1.10.0"
+    "@lumino/widgets" "^1.33.0"
+
+"@jupyterlab/nbformat@^3.4.8", "@jupyterlab/nbformat@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/nbformat/-/nbformat-3.5.0.tgz#ea3d926b90db9ff2da988db1ea3c8ac1dc3ba9fa"
+  integrity sha512-tQ0MCJ2NSlGTYM7auiL2vdqirIv39Pd2/gfFd4XdHClJgvT65b7XkNDOwBv6mqIuhNdHo3Mc3RXiODTo1tle7Q==
   dependencies:
     "@lumino/coreutils" "^1.11.0"
 
@@ -781,10 +796,10 @@
     "@lumino/widgets" "^1.33.0"
     react "^17.0.1"
 
-"@jupyterlab/observables@^4.4.8":
-  version "4.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/observables/-/observables-4.4.8.tgz#a73833e4f33b3d7e9c2a59306e8f526e13c043d9"
-  integrity sha512-TT7YQNxvLnfuzbHQjoovfVN02dXDG/zxfWiA1RkycAJnQ/aTgRtEMlLMs7dUqNCh6ej6zNQOUEduJInro/OL4A==
+"@jupyterlab/observables@^4.4.8", "@jupyterlab/observables@^4.5.0":
+  version "4.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/observables/-/observables-4.5.0.tgz#3cba31bf2358c11f3c7ba39b7aa840e727733405"
+  integrity sha512-YiUljeHNz80YpIPDi0zoUC26AwAhyDu1UXm2kH5J/lPViycz8X22RWXkIBc40kvWoasUTSomZiEv/W2hFUs0Vw==
   dependencies:
     "@lumino/algorithm" "^1.9.0"
     "@lumino/coreutils" "^1.11.0"
@@ -812,28 +827,28 @@
     "@lumino/widgets" "^1.33.0"
     resize-observer-polyfill "^1.5.1"
 
-"@jupyterlab/rendermime-interfaces@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-3.4.8.tgz#2ed70bf936a03523fbd0a8838cded9c23af6211f"
-  integrity sha512-zaJbCv9fhgwAlNOcmWnz2rSmoRPR3QLUvhEQ7e08k0jrG6O3Zf19SObEOcp1TM0qwPNSEFT71AbAa81ari13Jg==
+"@jupyterlab/rendermime-interfaces@^3.4.8", "@jupyterlab/rendermime-interfaces@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/rendermime-interfaces/-/rendermime-interfaces-3.5.0.tgz#e833b1304ff1e86934fcb039dec7b5ffbf374ae9"
+  integrity sha512-SWpNX8dwRuAH0GMeuamN1O096Ypn2XcosNbo60P8860qi2KzTXgxADt5xcOf6FK+tXVQ+qi3hJi+055+1xjq+g==
   dependencies:
-    "@jupyterlab/translation" "^3.4.8"
+    "@jupyterlab/translation" "^3.5.0"
     "@lumino/coreutils" "^1.11.0"
     "@lumino/widgets" "^1.33.0"
 
-"@jupyterlab/rendermime@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/rendermime/-/rendermime-3.4.8.tgz#9775185c6d71f4159feebeec3d936c00c73d298b"
-  integrity sha512-9GrfLst9BRJpVAng2mcVCIt2zCIw0+ho8CEMnlq/gwUMUxW8fpstaUnxkLoW7yIvtSnFAYbUZ3GBKenc084nOQ==
-  dependencies:
-    "@jupyterlab/apputils" "^3.4.8"
-    "@jupyterlab/codemirror" "^3.4.8"
-    "@jupyterlab/coreutils" "^5.4.8"
-    "@jupyterlab/nbformat" "^3.4.8"
-    "@jupyterlab/observables" "^4.4.8"
-    "@jupyterlab/rendermime-interfaces" "^3.4.8"
-    "@jupyterlab/services" "^6.4.8"
-    "@jupyterlab/translation" "^3.4.8"
+"@jupyterlab/rendermime@^3.4.8", "@jupyterlab/rendermime@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/rendermime/-/rendermime-3.5.0.tgz#279a2672690b63ac23990b366f2dc5cdc786dacc"
+  integrity sha512-vA5bQA/v7/P/6a3WXdrSoTeGgIJy1iLvpVpJ3DfR9NIpPrXzazDtRplipwcHsNjtUn4P2oS8C46s/eTOEPsQOw==
+  dependencies:
+    "@jupyterlab/apputils" "^3.5.0"
+    "@jupyterlab/codemirror" "^3.5.0"
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/nbformat" "^3.5.0"
+    "@jupyterlab/observables" "^4.5.0"
+    "@jupyterlab/rendermime-interfaces" "^3.5.0"
+    "@jupyterlab/services" "^6.5.0"
+    "@jupyterlab/translation" "^3.5.0"
     "@lumino/algorithm" "^1.9.0"
     "@lumino/coreutils" "^1.11.0"
     "@lumino/messaging" "^1.10.0"
@@ -842,16 +857,16 @@
     lodash.escape "^4.0.1"
     marked "^4.0.17"
 
-"@jupyterlab/services@^6.4.8":
-  version "6.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/services/-/services-6.4.8.tgz#2da20fd5a5c94ab8f8200da633a252792927318a"
-  integrity sha512-/acj4d1A1V9KDN+k4CUokOA8e/IxaoJW2B+FJxVnTZvVOBh7093EIG+HYL1SQuQ8CUc2T4DNiq9mG3skiSe2fQ==
-  dependencies:
-    "@jupyterlab/coreutils" "^5.4.8"
-    "@jupyterlab/nbformat" "^3.4.8"
-    "@jupyterlab/observables" "^4.4.8"
-    "@jupyterlab/settingregistry" "^3.4.8"
-    "@jupyterlab/statedb" "^3.4.8"
+"@jupyterlab/services@^6.4.8", "@jupyterlab/services@^6.5.0":
+  version "6.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/services/-/services-6.5.0.tgz#cf89407d8f39ed708394e8ba14b5cba5b65b9cba"
+  integrity sha512-g5fa7oFu1I6i0agOmx6ud/1fjYAsr3zHzoymE4oAGN3nIbt8HTcmzLbiwmaWssGCVUF4h06GOYWcAe/x/ND8JA==
+  dependencies:
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/nbformat" "^3.5.0"
+    "@jupyterlab/observables" "^4.5.0"
+    "@jupyterlab/settingregistry" "^3.5.0"
+    "@jupyterlab/statedb" "^3.5.0"
     "@lumino/algorithm" "^1.9.0"
     "@lumino/coreutils" "^1.11.0"
     "@lumino/disposable" "^1.10.0"
@@ -860,12 +875,12 @@
     node-fetch "^2.6.0"
     ws "^7.4.6"
 
-"@jupyterlab/settingregistry@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-3.4.8.tgz#10f52898b8553639ed4d9d786294617ac599c4a9"
-  integrity sha512-w9MNFivKXUOLrEvWckpcYm3XAZr0sbcKQ33SkftaLSQODsFlUwkcsjCPJJATVyxiWXAsCAgUlOKdNcqWxYXvOA==
+"@jupyterlab/settingregistry@^3.4.8", "@jupyterlab/settingregistry@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/settingregistry/-/settingregistry-3.5.0.tgz#2a6a0c2771c61643ab4aff9355ac863b4c8fc0ab"
+  integrity sha512-y9H9U4iMVfe2btImp5DR9mGAs36Sow0hI6ajK61bhHVJ4CumYdFBd8drrQGuYcyk/Y4ypU5HO9EaLBQU3CLCug==
   dependencies:
-    "@jupyterlab/statedb" "^3.4.8"
+    "@jupyterlab/statedb" "^3.5.0"
     "@lumino/commands" "^1.19.0"
     "@lumino/coreutils" "^1.11.0"
     "@lumino/disposable" "^1.10.0"
@@ -873,22 +888,22 @@
     ajv "^6.12.3"
     json5 "^2.1.1"
 
-"@jupyterlab/shared-models@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/shared-models/-/shared-models-3.4.8.tgz#88156a1c584e50f03cb378ea56f42e8800ba8c3d"
-  integrity sha512-sUBGyYTAYDOG6S5537ZLTTlqR/ut6XU1sLz/khjQOjROhyAC5kH7Vs7oUoGoGfSCLIwCAlCX2NZHXcvhElNbIg==
+"@jupyterlab/shared-models@^3.4.8", "@jupyterlab/shared-models@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/shared-models/-/shared-models-3.5.0.tgz#d34b5ac6d0121ea8daf3862bb92926959aade209"
+  integrity sha512-QZL9BPCC+iV12AsUbUAwQvZeeo3fKh1X8h9odtlc+Oc+dyZAqREYXuZGjVlaG9qwbF62xDr7acfO4HqCK6Kjyw==
   dependencies:
-    "@jupyterlab/nbformat" "^3.4.8"
+    "@jupyterlab/nbformat" "^3.5.0"
     "@lumino/coreutils" "^1.11.0"
     "@lumino/disposable" "^1.10.0"
     "@lumino/signaling" "^1.10.0"
     y-protocols "^1.0.5"
     yjs "^13.5.17"
 
-"@jupyterlab/statedb@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-3.4.8.tgz#78654b5563f97e8f63683c54403bdc391f4c8df3"
-  integrity sha512-PMlo+x4R8uXPH1BgCJUVVIj/H8SY9scGJU0pqHhYa6mm3R2EHNAwr8JxyqGjAqT3C0VCCCIDzHtQ3f9inW+OXg==
+"@jupyterlab/statedb@^3.4.8", "@jupyterlab/statedb@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/statedb/-/statedb-3.5.0.tgz#6118a7cf339a3d5f033b7b61c3480bfd9bb97a48"
+  integrity sha512-S4/BjcfSN8tGMyL4jjrD4TMoLTABI3zkLjaqSNRfT6iyKnqN8VKcMHBZXOq90uWGkw+caQZ5GiL+L7uEtahE4w==
   dependencies:
     "@lumino/commands" "^1.19.0"
     "@lumino/coreutils" "^1.11.0"
@@ -896,16 +911,16 @@
     "@lumino/properties" "^1.8.0"
     "@lumino/signaling" "^1.10.0"
 
-"@jupyterlab/statusbar@3", "@jupyterlab/statusbar@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/statusbar/-/statusbar-3.4.8.tgz#ac99f5304cf54ec06b7fb02badde8f22201f713e"
-  integrity sha512-WdeiqMmUEfxfOSMyOTRHQwPKSpcE9ucmq8SL0KN5pY5jcEfw3JTJGSb1tKV3WpI6lqErCQITcu29i1jiP/p+Vg==
+"@jupyterlab/statusbar@3", "@jupyterlab/statusbar@^3.4.8", "@jupyterlab/statusbar@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/statusbar/-/statusbar-3.5.0.tgz#62155a3938f6aff6081820c54007d7cbae93bf68"
+  integrity sha512-kXgMN7x5V3bTWP45mahOt2SaNOMXgeohlMsIou9f+OHZeR++6dmMMKyHPnE7QXES4At26FQu3swRI+8+A2klgA==
   dependencies:
-    "@jupyterlab/apputils" "^3.4.8"
-    "@jupyterlab/codeeditor" "^3.4.8"
-    "@jupyterlab/services" "^6.4.8"
-    "@jupyterlab/translation" "^3.4.8"
-    "@jupyterlab/ui-components" "^3.4.8"
+    "@jupyterlab/apputils" "^3.5.0"
+    "@jupyterlab/codeeditor" "^3.5.0"
+    "@jupyterlab/services" "^6.5.0"
+    "@jupyterlab/translation" "^3.5.0"
+    "@jupyterlab/ui-components" "^3.5.0"
     "@lumino/algorithm" "^1.9.0"
     "@lumino/coreutils" "^1.11.0"
     "@lumino/disposable" "^1.10.0"
@@ -916,25 +931,25 @@
     react "^17.0.1"
     typestyle "^2.0.4"
 
-"@jupyterlab/translation@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/translation/-/translation-3.4.8.tgz#9958bbbcabdcf913e96fcb267b223dccfa4d1151"
-  integrity sha512-5hIdMcA33qQpa2jR2Ho+bslfrf+feMyZbu37eCV58kHZjG3BsW47PWe5M0PCCe8WEIkTDu9z7BAmIUtbfwaZgA==
+"@jupyterlab/translation@^3.4.8", "@jupyterlab/translation@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/translation/-/translation-3.5.0.tgz#4f8cfd7382009365297df112abb51b7a8d531081"
+  integrity sha512-68Cyc9gVKef/Gr9tx9YisiPEIzXUk+mnM7u9huthq5A0aHh1W0E51CM/m0BwJDBurbY+W7erphy0nSWSEk7vCg==
   dependencies:
-    "@jupyterlab/coreutils" "^5.4.8"
-    "@jupyterlab/services" "^6.4.8"
-    "@jupyterlab/statedb" "^3.4.8"
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/services" "^6.5.0"
+    "@jupyterlab/statedb" "^3.5.0"
     "@lumino/coreutils" "^1.11.0"
 
-"@jupyterlab/ui-components@3", "@jupyterlab/ui-components@^3.4.8":
-  version "3.4.8"
-  resolved "https://registry.npmjs.org/@jupyterlab/ui-components/-/ui-components-3.4.8.tgz#1aec1b7c6a07abe8d84424d8b3085d8f5627b360"
-  integrity sha512-mkbJnllKCHaKEtUAtCwQAHrJjoD13njlcaDM2Ml9x8vF7PQB8bwRfp/ml4d6n1jOEJjd+a8HRrpzD2X1mTneZQ==
+"@jupyterlab/ui-components@3", "@jupyterlab/ui-components@^3.4.8", "@jupyterlab/ui-components@^3.5.0":
+  version "3.5.0"
+  resolved "https://registry.npmjs.org/@jupyterlab/ui-components/-/ui-components-3.5.0.tgz#329d0d0b3db666c041fa1a647af68400909d09e9"
+  integrity sha512-1AIKMUhyLgPYh3R3qvEPRhLKkiVwBtPg571If9UxTvDEJqVwtNTayn47sRsWlOKlueLVwebgEHVSkk2ahxgF6Q==
   dependencies:
     "@blueprintjs/core" "^3.36.0"
     "@blueprintjs/select" "^3.15.0"
-    "@jupyterlab/coreutils" "^5.4.8"
-    "@jupyterlab/translation" "^3.4.8"
+    "@jupyterlab/coreutils" "^5.5.0"
+    "@jupyterlab/translation" "^3.5.0"
     "@lumino/algorithm" "^1.9.0"
     "@lumino/commands" "^1.19.0"
     "@lumino/coreutils" "^1.11.0"


Test patch:
diff --git a/atest/resources/Deck.resource b/atest/resources/Deck.resource
index fdd6851..9cf314c 100644
--- a/atest/resources/Deck.resource
+++ b/atest/resources/Deck.resource
@@ -13,15 +13,38 @@ ${ZERO_PAD}     {0:03d}
 *** Keywords ***
 Start Deck With Notebook Toolbar Button
     [Documentation]    Use the notebook toolbar to start deck.
-    Wait Until Element Is Not Visible    css:${CSS_LAB_SPINNER}
+    Wait Until Element Is Not Visible    css:${CSS_LAB_SPINNER}    timeout=1s
     Click Element    css:${JLAB CSS ACTIVE DOC}
     Click Element    css:${CSS_DECK_NOTEBOOK_BUTTON}
-    Wait Until Element Is Visible    css:${CSS_DECK_PRESENTING}
-    Wait Until Element Is Visible    css:${CSS_DECK_VISIBLE}
+    Wait Until Element Is Visible    css:${CSS_DECK_PRESENTING}    timeout=1s
+    Wait Until Element Is Visible    css:${CSS_DECK_VISIBLE}    timeout=1s
 
 Really Start Deck With Notebook Toolbar Button
     [Documentation]    REALLY use the notebook toolbar to start deck.
+    Send Error Screenshots To Trash
     Wait Until Keyword Succeeds    5x    0.1s    Start Deck With Notebook Toolbar Button
+    [Teardown]    Resume Screenshots
+
+Start Markdown Deck From Editor
+    [Documentation]    Start a deck from a markdown editor with the context menu.
+    [Arguments]    ${host}
+    Select From Context Menu    ${CSS_LAB_EDITOR}    li${CSS_LAB_CMD_MARKDOWN_PREVIEW}
+    Close JupyterLab Dock Panel Tab    ${host}
+    Close JupyterLab Dock Panel Tab    Launcher
+    Click Element    css:${CSS_LAB_MARKDOWN_VIEWER}
+    Execute JupyterLab Command    Start Deck
+    Wait Until Element Is Visible    css:${CSS_DECK}
+    Click Element    css:${CSS_DECK}
+
+Select From Context Menu
+    [Documentation]    Open the context menu and click an item.
+    [Arguments]    ${host}    ${item}
+    Open Context Menu    css:${host}
+    Wait Until Element Is Visible    css:${item}
+    Mouse Over    css:${item} ${CSS_LM_MENU_ITEM_LABEL}
+    Wait Until Element Is Visible    css:${item}${CSS_LM_MOD_ACTIVE}
+    Click Element    css:${item}${CSS_LM_MOD_ACTIVE} ${CSS_LM_MENU_ITEM_LABEL}
+    Wait Until Element Is Not Visible    css:${item}
 
 Stop Deck With Remote
     [Documentation]    Use the on-screen remote to stop deck.
@@ -60,12 +83,7 @@ Advance Deck With Remote And Screenshot
 Maybe Click An Anchor And Return
     [Documentation]    If a `#` anchor exists, click it, and come back
     [Arguments]    ${host}    ${screenshot}=${EMPTY}
-    IF    ${host.__contains__(".ipynb")}
-        ${anchor_links} =    Get WebElements
-        ...    css:${JLAB CSS ACTIVE CELL}${CSS_DECK_VISIBLE} ${CSS_LAB_NOT_INTERNAL_ANCHOR}
-    ELSE
-        ${anchor_links} =    Get WebElements    css:${JLAB CSS ACTIVE DOC} ${CSS_LAB_NOT_INTERNAL_ANCHOR}
-    END
+    ${anchor_links} =    Find Cross-Document Anchor Links    ${host}
     IF    not ${anchor_links.__len__()}    RETURN
     ${href} =    Get Element Attribute    ${anchor_links[0]}    href
     IF    ${href.__contains__('''${host}''')}    RETURN
@@ -75,10 +93,41 @@ Maybe Click An Anchor And Return
     Click Element    css:${CSS_DECK_DIR_STACK} button
     Sleep    0.1s
 
-Advance Deck With Keyboard
+Find Cross-Document Anchor Links
+    [Documentation]    Find anchor links that don't reference their host.
+    [Arguments]    ${host}
+    ${anchor_links} =    Create List
+    IF    ${host.__contains__(".ipynb")}
+        ${anchor_links} =    Get WebElements
+        ...    css:${JLAB CSS ACTIVE CELL}${CSS_DECK_VISIBLE} ${CSS_LAB_NOT_INTERNAL_ANCHOR}
+    ELSE IF    ${host.__contains__(".md")}
+        ${anchor_links} =    Get WebElements
+        ...    css:${CSS_LAB_MARKDOWN_VIEWER} ${CSS_LAB_NOT_INTERNAL_ANCHOR}:not([href^\='#'])
+    END
+    ${anchor_links} =    Filter Visible Elements    ${anchor_links}
+    RETURN    ${anchor_links}
+
+Filter Visible Elements
+    [Documentation]    Filter a list down to just the visible ones.
+    [Arguments]    ${elements}
+    ${visible} =    Create List
+    Send Error Screenshots To Trash
+    FOR    ${el}    IN    @{elements}
+        TRY
+            Element Should Be Visible    ${el}
+            Append To List    ${visible}    ${el}
+        EXCEPT
+            Log    ${el} is not visible
+        END
+    END
+    RETURN    ${visible}
+    [Teardown]    Resume Screenshots
+
+Advance Notebook Deck With Keyboard
     [Documentation]    Go to the down/forward slide with space, wait a bit, then screenshot.
     [Arguments]    ${screenshot}=${EMPTY}    ${expect}=${EMPTY}    ${backup}=${FALSE}
     ${index} =    Get Active Cell Index
+    Send Error Screenshots To Trash
     IF    ${backup}
         Press Keys    css:body    SHIFT+SPACE
     ELSE
@@ -87,23 +136,23 @@ Advance Deck With Keyboard
     Wait Until Cell Is Not Active    ${index}    1s
     IF    ${expect.__len__()}
         Wait Until Element Contains    css:${JLAB CSS ACTIVE CELL}    ${expect}
-    ELSE
-        Sleep    0.2s
     END
+    Resume Screenshots
     IF    ${screenshot.__len__()}    Capture Page Screenshot    ${screenshot}
+    [Teardown]    Resume Screenshots
 
 Back Up Deck With Keyboard
     [Documentation]    Go to the up/back slide with space, wait a bit, then screenshot.
     [Arguments]    ${screenshot}=${EMPTY}    ${expect}=${EMPTY}
-    Advance Deck With Keyboard    ${screenshot}    ${expect}    backup=${TRUE}
+    Advance Notebook Deck With Keyboard    ${screenshot}    ${expect}    backup=${TRUE}
 
-Really Advance Deck With Keyboard
+Really Advance Notebook Deck With Keyboard
     [Documentation]    REALLY go to the down/forward slide with space, wait a bit, then screenshot.
     [Arguments]    ${screenshot}=${EMPTY}    ${expect}=${EMPTY}    ${backup}=${FALSE}
     Wait Until Keyword Succeeds    5x    0.5s
-    ...    Advance Deck With Keyboard    ${screenshot}    ${expect}    ${backup}
+    ...    Advance Notebook Deck With Keyboard    ${screenshot}    ${expect}    ${backup}
 
 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}
+    Really Advance Notebook Deck With Keyboard    ${screenshot}    ${expect}    backup=${TRUE}
diff --git a/atest/resources/DeckSelectors.resource b/atest/resources/DeckSelectors.resource
index 5d2a5b5..f7e721e 100644
--- a/atest/resources/DeckSelectors.resource
+++ b/atest/resources/DeckSelectors.resource
@@ -6,9 +6,8 @@ Documentation       Selectors defined in this repo... could be loaded from JSON?
 # body
 ${CSS_DECK_PRESENTING}              [data-jp-deck-mode="presenting"]
 
-# cells
-${CSS_DECK_VISIBLE}                 .jp-deck-mod-visible
-${CSS_DECK_ONSCREEN}                .jp-deck-mod-onscreen
+# deck
+${CSS_DECK}                         .jp-Deck
 
 # remote
 ${CSS_DECK_STOP}                    .jp-Deck-Remote .jp-deck-mod-stop
@@ -21,8 +20,11 @@ ${CSS_DECK_DIR_STACK}               .jp-Deck-Remote .jp-Deck-Remote-WidgetStack
 
 @{CSS_DECK_NEXT}                    down    forward
 @{CSS_DECK_PREV}                    up    back
+
 # notebook
 ${CSS_DECK_NOTEBOOK_BUTTON}         .jp-ToolbarButtonComponent\[data-command="deck:toggle"]
+${CSS_DECK_VISIBLE}                 .jp-deck-mod-visible
+${CSS_DECK_ONSCREEN}                .jp-deck-mod-onscreen
 
 # metadata
 ${CSS_DECK_LAYER_SELECT}            \#id-jp-decktools-select-layer
diff --git a/atest/resources/Design.resource b/atest/resources/Design.resource
index 15deabb..83f40ba 100644
--- a/atest/resources/Design.resource
+++ b/atest/resources/Design.resource
@@ -62,8 +62,8 @@ Resize A Part
     IF    not ${screenshot.__len__()}    RETURN
     Capture Page Screenshot    ${screenshot}
 
-Unstyle A Part
-    [Documentation]    Use the reset button to unstyle a part.
+Unfix A Part
+    [Documentation]    Use the reset button to unfix a part.
     [Arguments]    ${index}    ${screenshot}=${EMPTY}
     ${part} =    Set Variable    ${CSS_DECK_LAYOVER_PART}:nth-child(${index})
     ${unstyle} =    Set Variable    ${part} ${CSS_DECK_LAYOVER_UNSTYLE}
diff --git a/atest/resources/Lab.resource b/atest/resources/Lab.resource
index cb2dbdb..efc4568 100644
--- a/atest/resources/Lab.resource
+++ b/atest/resources/Lab.resource
@@ -2,6 +2,7 @@
 Documentation       Keywords for working with decks.
 
 Resource            ./LabSelectors.resource
+Resource            ./Screenshots.resource
 Library             Collections
 Library             JupyterLibrary
 
@@ -26,6 +27,7 @@ Enter Command Mode
 Really Set Cell Type With Keyboard
     [Documentation]    Use the keyboard to change the cell type.
     [Arguments]    ${index}    ${type}    ${limit}=10    ${timeout}=1s
+    Send Error Screenshots To Trash
     ${ok} =    Set Variable    ${FALSE}
     WHILE    not ${ok}    limit=${limit}
         ${ok} =    Run Keyword And Return Status    Set Cell Type With Keyboard    ${index}    ${type}    ${timeout}
@@ -39,6 +41,7 @@ Really Set Cell Type With Keyboard
         END
         Sleep    0.1s
     END
+    [Teardown]    Resume Screenshots
 
 Set Cell Type With Keyboard
     [Documentation]    Use the keyboard to change the cell type.
diff --git a/atest/resources/LabSelectors.resource b/atest/resources/LabSelectors.resource
index f7892a8..9efa1f2 100644
--- a/atest/resources/LabSelectors.resource
+++ b/atest/resources/LabSelectors.resource
@@ -3,6 +3,12 @@ Documentation       Selectors that should maybe go upstream.
 
 
 *** Variables ***
+# lumino
+${CSS_LM_MOD_ACTIVE}                    .lm-mod-active
+${CSS_LM_MENU_ITEM_LABEL}               .lm-Menu-itemLabel
+${CSS_LM_CLOSE_ICON}                    .lm-TabBar-tabCloseIcon
+
+# lab
 ${CSS_LAB_FILES_HOME}                   .jp-BreadCrumbs-home
 ${CSS_LAB_FILES_DIR_ITEM}               .jp-DirListing-item
 ${CSS_LAB_SPINNER}                      .jp-Spinner
@@ -21,7 +27,13 @@ ${CSS_LAB_CELL_META_JSON_CM_HIDDEN}     ${CSS_LAB_MOD_HIDDEN} ${CSS_LAB_CELL_MET
 ${CSS_LAB_ADVANCED_COLLAPSE}            .jp-NotebookTools .jp-Collapse-header
 ${CSS_LAB_INTERNAL_ANCHOR}              .jp-InternalAnchorLink
 ${CSS_LAB_NOT_INTERNAL_ANCHOR}          a[href*\="#"]:not([href^="https"]):not(${CSS_LAB_INTERNAL_ANCHOR})
+${CSS_LAB_TAB_NOT_CURRENT}              .lm-DockPanel .lm-TabBar-tab:not(.jp-mod-current)
 
 # icons
 ${CSS_LAB_ICON_ELLIPSES}                [data-icon="ui-components:ellipses"]
 ${CSS_LAB_ICON_CARET_LEFT}              [data-icon="ui-components:caret-left"]
+
+# markdown
+${CSS_LAB_EDITOR}                       .jp-FileEditor
+${CSS_LAB_MARKDOWN_VIEWER}              .jp-MarkdownViewer
+${CSS_LAB_CMD_MARKDOWN_PREVIEW}         [data-command="fileeditor:markdown-preview"]
diff --git a/atest/resources/Screenshots.resource b/atest/resources/Screenshots.resource
index 42b132c..77fabd2 100644
--- a/atest/resources/Screenshots.resource
+++ b/atest/resources/Screenshots.resource
@@ -1,12 +1,37 @@
 *** Settings ***
 Documentation       Keywords for working with screenshots
 
+Library             OperatingSystem
 Library             JupyterLibrary
 
 
+*** Variables ***
+${SCREENSHOT_TRASH}     ${OUTPUT_DIR}${/}__trash__
+${OLD_SCREENSHOTS}      ${OUTPUT_DIR}${/}screenshots
+
+
 *** Keywords ***
 Set Attempt Screenshot Directory
     [Documentation]    Set a screenshot directory that includes the attempt
     [Arguments]    ${path}
     Set Screenshot Directory
     ...    ${OUTPUT_DIR}${/}screenshots${/}${OS.lower()[:2]}_${ATTEMPT}${/}${path}
+
+Send Error Screenshots To Trash
+    [Documentation]    Throw screenshots in the trash for a while.
+    ${old_screens} =    Set Screenshot Directory    ${SCREENSHOT_TRASH}
+    Set Global Variable    ${OLD_SCREENSHOTS}    ${old_screens}
+
+Resume Screenshots
+    [Documentation]    Restore Screenshots
+    Set Screenshot Directory    ${OLD_SCREENSHOTS}
+
+Empty Screenshot Trash
+    [Documentation]    Clean out trash.
+    Run Keyword And Ignore Error
+    ...    Remove Directory    ${SCREENSHOT_TRASH}    ${TRUE}
+
+Capture Page Screenshot And Tag With Error
+    [Documentation]    Capture a screenshot if not going to the trash
+    ${path} =    Capture Page Screenshot
+    IF    "__trash__" not in "${path}"    Set Tags    screenshot:unexpected
diff --git a/atest/suites/__init__.robot b/atest/suites/__init__.robot
index 566e25a..080d79b 100644
--- a/atest/suites/__init__.robot
+++ b/atest/suites/__init__.robot
@@ -15,8 +15,10 @@ Force Tags          attempt:${attempt}    os:${os}    py:${py}
 Set Up Root Suite
     [Documentation]    Do global suite setup.
     Set Attempt Screenshot Directory    ${EMPTY}
+    Register Keyword To Run On Failure    Capture Page Screenshot And Tag With Error
 
 Tear Down Root Suite
     [Documentation]    Do global suite teardown.
     Close All Browsers
     Terminate All Processes
+    Empty Screenshot Trash
diff --git a/atest/suites/lab/01-examples.robot b/atest/suites/lab/01-examples.robot
index e3e8a0f..05abc48 100644
--- a/atest/suites/lab/01-examples.robot
+++ b/atest/suites/lab/01-examples.robot
@@ -14,13 +14,37 @@ Force Tags          suite:examples
 
 
 *** Test Cases ***
-The Example Can Be Navigated
+The README Markdown Can Be Navigated
     [Documentation]    All slides and fragments are reachable.
-    [Template]    Visit All Example Slides And Fragments
-    ${README_IPYNB}
-    ${README_MD}
-    ${HISTORY_IPYNB}
-    ${LAYERS_IPYNB}
+    [Tags]    activity:markdown
+    Visit All Example Slides And Fragments    ${README_MD}
+    Execute JupyterLab Command    Start Deck
+    Wait Until Element Is Visible    css:${CSS_DECK}
+    ${anchors} =    Get WebElements    css:${CSS_LAB_MARKDOWN_VIEWER} a[href\^="#"]
+    ${anchors} =    Filter Visible Elements    ${anchors}
+    Click Element    ${anchors[0]}
+    Capture Page Screenshot    readme.md-10-post-deck-anchor.png
+    Press Keys    css:body    SHIFT+SPACE
+    Capture Page Screenshot    readme.md-10-post-deck-reverse.png
+    [Teardown]    Reset Example Test
+
+The README Notebook Can Be Navigated
+    [Documentation]    All slides and fragments are reachable.
+    [Tags]    activity:notebook
+    Visit All Example Slides And Fragments    ${README_IPYNB}
+    [Teardown]    Reset Example Test
+
+The History Notebook Can Be Navigated
+    [Documentation]    All slides and fragments are reachable.
+    [Tags]    activity:notebook
+    Visit All Example Slides And Fragments    ${HISTORY_IPYNB}
+    [Teardown]    Reset Example Test
+
+The Layers Notebook Can Be Navigated
+    [Documentation]    All slides and fragments are reachable.
+    [Tags]    activity:notebook    feature:layers
+    Visit All Example Slides And Fragments    ${LAYERS_IPYNB}
+    [Teardown]    Reset Example Test
 
 
 *** Keywords ***
@@ -32,6 +56,8 @@ Visit All Example Slides And Fragments
     Capture Page Screenshot    ${stem}-00-before-deck.png
     IF    ${example.endswith('.ipynb')}
         Really Start Deck With Notebook Toolbar Button
+    ELSE IF    ${example.endswith('.md')}
+        Start Markdown Deck From Editor    ${example}
     ELSE
         Execute JupyterLab Command    Start Deck
     END
@@ -39,7 +65,6 @@ Visit All Example Slides And Fragments
     Visit Slides And Fragments With Remote    ${example}    ${stem}-02-walk
     Stop Deck With Remote
     Capture Page Screenshot    ${stem}-03-after-deck.png
-    [Teardown]    Reset Example Test
 
 Set Up Example Suite
     [Documentation]    Prepare for this suite.
@@ -50,4 +75,5 @@ Reset Example Test
     [Documentation]    Clean up after each test.
     Maybe Open JupyterLab Sidebar    Commands
     Execute JupyterLab Command    Stop Deck
+    Execute JupyterLab Command    Save
     Execute JupyterLab Command    Close All Tabs
diff --git a/atest/suites/lab/02-navigate.robot b/atest/suites/lab/02-navigate.robot
index 827f448..ee60e59 100644
--- a/atest/suites/lab/02-navigate.robot
+++ b/atest/suites/lab/02-navigate.robot
@@ -14,7 +14,7 @@ Suite Setup         Set Up Interactive Suite    navigate
 Suite Teardown      Tear Down Interactive Suite
 Test Teardown       Reset Interactive Test
 
-Force Tags          suite:navigate
+Force Tags          suite:navigate    activity:notebook
 
 
 *** Test Cases ***
@@ -24,8 +24,8 @@ Build and Navigate a Notebook Slide With Keyboard
     Start Basic Notebook Deck
     Really Back Up Deck With Keyboard    s0-03-backup.png    item1234
     Really Back Up Deck With Keyboard    s0-04-backup.png    World
-    Really Advance Deck With Keyboard    s0-05-advance.png    item1234
-    Really Advance Deck With Keyboard    s0-06-advance.png    item4567
+    Really Advance Notebook Deck With Keyboard    s0-05-advance.png    item1234
+    Really Advance Notebook Deck With Keyboard    s0-06-advance.png    item4567
 
 Build and Navigate a Notebook Slide With Anchors
     [Documentation]    Build and navigate a basic slide.
diff --git a/atest/suites/lab/03-layers.robot b/atest/suites/lab/03-layers.robot
index 4485241..4b1f546 100644
--- a/atest/suites/lab/03-layers.robot
+++ b/atest/suites/lab/03-layers.robot
@@ -14,7 +14,7 @@ Suite Setup         Set Up Interactive Suite    layers
 Suite Teardown      Tear Down Interactive Suite
 Test Teardown       Reset Interactive Test
 
-Force Tags          suite:layers
+Force Tags          suite:layers    feature:layers    activity:notebook
 
 
 *** Variables ***
diff --git a/atest/suites/lab/04-design-tools.robot b/atest/suites/lab/04-design-tools.robot
index 10e29db..12225d5 100644
--- a/atest/suites/lab/04-design-tools.robot
+++ b/atest/suites/lab/04-design-tools.robot
@@ -14,12 +14,13 @@ Suite Setup         Set Up Interactive Suite    design
 Suite Teardown      Tear Down Interactive Suite
 Test Teardown       Reset Design Tools Test
 
-Force Tags          suite:design
+Force Tags          suite:design    activity:notebook
 
 
 *** Test Cases ***
 Slide Types
     [Documentation]    Use the slide type tool to work with parts.
+    [Tags]    feature:slidetype
     Set Attempt Screenshot Directory    lab${/}design${/}slide-types
     Start Basic Notebook Deck
     Maybe Open Design Tools
@@ -30,6 +31,7 @@ Slide Types
 
 Layer Scopes
     [Documentation]    Use the layer scope tool to work with parts.
+    [Tags]    feature:layers
     Set Attempt Screenshot Directory    lab${/}design${/}layer-scopes
     Start Basic Notebook Deck
     Make Markdown Cell    - itemA    itemA
@@ -44,6 +46,7 @@ Layer Scopes
 
 Sliders
     [Documentation]    Use the slider tools to work with parts.
+    [Tags]    feature:slidestyle
     Set Attempt Screenshot Directory    lab${/}design${/}sliders
     Start Basic Notebook Deck
     Maybe Open Design Tools
diff --git a/atest/suites/lab/05-slide-layout.robot b/atest/suites/lab/05-slide-layout.robot
index bc05da5..a1e057f 100644
--- a/atest/suites/lab/05-slide-layout.robot
+++ b/atest/suites/lab/05-slide-layout.robot
@@ -20,6 +20,7 @@ Force Tags          suite:design
 *** Test Cases ***
 Slide Layout
     [Documentation]    Use the design tools to work with parts.
+    [Tags]    activity:notebook    feature:layover
     Set Attempt Screenshot Directory    lab${/}layout${/}layover
     Start Basic Notebook Deck
     Maybe Open Slide Layout
@@ -28,6 +29,6 @@ Slide Layout
     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
+    Unfix A Part    1    03-unfixed.png
     Maybe Close Design Tools
     Capture Page Screenshot    04-presenting.png