diff --git a/Cargo.toml b/Cargo.toml
index 988908b05..c3946b467 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,14 +22,12 @@ rustdoc-args = ["--cfg", "doc_cfg"]
 [features]
 #########  meta / build features  #########
 
-# Include only "required" features.
-#
-#  A shell is required and wgpu is currently the only shell; this shell uses and
-#  requires theme support to function.
+# The minimal feature set needed to build basic applications (with assumptions
+# about target platforms).
 #
 # Note: only some examples build in this configuration; others need view,
 # markdown, resvg. Recommended also: clipboard, yaml (or some config format).
-minimal = ["wgpu", "winit", "wayland", "x11"]
+minimal = ["wgpu", "winit", "wayland"]
 # All recommended features for optimal experience
 default = ["minimal", "view", "image", "resvg", "clipboard", "markdown", "shaping", "spawn"]
 # All standard test target features
@@ -54,7 +52,7 @@ view = ["dep:kas-view"]
 #Enable WGPU backend:
 wgpu = ["dep:kas-wgpu"]
 
-# Enables documentation of APIs for shells and internal usage.
+# Enables documentation of APIs for graphics library and platform backends.
 # This API is not intended for use by end-user applications and
 # thus is omitted from built documentation by default.
 # This flag does not change the API, only built documentation.
diff --git a/ROADMAP.md b/ROADMAP.md
index 6bf429177..b348a2739 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -222,7 +222,7 @@ their input as a message key.
 ### Embedded video
 
 To investigate. Gstreamer integration should be viable when using a (new) OpenGL
-shell.
+backend.
 
 Integrating any video player as a child window should be possible (see Winit's
 `WindowBuilder::with_parent_window`, which is not yet supported everywhere).
