diff --git a/src/apps/rednatco/idents.ts b/src/apps/rednatco/idents.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e7ae6c06d071a022a2664d02342c88a9ea72ec25
--- /dev/null
+++ b/src/apps/rednatco/idents.ts
@@ -0,0 +1,5 @@
+export type ID ='data'|'structure'|'visual'|'pyramids';
+
+export function ID(id: ID, ref: string) {
+    return `${id}_${ref}`;
+}
diff --git a/src/apps/rednatco/index.html b/src/apps/rednatco/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..777ac354b294e1f203afef1232bd73c8b62437ec
--- /dev/null
+++ b/src/apps/rednatco/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <link rel="stylesheet" type="text/css" href="molstar.css" />
+    </head>
+    <body>
+        <div id="app"></div>
+        <script type="text/javascript" src="./molstar.js"></script>
+    </body>
+</html>
diff --git a/src/apps/rednatco/index.tsx b/src/apps/rednatco/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b9cd142aa682759e7393f5f0697a28add4809b56
--- /dev/null
+++ b/src/apps/rednatco/index.tsx
@@ -0,0 +1,328 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import * as IDs from './idents';
+import { DnatcoConfalPyramids } from '../../extensions/dnatco';
+import { ConfalPyramidsParams } from '../../extensions/dnatco/confal-pyramids/representation';
+import { ConfalPyramidsColorThemeParams } from '../../extensions/dnatco/confal-pyramids/color';
+import { Loci } from '../../mol-model/loci';
+import { Structure } from '../../mol-model/structure';
+import { PluginBehavior, PluginBehaviors } from '../../mol-plugin/behavior';
+import { PluginCommands } from '../../mol-plugin/commands';
+import { PluginContext } from '../../mol-plugin/context';
+import { PluginSpec } from '../../mol-plugin/spec';
+import { LociLabel } from '../../mol-plugin-state/manager/loci-label';
+import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
+import { StateTransforms } from '../../mol-plugin-state/transforms';
+import { RawData } from '../../mol-plugin-state/transforms/data';
+import { createPluginUI } from '../../mol-plugin-ui';
+import { PluginUIContext } from '../../mol-plugin-ui/context';
+import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
+import { Representation } from '../../mol-repr/representation';
+import { StateSelection } from '../../mol-state';
+import { StateTreeSpine } from '../../mol-state/tree/spine';
+import { lociLabel } from '../../mol-theme/label';
+import { Color } from '../../mol-util/color';
+import { arrayMax } from '../../mol-util/array';
+import { Binding } from '../../mol-util/binding';
+import { ButtonsType, ModifiersKeys } from '../../mol-util/input/input-observer';
+import { MarkerAction } from '../../mol-util/marker-action';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ObjectKeys } from '../../mol-util/type-helpers';
+import './index.html';
+
+const Extensions = {
+    'ntc-balls-pyramids-prop': PluginSpec.Behavior(DnatcoConfalPyramids),
+};
+
+const BaseRef = 'rdo';
+const AnimationDurationMsec = 150;
+
+const ReDNATCOLociLabelProvider = PluginBehavior.create({
+    name: 'watlas-loci-label-provider',
+    category: 'interaction',
+    ctor: class implements PluginBehavior<undefined> {
+        private f = {
+            label: (loci: Loci) => {
+                switch (loci.kind) {
+                    case 'structure-loci':
+                    case 'element-loci':
+                        return lociLabel(loci);
+                    default:
+                        return '';
+                }
+            },
+            group: (label: LociLabel) => label.toString().replace(/Model [0-9]+/g, 'Models'),
+            priority: 100
+        };
+        register() { this.ctx.managers.lociLabels.addProvider(this.f); }
+        unregister() { this.ctx.managers.lociLabels.removeProvider(this.f); }
+        constructor(protected ctx: PluginContext) { }
+    },
+    display: { name: 'ReDNATCO labeling' }
+});
+
+const ReDNATCOLociSelectionBindings = {
+    clickFocus: Binding([Binding.Trigger(ButtonsType.Flag.Secondary)], 'Focus camera on selected loci using ${triggers}'),
+    clickToggle: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Set selection to clicked element using ${triggers}.'),
+    clickDeselectAllOnEmpty: Binding([Binding.Trigger(ButtonsType.Flag.Primary)], 'Deselect all when clicking on nothing using ${triggers}.'),
+};
+const ReDNATCOLociSelectionParams = {
+    bindings: PD.Value(ReDNATCOLociSelectionBindings, { isHidden: true }),
+};
+type WatlasLociSelectionProps = PD.Values<typeof ReDNATCOLociSelectionParams>;
+
+const ReDNATCOLociSelectionProvider = PluginBehavior.create({
+    name: 'watlas-loci-selection-provider',
+    category: 'interaction',
+    display: { name: 'Interactive loci selection' },
+    params: () => ReDNATCOLociSelectionParams,
+    ctor: class extends PluginBehavior.Handler<WatlasLociSelectionProps> {
+        private spine: StateTreeSpine.Impl;
+        private lociMarkProvider = (reprLoci: Representation.Loci, action: MarkerAction) => {
+            if (!this.ctx.canvas3d) return;
+            this.ctx.canvas3d.mark({ loci: reprLoci.loci }, action);
+        };
+        private applySelectMark(ref: string, clear?: boolean) {
+            const cell = this.ctx.state.data.cells.get(ref);
+            if (cell && PSO.isRepresentation3D(cell.obj)) {
+                this.spine.current = cell;
+                const so = this.spine.getRootOfType(PSO.Molecule.Structure);
+                if (so) {
+                    if (clear) {
+                        this.lociMarkProvider({ loci: Structure.Loci(so.data) }, MarkerAction.Deselect);
+                    }
+                    const loci = this.ctx.managers.structure.selection.getLoci(so.data);
+                    this.lociMarkProvider({ loci }, MarkerAction.Select);
+                }
+            }
+        }
+        private focusOnLoci(loci: Representation.Loci) {
+            if (!this.ctx.canvas3d)
+                return;
+
+            const sphere = Loci.getBoundingSphere(loci.loci);
+            if (!sphere)
+                return;
+            const snapshot = this.ctx.canvas3d.camera.getSnapshot();
+            snapshot.target = sphere.center;
+
+            PluginCommands.Camera.SetSnapshot(this.ctx, { snapshot, durationMs: AnimationDurationMsec });
+        }
+        register() {
+            const lociIsEmpty = (current: Representation.Loci) => Loci.isEmpty(current.loci);
+            const lociIsNotEmpty = (current: Representation.Loci) => !Loci.isEmpty(current.loci);
+
+            const actions: [keyof typeof ReDNATCOLociSelectionBindings, (current: Representation.Loci) => void, ((current: Representation.Loci) => boolean) | undefined][] = [
+                ['clickFocus', current => this.focusOnLoci(current), lociIsNotEmpty],
+                ['clickDeselectAllOnEmpty', () => this.ctx.managers.interactivity.lociSelects.deselectAll(), lociIsEmpty],
+                ['clickToggle', current => {
+                    if (current.loci.kind === 'element-loci')
+                        this.ctx.managers.interactivity.lociSelects.toggle(current, true);
+                },
+                lociIsNotEmpty],
+            ];
+
+            // sort the action so that the ones with more modifiers trigger sooner.
+            actions.sort((a, b) => {
+                const x = this.params.bindings[a[0]], y = this.params.bindings[b[0]];
+                const k = x.triggers.length === 0 ? 0 : arrayMax(x.triggers.map(t => ModifiersKeys.size(t.modifiers)));
+                const l = y.triggers.length === 0 ? 0 : arrayMax(y.triggers.map(t => ModifiersKeys.size(t.modifiers)));
+                return l - k;
+            });
+
+            this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
+                if (!this.ctx.canvas3d) return;
+
+                // only trigger the 1st action that matches
+                for (const [binding, action, condition] of actions) {
+                    if (Binding.match(this.params.bindings[binding], button, modifiers) && (!condition || condition(current))) {
+                        action(current);
+                        break;
+                    }
+                }
+            });
+
+            this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
+
+            this.subscribeObservable(this.ctx.state.events.object.created, ({ ref }) => this.applySelectMark(ref));
+
+            // re-apply select-mark to all representation of an updated structure
+            this.subscribeObservable(this.ctx.state.events.object.updated, ({ ref, obj, oldObj, oldData, action }) => {
+                const cell = this.ctx.state.data.cells.get(ref);
+                if (cell && PSO.Molecule.Structure.is(cell.obj)) {
+                    const structure: Structure = obj.data;
+                    const oldStructure: Structure | undefined = action === 'recreate' ? oldObj?.data :
+                        action === 'in-place' ? oldData : undefined;
+                    if (oldStructure &&
+                        Structure.areEquivalent(structure, oldStructure) &&
+                        Structure.areHierarchiesEqual(structure, oldStructure)) return;
+
+                    const reprs = this.ctx.state.data.select(StateSelection.Generators.ofType(PSO.Molecule.Structure.Representation3D, ref));
+                    for (const repr of reprs) this.applySelectMark(repr.transform.ref, true);
+                }
+            });
+
+        }
+        unregister() {
+        }
+        constructor(ctx: PluginContext, params: WatlasLociSelectionProps) {
+            super(ctx, params);
+            this.spine = new StateTreeSpine.Impl(ctx.state.data.cells);
+        }
+    },
+});
+
+class ReDNATCOMspViewer {
+    constructor(public plugin: PluginUIContext) {
+    }
+
+    private pyramidsParams(colors: Map<string, Color>, visible: Map<string, boolean>, transparent: boolean) {
+        const typeParams = {} as PD.Values<ConfalPyramidsParams>;
+        for (const k of Reflect.ownKeys(ConfalPyramidsParams) as (keyof ConfalPyramidsParams)[]) {
+            if (ConfalPyramidsParams[k].type === 'boolean')
+                (typeParams[k] as any) = visible.get(k) ?? ConfalPyramidsParams[k]['defaultValue'];
+        }
+
+        const colorParams = {} as Record<string, Color>; // HAKZ until we implement changeable pyramid colors in Molstar !!!
+        for (const k of Reflect.ownKeys(ConfalPyramidsColorThemeParams) as (keyof ConfalPyramidsColorThemeParams)[])
+            colorParams[k] = colors.get(k) ?? ConfalPyramidsColorThemeParams[k]['defaultValue'];
+
+        return {
+            type: { name: 'confal-pyramids', params: { ...typeParams, alpha: transparent ? 0.5 : 1.0 } },
+            colorTheme: { name: 'confal-pyramids', params: colorParams }
+        };
+    }
+
+    static async create(target: HTMLElement) {
+        const defaultSpec = DefaultPluginUISpec();
+        const spec: PluginUISpec = {
+            ...defaultSpec,
+            behaviors: [
+                PluginSpec.Behavior(ReDNATCOLociLabelProvider),
+                PluginSpec.Behavior(PluginBehaviors.Representation.HighlightLoci),
+                PluginSpec.Behavior(ReDNATCOLociSelectionProvider),
+                ...ObjectKeys(Extensions).map(k => Extensions[k]),
+            ],
+            components: {
+                ...defaultSpec.components,
+                controls: {
+                    ...defaultSpec.components?.controls,
+                    top: 'none',
+                    right: 'none',
+                    bottom: 'none',
+                    left: 'none'
+                },
+            },
+            layout: {
+                initial: {
+                    isExpanded: false,
+                    showControls: false,
+                },
+            },
+        };
+
+        const plugin = await createPluginUI(target, spec);
+
+        plugin.managers.interactivity.setProps({ granularity: 'two-residues' });
+        plugin.selectionMode = true;
+
+        return new ReDNATCOMspViewer(plugin);
+    }
+
+    async loadStructure(data: string, type: 'pdb'|'cif') {
+        await this.plugin.state.data.build().toRoot().commit();
+
+        const b = (t => type === 'pdb'
+            ? t.apply(StateTransforms.Model.TrajectoryFromPDB)
+            : t.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
+        )(this.plugin.state.data.build().toRoot().apply(RawData, { data }, { ref: IDs.ID('data', BaseRef) }))
+            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
+            .apply(StateTransforms.Model.StructureFromModel, {}, { ref: IDs.ID('structure', BaseRef) })
+            .apply(
+                StateTransforms.Representation.StructureRepresentation3D,
+                {
+                    type: { name: 'cartoon', params: { sizeFactor: 0.2, sizeAspectRatio: 0.35, aromaticBonds: false } },
+                },
+                { ref: IDs.ID('visual', BaseRef) }
+            )
+            .to(IDs.ID('structure', BaseRef))
+            .apply(
+                StateTransforms.Representation.StructureRepresentation3D,
+                this.pyramidsParams(new Map(), new Map(), false),
+                { ref: IDs.ID('pyramids', BaseRef) }
+            );
+
+        b.commit();
+    }
+}
+
+class ReDNATCOMsp extends React.Component<ReDNATCOMsp.Props> {
+    private viewer: ReDNATCOMspViewer|null = null;
+
+    loadStructure(data: string, type: 'pdb'|'cif') {
+        if (this.viewer)
+            this.viewer.loadStructure(data, type);
+    }
+
+    componentDidMount() {
+        if (!this.viewer) {
+            const elem = document.getElementById(this.props.elemId + '-viewer');
+            ReDNATCOMspViewer.create(elem!).then(viewer => {
+                this.viewer = viewer;
+                ReDNATCOMspApi.bind(this);
+
+                if (this.props.onInited)
+                    this.props.onInited();
+            });
+        }
+    }
+
+    render() {
+        return (
+            <div className='rmsp-app'>
+                <div id={this.props.elemId + '-viewer'}></div>
+                <div>Controls</div>
+            </div>
+        );
+    }
+}
+
+namespace ReDNATCOMsp {
+    export interface Props {
+        elemId: string;
+        onInited?: () => void;
+    }
+
+    export function init(elemId: string, onInited?: () => void) {
+        const elem = document.getElementById(elemId);
+        if (!elem)
+            throw new Error(`Element ${elemId} does not exist`);
+
+        ReactDOM.render(<ReDNATCOMsp elemId={elemId} onInited={onInited} />, elem);
+    }
+}
+
+class _ReDNATCOMspApi {
+    private target: ReDNATCOMsp|null = null;
+
+    private check() {
+        if (!this.target)
+            throw new Error('ReDNATCOMsp object not bound');
+    }
+
+    bind(target: ReDNATCOMsp) {
+        this.target = target;
+    }
+
+    init(elemId: string, onInited?: () => void) {
+        ReDNATCOMsp.init(elemId, onInited);
+    }
+
+    loadStructure(data: string) {
+        this.check();
+        this.target!.loadStructure(data, 'cif');
+    }
+}
+
+export const ReDNATCOMspApi = new _ReDNATCOMspApi();
+
diff --git a/webpack.config.js b/webpack.config.js
index 2610aab44cd972f6cafafd5e27e2d74c1296a2f6..69b56fc54cf396e8ffbbf20cfc3d1f8cb6c62a0c 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -9,8 +9,7 @@ const tests = [
 ];
 
 module.exports = [
-    createApp('viewer', 'molstar'),
-    createApp('docking-viewer', 'molstar'),
+    createApp('rednatco', 'molstar'),
     ...examples.map(createExample),
     ...tests.map(createBrowserTest)
-];
\ No newline at end of file
+];
diff --git a/webpack.config.production.js b/webpack.config.production.js
index c7f1119b03b2dba19a31ceec1aa4d775683f1e0b..2a4e56a42d6ce82b4ad751b841a06d5ae58679b4 100644
--- a/webpack.config.production.js
+++ b/webpack.config.production.js
@@ -3,7 +3,6 @@ const { createApp, createExample } = require('./webpack.config.common.js');
 const examples = ['proteopedia-wrapper', 'basic-wrapper', 'lighting', 'alpha-orbitals'];
 
 module.exports = [
-    createApp('viewer', 'molstar'),
-    createApp('docking-viewer', 'molstar'),
+    createApp('rednatco', 'molstar'),
     ...examples.map(createExample)
-];
\ No newline at end of file
+];