diff --git a/package.json b/package.json
index 8a01d833e17bbe46d36c014ce4eea40a3833c246..9e7a0cfc782521d820e95d341e586af6f9a1a23d 100644
--- a/package.json
+++ b/package.json
@@ -105,10 +105,10 @@
     "redux-throttle": "0.1.1",
     "rimraf": "^2.6.1",
     "scratch-audio": "0.1.0-prerelease.20190114210212",
-    "scratch-blocks": "0.1.0-prerelease.1557852594",
-    "scratch-l10n": "3.3.20190522144128",
+    "scratch-l10n": "3.3.20190612144059",
+    "scratch-blocks": "0.1.0-prerelease.1559136797",
     "scratch-paint": "0.2.0-prerelease.20190524133325",
-    "scratch-render": "0.1.0-prerelease.20190524125950",
+    "scratch-render": "0.1.0-prerelease.20190605151415",
     "scratch-storage": "1.3.2",
     "scratch-svg-renderer": "0.2.0-prerelease.20190523193400",
     "scratch-vm": "0.2.0-prerelease.20190618225856",
diff --git a/src/components/cards/card.css b/src/components/cards/card.css
index a0a8b88d54bae5234a4be3ee797f8f076054f06d..265ca22d1527f3c460e9e7724177eac0c33cf7fc 100644
--- a/src/components/cards/card.css
+++ b/src/components/cards/card.css
@@ -162,6 +162,7 @@
 }
 
 .step-body {
+    width: 100%;
     background: $ui-white;
     display: flex;
     flex-direction: column;
@@ -177,6 +178,8 @@
 
 .step-image {
     max-width: 450px;
+    max-height: 200px;
+    object-fit: contain;
     background: #F9F9F9;
     border: 1px solid #ddd;
     border-radius: 0.5rem;
@@ -275,14 +278,6 @@
     margin-right: 0.5rem;
 }
 
-.video-cover {
-    width: 100%;
-    height: 100%;
-    position: absolute;
-    top: 0;
-    left: 0;
-}
-
 .steps-list {
     display: flex;
     flex-direction: row;
diff --git a/src/components/cards/cards.jsx b/src/components/cards/cards.jsx
index 83e40bf49a173449ed29bf3fffd763829cdecbb9..9bb147f130f4c1f74e2d91bff34bc3a80c949f07 100644
--- a/src/components/cards/cards.jsx
+++ b/src/components/cards/cards.jsx
@@ -85,32 +85,64 @@ const CardHeader = ({onCloseCards, onShrinkExpandCards, onShowAll, totalSteps, s
     </div>
 );
 
-// Video step needs to know if the card is being dragged to cover the video
-// so that the mouseup is not swallowed by the iframe.
-const VideoStep = ({video, dragging}) => (
-    <div className={styles.stepVideo}>
-        {dragging ? (
-            <div className={styles.videoCover} />
-        ) : null}
-        <iframe
-            allowFullScreen
-            allowTransparency="true"
-            frameBorder="0"
-            height="257"
-            scrolling="no"
-            src={`https://fast.wistia.net/embed/iframe/${video}?seo=false&videoFoam=true`}
-            title="📹"
-            width="466"
-        />
-        <script
-            async
-            src="https://fast.wistia.net/assets/external/E-v1.js"
-        />
-    </div>
-);
+class VideoStep extends React.Component {
+
+    componentDidMount () {
+        const script = document.createElement('script');
+        script.src = `https://fast.wistia.com/embed/medias/${this.props.video}.jsonp`;
+        script.async = true;
+        script.setAttribute('id', 'wistia-video-content');
+        document.body.appendChild(script);
+
+        const script2 = document.createElement('script');
+        script2.src = 'https://fast.wistia.com/assets/external/E-v1.js';
+        script2.async = true;
+        script2.setAttribute('id', 'wistia-video-api');
+        document.body.appendChild(script2);
+    }
+
+    // We use the Wistia API here to update or pause the video dynamically:
+    // https://wistia.com/support/developers/player-api
+    componentDidUpdate (prevProps) {
+        // Get a handle on the currently loaded video
+        const video = window.Wistia.api(prevProps.video);
+
+        // Reset the video source if a new video has been chosen from the library
+        if (prevProps.video !== this.props.video) {
+            video.replaceWith(this.props.video);
+        }
+
+        // Pause the video if the modal is being shrunken
+        if (!this.props.expanded) {
+            video.pause();
+        }
+    }
+
+    componentWillUnmount () {
+        const script = document.getElementById('wistia-video-content');
+        script.parentNode.removeChild(script);
+
+        const script2 = document.getElementById('wistia-video-api');
+        script2.parentNode.removeChild(script2);
+    }
+
+    render () {
+        return (
+            <div className={styles.stepVideo}>
+                <div
+                    className={`wistia_embed wistia_async_${this.props.video}`}
+                    id="video-div"
+                    style={{height: `257px`, width: `466px`}}
+                >
+                    &nbsp;
+                </div>
+            </div>
+        );
+    }
+}
 
 VideoStep.propTypes = {
-    dragging: PropTypes.bool.isRequired,
+    expanded: PropTypes.bool.isRequired,
     video: PropTypes.string.isRequired
 };
 
@@ -256,6 +288,7 @@ const Cards = props => {
         onShowAll,
         onNextStep,
         onPrevStep,
+        showVideos,
         step,
         expanded,
         ...posProps
@@ -298,6 +331,7 @@ const Cards = props => {
         >
             <Draggable
                 bounds="parent"
+                cancel="#video-div" // disable dragging on video div
                 position={{x: x, y: y}}
                 onDrag={onDrag}
                 onStart={onStartDrag}
@@ -323,10 +357,18 @@ const Cards = props => {
                                 />
                             ) : (
                                 steps[step].video ? (
-                                    <VideoStep
-                                        dragging={dragging}
-                                        video={translateVideo(steps[step].video, locale)}
-                                    />
+                                    showVideos ? (
+                                        <VideoStep
+                                            dragging={dragging}
+                                            expanded={expanded}
+                                            video={translateVideo(steps[step].video, locale)}
+                                        />
+                                    ) : ( // Else show the deck image and title
+                                        <ImageStep
+                                            image={content[activeDeckId].img}
+                                            title={content[activeDeckId].name}
+                                        />
+                                    )
                                 ) : (
                                     <ImageStep
                                         image={translateImage(steps[step].image, locale)}
@@ -376,9 +418,19 @@ Cards.propTypes = {
     onShowAll: PropTypes.func,
     onShrinkExpandCards: PropTypes.func.isRequired,
     onStartDrag: PropTypes.func,
+    showVideos: PropTypes.bool,
     step: PropTypes.number.isRequired,
     x: PropTypes.number,
     y: PropTypes.number
 };
 
-export default Cards;
+Cards.defaultProps = {
+    showVideos: true
+};
+
+export {
+    Cards as default,
+    // Others exported for testability
+    ImageStep,
+    VideoStep
+};
diff --git a/src/components/gui/gui.jsx b/src/components/gui/gui.jsx
index 2e94229f282e21adc1ecd6e181123cf2b8d3866b..dfcc7d8ddc9109788665cbf76eb776aefd9a9e9c 100644
--- a/src/components/gui/gui.jsx
+++ b/src/components/gui/gui.jsx
@@ -129,11 +129,7 @@ const GUIComponent = props => {
     };
 
     if (isRendererSupported === null) {
-        if (vm.renderer) {
-            isRendererSupported = true;
-        } else {
-            isRendererSupported = Renderer.isSupported();
-        }
+        isRendererSupported = Renderer.isSupported();
     }
 
     return (<MediaQuery minWidth={layout.fullSizeMinWidth}>{isFullSize => {
diff --git a/src/components/library/library.css b/src/components/library/library.css
index bb5d70c67d05c08fa5a7f875360673c9cbd8e29a..51287daa7216ab95a06b13c150606c7702770cb3 100644
--- a/src/components/library/library.css
+++ b/src/components/library/library.css
@@ -12,6 +12,7 @@
     overflow-y: auto;
     height: auto;
     padding: 0.5rem;
+    height: calc(100% - $library-header-height);
 }
 
 .library-scroll-grid.withFilterBar {
@@ -59,3 +60,11 @@
     height: $library-filter-bar-height;
     overflow: hidden;
 }
+
+.spinner-wrapper {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
diff --git a/src/components/library/library.jsx b/src/components/library/library.jsx
index 60f65a06d5760df690a9bbea8788b426e1629a23..9ed7f090791e7bc61b1212c9d4b6599ecf71e976 100644
--- a/src/components/library/library.jsx
+++ b/src/components/library/library.jsx
@@ -9,6 +9,7 @@ import Modal from '../../containers/modal.jsx';
 import Divider from '../divider/divider.jsx';
 import Filter from '../filter/filter.jsx';
 import TagButton from '../../containers/tag-button.jsx';
+import Spinner from '../spinner/spinner.jsx';
 
 import styles from './library.css';
 
@@ -44,9 +45,16 @@ class LibraryComponent extends React.Component {
         this.state = {
             selectedItem: null,
             filterQuery: '',
-            selectedTag: ALL_TAG.tag
+            selectedTag: ALL_TAG.tag,
+            loaded: false
         };
     }
+    componentDidMount () {
+        // Allow the spinner to display before loading the content
+        setTimeout(() => {
+            this.setState({loaded: true});
+        });
+    }
     componentDidUpdate (prevProps, prevState) {
         if (prevState.filterQuery !== this.state.filterQuery ||
             prevState.selectedTag !== this.state.selectedTag) {
@@ -162,7 +170,7 @@ class LibraryComponent extends React.Component {
                     })}
                     ref={this.setFilteredDataRef}
                 >
-                    {this.getFilteredData().map((dataItem, index) => (
+                    {this.state.loaded ? this.getFilteredData().map((dataItem, index) => (
                         <LibraryItem
                             bluetoothRequired={dataItem.bluetoothRequired}
                             collaborator={dataItem.collaborator}
@@ -183,7 +191,14 @@ class LibraryComponent extends React.Component {
                             onMouseLeave={this.handleMouseLeave}
                             onSelect={this.handleSelect}
                         />
-                    ))}
+                    )) : (
+                        <div className={styles.spinnerWrapper}>
+                            <Spinner
+                                large
+                                level="primary"
+                            />
+                        </div>
+                    )}
                 </div>
             </Modal>
         );
diff --git a/src/components/spinner/spinner.css b/src/components/spinner/spinner.css
index 3abc8ffbbea117d723b8abe36a98ac5cdb36aa9b..16d323f5fa5a113da1fff9f484e2cf44453f6c8a 100644
--- a/src/components/spinner/spinner.css
+++ b/src/components/spinner/spinner.css
@@ -39,6 +39,16 @@
     height: .5rem;
 }
 
+.large {
+    width: 2.5rem;
+    height: 2.5rem;
+}
+
+.large::before, .large::after {
+    width: 2.5rem;
+    height: 2.5rem;
+}
+
 @keyframes spin {
   0% {
     transform: rotate(0deg);
@@ -71,3 +81,10 @@
 .spinner.info::after {
     border-top-color: $ui-white;
 }
+
+.spinner.primary {
+    border-color: $motion-transparent;
+}
+.spinner.primary::after {
+    border-top-color: $motion-primary;
+}
diff --git a/src/components/spinner/spinner.jsx b/src/components/spinner/spinner.jsx
index f25e3ebb18771e9c94a1010930f9d2a65337e92e..6fc23ed80a85c286b6c8d7ffb295bc5c1304c9ed 100644
--- a/src/components/spinner/spinner.jsx
+++ b/src/components/spinner/spinner.jsx
@@ -8,7 +8,8 @@ const SpinnerComponent = function (props) {
     const {
         className,
         level,
-        small
+        small,
+        large
     } = props;
     return (
         <div
@@ -16,18 +17,24 @@ const SpinnerComponent = function (props) {
                 className,
                 styles.spinner,
                 styles[level],
-                {[styles.small]: small}
+                {
+                    [styles.small]: small,
+                    [styles.large]: large
+                }
             )}
         />
     );
 };
 SpinnerComponent.propTypes = {
     className: PropTypes.string,
+    large: PropTypes.bool,
     level: PropTypes.string,
     small: PropTypes.bool
 };
 SpinnerComponent.defaultProps = {
     className: '',
-    level: 'info'
+    large: false,
+    level: 'info',
+    small: false
 };
 export default SpinnerComponent;
diff --git a/src/containers/backdrop-library.jsx b/src/containers/backdrop-library.jsx
index 5c2a315dcf9df8fdd70a42c266ffa85c4dded334..9d6b559c56c15ce1843a84964e286a7cecf9f5c8 100644
--- a/src/containers/backdrop-library.jsx
+++ b/src/containers/backdrop-library.jsx
@@ -2,7 +2,6 @@ import bindAll from 'lodash.bindall';
 import PropTypes from 'prop-types';
 import React from 'react';
 import {defineMessages, injectIntl, intlShape} from 'react-intl';
-import {connect} from 'react-redux';
 import VM from 'scratch-vm';
 
 import backdropLibraryContent from '../lib/libraries/backdrops.json';
@@ -33,7 +32,7 @@ class BackdropLibrary extends React.Component {
             bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
             skinId: null
         };
-        this.props.vm.setEditingTarget(this.props.stageID);
+        // Do not switch to stage, just add the backdrop
         this.props.vm.addBackdrop(item.md5, vmBackdrop);
     }
     render () {
@@ -53,17 +52,7 @@ class BackdropLibrary extends React.Component {
 BackdropLibrary.propTypes = {
     intl: intlShape.isRequired,
     onRequestClose: PropTypes.func,
-    stageID: PropTypes.string.isRequired,
     vm: PropTypes.instanceOf(VM).isRequired
 };
 
-const mapStateToProps = state => ({
-    stageID: state.scratchGui.targets.stage.id
-});
-
-const mapDispatchToProps = () => ({});
-
-export default injectIntl(connect(
-    mapStateToProps,
-    mapDispatchToProps
-)(BackdropLibrary));
+export default injectIntl(BackdropLibrary);
diff --git a/src/containers/blocks.jsx b/src/containers/blocks.jsx
index 6ec5771c65e77606da95ce4a7baa06e4d5ca27a0..fb01ce15d5dd321908d8fc38960e7b3ff1d65244 100644
--- a/src/containers/blocks.jsx
+++ b/src/containers/blocks.jsx
@@ -31,10 +31,6 @@ import {
     SOUNDS_TAB_INDEX
 } from '../reducers/editor-tab';
 
-const UNINITIALIZED_TOOLBOX_XML = `<xml style="display: none">
-    <category name="%{BKY_CATEGORY_MOTION}" id="motion" colour="#4C97FF" secondaryColour="#3373CC"></category>
-</xml>`;
-
 const addFunctionListener = (object, property, callback) => {
     const oldFn = object[property];
     object[property] = function () {
@@ -88,19 +84,16 @@ class Blocks extends React.Component {
         };
         this.onTargetsUpdate = debounce(this.onTargetsUpdate, 100);
         this.toolboxUpdateQueue = [];
-        this.initializedWorkspace = false;
     }
     componentDidMount () {
         this.ScratchBlocks.FieldColourSlider.activateEyedropper_ = this.props.onActivateColorPicker;
         this.ScratchBlocks.Procedures.externalProcedureDefCallback = this.props.onActivateCustomProcedures;
         this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
 
-        const toolboxXML = UNINITIALIZED_TOOLBOX_XML;
-
         const workspaceConfig = defaultsDeep({},
             Blocks.defaultOptions,
             this.props.options,
-            {rtl: this.props.isRtl, toolbox: toolboxXML}
+            {rtl: this.props.isRtl, toolbox: this.props.toolboxXML}
         );
         this.workspace = this.ScratchBlocks.inject(this.blocks, workspaceConfig);
 
@@ -122,7 +115,7 @@ class Blocks extends React.Component {
         // Store the xml of the toolbox that is actually rendered.
         // This is used in componentDidUpdate instead of prevProps, because
         // the xml can change while e.g. on the costumes tab.
-        this._renderedToolboxXML = toolboxXML;
+        this._renderedToolboxXML = this.props.toolboxXML;
 
         // we actually never want the workspace to enable "refresh toolbox" - this basically re-renders the
         // entire toolbox every time we reset the workspace.  We call updateToolbox as a part of
@@ -196,19 +189,13 @@ class Blocks extends React.Component {
     componentWillUnmount () {
         this.detachVM();
         this.workspace.dispose();
-        if (this.toolboxUpdateTimeout) this.toolboxUpdateTimeout.cancel();
+        clearTimeout(this.toolboxUpdateTimeout);
     }
     requestToolboxUpdate () {
-        if (this.toolboxUpdateTimeout) this.toolboxUpdateTimeout.cancel();
-        let running = true;
-        this.toolboxUpdateTimeout = Promise.resolve().then(() => {
-            if (running) {
-                this.updateToolbox();
-            }
-        });
-        this.toolboxUpdateTimeout.cancel = () => {
-            running = false;
-        };
+        clearTimeout(this.toolboxUpdateTimeout);
+        this.toolboxUpdateTimeout = setTimeout(() => {
+            this.updateToolbox();
+        }, 0);
     }
     setLocale () {
         this.ScratchBlocks.ScratchMsgs.setLocale(this.props.locale);
@@ -228,15 +215,8 @@ class Blocks extends React.Component {
 
         const categoryId = this.workspace.toolbox_.getSelectedCategoryId();
         const offset = this.workspace.toolbox_.getCategoryScrollOffset();
-
-        let toolboxXML = this.props.toolboxXML;
-        if (!this.initializedWorkspace) {
-            toolboxXML = UNINITIALIZED_TOOLBOX_XML;
-        }
-        if (this._renderedToolboxXML !== toolboxXML) {
-            this.workspace.updateToolbox(toolboxXML);
-            this._renderedToolboxXML = toolboxXML;
-        }
+        this.workspace.updateToolbox(this.props.toolboxXML);
+        this._renderedToolboxXML = this.props.toolboxXML;
 
         // In order to catch any changes that mutate the toolbox during "normal runtime"
         // (variable changes/etc), re-enable toolbox refresh.
@@ -372,7 +352,6 @@ class Blocks extends React.Component {
         // When we change sprites, update the toolbox to have the new sprite's blocks
         const toolboxXML = this.getToolboxXML();
         if (toolboxXML) {
-            this.initializedWorkspace = true;
             this.props.updateToolboxState(toolboxXML);
         }
 
diff --git a/src/containers/cards.jsx b/src/containers/cards.jsx
index 751d523f7153e1711cda00894a12b7dd0092a2ca..9777a5a9509d540b92e2f95e4f51c68889361299 100644
--- a/src/containers/cards.jsx
+++ b/src/containers/cards.jsx
@@ -19,6 +19,7 @@ import {
 
 import CardsComponent from '../components/cards/cards.jsx';
 import {loadImageData} from '../lib/libraries/decks/translate-image.js';
+import {notScratchDesktop} from '../lib/isScratchDesktop';
 
 class Cards extends React.Component {
     componentDidMount () {
@@ -52,7 +53,8 @@ const mapStateToProps = state => ({
     y: state.scratchGui.cards.y,
     isRtl: state.locales.isRtl,
     locale: state.locales.locale,
-    dragging: state.scratchGui.cards.dragging
+    dragging: state.scratchGui.cards.dragging,
+    showVideos: notScratchDesktop()
 });
 
 const mapDispatchToProps = dispatch => ({
diff --git a/src/containers/gui.jsx b/src/containers/gui.jsx
index 2a4ecdea6aa257b2fffd0e3b54de15ddbcfdd487..173f1218a6e3c12bfb3cf3fa812796b5f6ee5d5d 100644
--- a/src/containers/gui.jsx
+++ b/src/containers/gui.jsx
@@ -35,13 +35,10 @@ import storage from '../lib/storage';
 import vmListenerHOC from '../lib/vm-listener-hoc.jsx';
 import vmManagerHOC from '../lib/vm-manager-hoc.jsx';
 import cloudManagerHOC from '../lib/cloud-manager-hoc.jsx';
-import Loader from '../components/loader/loader.jsx';
 
-// import GUIComponent from '../components/gui/gui.jsx';
+import GUIComponent from '../components/gui/gui.jsx';
 import {setIsScratchDesktop} from '../lib/isScratchDesktop.js';
 
-import VideoProvider from '../lib/video/video-provider';
-
 const messages = defineMessages({
     defaultProjectTitle: {
         id: 'gui.gui.defaultProjectTitle',
@@ -55,22 +52,6 @@ class GUI extends React.Component {
         setIsScratchDesktop(this.props.isScratchDesktop);
         this.setReduxTitle(this.props.projectTitle);
         this.props.onStorageInit(storage);
-
-        // Use setTimeout. Do not use requestAnimationFrame or a resolved
-        // Promise. We want this work delayed until after the data request is
-        // made.
-        setTimeout(this.ensureRenderer.bind(this));
-
-        // Once the GUI component has been rendered, always render GUI and do
-        // not revert back to a Loader in this component.
-        //
-        // This makes GUI container not a pure component. We don't want to use
-        // state for this. That would possibly cause a full second render of GUI
-        // after the first one.
-        const {fontsLoaded, fetchingProject, isLoading} = this.props;
-        this.isAfterGUI = this.isAfterGUI || (
-            fontsLoaded && !fetchingProject && !isLoading
-        );
     }
     componentDidUpdate (prevProps) {
         if (this.props.projectId !== prevProps.projectId && this.props.projectId !== null) {
@@ -84,17 +65,6 @@ class GUI extends React.Component {
             // At this time the project view in www doesn't need to know when a project is unloaded
             this.props.onProjectLoaded();
         }
-
-        // Once the GUI component has been rendered, always render GUI and do
-        // not revert back to a Loader in this component.
-        //
-        // This makes GUI container not a pure component. We don't want to use
-        // state for this. That would possibly cause a full second render of GUI
-        // after the first one.
-        const {fontsLoaded, fetchingProject, isLoading} = this.props;
-        this.isAfterGUI = this.isAfterGUI || (
-            fontsLoaded && !fetchingProject && !isLoading
-        );
     }
     setReduxTitle (newTitle) {
         if (newTitle === null || typeof newTitle === 'undefined') {
@@ -105,36 +75,6 @@ class GUI extends React.Component {
             this.props.onUpdateReduxProjectTitle(newTitle);
         }
     }
-    ensureRenderer () {
-        if (this.props.vm.renderer) {
-            return;
-        }
-
-        // Wait to load svg-renderer and render after the data request. This
-        // way the data request is made earlier.
-        const Renderer = require('scratch-render');
-        const {
-            SVGRenderer: V2SVGAdapter,
-            BitmapAdapter: V2BitmapAdapter
-        } = require('scratch-svg-renderer');
-
-        const vm = this.props.vm;
-        this.canvas = document.createElement('canvas');
-        this.renderer = new Renderer(this.canvas);
-        vm.attachRenderer(this.renderer);
-
-        vm.attachV2SVGAdapter(new V2SVGAdapter());
-        vm.attachV2BitmapAdapter(new V2BitmapAdapter());
-
-        // Only attach a video provider once because it is stateful
-        vm.setVideoProvider(new VideoProvider());
-
-        // Calling draw a single time before any project is loaded just
-        // makes the canvas white instead of solid black–needed because it
-        // is not possible to use CSS to style the canvas to have a
-        // different default color
-        vm.renderer.draw();
-    }
     render () {
         if (this.props.isError) {
             throw new Error(
@@ -145,7 +85,6 @@ class GUI extends React.Component {
             assetHost,
             cloudHost,
             error,
-            fontsLoaded,
             isError,
             isScratchDesktop,
             isShowingProject,
@@ -163,16 +102,6 @@ class GUI extends React.Component {
             loadingStateVisible,
             ...componentProps
         } = this.props;
-
-        if (!this.isAfterGUI && (
-            !fontsLoaded || fetchingProject || isLoading
-        )) {
-            // Make sure a renderer exists.
-            if (fontsLoaded && !fetchingProject) this.ensureRenderer();
-            return <Loader />;
-        }
-
-        const GUIComponent = require('../components/gui/gui.jsx').default;
         return (
             <GUIComponent
                 loading={fetchingProject || isLoading || loadingStateVisible}
@@ -190,7 +119,6 @@ GUI.propTypes = {
     cloudHost: PropTypes.string,
     error: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
     fetchingProject: PropTypes.bool,
-    fontsLoaded: PropTypes.bool,
     intl: intlShape,
     isError: PropTypes.bool,
     isLoading: PropTypes.bool,
@@ -229,7 +157,6 @@ const mapStateToProps = state => {
         costumeLibraryVisible: state.scratchGui.modals.costumeLibrary,
         costumesTabVisible: state.scratchGui.editorTab.activeTabIndex === COSTUMES_TAB_INDEX,
         error: state.scratchGui.projectState.error,
-        fontsLoaded: state.scratchGui.fontsLoaded,
         isError: getIsError(loadingState),
         isFullScreen: state.scratchGui.mode.isFullScreen,
         isPlayerOnly: state.scratchGui.mode.isPlayerOnly,
diff --git a/src/containers/stage-selector.jsx b/src/containers/stage-selector.jsx
index 4babbc014233a8bc817e2095a2fb3cd6f9463697..d091c9587438461d3864333c6b074fc5b9eebf4d 100644
--- a/src/containers/stage-selector.jsx
+++ b/src/containers/stage-selector.jsx
@@ -50,7 +50,7 @@ class StageSelector extends React.Component {
             'setFileInput'
         ]);
     }
-    addBackdropFromLibraryItem (item) {
+    addBackdropFromLibraryItem (item, shouldActivateTab = true) {
         const vmBackdrop = {
             name: item.name,
             md5: item.md5,
@@ -59,25 +59,30 @@ class StageSelector extends React.Component {
             bitmapResolution: item.info.length > 2 ? item.info[2] : 1,
             skinId: null
         };
-        this.handleNewBackdrop(vmBackdrop);
+        this.handleNewBackdrop(vmBackdrop, shouldActivateTab);
     }
     handleClick () {
         this.props.onSelect(this.props.id);
     }
-    handleNewBackdrop (backdrops_) {
+    handleNewBackdrop (backdrops_, shouldActivateTab = true) {
         const backdrops = Array.isArray(backdrops_) ? backdrops_ : [backdrops_];
         return Promise.all(backdrops.map(backdrop =>
             this.props.vm.addBackdrop(backdrop.md5, backdrop)
-        )).then(() =>
-            this.props.onActivateTab(COSTUMES_TAB_INDEX)
-        );
+        )).then(() => {
+            if (shouldActivateTab) {
+                return this.props.onActivateTab(COSTUMES_TAB_INDEX);
+            }
+        });
     }
-    handleSurpriseBackdrop () {
+    handleSurpriseBackdrop (e) {
+        e.stopPropagation(); // Prevent click from falling through to selecting stage.
         // @todo should this not add a backdrop you already have?
         const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)];
-        this.addBackdropFromLibraryItem(item);
+        this.addBackdropFromLibraryItem(item, false);
     }
-    handleEmptyBackdrop () {
+    handleEmptyBackdrop (e) {
+        e.stopPropagation(); // Prevent click from falling through to stage selector, select it manually below
+        this.props.vm.setEditingTarget(this.props.id);
         this.handleNewBackdrop(emptyCostume(this.props.intl.formatMessage(sharedMessages.backdrop, {index: 1})));
     }
     handleBackdropUpload (e) {
@@ -85,6 +90,7 @@ class StageSelector extends React.Component {
         this.props.onShowImporting();
         handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => {
             costumeUpload(buffer, fileType, storage, vmCostumes => {
+                this.props.vm.setEditingTarget(this.props.id);
                 vmCostumes.forEach((costume, i) => {
                     costume.name = `${fileName}${i ? i + 1 : ''}`;
                 });
@@ -96,7 +102,8 @@ class StageSelector extends React.Component {
             }, this.props.onCloseImporting);
         }, this.props.onCloseImporting);
     }
-    handleFileUploadClick () {
+    handleFileUploadClick (e) {
+        e.stopPropagation(); // Prevent click from selecting the stage, that is handled manually in backdrop upload
         this.fileInput.click();
     }
     handleMouseEnter () {
diff --git a/src/containers/tips-library.jsx b/src/containers/tips-library.jsx
index db252e707d8c476c82a34a0208d8ee66f088061f..5ed0beb9ba01ce8807bad443d24cf96bc7544755 100644
--- a/src/containers/tips-library.jsx
+++ b/src/containers/tips-library.jsx
@@ -62,10 +62,16 @@ class TipsLibrary extends React.PureComponent {
     }
     render () {
         const decksLibraryThumbnailData = Object.keys(decksLibraryContent)
-            .filter(id =>
+            .filter(id => {
+                if (notScratchDesktop()) return true; // Do not filter anything in online editor
+                const deck = decksLibraryContent[id];
                 // Scratch Desktop doesn't want tutorials with `requiredProjectId`
-                notScratchDesktop() || !decksLibraryContent[id].hasOwnProperty('requiredProjectId')
-            )
+                if (deck.hasOwnProperty('requiredProjectId')) return false;
+                // Scratch Desktop should not load tutorials that are _only_ videos
+                if (deck.steps.filter(s => s.title).length === 0) return false;
+                // Allow any other tutorials
+                return true;
+            })
             .map(id => ({
                 rawURL: decksLibraryContent[id].img,
                 id: id,
diff --git a/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg b/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg
deleted file mode 100755
index cf8e01588d5d0db6eea9711456286ec72759a286..0000000000000000000000000000000000000000
Binary files a/src/lib/default-project/09dc888b0b7df19f70d81588ae73420e.svg and /dev/null differ
diff --git a/src/lib/default-project/3696356a03a8d938318876a593572843.svg b/src/lib/default-project/3696356a03a8d938318876a593572843.svg
deleted file mode 100755
index 657f5b598fca1118f7a8e7373f849910011df88d..0000000000000000000000000000000000000000
Binary files a/src/lib/default-project/3696356a03a8d938318876a593572843.svg and /dev/null differ
diff --git a/src/lib/default-project/b7853f557e4426412e64bb3da6531a99.svg b/src/lib/default-project/b7853f557e4426412e64bb3da6531a99.svg
new file mode 100644
index 0000000000000000000000000000000000000000..a537afb3aa548df6e3565cd16b522c88e92b8d6e
Binary files /dev/null and b/src/lib/default-project/b7853f557e4426412e64bb3da6531a99.svg differ
diff --git a/src/lib/default-project/e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg b/src/lib/default-project/e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d49c68211a4fc5614387c261a8dec0a9ffb6304e
Binary files /dev/null and b/src/lib/default-project/e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg differ
diff --git a/src/lib/default-project/index.js b/src/lib/default-project/index.js
index 92f01c908c1b3dfd05a71636fc71cd0eddfb3181..08dbdb32e60fafb136057e413a40c85b52f6ed1a 100644
--- a/src/lib/default-project/index.js
+++ b/src/lib/default-project/index.js
@@ -4,8 +4,8 @@ import projectData from './project-data';
 import popWav from '!arraybuffer-loader!./83a9787d4cb6f3b7632b4ddfebf74367.wav';
 import meowWav from '!arraybuffer-loader!./83c36d806dc92327b9e7049a565c6bff.wav';
 import backdrop from '!raw-loader!./cd21514d0531fdffb22204e0ec5ed84a.svg';
-import costume1 from '!raw-loader!./09dc888b0b7df19f70d81588ae73420e.svg';
-import costume2 from '!raw-loader!./3696356a03a8d938318876a593572843.svg';
+import costume1 from '!raw-loader!./b7853f557e4426412e64bb3da6531a99.svg';
+import costume2 from '!raw-loader!./e6ddc55a6ddd9cc9d84fe0b4c21e016f.svg';
 /* eslint-enable import/no-unresolved */
 
 const defaultProject = translator => {
@@ -40,12 +40,12 @@ const defaultProject = translator => {
         dataFormat: 'SVG',
         data: encoder.encode(backdrop)
     }, {
-        id: '09dc888b0b7df19f70d81588ae73420e',
+        id: 'b7853f557e4426412e64bb3da6531a99',
         assetType: 'ImageVector',
         dataFormat: 'SVG',
         data: encoder.encode(costume1)
     }, {
-        id: '3696356a03a8d938318876a593572843',
+        id: 'e6ddc55a6ddd9cc9d84fe0b4c21e016f',
         assetType: 'ImageVector',
         dataFormat: 'SVG',
         data: encoder.encode(costume2)
diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js
index ddafe8704947d664768d89e071c17c481999199a..f10f0800a557413590e4b181bc862faf8b57d5ce 100644
--- a/test/helpers/selenium-helper.js
+++ b/test/helpers/selenium-helper.js
@@ -26,8 +26,7 @@ class SeleniumHelper {
             'getSauceDriver',
             'getLogs',
             'loadUri',
-            'rightClickText',
-            'waitUntilGone'
+            'rightClickText'
         ]);
     }
 
@@ -85,7 +84,11 @@ class SeleniumHelper {
     }
 
     findByXpath (xpath, timeoutMessage = `findByXpath timed out for path: ${xpath}`) {
-        return this.driver.wait(until.elementLocated(By.xpath(xpath)), DEFAULT_TIMEOUT_MILLISECONDS, timeoutMessage);
+        return this.driver.wait(until.elementLocated(By.xpath(xpath)), DEFAULT_TIMEOUT_MILLISECONDS, timeoutMessage)
+            .then(el => (
+                this.driver.wait(el.isDisplayed(), DEFAULT_TIMEOUT_MILLISECONDS, `${xpath} is not visible`)
+                    .then(() => el)
+            ));
     }
 
     findByText (text, scope) {
@@ -125,10 +128,6 @@ class SeleniumHelper {
         return this.clickXpath(`//button//*[contains(text(), '${text}')]`);
     }
 
-    waitUntilGone (element, timeoutMessage = 'waitUntilGone timed out') {
-        return this.driver.wait(until.stalenessOf(element), DEFAULT_TIMEOUT_MILLISECONDS, timeoutMessage);
-    }
-
     getLogs (whitelist) {
         if (!whitelist) {
             // Default whitelist
diff --git a/test/integration/backdrops.test.js b/test/integration/backdrops.test.js
index 3d2f64afa29c56a4e9830a9a12827fe15c78653f..95a268b76c3ee4016013c110d012f555be62717f 100644
--- a/test/integration/backdrops.test.js
+++ b/test/integration/backdrops.test.js
@@ -25,7 +25,7 @@ describe('Working with backdrops', () => {
         await driver.quit();
     });
 
-    test('Adding a backdrop from the library', async () => {
+    test('Adding a backdrop from the library should not switch to stage', async () => {
         await loadUri(uri);
 
         // Start on the sounds tab of sprite1 to test switching behavior
@@ -37,12 +37,11 @@ describe('Working with backdrops', () => {
         await el.sendKeys('blue');
         await clickText('Blue Sky'); // Adds the backdrop
 
-        // Make sure the stage is selected and the sound tab remains selected.
-        // This is different from Scratch2 which selected backdrop tab automatically
-        // See issue #3500
-        await clickText('pop', scope.soundsTab);
+        // Make sure the sprite is still selected, and that the tab has not changed
+        await clickText('Meow', scope.soundsTab);
 
         // Make sure the backdrop was actually added by going to the backdrops tab
+        await clickXpath('//span[text()="Stage"]');
         await clickText('Backdrops');
         await clickText('Blue Sky', scope.costumesTab);
 
@@ -50,7 +49,48 @@ describe('Working with backdrops', () => {
         await expect(logs).toEqual([]);
     });
 
-    test('Adding multiple backdrops at the same time', async () => {
+    test('Adding backdrop via paint should switch to stage', async () => {
+        await loadUri(uri);
+
+        const buttonXpath = '//button[@aria-label="Choose a Backdrop"]';
+        const paintXpath = `${buttonXpath}/following-sibling::div//button[@aria-label="Paint"]`;
+
+        const el = await findByXpath(buttonXpath);
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        await clickXpath(paintXpath);
+
+        // Stage should become selected and costume tab activated
+        await findByText('backdrop2', scope.costumesTab);
+
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+
+    test('Adding backdrop via surprise should not switch to stage', async () => {
+        await loadUri(uri);
+
+        // Start on the sounds tab of sprite1 to test switching behavior
+        await clickText('Sounds');
+
+        const buttonXpath = '//button[@aria-label="Choose a Backdrop"]';
+        const surpriseXpath = `${buttonXpath}/following-sibling::div//button[@aria-label="Surprise"]`;
+
+        const el = await findByXpath(buttonXpath);
+        await driver.actions().mouseMove(el)
+            .perform();
+        await driver.sleep(500); // Wait for thermometer menu to come up
+        await clickXpath(surpriseXpath);
+
+        // Make sure the sprite is still selected, and that the tab has not changed
+        await clickText('Meow', scope.soundsTab);
+
+        const logs = await getLogs();
+        await expect(logs).toEqual([]);
+    });
+
+    test('Adding multiple backdrops from file should switch to stage', async () => {
         const files = [
             path.resolve(__dirname, '../fixtures/gh-3582-png.png'),
             path.resolve(__dirname, '../fixtures/100-100.svg')
@@ -67,7 +107,7 @@ describe('Working with backdrops', () => {
         const input = await findByXpath(fileXpath);
         await input.sendKeys(files.join('\n'));
 
-        await clickXpath('//span[text()="Stage"]');
+        // Should have been switched to stage/costume tab already
         await findByText('gh-3582-png', scope.costumesTab);
         await findByText('100-100', scope.costumesTab);
 
diff --git a/test/integration/examples.test.js b/test/integration/examples.test.js
index aa867b0414503bbb5ab1460c24cfb6c8633aea75..3554151db07dc3e2b7b21b4e0825747e845bffff 100644
--- a/test/integration/examples.test.js
+++ b/test/integration/examples.test.js
@@ -4,15 +4,13 @@ import path from 'path';
 import SeleniumHelper from '../helpers/selenium-helper';
 
 const {
-    findByText,
     clickButton,
     clickText,
     clickXpath,
     findByXpath,
     getDriver,
     getLogs,
-    loadUri,
-    waitUntilGone
+    loadUri
 } = new SeleniumHelper();
 
 let driver;
@@ -28,10 +26,9 @@ describe('player example', () => {
         await driver.quit();
     });
 
-    test('Load a project by ID', async () => {
+    test.skip('Player: load a project by ID', async () => {
         const projectId = '96708228';
         await loadUri(`${uri}#${projectId}`);
-        await waitUntilGone(findByText('Loading'));
         await clickXpath('//img[@title="Go"]');
         await new Promise(resolve => setTimeout(resolve, 2000));
         await clickXpath('//img[@title="Stop"]');
@@ -59,7 +56,7 @@ describe('blocks example', () => {
         await driver.quit();
     });
 
-    test('Load a project by ID', async () => {
+    test.skip('Blocks: load a project by ID', async () => {
         const projectId = '96708228';
         await loadUri(`${uri}#${projectId}`);
         await new Promise(resolve => setTimeout(resolve, 2000));
diff --git a/test/integration/menu-bar.test.js b/test/integration/menu-bar.test.js
index 7c653a9079d9f6f4df40c8d511de8df8ebaa3962..3f6457e5208ae171685e62f70327a3f3463a5f4c 100644
--- a/test/integration/menu-bar.test.js
+++ b/test/integration/menu-bar.test.js
@@ -9,8 +9,7 @@ const {
     getDriver,
     loadUri,
     rightClickText,
-    scope,
-    waitUntilGone
+    scope
 } = new SeleniumHelper();
 
 const uri = path.resolve(__dirname, '../../build/index.html');
@@ -78,7 +77,6 @@ describe('Menu bar settings', () => {
         await clickText('File');
         const input = await findByXpath('//input[@accept=".sb,.sb2,.sb3"]');
         await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
-        await waitUntilGone(findByText('Loading'));
         // No replace alert since no changes were made
         await findByText('project1-sprite');
     });
@@ -95,7 +93,6 @@ describe('Menu bar settings', () => {
         await input.sendKeys(path.resolve(__dirname, '../fixtures/project1.sb3'));
         await driver.switchTo().alert()
             .accept();
-        await waitUntilGone(findByText('Loading'));
         await findByText('project1-sprite');
     });
 });
diff --git a/test/integration/project-loading.test.js b/test/integration/project-loading.test.js
index e0c5e5d1f9dc27392780ae5a345fd6b157a9fb14..b11a5bf594d6bd35bf68202bce1c9b3bf0a34cd0 100644
--- a/test/integration/project-loading.test.js
+++ b/test/integration/project-loading.test.js
@@ -9,8 +9,7 @@ const {
     getDriver,
     getLogs,
     loadUri,
-    scope,
-    waitUntilGone
+    scope
 } = new SeleniumHelper();
 
 const uri = path.resolve(__dirname, '../../build/index.html');
@@ -33,13 +32,14 @@ describe('Loading scratch gui', () => {
             await clickText('Oops! Something went wrong.');
         });
 
-        test('Load a project by ID directly through url', async () => {
+        // skipping because it relies on network speed, and tests a method
+        // of loading projects that we are not actively using anymore
+        test.skip('Load a project by ID directly through url', async () => {
             await driver.quit(); // Reset driver to test hitting # url directly
             driver = getDriver();
 
             const projectId = '96708228';
             await loadUri(`${uri}#${projectId}`);
-            await waitUntilGone(findByText('Loading'));
             await clickXpath('//img[@title="Go"]');
             await new Promise(resolve => setTimeout(resolve, 2000));
             await clickXpath('//img[@title="Stop"]');
@@ -47,7 +47,9 @@ describe('Loading scratch gui', () => {
             await expect(logs).toEqual([]);
         });
 
-        test('Load a project by ID (fullscreen)', async () => {
+        // skipping because it relies on network speed, and tests a method
+        // of loading projects that we are not actively using anymore
+        test.skip('Load a project by ID (fullscreen)', async () => {
             await driver.quit(); // Reset driver to test hitting # url directly
             driver = getDriver();
 
@@ -60,7 +62,6 @@ describe('Loading scratch gui', () => {
                 .setSize(1920, 1080);
             const projectId = '96708228';
             await loadUri(`${uri}#${projectId}`);
-            await waitUntilGone(findByText('Loading'));
             await clickXpath('//img[@title="Full Screen Control"]');
             await new Promise(resolve => setTimeout(resolve, 500));
             await clickXpath('//img[@title="Go"]');
@@ -77,7 +78,6 @@ describe('Loading scratch gui', () => {
 
         test('Creating new project resets active tab to Code tab', async () => {
             await loadUri(uri);
-            await new Promise(resolve => setTimeout(resolve, 2000));
             await findByXpath('//*[span[text()="Costumes"]]');
             await clickText('Costumes');
             await clickXpath(
@@ -85,13 +85,12 @@ describe('Loading scratch gui', () => {
                 'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
             );
             await clickXpath('//li[span[text()="New"]]');
-            await findByXpath('//*[div[@class="scratchCategoryMenu"]]');
+            await findByXpath('//div[@class="scratchCategoryMenu"]');
             await clickText('Operators', scope.blocksTab);
         });
 
         test('Not logged in->made no changes to project->create new project should not show alert', async () => {
             await loadUri(uri);
-            await new Promise(resolve => setTimeout(resolve, 2000));
             await clickXpath(
                 '//div[contains(@class, "menu-bar_menu-bar-item") and ' +
                 'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
@@ -103,10 +102,10 @@ describe('Loading scratch gui', () => {
 
         test('Not logged in->made a change to project->create new project should show alert', async () => {
             await loadUri(uri);
-            await new Promise(resolve => setTimeout(resolve, 2000));
             await clickText('Sounds');
             await clickXpath('//button[@aria-label="Choose a Sound"]');
             await clickText('A Bass', scope.modal); // Should close the modal
+            await findByText('1.28'); // length of A Bass sound
             await clickXpath(
                 '//div[contains(@class, "menu-bar_menu-bar-item") and ' +
                 'contains(@class, "menu-bar_hoverable")][span[text()="File"]]'
diff --git a/test/unit/components/cards.test.jsx b/test/unit/components/cards.test.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..617891e020149bb88f4c9446babc17f2118f9caa
--- /dev/null
+++ b/test/unit/components/cards.test.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
+
+// Mock this utility because it uses dynamic imports that do not work with jest
+jest.mock('../../../src/lib/libraries/decks/translate-image.js', () => {});
+
+import Cards, {ImageStep, VideoStep} from '../../../src/components/cards/cards.jsx';
+
+describe('Cards component', () => {
+    const defaultProps = () => ({
+        activeDeckId: 'id1',
+        content: {
+            id1: {
+                name: 'id1 - name',
+                img: 'id1 - img',
+                steps: [{video: 'videoUrl'}]
+            }
+        },
+        dragging: false,
+        expanded: true,
+        isRtl: false,
+        locale: 'en',
+        onActivateDeckFactory: jest.fn(),
+        onCloseCards: jest.fn(),
+        onDrag: jest.fn(),
+        onEndDrag: jest.fn(),
+        onNextStep: jest.fn(),
+        onPrevStep: jest.fn(),
+        onShowAll: jest.fn(),
+        onShrinkExpandCards: jest.fn(),
+        onStartDrag: jest.fn(),
+        showVideos: true,
+        step: 0,
+        x: 0,
+        y: 0
+    });
+
+    test('showVideos=true shows the video step', () => {
+        const component = mountWithIntl(
+            <Cards
+                {...defaultProps()}
+                showVideos
+            />
+        );
+        expect(component.find(ImageStep).exists()).toEqual(false);
+        expect(component.find(VideoStep).exists()).toEqual(true);
+    });
+
+    test('showVideos=false shows the title image/name instead of video step', () => {
+        const component = mountWithIntl(
+            <Cards
+                {...defaultProps()}
+                showVideos={false}
+            />
+        );
+        expect(component.find(VideoStep).exists()).toEqual(false);
+
+        const imageStep = component.find(ImageStep);
+        expect(imageStep.props().image).toEqual('id1 - img');
+        expect(imageStep.props().title).toEqual('id1 - name');
+    });
+});
diff --git a/test/unit/util/default-project.test.js b/test/unit/util/default-project.test.js
new file mode 100644
index 0000000000000000000000000000000000000000..726519c4160211ffdbeff95669f147c76c31a98b
--- /dev/null
+++ b/test/unit/util/default-project.test.js
@@ -0,0 +1,21 @@
+import defaultProjectGenerator from '../../../src/lib/default-project/index.js';
+
+describe('defaultProject', () => {
+    // This test ensures that the assets referenced in the default project JSON
+    // do not get out of sync with the raw assets that are included alongside.
+    // see https://github.com/LLK/scratch-gui/issues/4844
+    test('assets referenced by the project are included', () => {
+        const translatorFn = () => '';
+        const defaultProject = defaultProjectGenerator(translatorFn);
+        const includedAssetIds = defaultProject.map(obj => obj.id);
+        const projectData = JSON.parse(defaultProject[0].data);
+        projectData.targets.forEach(target => {
+            target.costumes.forEach(costume => {
+                expect(includedAssetIds.includes(costume.assetId)).toBe(true);
+            });
+            target.sounds.forEach(sound => {
+                expect(includedAssetIds.includes(sound.assetId)).toBe(true);
+            });
+        });
+    });
+});