diff --git a/crates/kas-core/Cargo.toml b/crates/kas-core/Cargo.toml
index 4597fec8f..62568e09b 100644
--- a/crates/kas-core/Cargo.toml
+++ b/crates/kas-core/Cargo.toml
@@ -25,7 +25,7 @@ stable = ["winit", "x11", "wayland", "markdown", "yaml", "json", "ron", "shaping
 # Use full specialization
 spec = []
 
-# Enables documentation of APIs for shells and internal usage.
+# Enables documentation of APIs for graphics library and platform backends.
 # This API is not intended for use by end-user applications and
 # thus is omitted from built documentation by default.
 # This flag does not change the API, only built documentation.
diff --git a/crates/kas-core/src/shell/shell.rs b/crates/kas-core/src/app/app.rs
similarity index 83%
rename from crates/kas-core/src/shell/shell.rs
rename to crates/kas-core/src/app/app.rs
index d23375d0d..f40b29924 100644
--- a/crates/kas-core/src/shell/shell.rs
+++ b/crates/kas-core/src/app/app.rs
@@ -3,9 +3,9 @@
 // You may obtain a copy of the License in the LICENSE-APACHE file or at:
 //     https://www.apache.org/licenses/LICENSE-2.0
 
-//! [`Shell`] and supporting elements
+//! [`Application`] and supporting elements
 
-use super::{GraphicalShell, Platform, ProxyAction, Result, SharedState};
+use super::{AppGraphicsBuilder, AppState, Platform, ProxyAction, Result};
 use crate::config::Options;
 use crate::draw::{DrawShared, DrawSharedImpl};
 use crate::event;
@@ -16,17 +16,15 @@ use std::cell::RefCell;
 use std::rc::Rc;
 use winit::event_loop::{EventLoop, EventLoopBuilder, EventLoopProxy};
 
-/// The KAS shell
-///
-/// The "shell" is the layer over widgets, windows, events and graphics.
-pub struct Shell<Data: AppData, G: GraphicalShell, T: Theme<G::Shared>> {
+/// Application pre-launch state
+pub struct Application<Data: AppData, G: AppGraphicsBuilder, T: Theme<G::Shared>> {
     el: EventLoop<ProxyAction>,
     windows: Vec<Box<super::Window<Data, G::Surface, T>>>,
-    shared: SharedState<Data, G::Surface, T>,
+    state: AppState<Data, G::Surface, T>,
 }
 
 impl_scope! {
-    pub struct ShellBuilder<G: GraphicalShell, T: Theme<G::Shared>> {
+    pub struct AppBuilder<G: AppGraphicsBuilder, T: Theme<G::Shared>> {
         graphical: G,
         theme: T,
         options: Option<Options>,
@@ -34,11 +32,11 @@ impl_scope! {
     }
 
     impl Self {
-        /// Construct from a graphical shell and a theme
+        /// Construct from a graphics backend and a theme
         #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
         #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
         pub fn new(graphical: G, theme: T) -> Self {
-            ShellBuilder {
+            AppBuilder {
                 graphical,
                 theme,
                 options: None,
@@ -75,7 +73,7 @@ impl_scope! {
         }
 
         /// Build with `data`
-        pub fn build<Data: AppData>(self, data: Data) -> Result<Shell<Data, G, T>> {
+        pub fn build<Data: AppData>(self, data: Data) -> Result<Application<Data, G, T>> {
             let mut theme = self.theme;
 
             let options = self.options.unwrap_or_else(Options::from_env);
@@ -84,7 +82,7 @@ impl_scope! {
             let config = self.config.unwrap_or_else(|| match options.read_config() {
                 Ok(config) => Rc::new(RefCell::new(config)),
                 Err(error) => {
-                    warn_about_error("Shell::new_custom: failed to read config", &error);
+                    warn_about_error("AppBuilder::build: failed to read config", &error);
                     Default::default()
                 }
             });
@@ -95,26 +93,26 @@ impl_scope! {
             draw_shared.set_raster_config(theme.config().raster());
 
             let pw = PlatformWrapper(&el);
-            let shared = SharedState::new(data, pw, draw_shared, theme, options, config)?;
+            let state = AppState::new(data, pw, draw_shared, theme, options, config)?;
 
-            Ok(Shell {
+            Ok(Application {
                 el,
                 windows: vec![],
-                shared,
+                state,
             })
         }
     }
 }
 
-/// Shell associated types
+/// Application associated types
 ///
-/// Note: these could be inherent associated types of [`Shell`] when Rust#8995 is stable.
-pub trait ShellAssoc {
+/// Note: these could be inherent associated types of [`Application`] when Rust#8995 is stable.
+pub trait AppAssoc {
     /// Shared draw state type
     type DrawShared: DrawSharedImpl;
 }
 
-impl<A: AppData, G: GraphicalShell, T> ShellAssoc for Shell<A, G, T>
+impl<A: AppData, G: AppGraphicsBuilder, T> AppAssoc for Application<A, G, T>
 where
     T: Theme<G::Shared> + 'static,
     T::Window: theme::Window,
@@ -122,9 +120,9 @@ where
     type DrawShared = G::Shared;
 }
 
-impl<Data: AppData, G> Shell<Data, G, G::DefaultTheme>
+impl<Data: AppData, G> Application<Data, G, G::DefaultTheme>
 where
-    G: GraphicalShell + Default,
+    G: AppGraphicsBuilder + Default,
 {
     /// Construct a new instance with default options and theme
     ///
@@ -141,24 +139,24 @@ where
 
     /// Construct a builder with the default theme
     #[inline]
-    pub fn with_default_theme() -> ShellBuilder<G, G::DefaultTheme> {
-        ShellBuilder::new(G::default(), G::DefaultTheme::default())
+    pub fn with_default_theme() -> AppBuilder<G, G::DefaultTheme> {
+        AppBuilder::new(G::default(), G::DefaultTheme::default())
     }
 }
 
-impl<G, T> Shell<(), G, T>
+impl<G, T> Application<(), G, T>
 where
-    G: GraphicalShell + Default,
+    G: AppGraphicsBuilder + Default,
     T: Theme<G::Shared>,
 {
     /// Construct a builder with the given `theme`
     #[inline]
-    pub fn with_theme(theme: T) -> ShellBuilder<G, T> {
-        ShellBuilder::new(G::default(), theme)
+    pub fn with_theme(theme: T) -> AppBuilder<G, T> {
+        AppBuilder::new(G::default(), theme)
     }
 }
 
-impl<Data: AppData, G: GraphicalShell, T> Shell<Data, G, T>
+impl<Data: AppData, G: AppGraphicsBuilder, T> Application<Data, G, T>
 where
     T: Theme<G::Shared> + 'static,
     T::Window: theme::Window,
@@ -166,26 +164,26 @@ where
     /// Access shared draw state
     #[inline]
     pub fn draw_shared(&mut self) -> &mut dyn DrawShared {
-        &mut self.shared.shell.draw
+        &mut self.state.shared.draw
     }
 
     /// Access the theme by ref
     #[inline]
     pub fn theme(&self) -> &T {
-        &self.shared.shell.theme
+        &self.state.shared.theme
     }
 
     /// Access the theme by ref mut
     #[inline]
     pub fn theme_mut(&mut self) -> &mut T {
-        &mut self.shared.shell.theme
+        &mut self.state.shared.theme
     }
 
     /// Assume ownership of and display a window
     #[inline]
     pub fn add(&mut self, window: Window<Data>) -> WindowId {
-        let id = self.shared.shell.next_window_id();
-        let win = Box::new(super::Window::new(&self.shared.shell, id, window));
+        let id = self.state.shared.next_window_id();
+        let win = Box::new(super::Window::new(&self.state.shared, id, window));
         self.windows.push(win);
         id
     }
@@ -205,7 +203,7 @@ where
     /// Run the main loop.
     #[inline]
     pub fn run(self) -> Result<()> {
-        let mut el = super::EventLoop::new(self.windows, self.shared);
+        let mut el = super::EventLoop::new(self.windows, self.state);
         self.el.run(move |event, elwt| el.handle(event, elwt))?;
         Ok(())
     }
@@ -303,14 +301,14 @@ impl<'a> PlatformWrapper<'a> {
     }
 }
 
-/// A proxy allowing control of a [`Shell`] from another thread.
+/// A proxy allowing control of an application from another thread.
 ///
-/// Created by [`Shell::create_proxy`].
+/// Created by [`Application::create_proxy`].
 pub struct Proxy(EventLoopProxy<ProxyAction>);
 
 /// Error type returned by [`Proxy`] functions.
 ///
-/// This error occurs only if the [`Shell`] already terminated.
+/// This error occurs only if the application already terminated.
 pub struct ClosedError;
 
 impl Proxy {
diff --git a/crates/kas-core/src/shell/common.rs b/crates/kas-core/src/app/common.rs
similarity index 96%
rename from crates/kas-core/src/shell/common.rs
rename to crates/kas-core/src/app/common.rs
index d23c808bd..d1ae62a77 100644
--- a/crates/kas-core/src/shell/common.rs
+++ b/crates/kas-core/src/app/common.rs
@@ -3,7 +3,7 @@
 // You may obtain a copy of the License in the LICENSE-APACHE file or at:
 //     https://www.apache.org/licenses/LICENSE-2.0
 
-//! Public shell stuff common to all backends
+//! Public items common to all backends
 
 use crate::draw::DrawSharedImpl;
 use crate::draw::{color::Rgba, DrawIface, WindowCommon};
@@ -13,7 +13,7 @@ use raw_window_handle as raw;
 use std::time::Instant;
 use thiserror::Error;
 
-/// Possible failures from constructing a [`Shell`](super::Shell)
+/// Possible failures from constructing an [`Application`](super::Application)
 ///
 /// Some variants are undocumented. Users should not match these variants since
 /// they are not considered part of the public API.
@@ -168,12 +168,12 @@ impl Platform {
     }
 }
 
-/// API for the graphical implementation of a shell
+/// Builder for a graphics backend
 ///
-/// See also [`Shell`](super::Shell).
+/// See also [`Application`](super::Application).
 #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
 #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
-pub trait GraphicalShell {
+pub trait AppGraphicsBuilder {
     /// The default theme
     type DefaultTheme: Default + Theme<Self::Shared>;
 
diff --git a/crates/kas-core/src/shell/event_loop.rs b/crates/kas-core/src/app/event_loop.rs
similarity index 89%
rename from crates/kas-core/src/shell/event_loop.rs
rename to crates/kas-core/src/app/event_loop.rs
index 4657a7a80..a12514010 100644
--- a/crates/kas-core/src/shell/event_loop.rs
+++ b/crates/kas-core/src/app/event_loop.rs
@@ -5,7 +5,7 @@
 
 //! Event loop and handling
 
-use super::{Pending, SharedState};
+use super::{AppState, Pending};
 use super::{ProxyAction, Window, WindowSurface};
 use kas::theme::Theme;
 use kas::{Action, AppData, WindowId};
@@ -27,8 +27,8 @@ where
     popups: HashMap<WindowId, WindowId>,
     /// Translates our WindowId to winit's
     id_map: HashMap<ww::WindowId, WindowId>,
-    /// Shared data passed from Toolkit
-    shared: SharedState<A, S, T>,
+    /// Application state passed from Toolkit
+    state: AppState<A, S, T>,
     /// Timer resumes: (time, window identifier)
     resumes: Vec<(Instant, WindowId)>,
 }
@@ -37,16 +37,13 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Loop<A, S, T>
 where
     T::Window: kas::theme::Window,
 {
-    pub(super) fn new(
-        mut windows: Vec<Box<Window<A, S, T>>>,
-        shared: SharedState<A, S, T>,
-    ) -> Self {
+    pub(super) fn new(mut windows: Vec<Box<Window<A, S, T>>>, state: AppState<A, S, T>) -> Self {
         Loop {
             suspended: true,
             windows: windows.drain(..).map(|w| (w.window_id, w)).collect(),
             popups: Default::default(),
             id_map: Default::default(),
-            shared,
+            state,
             resumes: vec![],
         }
     }
@@ -74,7 +71,7 @@ where
                         log::trace!("Wakeup: timer (window={:?})", item.1);
 
                         let resume = if let Some(w) = self.windows.get_mut(&item.1) {
-                            w.update_timer(&mut self.shared)
+                            w.update_timer(&mut self.state)
                         } else {
                             // presumably, some window with active timers was removed
                             None
@@ -100,7 +97,7 @@ where
 
                 if let Some(id) = self.id_map.get(&window_id) {
                     if let Some(window) = self.windows.get_mut(id) {
-                        if window.handle_event(&mut self.shared, event) {
+                        if window.handle_event(&mut self.state, event) {
                             elwt.set_control_flow(ControlFlow::Poll);
                         }
                     }
@@ -123,7 +120,7 @@ where
                 ProxyAction::Message(msg) => {
                     let mut stack = crate::ErasedStack::new();
                     stack.push_erased(msg.into_erased());
-                    self.shared.handle_messages(&mut stack);
+                    self.state.handle_messages(&mut stack);
                 }
                 ProxyAction::WakeAsync => {
                     // We don't need to do anything: MainEventsCleared will
@@ -141,7 +138,7 @@ where
             Event::Suspended => (),
             Event::Resumed if self.suspended => {
                 for window in self.windows.values_mut() {
-                    match window.resume(&mut self.shared, elwt) {
+                    match window.resume(&mut self.state, elwt) {
                         Ok(winit_id) => {
                             self.id_map.insert(winit_id, window.window_id);
                         }
@@ -169,7 +166,7 @@ where
             }
 
             Event::LoopExiting => {
-                self.shared.on_exit();
+                self.state.on_exit();
             }
 
             Event::MemoryWarning => (), // TODO ?
@@ -177,22 +174,21 @@ where
     }
 
     fn flush_pending(&mut self, elwt: &EventLoopWindowTarget<ProxyAction>) {
-        while let Some(pending) = self.shared.shell.pending.pop_front() {
+        while let Some(pending) = self.state.shared.pending.pop_front() {
             match pending {
                 Pending::AddPopup(parent_id, id, popup) => {
                     log::debug!("Pending: adding overlay");
                     // TODO: support pop-ups as a special window, where available
-                    self.windows.get_mut(&parent_id).unwrap().add_popup(
-                        &mut self.shared,
-                        id,
-                        popup,
-                    );
+                    self.windows
+                        .get_mut(&parent_id)
+                        .unwrap()
+                        .add_popup(&mut self.state, id, popup);
                     self.popups.insert(id, parent_id);
                 }
                 Pending::AddWindow(id, mut window) => {
                     log::debug!("Pending: adding window {}", window.widget.title());
                     if !self.suspended {
-                        match window.resume(&mut self.shared, elwt) {
+                        match window.resume(&mut self.state, elwt) {
                             Ok(winit_id) => {
                                 self.id_map.insert(winit_id, id);
                             }
@@ -209,7 +205,7 @@ where
                         win_id = id;
                     }
                     if let Some(window) = self.windows.get_mut(&win_id) {
-                        window.send_close(&mut self.shared, target);
+                        window.send_close(&mut self.state, target);
                     }
                 }
                 Pending::Action(action) => {
@@ -219,7 +215,7 @@ where
                         elwt.set_control_flow(ControlFlow::Poll);
                     } else {
                         for (_, window) in self.windows.iter_mut() {
-                            window.handle_action(&mut self.shared, action);
+                            window.handle_action(&mut self.state, action);
                         }
                     }
                 }
@@ -229,7 +225,7 @@ where
         let mut close_all = false;
         self.resumes.clear();
         self.windows.retain(|window_id, window| {
-            let (action, resume) = window.flush_pending(&mut self.shared);
+            let (action, resume) = window.flush_pending(&mut self.state);
             if let Some(instant) = resume {
                 self.resumes.push((instant, *window_id));
             }
diff --git a/crates/kas-core/src/shell/mod.rs b/crates/kas-core/src/app/mod.rs
similarity index 93%
rename from crates/kas-core/src/shell/mod.rs
rename to crates/kas-core/src/app/mod.rs
index 42e04325e..cc0151f79 100644
--- a/crates/kas-core/src/shell/mod.rs
+++ b/crates/kas-core/src/app/mod.rs
@@ -3,33 +3,30 @@
 // You may obtain a copy of the License in the LICENSE-APACHE file or at:
 //     https://www.apache.org/licenses/LICENSE-2.0
 
-//! Shell
+//! Application, platforms and backends
 
+#[cfg(winit)] mod app;
 mod common;
 #[cfg(winit)] mod event_loop;
 #[cfg(winit)] mod shared;
-#[cfg(winit)] mod shell;
 #[cfg(winit)] mod window;
 
 #[cfg(winit)] use crate::WindowId;
+#[cfg(winit)] use app::PlatformWrapper;
 #[cfg(winit)] use event_loop::Loop as EventLoop;
-#[cfg(winit)]
-pub(crate) use shared::{SharedState, ShellSharedErased};
-#[cfg(winit)] use shell::PlatformWrapper;
+#[cfg(winit)] pub(crate) use shared::{AppShared, AppState};
 #[cfg(winit)]
 pub(crate) use window::{Window, WindowDataErased};
 
-pub use common::{Error, Platform, Result};
-#[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
-pub use common::{GraphicalShell, WindowSurface};
 #[cfg(winit)]
-pub use shell::{ClosedError, Proxy, Shell, ShellAssoc, ShellBuilder};
+pub use app::{AppAssoc, AppBuilder, Application, ClosedError, Proxy};
+#[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
+pub use common::{AppGraphicsBuilder, WindowSurface};
+pub use common::{Error, Platform, Result};
+
 #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
 pub extern crate raw_window_handle;
 
-// TODO(opt): Clippy is probably right that we shouldn't copy a large value
-// around (also applies when constructing a shell::Window).
-#[allow(clippy::large_enum_variant)]
 #[crate::autoimpl(Debug)]
 #[cfg(winit)]
 enum Pending<A: kas::AppData, S: WindowSurface, T: kas::theme::Theme<S::Shared>> {
diff --git a/crates/kas-core/src/shell/shared.rs b/crates/kas-core/src/app/shared.rs
similarity index 90%
rename from crates/kas-core/src/shell/shared.rs
rename to crates/kas-core/src/app/shared.rs
index 6d462f260..0407da814 100644
--- a/crates/kas-core/src/shell/shared.rs
+++ b/crates/kas-core/src/app/shared.rs
@@ -5,10 +5,9 @@
 
 //! Shared state
 
-use super::{Pending, Platform, WindowSurface};
+use super::{Error, Pending, Platform, WindowSurface};
 use kas::config::Options;
 use kas::draw::DrawShared;
-use kas::shell::Error;
 use kas::theme::{Theme, ThemeControl};
 use kas::util::warn_about_error;
 use kas::{draw, Action, AppData, ErasedStack, WindowId};
@@ -21,8 +20,8 @@ use std::task::Waker;
 
 #[cfg(feature = "clipboard")] use arboard::Clipboard;
 
-/// Shell interface state
-pub(crate) struct ShellShared<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> {
+/// Application state used by [`AppShared`]
+pub(crate) struct AppSharedState<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> {
     pub(super) platform: Platform,
     pub(super) config: Rc<RefCell<kas::event::Config>>,
     #[cfg(feature = "clipboard")]
@@ -34,15 +33,15 @@ pub(crate) struct ShellShared<Data: AppData, S: WindowSurface, T: Theme<S::Share
     window_id: u32,
 }
 
-/// State shared between windows
-pub(crate) struct SharedState<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> {
-    pub(super) shell: ShellShared<Data, S, T>,
+/// Application state shared by all windows
+pub(crate) struct AppState<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> {
+    pub(super) shared: AppSharedState<Data, S, T>,
     pub(super) data: Data,
     /// Estimated scale factor (from last window constructed or available screens)
     options: Options,
 }
 
-impl<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> SharedState<Data, S, T>
+impl<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> AppState<Data, S, T>
 where
     T::Window: kas::theme::Window,
 {
@@ -68,8 +67,8 @@ where
             }
         };
 
-        Ok(SharedState {
-            shell: ShellShared {
+        Ok(AppState {
+            shared: AppSharedState {
                 platform,
                 config,
                 #[cfg(feature = "clipboard")]
@@ -89,14 +88,14 @@ where
     pub(crate) fn handle_messages(&mut self, messages: &mut ErasedStack) {
         if messages.reset_and_has_any() {
             let action = self.data.handle_messages(messages);
-            self.shell.pending.push_back(Pending::Action(action));
+            self.shared.pending.push_back(Pending::Action(action));
         }
     }
 
     pub(crate) fn on_exit(&self) {
         match self
             .options
-            .write_config(&self.shell.config.borrow(), &self.shell.theme)
+            .write_config(&self.shared.config.borrow(), &self.shared.theme)
         {
             Ok(()) => (),
             Err(error) => warn_about_error("Failed to save config", &error),
@@ -104,7 +103,7 @@ where
     }
 }
 
-impl<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> ShellShared<Data, S, T> {
+impl<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> AppSharedState<Data, S, T> {
     /// Return the next window identifier
     ///
     /// TODO(opt): this should recycle used identifiers since Id does not
@@ -116,7 +115,10 @@ impl<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> ShellShared<Data, S,
     }
 }
 
-pub(crate) trait ShellSharedErased {
+/// Application shared-state type-erased interface
+///
+/// A `dyn AppShared` object is used by [crate::event::`EventCx`].
+pub(crate) trait AppShared {
     /// Add a pop-up
     ///
     /// A pop-up may be presented as an overlay layer in the current window or
@@ -138,7 +140,7 @@ pub(crate) trait ShellSharedErased {
     /// event handler, albeit without error handling.
     ///
     /// Safety: this method *should* require generic parameter `Data` (data type
-    /// passed to the `Shell`). Realising this would require adding this type
+    /// passed to the `Application`). Realising this would require adding this type
     /// parameter to `EventCx` and thus to all widgets (not necessarily the
     /// type accepted by the widget as input). As an alternative we require the
     /// caller to type-cast `Window<Data>` to `Window<()>` and pass in
@@ -200,8 +202,8 @@ pub(crate) trait ShellSharedErased {
     fn waker(&self) -> &std::task::Waker;
 }
 
-impl<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> ShellSharedErased
-    for ShellShared<Data, S, T>
+impl<Data: AppData, S: WindowSurface, T: Theme<S::Shared>> AppShared
+    for AppSharedState<Data, S, T>
 {
     fn add_popup(&mut self, parent_id: WindowId, popup: kas::PopupDescriptor) -> WindowId {
         let id = self.next_window_id();
diff --git a/crates/kas-core/src/shell/window.rs b/crates/kas-core/src/app/window.rs
similarity index 85%
rename from crates/kas-core/src/shell/window.rs
rename to crates/kas-core/src/app/window.rs
index 0560af74c..7fd82a868 100644
--- a/crates/kas-core/src/shell/window.rs
+++ b/crates/kas-core/src/app/window.rs
@@ -6,7 +6,7 @@
 //! Window types
 
 use super::common::WindowSurface;
-use super::shared::{SharedState, ShellShared};
+use super::shared::{AppSharedState, AppState};
 use super::ProxyAction;
 use kas::cast::{Cast, Conv};
 use kas::draw::{color::Rgba, AnimationState, DrawSharedImpl};
@@ -54,7 +54,7 @@ pub struct Window<A: AppData, S: WindowSurface, T: Theme<S::Shared>> {
 impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
     /// Construct window state (widget)
     pub(super) fn new(
-        shared: &ShellShared<A, S, T>,
+        shared: &AppSharedState<A, S, T>,
         window_id: WindowId,
         widget: kas::Window<A>,
     ) -> Self {
@@ -71,30 +71,30 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
     /// Open (resume) a window
     pub(super) fn resume(
         &mut self,
-        shared: &mut SharedState<A, S, T>,
+        state: &mut AppState<A, S, T>,
         elwt: &EventLoopWindowTarget<ProxyAction>,
     ) -> super::Result<winit::window::WindowId> {
         let time = Instant::now();
 
         // We cannot reliably determine the scale factor before window creation.
         // A factor of 1.0 lets us estimate the size requirements (logical).
-        let mut theme_window = shared.shell.theme.new_window(1.0);
+        let mut theme_window = state.shared.theme.new_window(1.0);
         let dpem = theme_window.size().dpem();
         self.ev_state.update_config(1.0, dpem);
         self.ev_state.full_configure(
             theme_window.size(),
             self.window_id,
             &mut self.widget,
-            &shared.data,
+            &state.data,
         );
 
-        let node = self.widget.as_node(&shared.data);
+        let node = self.widget.as_node(&state.data);
         let sizer = SizeCx::new(theme_window.size());
         let mut solve_cache = SolveCache::find_constraints(node, sizer);
 
         // Opening a zero-size window causes a crash, so force at least 1x1:
         let min_size = Size(1, 1);
-        let max_size = Size::splat(shared.shell.draw.draw.max_texture_dimension_2d().cast());
+        let max_size = Size::splat(state.shared.draw.draw.max_texture_dimension_2d().cast());
 
         let ideal = solve_cache
             .ideal(true)
@@ -123,10 +123,10 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
         let apply_size;
         if scale_factor != 1.0 {
             let sf32 = scale_factor as f32;
-            shared.shell.theme.update_window(&mut theme_window, sf32);
+            state.shared.theme.update_window(&mut theme_window, sf32);
             let dpem = theme_window.size().dpem();
             self.ev_state.update_config(sf32, dpem);
-            let node = self.widget.as_node(&shared.data);
+            let node = self.widget.as_node(&state.data);
             let sizer = SizeCx::new(theme_window.size());
             solve_cache = SolveCache::find_constraints(node, sizer);
 
@@ -167,9 +167,9 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
             _ => None,
         };
 
-        let mut surface = S::new(&mut shared.shell.draw.draw, &window)?;
+        let mut surface = S::new(&mut state.shared.draw.draw, &window)?;
         if apply_size {
-            surface.do_resize(&mut shared.shell.draw.draw, size);
+            surface.do_resize(&mut state.shared.draw.draw, size);
         }
 
         let winit_id = window.id();
@@ -189,7 +189,7 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
         });
 
         if apply_size {
-            self.apply_size(shared, true);
+            self.apply_size(state, true);
         }
 
         log::trace!(target: "kas_perf::wgpu::window", "resume: {}µs", time.elapsed().as_micros());
@@ -207,7 +207,7 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
     /// Returns `true` to force polling temporarily.
     pub(super) fn handle_event(
         &mut self,
-        shared: &mut SharedState<A, S, T>,
+        state: &mut AppState<A, S, T>,
         event: WindowEvent,
     ) -> bool {
         let Some(ref mut window) = self.window else {
@@ -218,17 +218,17 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
             WindowEvent::Resized(size) => {
                 if window
                     .surface
-                    .do_resize(&mut shared.shell.draw.draw, size.cast())
+                    .do_resize(&mut state.shared.draw.draw, size.cast())
                 {
-                    self.apply_size(shared, false);
+                    self.apply_size(state, false);
                 }
                 false
             }
             WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
                 // Note: API allows us to set new window size here.
                 let scale_factor = scale_factor as f32;
-                shared
-                    .shell
+                state
+                    .shared
                     .theme
                     .update_window(&mut window.theme_window, scale_factor);
                 let dpem = window.theme_window.size().dpem();
@@ -239,18 +239,18 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
                 window.solve_cache.invalidate_rule_cache();
                 false
             }
-            WindowEvent::RedrawRequested => self.do_draw(shared).is_err(),
+            WindowEvent::RedrawRequested => self.do_draw(state).is_err(),
             event => {
                 let mut messages = ErasedStack::new();
                 self.ev_state
-                    .with(&mut shared.shell, window, &mut messages, |cx| {
-                        cx.handle_winit(&mut self.widget, &shared.data, event);
+                    .with(&mut state.shared, window, &mut messages, |cx| {
+                        cx.handle_winit(&mut self.widget, &state.data, event);
                     });
-                shared.handle_messages(&mut messages);
+                state.handle_messages(&mut messages);
 
                 if self.ev_state.action.contains(Action::RECONFIGURE) {
                     // Reconfigure must happen before further event handling
-                    self.reconfigure(shared);
+                    self.reconfigure(state);
                     self.ev_state.action.remove(Action::RECONFIGURE);
                 }
                 false
@@ -261,25 +261,25 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
     /// Handle all pending items before event loop sleeps
     pub(super) fn flush_pending(
         &mut self,
-        shared: &mut SharedState<A, S, T>,
+        state: &mut AppState<A, S, T>,
     ) -> (Action, Option<Instant>) {
         let Some(ref window) = self.window else {
             return (Action::empty(), None);
         };
         let mut messages = ErasedStack::new();
         let action = self.ev_state.flush_pending(
-            &mut shared.shell,
+            &mut state.shared,
             window,
             &mut messages,
             &mut self.widget,
-            &shared.data,
+            &state.data,
         );
-        shared.handle_messages(&mut messages);
+        state.handle_messages(&mut messages);
 
         if action.contains(Action::CLOSE | Action::EXIT) {
             return (action, None);
         }
-        self.handle_action(shared, action);
+        self.handle_action(state, action);
 
         let mut resume = self.ev_state.next_resume();
 
@@ -297,7 +297,7 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
     }
 
     /// Handle an action (excludes handling of CLOSE and EXIT)
-    pub(super) fn handle_action(&mut self, shared: &SharedState<A, S, T>, mut action: Action) {
+    pub(super) fn handle_action(&mut self, state: &AppState<A, S, T>, mut action: Action) {
         if action.contains(Action::EVENT_CONFIG) {
             if let Some(ref mut window) = self.window {
                 let scale_factor = window.scale_factor() as f32;
@@ -307,15 +307,15 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
             }
         }
         if action.contains(Action::RECONFIGURE) {
-            self.reconfigure(shared);
+            self.reconfigure(state);
         } else if action.contains(Action::UPDATE) {
-            self.update(shared);
+            self.update(state);
         }
         if action.contains(Action::THEME_UPDATE) {
             if let Some(ref mut window) = self.window {
                 let scale_factor = window.scale_factor() as f32;
-                shared
-                    .shell
+                state
+                    .shared
                     .theme
                     .update_window(&mut window.theme_window, scale_factor);
             }
@@ -324,9 +324,9 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
             if let Some(ref mut window) = self.window {
                 window.solve_cache.invalidate_rule_cache();
             }
-            self.apply_size(shared, false);
+            self.apply_size(state, false);
         } else if !(action & (Action::SET_RECT | Action::SCROLLED)).is_empty() {
-            self.apply_size(shared, false);
+            self.apply_size(state, false);
         }
         debug_assert!(!action.contains(Action::REGION_MOVED));
         if !action.is_empty() {
@@ -336,23 +336,23 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
         }
     }
 
-    pub(super) fn update_timer(&mut self, shared: &mut SharedState<A, S, T>) -> Option<Instant> {
+    pub(super) fn update_timer(&mut self, state: &mut AppState<A, S, T>) -> Option<Instant> {
         let Some(ref window) = self.window else {
             return None;
         };
-        let widget = self.widget.as_node(&shared.data);
+        let widget = self.widget.as_node(&state.data);
         let mut messages = ErasedStack::new();
         self.ev_state
-            .with(&mut shared.shell, window, &mut messages, |cx| {
+            .with(&mut state.shared, window, &mut messages, |cx| {
                 cx.update_timer(widget)
             });
-        shared.handle_messages(&mut messages);
+        state.handle_messages(&mut messages);
         self.next_resume()
     }
 
     pub(super) fn add_popup(
         &mut self,
-        shared: &mut SharedState<A, S, T>,
+        state: &mut AppState<A, S, T>,
         id: WindowId,
         popup: kas::PopupDescriptor,
     ) {
@@ -361,34 +361,34 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
         };
         let mut messages = ErasedStack::new();
         self.ev_state
-            .with(&mut shared.shell, window, &mut messages, |cx| {
-                self.widget.add_popup(cx, &shared.data, id, popup)
+            .with(&mut state.shared, window, &mut messages, |cx| {
+                self.widget.add_popup(cx, &state.data, id, popup)
             });
-        shared.handle_messages(&mut messages);
+        state.handle_messages(&mut messages);
     }
 
     pub(super) fn send_action(&mut self, action: Action) {
         self.ev_state.action(Id::ROOT, action);
     }
 
-    pub(super) fn send_close(&mut self, shared: &mut SharedState<A, S, T>, id: WindowId) {
+    pub(super) fn send_close(&mut self, state: &mut AppState<A, S, T>, id: WindowId) {
         if id == self.window_id {
             self.ev_state.action(Id::ROOT, Action::CLOSE);
         } else if let Some(window) = self.window.as_ref() {
             let widget = &mut self.widget;
             let mut messages = ErasedStack::new();
             self.ev_state
-                .with(&mut shared.shell, window, &mut messages, |cx| {
+                .with(&mut state.shared, window, &mut messages, |cx| {
                     widget.remove_popup(cx, id)
                 });
-            shared.handle_messages(&mut messages);
+            state.handle_messages(&mut messages);
         }
     }
 }
 
 // Internal functions
 impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
-    fn reconfigure(&mut self, shared: &SharedState<A, S, T>) {
+    fn reconfigure(&mut self, state: &AppState<A, S, T>) {
         let time = Instant::now();
         let Some(ref mut window) = self.window else {
             return;
@@ -398,13 +398,13 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
             window.theme_window.size(),
             self.window_id,
             &mut self.widget,
-            &shared.data,
+            &state.data,
         );
 
         log::trace!(target: "kas_perf::wgpu::window", "reconfigure: {}µs", time.elapsed().as_micros());
     }
 
-    fn update(&mut self, shared: &SharedState<A, S, T>) {
+    fn update(&mut self, state: &AppState<A, S, T>) {
         let time = Instant::now();
         let Some(ref mut window) = self.window else {
             return;
@@ -412,12 +412,12 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
 
         let size = window.theme_window.size();
         let mut cx = ConfigCx::new(&size, &mut self.ev_state);
-        cx.update(self.widget.as_node(&shared.data));
+        cx.update(self.widget.as_node(&state.data));
 
         log::trace!(target: "kas_perf::wgpu::window", "update: {}µs", time.elapsed().as_micros());
     }
 
-    fn apply_size(&mut self, shared: &SharedState<A, S, T>, first: bool) {
+    fn apply_size(&mut self, state: &AppState<A, S, T>, first: bool) {
         let time = Instant::now();
         let Some(ref mut window) = self.window else {
             return;
@@ -427,11 +427,11 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
 
         let solve_cache = &mut window.solve_cache;
         let mut cx = ConfigCx::new(window.theme_window.size(), &mut self.ev_state);
-        solve_cache.apply_rect(self.widget.as_node(&shared.data), &mut cx, rect, true);
+        solve_cache.apply_rect(self.widget.as_node(&state.data), &mut cx, rect, true);
         if first {
             solve_cache.print_widget_heirarchy(self.widget.as_layout());
         }
-        self.widget.resize_popups(&mut cx, &shared.data);
+        self.widget.resize_popups(&mut cx, &state.data);
 
         // Size restrictions may have changed due to content or size (line wrapping)
         let (restrict_min, restrict_max) = self.widget.restrictions();
@@ -456,7 +456,7 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
     ///
     /// Returns an error when drawing is aborted and further event handling may
     /// be needed before a redraw.
-    pub(super) fn do_draw(&mut self, shared: &mut SharedState<A, S, T>) -> Result<(), ()> {
+    pub(super) fn do_draw(&mut self, state: &mut AppState<A, S, T>) -> Result<(), ()> {
         let start = Instant::now();
         let Some(ref mut window) = self.window else {
             return Ok(());
@@ -465,15 +465,15 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
         window.next_avail_frame_time = start + self.ev_state.config().frame_dur();
 
         {
-            let draw = window.surface.draw_iface(&mut shared.shell.draw);
+            let draw = window.surface.draw_iface(&mut state.shared.draw);
 
             let mut draw =
-                shared
-                    .shell
+                state
+                    .shared
                     .theme
                     .draw(draw, &mut self.ev_state, &mut window.theme_window);
             let draw_cx = DrawCx::new(&mut draw, self.widget.id());
-            self.widget.draw(&shared.data, draw_cx);
+            self.widget.draw(&state.data, draw_cx);
         }
         let time2 = Instant::now();
 
@@ -492,11 +492,11 @@ impl<A: AppData, S: WindowSurface, T: Theme<S::Shared>> Window<A, S, T> {
         let clear_color = if self.widget.transparent() {
             Rgba::TRANSPARENT
         } else {
-            shared.shell.theme.clear_color()
+            state.shared.theme.clear_color()
         };
         let time3 = window
             .surface
-            .present(&mut shared.shell.draw.draw, clear_color);
+            .present(&mut state.shared.draw.draw, clear_color);
 
         let text_dur_micros = take(&mut window.surface.common_mut().dur_text);
         let end = Instant::now();
diff --git a/crates/kas-core/src/config.rs b/crates/kas-core/src/config.rs
index c53ca8af2..4da8e7956 100644
--- a/crates/kas-core/src/config.rs
+++ b/crates/kas-core/src/config.rs
@@ -217,7 +217,7 @@ impl Format {
     }
 }
 
-/// Shell options
+/// Application configuration options
 #[derive(Clone, Debug, PartialEq, Eq, Hash)]
 pub struct Options {
     /// Config file path. Default: empty. See `KAS_CONFIG` doc.
diff --git a/crates/kas-core/src/draw/draw.rs b/crates/kas-core/src/draw/draw.rs
index 2fcf30b55..ce8e98c5e 100644
--- a/crates/kas-core/src/draw/draw.rs
+++ b/crates/kas-core/src/draw/draw.rs
@@ -40,8 +40,8 @@ use std::time::Instant;
 /// }
 /// ```
 ///
-/// Note that this object is little more than a mutable reference to the shell's
-/// per-window draw state. As such, it is normal to pass *a new copy* created
+/// Note that this object is little more than a mutable reference to application
+/// shared draw state. As such, it is normal to pass *a new copy* created
 /// via [`DrawIface::re`] as a method argument. (Note that Rust automatically
 /// "reborrows" reference types passed as method arguments, but cannot do so
 /// automatically for structs containing references.)
@@ -60,7 +60,7 @@ pub struct DrawIface<'a, DS: DrawSharedImpl> {
 impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> {
     /// Construct a new instance
     ///
-    /// For usage by the (graphical) shell.
+    /// For usage by graphics backends.
     #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
     #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
     pub fn new(draw: &'a mut DS::Draw, shared: &'a mut SharedState<DS>) -> Self {
@@ -76,8 +76,8 @@ impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> {
     /// Note: Rust does not (yet) support trait-object-downcast: it not possible
     /// to cast from `&mut dyn Draw` to (for example) `&mut dyn DrawRounded`.
     /// Instead, the target type must be the implementing object, which is
-    /// provided by the shell (e.g. `kas_wgpu`). See documentation on this type
-    /// for an example, or see examine
+    /// provided by the graphics backend (e.g. `kas_wgpu`).
+    /// See documentation on this type for an example, or examine
     /// [`clock.rs`](https://github.com/kas-gui/kas/blob/master/examples/clock.rs).
     pub fn downcast_from(obj: &'a mut dyn Draw) -> Option<Self> {
         let pass = obj.get_pass();
@@ -130,7 +130,7 @@ impl<'a, DS: DrawSharedImpl> DrawIface<'a, DS> {
 /// and [`Self::new_dyn_pass`].
 ///
 /// Additional draw routines are available through extension traits, depending
-/// on the shell. Since Rust does not (yet) support trait-object-downcast,
+/// on the graphics backend. Since Rust does not (yet) support trait-object-downcast,
 /// accessing these requires reconstruction of the implementing type via
 /// [`DrawIface::downcast_from`].
 pub trait Draw {
@@ -305,7 +305,7 @@ impl<'a, DS: DrawSharedImpl> Draw for DrawIface<'a, DS> {
 /// Base abstraction over drawing
 ///
 /// This trait covers only the bare minimum of functionality which *must* be
-/// provided by the shell; extension traits such as [`DrawRoundedImpl`]
+/// provided by the graphics backend; extension traits such as [`DrawRoundedImpl`]
 /// optionally provide more functionality.
 ///
 /// Coordinates for many primitives are specified using floating-point types
diff --git a/crates/kas-core/src/draw/draw_shared.rs b/crates/kas-core/src/draw/draw_shared.rs
index 3d9fe20bd..ec8af102c 100644
--- a/crates/kas-core/src/draw/draw_shared.rs
+++ b/crates/kas-core/src/draw/draw_shared.rs
@@ -65,7 +65,7 @@ pub struct AllocError;
 /// Shared draw state
 ///
 /// A single [`SharedState`] instance is shared by all windows and draw contexts.
-/// This struct is built over a [`DrawSharedImpl`] object provided by the shell,
+/// This struct is built over a [`DrawSharedImpl`] object provided by the graphics backend,
 /// which may be accessed directly for a lower-level API (though most methods
 /// are available through [`SharedState`] directly).
 ///
@@ -73,14 +73,14 @@ pub struct AllocError;
 /// allow usage where the `DS` type parameter is unknown. Some functionality is
 /// also implemented directly to avoid the need for downcasting.
 pub struct SharedState<DS: DrawSharedImpl> {
-    /// The shell's [`DrawSharedImpl`] object
+    /// The graphics backend's [`DrawSharedImpl`] object
     pub draw: DS,
 }
 
 #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
 #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
 impl<DS: DrawSharedImpl> SharedState<DS> {
-    /// Construct (this is only called by the shell)
+    /// Construct (this is only called by the graphics backend)
     pub fn new(draw: DS) -> Self {
         SharedState { draw }
     }
diff --git a/crates/kas-core/src/draw/mod.rs b/crates/kas-core/src/draw/mod.rs
index 18824c3b6..04419702e 100644
--- a/crates/kas-core/src/draw/mod.rs
+++ b/crates/kas-core/src/draw/mod.rs
@@ -11,7 +11,7 @@
 //!     [`DrawCx`]. This is the primary drawing interface for widgets.
 //! -   Basic drawing components (shapes) are available through [`DrawIface`]
 //!     in this module. This can be accessed via [`DrawCx::draw_device`].
-//! -   The shell may support custom graphics pipelines, for example
+//! -   The graphics backend may support custom pipelines, for example
 //!     [`kas-wgpu::draw::CustomPipe`](https://docs.rs/kas-wgpu/*/kas_wgpu/draw/trait.CustomPipe.html)
 //!     (used by the [Mandlebrot example](https://github.com/kas-gui/kas/tree/master/examples/mandlebrot)).
 //!
@@ -25,9 +25,10 @@
 //! [`DrawIface::new_pass`]). Draw passes are executed sequentially in the order
 //! defined.
 //!
-//! Within each pass, draw operations may be batched by the shell, thus draw
-//! operations may not happen in the order queued. In general, it may be
-//! expected that batches are executed in the following order:
+//! Within each pass, draw operations may be batched, thus draw operations may
+//! not happen in the order queued. Exact behaviour is defined by the graphics
+//! backend. In general, it may be expected that batches are executed in the
+//! following order:
 //!
 //! 1.  Square-edged primitives (e.g. [`Draw::rect`])
 //! 2.  Images
diff --git a/crates/kas-core/src/erased.rs b/crates/kas-core/src/erased.rs
index 6eb6e5657..7dee365ad 100644
--- a/crates/kas-core/src/erased.rs
+++ b/crates/kas-core/src/erased.rs
@@ -178,10 +178,9 @@ impl Drop for ErasedStack {
 
 /// Application state
 ///
-/// Kas allows state to be stored in `Adapt` and user-defined widgets within
-/// windows, but sometimes you want top-level application state too (especially
-/// for data shared between windows). Such state implements this trait and is
-/// passed to the shell/runner's constructor.
+/// Kas allows application state to be stored both in the  widget tree (in
+/// `Adapt` nodes and user-defined widgets) and by the application root (shared
+/// across windows). This trait must be implemented by the latter.
 ///
 /// When no top-level data is required, use `()` which implements this trait.
 pub trait AppData: 'static {
diff --git a/crates/kas-core/src/event/cx/cx_pub.rs b/crates/kas-core/src/event/cx/cx_pub.rs
index b4a8ad43b..f25ddb555 100644
--- a/crates/kas-core/src/event/cx/cx_pub.rs
+++ b/crates/kas-core/src/event/cx/cx_pub.rs
@@ -748,7 +748,7 @@ impl<'a> EventCx<'a> {
         log::trace!(target: "kas_core::event", "add_popup: {popup:?}");
 
         let parent_id = self.window.window_id();
-        let id = self.shell.add_popup(parent_id, popup.clone());
+        let id = self.shared.add_popup(parent_id, popup.clone());
         let nav_focus = self.nav_focus.clone();
         self.popups.push((id, popup, nav_focus));
         self.clear_nav_focus();
@@ -762,7 +762,8 @@ impl<'a> EventCx<'a> {
     /// available to a running UI. This method may be used instead.
     ///
     /// Requirement: the type `Data` must match the type of data passed to the
-    /// `Shell` and used by other windows. If not, a run-time error will result.
+    /// [`Application`](crate::app::Application) and used by other windows.
+    /// If not, a run-time error will result.
     ///
     /// Caveat: if an error occurs opening the new window it will not be
     /// reported (except via log messages).
@@ -771,7 +772,7 @@ impl<'a> EventCx<'a> {
         let data_type_id = std::any::TypeId::of::<Data>();
         unsafe {
             let window: Window<()> = std::mem::transmute(window);
-            self.shell.add_window(window, data_type_id)
+            self.shared.add_window(window, data_type_id)
         }
     }
 
@@ -788,7 +789,7 @@ impl<'a> EventCx<'a> {
         {
             let (wid, popup, onf) = self.popups.remove(index);
             self.popup_removed.push((popup.id, wid));
-            self.shell.close_window(wid);
+            self.shared.close_window(wid);
 
             if let Some(id) = onf {
                 self.set_nav_focus(id, FocusSource::Synthetic);
@@ -796,7 +797,7 @@ impl<'a> EventCx<'a> {
             return;
         }
 
-        self.shell.close_window(id);
+        self.shared.close_window(id);
     }
 
     /// Enable window dragging for current click
@@ -840,7 +841,7 @@ impl<'a> EventCx<'a> {
             };
         }
 
-        self.shell.get_clipboard()
+        self.shared.get_clipboard()
     }
 
     /// Attempt to set clipboard contents
@@ -852,7 +853,7 @@ impl<'a> EventCx<'a> {
             return;
         }
 
-        self.shell.set_clipboard(content)
+        self.shared.set_clipboard(content)
     }
 
     /// True if the primary buffer is enabled
@@ -884,7 +885,7 @@ impl<'a> EventCx<'a> {
             };
         }
 
-        self.shell.get_primary()
+        self.shared.get_primary()
     }
 
     /// Set contents of primary buffer
@@ -899,13 +900,13 @@ impl<'a> EventCx<'a> {
             return;
         }
 
-        self.shell.set_primary(content)
+        self.shared.set_primary(content)
     }
 
     /// Adjust the theme
     #[inline]
     pub fn adjust_theme<F: FnOnce(&mut dyn ThemeControl) -> Action>(&mut self, f: F) {
-        self.shell.adjust_theme(Box::new(f));
+        self.shared.adjust_theme(Box::new(f));
     }
 
     /// Get a [`SizeCx`]
@@ -926,7 +927,7 @@ impl<'a> EventCx<'a> {
 
     /// Get a [`DrawShared`]
     pub fn draw_shared(&mut self) -> &mut dyn DrawShared {
-        self.shell.draw_shared()
+        self.shared.draw_shared()
     }
 
     /// Directly access Winit Window
diff --git a/crates/kas-core/src/event/cx/mod.rs b/crates/kas-core/src/event/cx/mod.rs
index 280a466c3..b21a9ca1c 100644
--- a/crates/kas-core/src/event/cx/mod.rs
+++ b/crates/kas-core/src/event/cx/mod.rs
@@ -19,16 +19,16 @@ use std::u16;
 
 use super::config::WindowConfig;
 use super::*;
+use crate::app::{AppShared, Platform, WindowDataErased};
 use crate::cast::Cast;
 use crate::geom::Coord;
-use crate::shell::{Platform, ShellSharedErased, WindowDataErased};
 use crate::util::WidgetHierarchy;
 use crate::LayoutExt;
 use crate::{Action, Erased, ErasedStack, Id, NavAdvance, Node, Widget, WindowId};
 
 mod config;
 mod cx_pub;
-mod cx_shell;
+mod platform;
 mod press;
 
 pub use config::ConfigCx;
@@ -174,8 +174,9 @@ type AccessLayer = (bool, HashMap<Key, Id>);
 ///
 /// Besides event handling, this struct also configures widgets.
 ///
-/// Some methods are intended only for usage by KAS shells and are hidden from
-/// documentation unless the `internal_doc` feature is enabled. Only [winit]
+/// Some methods are intended only for usage by graphics and platform backends
+/// and are hidden from generated documentation unless the `internal_doc`
+/// feature is enabled. Only [winit]
 /// events are currently supported; changes will be required to generalise this.
 ///
 /// [winit]: https://github.com/rust-windowing/winit
@@ -364,7 +365,7 @@ impl EventState {
 #[must_use]
 pub struct EventCx<'a> {
     state: &'a mut EventState,
-    shell: &'a mut dyn ShellSharedErased,
+    shared: &'a mut dyn AppShared,
     window: &'a dyn WindowDataErased,
     messages: &'a mut ErasedStack,
     last_child: Option<usize>,
diff --git a/crates/kas-core/src/event/cx/cx_shell.rs b/crates/kas-core/src/event/cx/platform.rs
similarity index 98%
rename from crates/kas-core/src/event/cx/cx_shell.rs
rename to crates/kas-core/src/event/cx/platform.rs
index d4c62ff5b..8d6b3980e 100644
--- a/crates/kas-core/src/event/cx/cx_shell.rs
+++ b/crates/kas-core/src/event/cx/platform.rs
@@ -3,7 +3,7 @@
 // You may obtain a copy of the License in the LICENSE-APACHE file or at:
 //     https://www.apache.org/licenses/LICENSE-2.0
 
-//! Event manager — shell API
+//! Event manager — platform API
 
 use smallvec::SmallVec;
 use std::task::Poll;
@@ -20,7 +20,7 @@ const DOUBLE_CLICK_TIMEOUT: Duration = Duration::from_secs(1);
 
 const FAKE_MOUSE_BUTTON: MouseButton = MouseButton::Other(0);
 
-/// Shell API
+/// Platform API
 #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
 #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
 impl EventState {
@@ -108,14 +108,14 @@ impl EventState {
     #[inline]
     pub(crate) fn with<'a, F: FnOnce(&mut EventCx)>(
         &'a mut self,
-        shell: &'a mut dyn ShellSharedErased,
+        shared: &'a mut dyn AppShared,
         window: &'a dyn WindowDataErased,
         messages: &'a mut ErasedStack,
         f: F,
     ) {
         let mut cx = EventCx {
             state: self,
-            shell,
+            shared,
             window,
             messages,
             last_child: None,
@@ -127,13 +127,13 @@ impl EventState {
     /// Handle all pending items before event loop sleeps
     pub(crate) fn flush_pending<'a, A>(
         &'a mut self,
-        shell: &'a mut dyn ShellSharedErased,
+        shared: &'a mut dyn AppShared,
         window: &'a dyn WindowDataErased,
         messages: &'a mut ErasedStack,
         win: &mut Window<A>,
         data: &A,
     ) -> Action {
-        self.with(shell, window, messages, |cx| {
+        self.with(shared, window, messages, |cx| {
             while let Some((id, wid)) = cx.popup_removed.pop() {
                 cx.send_event(win.as_node(data), id, Event::PopupClosed(wid));
             }
@@ -249,7 +249,7 @@ impl EventState {
     }
 }
 
-/// Shell API
+/// Platform API
 #[cfg_attr(not(feature = "internal_doc"), doc(hidden))]
 #[cfg_attr(doc_cfg, doc(cfg(internal_doc)))]
 impl<'a> EventCx<'a> {
@@ -274,7 +274,7 @@ impl<'a> EventCx<'a> {
         let mut i = 0;
         while i < self.state.fut_messages.len() {
             let (_, fut) = &mut self.state.fut_messages[i];
-            let mut cx = std::task::Context::from_waker(self.shell.waker());
+            let mut cx = std::task::Context::from_waker(self.shared.waker());
             match fut.as_mut().poll(&mut cx) {
                 Poll::Pending => {
                     i += 1;
@@ -293,7 +293,7 @@ impl<'a> EventCx<'a> {
     /// Handle a winit `WindowEvent`.
     ///
     /// Note that some event types are not handled, since for these
-    /// events the shell must take direct action anyway:
+    /// events the graphics backend must take direct action anyway:
     /// `Resized(size)`, `RedrawRequested`, `HiDpiFactorChanged(factor)`.
     #[cfg(winit)]
     #[cfg_attr(doc_cfg, doc(cfg(feature = "winit")))]
diff --git a/crates/kas-core/src/lib.rs b/crates/kas-core/src/lib.rs
index 44a1567b2..674f36b1b 100644
--- a/crates/kas-core/src/lib.rs
+++ b/crates/kas-core/src/lib.rs
@@ -39,6 +39,7 @@ pub use kas_macros::*;
 pub use root::{Window, WindowCommand, WindowId};
 
 // public implementations:
+pub mod app;
 pub mod class;
 pub mod config;
 pub mod dir;
@@ -51,7 +52,6 @@ pub mod hidden;
 pub mod layout;
 pub mod message;
 pub mod prelude;
-pub mod shell;
 pub mod text;
 pub mod theme;
 pub mod util;
diff --git a/crates/kas-core/src/root.rs b/crates/kas-core/src/root.rs
index 491d22f61..2207437ba 100644
--- a/crates/kas-core/src/root.rs
+++ b/crates/kas-core/src/root.rs
@@ -26,7 +26,7 @@ pub struct WindowId(NonZeroU32);
 impl WindowId {
     /// Construct a [`WindowId`]
     ///
-    /// Only for use by the shell!
+    /// Only for use by the graphics/platform backend!
     #[allow(unused)]
     pub(crate) fn new(n: NonZeroU32) -> WindowId {
         WindowId(n)
diff --git a/crates/kas-core/src/theme/mod.rs b/crates/kas-core/src/theme/mod.rs
index f6ac9da1c..ad6661cd4 100644
--- a/crates/kas-core/src/theme/mod.rs
+++ b/crates/kas-core/src/theme/mod.rs
@@ -8,8 +8,8 @@
 //! Widgets expect the theme to provide an implementation of [`SizeCx`] and of
 //! [`DrawCx`].
 //!
-//! Constructing a shell requires a [`Theme`]. Two implementations are provided
-//! here: [`SimpleTheme`] and [`FlatTheme`].
+//! Constructing an application requires a [`Theme`]. Two implementations are
+//! provided here: [`SimpleTheme`] and [`FlatTheme`].
 //! An adapter, [`MultiTheme`], is also provided.
 
 mod anim;
diff --git a/crates/kas-wgpu/Cargo.toml b/crates/kas-wgpu/Cargo.toml
index efc40cfdd..db8bccbfe 100644
--- a/crates/kas-wgpu/Cargo.toml
+++ b/crates/kas-wgpu/Cargo.toml
@@ -15,7 +15,7 @@ documentation = "https://docs.rs/kas-wgpu/"
 # WARNING: if "raster" is disabled, an alternative like "kas-text/fontdue" is required!
 default = ["shaping", "raster"]
 
-# Enables documentation of APIs for shells and internal usage.
+# Enables documentation of APIs for graphics library and platform backends.
 # This API is not intended for use by end-user applications and
 # thus is omitted from built documentation by default.
 # This flag does not change the API, only built documentation.
diff --git a/crates/kas-wgpu/README.md b/crates/kas-wgpu/README.md
index 355c15595..a367b80b8 100644
--- a/crates/kas-wgpu/README.md
+++ b/crates/kas-wgpu/README.md
@@ -1,7 +1,7 @@
 KAS WGPU
 ======
 
-[KAS] shell interface over [wgpu].
+[KAS] graphcis backend over [wgpu].
 
 [KAS]: https://crates.io/crates/kas
 [wgpu]: https://github.com/gfx-rs/wgpu-rs
diff --git a/crates/kas-wgpu/src/draw/custom.rs b/crates/kas-wgpu/src/draw/custom.rs
index 26dc01c1a..f15067afd 100644
--- a/crates/kas-wgpu/src/draw/custom.rs
+++ b/crates/kas-wgpu/src/draw/custom.rs
@@ -12,7 +12,7 @@ use kas::geom::{Rect, Size};
 /// Allows use of the low-level graphics API
 ///
 /// To use this, write an implementation of [`CustomPipe`], then pass the
-/// corresponding [`CustomPipeBuilder`] to `Shell::new_custom`.
+/// corresponding [`CustomPipeBuilder`] to `Application::new_custom`.
 pub trait DrawCustom<CW: CustomWindow> {
     /// Call a custom draw pipe
     fn custom(&mut self, pass: PassId, rect: Rect, param: CW::Param);
@@ -52,7 +52,7 @@ pub trait CustomPipeBuilder {
 /// A custom pipe allows direct use of the `wgpu` graphics stack.
 ///
 /// To use this, pass the corresponding [`CustomPipeBuilder`] to
-/// `Shell::new_custom`.
+/// `Application::new_custom`.
 ///
 /// Note that `kas-wgpu` accepts only a single custom pipe. To use more than
 /// one custom graphics pipeline, you must implement your own multiplexer.
diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs
index f373ee4c7..8b21d412c 100644
--- a/crates/kas-wgpu/src/draw/draw_pipe.rs
+++ b/crates/kas-wgpu/src/draw/draw_pipe.rs
@@ -12,18 +12,15 @@ use wgpu::util::DeviceExt;
 use super::*;
 use crate::DrawShadedImpl;
 use crate::Options;
+use kas::app::Error;
 use kas::cast::traits::*;
 use kas::draw::color::Rgba;
 use kas::draw::*;
 use kas::geom::{Quad, Rect, Size, Vec2};
-use kas::shell::Error;
 use kas::text::{Effect, TextDisplay};
 use kas::theme::RasterConfig;
 
-/// Possible failures from constructing a [`Shell`]
-///
-/// Some variants are undocumented. Users should not match these variants since
-/// they are not considered part of the public API.
+/// Failure while constructing an [`Application`]: no graphics adapter found
 #[non_exhaustive]
 #[derive(thiserror::Error, Debug)]
 #[error("no graphics adapter found")]
diff --git a/crates/kas-wgpu/src/lib.rs b/crates/kas-wgpu/src/lib.rs
index 2de7cd6de..c221cafb9 100644
--- a/crates/kas-wgpu/src/lib.rs
+++ b/crates/kas-wgpu/src/lib.rs
@@ -3,7 +3,7 @@
 // You may obtain a copy of the License in the LICENSE-APACHE file or at:
 //     https://www.apache.org/licenses/LICENSE-2.0
 
-//! KAS shell over [WGPU]
+//! KAS graphics backend over [WGPU]
 //!
 //! This crate implements a KAS's drawing APIs over [WGPU].
 //!
@@ -27,7 +27,7 @@ mod shaded_theme;
 mod surface;
 
 use crate::draw::{CustomPipeBuilder, DrawPipe};
-use kas::shell::{GraphicalShell, Result, ShellBuilder};
+use kas::app::{AppBuilder, AppGraphicsBuilder, Result};
 use kas::theme::{FlatTheme, Theme};
 
 pub use draw_shaded::{DrawShaded, DrawShadedImpl};
@@ -35,14 +35,14 @@ pub use options::Options;
 pub use shaded_theme::ShadedTheme;
 pub extern crate wgpu;
 
-/// Builder for a KAS shell using WGPU
+/// Builder for a KAS application using WGPU
 pub struct WgpuBuilder<CB: CustomPipeBuilder> {
     custom: CB,
     options: Options,
     read_env_vars: bool,
 }
 
-impl<CB: CustomPipeBuilder> GraphicalShell for WgpuBuilder<CB> {
+impl<CB: CustomPipeBuilder> AppGraphicsBuilder for WgpuBuilder<CB> {
     type DefaultTheme = FlatTheme;
 
     type Shared = DrawPipe<CB::Pipe>;
@@ -97,15 +97,15 @@ impl<CB: CustomPipeBuilder> WgpuBuilder<CB> {
         self
     }
 
-    /// Convert to a [`ShellBuilder`] using the default theme
+    /// Convert to a [`AppBuilder`] using the default theme
     #[inline]
-    pub fn with_default_theme(self) -> ShellBuilder<Self, FlatTheme> {
-        ShellBuilder::new(self, FlatTheme::new())
+    pub fn with_default_theme(self) -> AppBuilder<Self, FlatTheme> {
+        AppBuilder::new(self, FlatTheme::new())
     }
 
-    /// Convert to a [`ShellBuilder`] using the specified `theme`
+    /// Convert to a [`AppBuilder`] using the specified `theme`
     #[inline]
-    pub fn with_theme<T: Theme<DrawPipe<CB::Pipe>>>(self, theme: T) -> ShellBuilder<Self, T> {
-        ShellBuilder::new(self, theme)
+    pub fn with_theme<T: Theme<DrawPipe<CB::Pipe>>>(self, theme: T) -> AppBuilder<Self, T> {
+        AppBuilder::new(self, theme)
     }
 }
diff --git a/crates/kas-wgpu/src/options.rs b/crates/kas-wgpu/src/options.rs
index 12ca0af29..24f982fd9 100644
--- a/crates/kas-wgpu/src/options.rs
+++ b/crates/kas-wgpu/src/options.rs
@@ -9,7 +9,7 @@ use std::env::var;
 use std::path::PathBuf;
 pub use wgpu::{Backends, PowerPreference};
 
-/// Shell options
+/// Graphics backend options
 #[derive(Clone, PartialEq, Eq, Hash)]
 pub struct Options {
     /// Adapter power preference. Default value: low power.
diff --git a/crates/kas-wgpu/src/surface.rs b/crates/kas-wgpu/src/surface.rs
index 8e68dfb49..05c42736b 100644
--- a/crates/kas-wgpu/src/surface.rs
+++ b/crates/kas-wgpu/src/surface.rs
@@ -6,11 +6,11 @@
 //! WGPU window surface
 
 use crate::draw::{CustomPipe, DrawPipe, DrawWindow};
+use kas::app::{raw_window_handle as raw, Error, WindowSurface};
 use kas::cast::Cast;
 use kas::draw::color::Rgba;
 use kas::draw::{DrawIface, DrawSharedImpl, WindowCommon};
 use kas::geom::Size;
-use kas::shell::{raw_window_handle as raw, Error, WindowSurface};
 use std::time::Instant;
 
 /// Per-window data
diff --git a/examples/calculator.rs b/examples/calculator.rs
index cea34d0ea..560674dd4 100644
--- a/examples/calculator.rs
+++ b/examples/calculator.rs
@@ -69,11 +69,11 @@ fn calc_ui() -> Window<()> {
     Window::new(ui, "Calculator")
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let theme = kas_wgpu::ShadedTheme::new().with_font_size(16.0);
-    kas::shell::Default::with_theme(theme)
+    kas::app::Default::with_theme(theme)
         .build(())?
         .with(calc_ui())
         .run()
diff --git a/examples/clock.rs b/examples/clock.rs
index e678b2842..5dc080bdf 100644
--- a/examples/clock.rs
+++ b/examples/clock.rs
@@ -18,14 +18,14 @@ use std::f32::consts::PI;
 use std::str::FromStr;
 use std::time::Duration;
 
+use kas::app::AppAssoc;
 use kas::draw::color::{Rgba, Rgba8Srgb};
 use kas::draw::{Draw, DrawRounded};
 use kas::geom::{Offset, Quad, Rect, Vec2};
 use kas::prelude::*;
-use kas::shell::ShellAssoc;
 use kas::text::Text;
 
-type Shell = kas::shell::Default<(), kas::theme::FlatTheme>;
+type Application = kas::app::Default<(), kas::theme::FlatTheme>;
 
 impl_scope! {
     #[derive(Clone)]
@@ -82,7 +82,7 @@ impl_scope! {
 
             // We use the low-level draw device to draw our clock. This means it is
             // not themeable, but gives us much more flexible draw routines.
-            let mut draw = draw.draw_iface::<<Shell as ShellAssoc>::DrawShared>().unwrap();
+            let mut draw = draw.draw_iface::<<Application as AppAssoc>::DrawShared>().unwrap();
 
             let rect = self.core.rect;
             let quad = Quad::conv(rect);
@@ -171,12 +171,12 @@ impl_scope! {
     }
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let window = Window::new(Clock::new(), "Clock")
         .with_decorations(kas::Decorations::None)
         .with_transparent(true);
 
-    Shell::new(())?.with(window).run()
+    Application::new(())?.with(window).run()
 }
diff --git a/examples/counter.rs b/examples/counter.rs
index 4a4b8e39c..852c8e910 100644
--- a/examples/counter.rs
+++ b/examples/counter.rs
@@ -24,11 +24,11 @@ fn counter() -> impl Widget<Data = ()> {
     Adapt::new(tree, 0).on_message(|_, count, Increment(add)| *count += add)
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let theme = kas::theme::SimpleTheme::new().with_font_size(24.0);
-    kas::shell::Default::with_theme(theme)
+    kas::app::Default::with_theme(theme)
         .build(())?
         .with(Window::new(counter(), "Counter"))
         .run()
diff --git a/examples/cursors.rs b/examples/cursors.rs
index ccac6185f..53db1161e 100644
--- a/examples/cursors.rs
+++ b/examples/cursors.rs
@@ -40,7 +40,7 @@ macro_rules! cursor {
     };
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     // These are winit::window::CursorIcon enum variants
@@ -82,5 +82,5 @@ fn main() -> kas::shell::Result<()> {
     ]);
 
     let window = Window::new(column, "Cursor gallery");
-    kas::shell::Default::new(())?.with(window).run()
+    kas::app::Default::new(())?.with(window).run()
 }
diff --git a/examples/data-list-view.rs b/examples/data-list-view.rs
index aaece0ec4..1a2a3002b 100644
--- a/examples/data-list-view.rs
+++ b/examples/data-list-view.rs
@@ -208,7 +208,7 @@ impl Driver<Item, Data> for MyDriver {
     }
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let controls = row![
@@ -243,5 +243,5 @@ fn main() -> kas::shell::Result<()> {
 
     let window = Window::new(ui, "Dynamic widget demo");
 
-    kas::shell::Default::new(())?.with(window).run()
+    kas::app::Default::new(())?.with(window).run()
 }
diff --git a/examples/data-list.rs b/examples/data-list.rs
index d1f3c9d23..d92b7592f 100644
--- a/examples/data-list.rs
+++ b/examples/data-list.rs
@@ -141,7 +141,7 @@ impl_scope! {
     }
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let controls = row![
@@ -185,5 +185,5 @@ fn main() -> kas::shell::Result<()> {
 
     let window = Window::new(ui, "Dynamic widget demo");
 
-    kas::shell::Default::new(())?.with(window).run()
+    kas::app::Default::new(())?.with(window).run()
 }
diff --git a/examples/gallery.rs b/examples/gallery.rs
index 646e4c7a8..dcf747ef8 100644
--- a/examples/gallery.rs
+++ b/examples/gallery.rs
@@ -513,7 +513,7 @@ KAS_CONFIG_MODE=readwrite
     Box::new(ui)
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let theme = kas::theme::MultiTheme::builder()
@@ -521,7 +521,7 @@ fn main() -> kas::shell::Result<()> {
         .add("simple", kas::theme::SimpleTheme::new())
         .add("shaded", kas_wgpu::ShadedTheme::new())
         .build();
-    let mut shell = kas::shell::Default::with_theme(theme).build(())?;
+    let mut app = kas::app::Default::with_theme(theme).build(())?;
 
     // TODO: use as logo of tab
     // let img_gallery = Svg::new(include_bytes!("../res/gallery-line.svg"));
@@ -545,9 +545,9 @@ fn main() -> kas::shell::Result<()> {
         })
         .menu("&Style", |menu| {
             menu.submenu("&Colours", |mut menu| {
-                // Enumerate colour schemes. Access through the shell since
+                // Enumerate colour schemes. Access through the app since
                 // this handles config loading.
-                for name in shell.theme().list_schemes().iter() {
+                for name in app.theme().list_schemes().iter() {
                     let mut title = String::with_capacity(name.len() + 1);
                     match name {
                         &"" => title.push_str("&Default"),
@@ -624,6 +624,6 @@ fn main() -> kas::shell::Result<()> {
         }
     };
 
-    shell.add(Window::new(ui, "Gallery — Widgets"));
-    shell.run()
+    app.add(Window::new(ui, "Gallery — Widgets"));
+    app.run()
 }
diff --git a/examples/hello.rs b/examples/hello.rs
index 9b2c4af6f..f19d6ab30 100644
--- a/examples/hello.rs
+++ b/examples/hello.rs
@@ -7,8 +7,8 @@
 
 use kas::widgets::dialog::MessageBox;
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     let window = MessageBox::new("Message").into_window("Hello world");
 
-    kas::shell::Default::new(())?.with(window).run()
+    kas::app::Default::new(())?.with(window).run()
 }
diff --git a/examples/layout.rs b/examples/layout.rs
index f906c1cd3..a2b638206 100644
--- a/examples/layout.rs
+++ b/examples/layout.rs
@@ -11,7 +11,7 @@ use kas::Window;
 const LIPSUM: &str = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi nunc mi, consequat eget urna ut, auctor luctus mi. Sed molestie mi est. Sed non ligula ante. Curabitur ac molestie ante, nec sodales eros. In non arcu at turpis euismod bibendum ut tincidunt eros. Suspendisse blandit maximus nisi, viverra hendrerit elit efficitur et. Morbi ut facilisis eros. Vivamus dignissim, sapien sed mattis consectetur, libero leo imperdiet turpis, ac pulvinar libero purus eu lorem. Etiam quis sollicitudin urna. Integer vitae erat vel neque gravida blandit ac non quam.";
 const CRASIT: &str = "Cras sit amet justo ipsum. Aliquam in nunc posuere leo egestas laoreet convallis eu libero. Nullam ut massa ante. Cras vitae velit pharetra, euismod nisl suscipit, feugiat nulla. Aenean consectetur, diam non tristique iaculis, nisl lectus hendrerit sapien, nec rhoncus mi sem non odio. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Nulla a lorem eu ipsum faucibus placerat ac quis quam. Curabitur justo ligula, laoreet nec ultrices eu, scelerisque non metus. Mauris sit amet est enim. Mauris risus eros, accumsan ut iaculis sit amet, sagittis facilisis neque. Nunc venenatis risus nec purus malesuada, a tristique arcu efficitur. Nulla suscipit arcu nibh. Cras facilisis nibh a gravida aliquet. Praesent fringilla felis a tristique luctus.";
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let ui = kas::grid! {
@@ -24,5 +24,5 @@ fn main() -> kas::shell::Result<()> {
     };
     let window = Window::new(ui, "Layout demo");
 
-    kas::shell::Default::new(())?.with(window).run()
+    kas::app::Default::new(())?.with(window).run()
 }
diff --git a/examples/mandlebrot/mandlebrot.rs b/examples/mandlebrot/mandlebrot.rs
index 7f21c483d..ebb2b1456 100644
--- a/examples/mandlebrot/mandlebrot.rs
+++ b/examples/mandlebrot/mandlebrot.rs
@@ -480,12 +480,12 @@ impl_scope! {
     }
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let window = Window::new(MandlebrotUI::new(), "Mandlebrot");
     let theme = kas::theme::FlatTheme::new().with_colours("dark");
-    kas::shell::WgpuBuilder::new(PipeBuilder)
+    kas::app::WgpuBuilder::new(PipeBuilder)
         .with_theme(theme)
         .build(())?
         .with(window)
diff --git a/examples/proxy.rs b/examples/proxy.rs
index 31a4a96e6..40601953a 100644
--- a/examples/proxy.rs
+++ b/examples/proxy.rs
@@ -6,8 +6,8 @@
 //! Asynchronous events using a proxy
 //!
 //! This is a copy-cat of Druid's async event example, demonstrating usage of
-//! shell.create_proxy(). For a more integrated approach to async, see
-//! EventState::push_async() and push_spawn().
+//! `Application::create_proxy()`. For a more integrated approach to async, see
+//! `EventState::push_async()` and `push_spawn()`.
 
 use std::thread;
 use std::time::{Duration, Instant};
@@ -35,20 +35,20 @@ impl kas::AppData for AppData {
     }
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let data = AppData { color: None };
-    let shell = kas::shell::Default::new(data)?;
+    let app = kas::app::Default::new(data)?;
 
-    // We construct a proxy from the shell to enable cross-thread communication.
-    let proxy = shell.create_proxy();
+    // We construct a proxy from the app to enable cross-thread communication.
+    let proxy = app.create_proxy();
     thread::spawn(move || generate_colors(proxy));
 
     let widget = ColourSquare::new();
     let window = Window::new(widget, "Async event demo");
 
-    shell.with(window).run()
+    app.with(window).run()
 }
 
 impl_scope! {
@@ -103,7 +103,7 @@ impl_scope! {
     }
 }
 
-fn generate_colors(mut proxy: kas::shell::Proxy) {
+fn generate_colors(mut proxy: kas::app::Proxy) {
     // Loading takes time:
     thread::sleep(Duration::from_secs(1));
 
diff --git a/examples/splitter.rs b/examples/splitter.rs
index fafc24468..93b908f23 100644
--- a/examples/splitter.rs
+++ b/examples/splitter.rs
@@ -14,7 +14,7 @@ enum Message {
     Incr,
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let ui = kas::column![
@@ -38,7 +38,7 @@ fn main() -> kas::shell::Result<()> {
     let window = Window::new(adapt, "Slitter panes");
 
     let theme = kas_wgpu::ShadedTheme::new();
-    kas::shell::Default::with_theme(theme)
+    kas::app::Default::with_theme(theme)
         .build(())?
         .with(window)
         .run()
diff --git a/examples/stopwatch.rs b/examples/stopwatch.rs
index 4f51a1972..dec5b2276 100644
--- a/examples/stopwatch.rs
+++ b/examples/stopwatch.rs
@@ -72,7 +72,7 @@ fn make_window() -> Box<dyn kas::Widget<Data = ()>> {
     })
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let window = Window::new_boxed(make_window(), "Stopwatch")
@@ -83,7 +83,7 @@ fn main() -> kas::shell::Result<()> {
     let theme = kas_wgpu::ShadedTheme::new()
         .with_colours("dark")
         .with_font_size(18.0);
-    kas::shell::Default::with_theme(theme)
+    kas::app::Default::with_theme(theme)
         .build(())?
         .with(window)
         .run()
diff --git a/examples/sync-counter.rs b/examples/sync-counter.rs
index 134f4f6f6..02271cf37 100644
--- a/examples/sync-counter.rs
+++ b/examples/sync-counter.rs
@@ -56,13 +56,13 @@ fn counter(title: &str) -> Window<Count> {
     Window::new(ui, title)
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let count = Count(0);
     let theme = kas_wgpu::ShadedTheme::new().with_font_size(24.0);
 
-    kas::shell::Default::with_theme(theme)
+    kas::app::Default::with_theme(theme)
         .build(count)?
         .with(counter("Counter 1"))
         .with(counter("Counter 2"))
diff --git a/examples/times-tables.rs b/examples/times-tables.rs
index e6fd2983b..a8d938d6b 100644
--- a/examples/times-tables.rs
+++ b/examples/times-tables.rs
@@ -46,7 +46,7 @@ impl MatrixData for TableSize {
     }
 }
 
-fn main() -> kas::shell::Result<()> {
+fn main() -> kas::app::Result<()> {
     env_logger::init();
 
     let table = MatrixView::new(driver::NavView)
@@ -76,7 +76,7 @@ fn main() -> kas::shell::Result<()> {
     let window = Window::new(ui, "Times-Tables");
 
     let theme = kas::theme::SimpleTheme::new().with_font_size(16.0);
-    kas::shell::Default::with_theme(theme)
+    kas::app::Default::with_theme(theme)
         .build(())?
         .with(window)
         .run()
diff --git a/src/lib.rs b/src/lib.rs
index 41656e8dd..553a49697 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -10,15 +10,12 @@
 //! (some optional, dependant on a feature flag) are re-exported by this crate:
 //!
 //! - [`kas_core`] is re-export at the top-level
-//! - [`easy-cast`](https://docs.rs/easy-cast/0.5) is re-export as [`kas::cast`](cast)
-//! - `kas_macros` is an extended version of [`impl-tools`](https://docs.rs/impl-tools/),
+//! - [`easy-cast`](https://crates.io/crates/easy-cast) is re-export as [`cast`]
+//! - `kas_macros` is an extended version of [`impl-tools`](https://crates.io/crates/impl-tools),
 //!     re-export at the top-level
-//! - [`kas_widgets`](https://docs.rs/kas-widgets/0.11) is re-export as [`kas::widgets`](mod@widgets)
-//! - [`kas_resvg`] is re-export as [`kas::resvg`](resvg) (`resvg` or `tiny-skia` feature)
-//! - [`kas_view`](https://docs.rs/kas-view/0.11) is re-export as [`kas::view`](view) (`view` feature)
-//! - [`kas_wgpu`](https://docs.rs/kas-wgpu/0.11) is re-export as [`kas::shell`](shell); in the current version
-//!     this is dependant on [WGPU](https://github.com/gfx-rs/wgpu), but in the
-//!     future this should become a shim over multiple back-ends
+//! - [`kas_widgets`](https://crates.io/crates/kas-widgets) is re-export as [`widgets`](mod@widgets)
+//! - [`kas_resvg`](https://crates.io/crates/kas-resvg) is re-export as [`resvg`] (`resvg` or `tiny-skia` feature)
+//! - [`kas_view`](https://crates.io/crates/kas-view) is re-export as [`view`] (`view` feature)
 //!
 //! Also refer to:
 //!
@@ -71,19 +68,20 @@ pub mod resvg {
     pub use kas_resvg::*;
 }
 
-pub mod shell {
-    //! Shell: window runtime environment
+pub mod app {
+    //! Application, platforms and backends
     //!
-    //! A [`Shell`] is used to manage a GUI. Most GUIs will use the [`Default`](type@Default)
-    //! shell type-def (requires a backend be enabled, e.g. "wgpu").
+    //! Start by constructing an [`Application`] or its [`Default`](type@Default)
+    //! type-def (requires a backend be enabled, e.g. "wgpu").
 
-    pub use kas_core::shell::*;
+    pub use kas_core::app::*;
 
     #[cfg(feature = "wgpu")] pub use kas_wgpu::WgpuBuilder;
 
-    /// The default (configuration-specific) shell
+    /// Application pre-launch state, configured with the default graphics backend
     #[cfg(feature = "wgpu")]
-    pub type Default<Data, T> = kas_core::shell::Shell<Data, kas_wgpu::WgpuBuilder<()>, T>;
+    pub type Default<Data, T = crate::theme::FlatTheme> =
+        kas_core::app::Application<Data, kas_wgpu::WgpuBuilder<()>, T>;
 }
 
 #[cfg(feature = "dynamic")]