diff --git a/repos/os/src/server/nitpicker/README b/repos/os/src/server/nitpicker/README
index d9cb871626..e12763f1ac 100644
--- a/repos/os/src/server/nitpicker/README
+++ b/repos/os/src/server/nitpicker/README
@@ -244,6 +244,47 @@ client. This report is useful for a focus-managing component to implement a
 focus-on-click policy.
 
 
+Multi-monitor support
+~~~~~~~~~~~~~~~~~~~~~
+
+Display drivers obtain pixel data from the nitpicker GUI server using
+nitpicker's capture service. For each connected monitor, the driver creates a
+distinct capture session labeled after the name of the connector. Therefore,
+from nitpicker's perspective, each monitor corresponds to one capture client.
+Each capture client can have a different size, which corresponds to the
+respective display resolution. Together, all capture clients span a panorama,
+which is the bounding box of all capture clients. Each capture client shows a
+part of the panorama.
+
+By default, when configuring nitpicker with an empty '<capture/>' node, the
+top-left corner of each capture client corresponds to the coordinate origin
+(0, 0) of the panorama. Hence, each client obtains a mirror of the panorama.
+This default policy can be overridden by explicit rules as follows:
+
+! <capture>
+!   <policy label="intel_fb -> eDP-1" width_mm="160" height_mm="90" />
+!   <policy label="intel_fb -> HDMI-A-1" xpos="1024" ypos="0"
+!                                        width="1280" height="800"/>
+!   <default-policy/>
+! </capture>
+
+The policy for the 'eDP-1' connector merely overrides the physical dimensions
+of the display as reported by the driver. This is useful in situations where
+the display's EDID information are incorrect. Apart from that tweak, the
+client obtains the default part of the panorama at the coordinate origin.
+
+The policy for the HDMI-A-1 connector dictates an explicit placement within
+the panorama. So a data projector connected via HDMI shows a mere window of
+the panorama where the top-left corner corresponds to the panorama position
+(1024, 0). The client won't observe any pixels outside the specified window.
+It is possible to specify only a subset of attributes. E.g., by only
+specifying the 'xpos', the width and height remain unconstrained.
+
+The default policy tells nitpicker to regard any other capture client as a
+mirror of the panorama's coordinate origin. If absent, a client with no
+policy, won't obtain any picture.
+
+
 Cascaded usage scenarios
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
diff --git a/repos/os/src/server/nitpicker/capture_session.h b/repos/os/src/server/nitpicker/capture_session.h
index 2ef70eb120..96cc857766 100644
--- a/repos/os/src/server/nitpicker/capture_session.h
+++ b/repos/os/src/server/nitpicker/capture_session.h
@@ -36,6 +36,65 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 			virtual void capture_requested(Label const &) = 0;
 		};
 
+		struct Policy
+		{
+			template <typename T>
+			struct Attr
+			{
+				bool _defined { };
+				T    _value   { };
+
+				Attr() { }
+
+				Attr(T value) : _defined(true), _value(value) { }
+
+				Attr(Xml_node const node, auto const &attr)
+				{
+					if (node.has_attribute(attr)) {
+						_value   = node.attribute_value(attr, T { });
+						_defined = true;
+					}
+				}
+
+				/**
+				 * Return defined attribute value, or default value
+				 */
+				T or_default(T def) const { return _defined ? _value : def; }
+			};
+
+			Attr<int>      x, y;       /* placement within panorama */
+			Attr<unsigned> w, h;       /* capture contraints */
+			Attr<unsigned> w_mm, h_mm; /* physical size overrides */
+
+			static Policy from_xml(Xml_node const &policy)
+			{
+				return { .x    = { policy, "xpos"      },
+				         .y    = { policy, "ypos"      },
+				         .w    = { policy, "width"     },
+				         .h    = { policy, "height"    },
+				         .w_mm = { policy, "width_mm"  },
+				         .h_mm = { policy, "height_mm" } };
+			}
+
+			static Policy unconstrained() { return { }; }
+
+			static Policy blocked()
+			{
+				Policy result { };
+				result.w = 0, result.h = 0;
+				return result;
+			}
+		};
+
+		static void gen_attr(Xml_generator &xml, Rect const rect)
+		{
+			if (rect.x1()) xml.attribute("xpos", rect.x1());
+			if (rect.y1()) xml.attribute("ypos", rect.y1());
+
+			xml.attribute("width",  rect.w());
+			xml.attribute("height", rect.h());
+		}
+
 	private:
 
 		Env &_env;
@@ -46,6 +105,8 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 
 		View_stack const &_view_stack;
 
+		Policy _policy = Policy::blocked();
+
 		Buffer_attr _buffer_attr { };
 
 		Constructible<Attached_ram_dataspace> _buffer { };
@@ -68,6 +129,18 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 			}
 		}
 
+		Point _anchor_point() const
+		{
+			return { .x = _policy.x.or_default(0),
+			         .y = _policy.y.or_default(0) };
+		}
+
+		Area _area_bounds() const
+		{
+			return { .w = _policy.w.or_default(_buffer_attr.px.w),
+			         .h = _policy.h.or_default(_buffer_attr.px.h) };
+		}
+
 	public:
 
 		Capture_session(Env              &env,
@@ -83,7 +156,7 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 			_handler(handler),
 			_view_stack(view_stack)
 		{
-			_dirty_rect.mark_as_dirty(Rect(Point(0, 0), view_stack.size()));
+			_dirty_rect.mark_as_dirty(view_stack.bounding_box());
 		}
 
 		~Capture_session() { }
@@ -93,7 +166,10 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 		 ** Interface used by 'Nitpicker::Main' **
 		 *****************************************/
 
-		Area buffer_size() const { return _buffer_attr.px; }
+		/**
+		 * Geometry within the panorama, depending on policy and client buffer
+		 */
+		Rect bounding_box() const { return { _anchor_point(), _area_bounds() }; }
 
 		void mark_as_damaged(Rect rect)
 		{
@@ -107,12 +183,17 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 				Signal_transmitter(_screen_size_sigh).submit();
 		}
 
+		void apply_policy(Policy const &policy) { _policy = policy; }
+
 
 		/*******************************
 		 ** Capture session interface **
 		 *******************************/
 
-		Area screen_size() const override { return _view_stack.size(); }
+		Area screen_size() const override
+		{
+			return Rect::intersect(_view_stack.bounding_box(), bounding_box()).area;
+		}
 
 		void screen_size_sigh(Signal_context_capability sigh) override
 		{
@@ -131,7 +212,7 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 
 			_buffer_attr = { };
 
-			if (attr.px.count() == 0) {
+			if (!attr.px.valid()) {
 				_buffer.destruct();
 				return result;
 			}
@@ -146,7 +227,7 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 			_handler.capture_buffer_size_changed();
 
 			/* report complete buffer as dirty on next call of 'capture_at' */
-			mark_as_damaged({ { 0, 0 }, attr.px });
+			mark_as_damaged({ _anchor_point(), attr.px });
 
 			return result;
 		}
@@ -159,18 +240,19 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 			return Dataspace_capability();
 		}
 
-		Affected_rects capture_at(Point pos) override
+		Affected_rects capture_at(Point const pos) override
 		{
 			_handler.capture_requested(label());
 
 			if (!_buffer.constructed())
 				return Affected_rects { };
 
-			using Pixel = Pixel_rgb888;
+			Canvas<Pixel_rgb888> canvas { _buffer->local_addr<Pixel_rgb888>(),
+			                              _anchor_point() + pos, _buffer_attr.px };
 
-			Canvas<Pixel> canvas = { _buffer->local_addr<Pixel>(), pos, _buffer_attr.px };
+			canvas.clip(Rect::intersect(bounding_box(), _view_stack.bounding_box()));
 
-			Rect const buffer_rect(Point(0, 0), _buffer_attr.px);
+			Rect const buffer_rect { { }, _buffer_attr.px };
 
 			Affected_rects affected { };
 			unsigned i = 0;
@@ -179,7 +261,7 @@ class Nitpicker::Capture_session : public Session_object<Capture::Session>
 				_view_stack.draw(canvas, rect);
 
 				if (i < Affected_rects::NUM_RECTS) {
-					Rect const translated(rect.p1() - pos, rect.area);
+					Rect const translated(rect.p1() - _anchor_point() - pos, rect.area);
 					Rect const clipped = Rect::intersect(translated, buffer_rect);
 					affected.rects[i++] = clipped;
 				}
diff --git a/repos/os/src/server/nitpicker/domain_registry.h b/repos/os/src/server/nitpicker/domain_registry.h
index 3d20ce40cf..486f3c2cbd 100644
--- a/repos/os/src/server/nitpicker/domain_registry.h
+++ b/repos/os/src/server/nitpicker/domain_registry.h
@@ -64,17 +64,16 @@ class Nitpicker::Domain_registry
 					_origin(origin), _layer(layer), _offset(offset), _area(area)
 				{ }
 
-				Point _corner(Area const screen_area) const
+				Point _corner(Rect const rect) const
 				{
 					switch (_origin) {
-					case Origin::POINTER:      return Point(0, 0);
-					case Origin::TOP_LEFT:     return Point(0, 0);
-					case Origin::TOP_RIGHT:    return Point(screen_area.w, 0);
-					case Origin::BOTTOM_LEFT:  return Point(0, screen_area.h);
-					case Origin::BOTTOM_RIGHT: return Point(screen_area.w,
-					                                        screen_area.h);
+					case Origin::POINTER:      return { 0, 0 };
+					case Origin::TOP_LEFT:     return { rect.x1(), rect.y1() };
+					case Origin::TOP_RIGHT:    return { rect.x2(), rect.y1() };
+					case Origin::BOTTOM_LEFT:  return { rect.x1(), rect.y2() };
+					case Origin::BOTTOM_RIGHT: return { rect.x2(), rect.y2() };
 					}
-					return Point(0, 0);
+					return { 0, 0 };
 				}
 
 			public:
@@ -95,22 +94,22 @@ class Nitpicker::Domain_registry
 				bool focus_transient() const { return _focus   == Focus::TRANSIENT; }
 				bool origin_pointer()  const { return _origin  == Origin::POINTER; }
 
-				Point phys_pos(Point pos, Area screen_area) const
+				Point phys_pos(Point pos, Rect panorama) const
 				{
-					return pos + _corner(screen_area) + _offset;
+					return pos + _corner(panorama) + _offset;
 				}
 
-				Area screen_area(Area phys_screen_area) const
+				Rect screen_rect(Area screen_area) const
 				{
-					int const w = _area.x > 0
-					            ? _area.x
-					            : max(0, (int)phys_screen_area.w + _area.x);
+					/* align value to zero or to limit, depending on its sign */
+					auto aligned = [&] (unsigned limit, int v)
+					{
+						return unsigned((v > 0) ? v : max(0, int(limit) + v));
+					};
 
-					int const h = _area.y > 0
-					            ? _area.y
-					            : max(0, (int)phys_screen_area.h + _area.y);
-
-					return Area(w, h);
+					return { .at   = _offset,
+					         .area = { .w = aligned(screen_area.w, _area.x),
+					                   .h = aligned(screen_area.h, _area.y) } };
 				}
 		};
 
diff --git a/repos/os/src/server/nitpicker/gui_session.cc b/repos/os/src/server/nitpicker/gui_session.cc
index 16cdcf927e..ba705866a0 100644
--- a/repos/os/src/server/nitpicker/gui_session.cc
+++ b/repos/os/src/server/nitpicker/gui_session.cc
@@ -64,7 +64,7 @@ void Gui_session::_execute_command(Command const &command)
 
 			/* transpose position of top-level views by vertical session offset */
 			if (view.top_level())
-				pos = _phys_pos(pos, _view_stack.size());
+				pos = _phys_pos(pos, _view_stack.bounding_box());
 
 			_view_stack.geometry(view, Rect(pos, args.rect.area));
 		});
@@ -175,7 +175,7 @@ void Gui_session::submit_input_event(Input::Event e)
 {
 	using namespace Input;
 
-	Point const origin_offset = _phys_pos(Point(0, 0), _view_stack.size());
+	Point const origin_offset = _phys_pos({ 0, 0 }, _view_stack.bounding_box());
 
 	/*
 	 * Transpose absolute coordinates by session-specific vertical offset.
@@ -377,14 +377,14 @@ void Gui_session::execute()
 
 Framebuffer::Mode Gui_session::mode()
 {
-	Area const screen = screen_area(_view_stack.size());
+	Rect const screen = screen_rect(_view_stack.bounding_box().area);
 
 	/*
 	 * Return at least a size of 1x1 to spare the clients the need to handle
 	 * the special case of 0x0, which can happen at boot time before the
 	 * framebuffer driver is running.
 	 */
-	return { .area  = { max(screen.w, 1u), max(screen.h, 1u) },
+	return { .area  = { max(screen.w(), 1u), max(screen.h(), 1u) },
 	         .alpha = uses_alpha() };
 }
 
diff --git a/repos/os/src/server/nitpicker/gui_session.h b/repos/os/src/server/nitpicker/gui_session.h
index 6021c6a17e..40d5b3c50f 100644
--- a/repos/os/src/server/nitpicker/gui_session.h
+++ b/repos/os/src/server/nitpicker/gui_session.h
@@ -153,14 +153,14 @@ class Nitpicker::Gui_session : public  Session_object<Gui::Session>,
 		Gui_session *_forwarded_focus = nullptr;
 
 		/**
-		 * Calculate session-local coordinate to physical screen position
+		 * Calculate session-local coordinate to position within panorama
 		 *
-		 * \param pos          coordinate in session-local coordinate system
-		 * \param screen_area  session-local screen size
+		 * \param pos   coordinate in session-local coordinate system
+		 * \param rect  geometry within panorama
 		 */
-		Point _phys_pos(Point pos, Area screen_area) const
+		Point _phys_pos(Point pos, Rect panorama) const
 		{
-			return _domain ? _domain->phys_pos(pos, screen_area) : Point(0, 0);
+			return _domain ? _domain->phys_pos(pos, panorama) : Point(0, 0);
 		}
 
 		void _execute_command(Command const &);
@@ -320,13 +320,11 @@ class Nitpicker::Gui_session : public  Session_object<Gui::Session>,
 		void visible(bool visible) { _visible = visible; }
 
 		/**
-		 * Return session-local screen area
-		 *
-		 * \param phys_pos  size of physical screen
+		 * Return session-local screen geometry
 		 */
-		Area screen_area(Area phys_area) const
+		Rect screen_rect(Area screen_area) const
 		{
-			return _domain ? _domain->screen_area(phys_area) : Area(0, 0);
+			return _domain ? _domain->screen_rect(screen_area) : Rect { };
 		}
 
 		void reset_domain() { _domain = nullptr; }
diff --git a/repos/os/src/server/nitpicker/main.cc b/repos/os/src/server/nitpicker/main.cc
index 2cfc043b68..37c2c01c8a 100644
--- a/repos/os/src/server/nitpicker/main.cc
+++ b/repos/os/src/server/nitpicker/main.cc
@@ -201,27 +201,38 @@ class Nitpicker::Gui_root : public Root_component<Gui_session>
 
 class Nitpicker::Capture_root : public Root_component<Capture_session>
 {
+	public:
+
+		struct Action : Interface
+		{
+			virtual void capture_client_appeared_or_disappeared() = 0;
+		};
+
 	private:
 
 		using Sessions = Registry<Registered<Capture_session>>;
 
 		Env                      &_env;
+		Action                   &_action;
 		Sessions                  _sessions { };
 		View_stack         const &_view_stack;
 		Capture_session::Handler &_handler;
 
-		Area _fallback_bounding_box { 0, 0 };
+		Rect _fallback_bounding_box { };
 
 	protected:
 
 		Capture_session *_create_session(const char *args) override
 		{
-			return new (md_alloc())
+			Capture_session &session = *new (md_alloc())
 				Registered<Capture_session>(_sessions, _env,
 				                            session_resources_from_args(args),
 				                            session_label_from_args(args),
 				                            session_diag_from_args(args),
 				                            _handler, _view_stack);
+
+			_action.capture_client_appeared_or_disappeared();
+			return &session;
 		}
 
 		void _upgrade_session(Capture_session *s, const char *args) override
@@ -237,12 +248,11 @@ class Nitpicker::Capture_root : public Root_component<Capture_session>
 			 * mode switches when the only capture client temporarily
 			 * disappears (driver restart).
 			 */
-			_fallback_bounding_box = session->buffer_size();
+			_fallback_bounding_box = session->bounding_box();
 
 			Genode::destroy(md_alloc(), session);
 
-			/* shrink screen according to the remaining output back ends */
-			_handler.capture_buffer_size_changed();
+			_action.capture_client_appeared_or_disappeared();
 		}
 
 	public:
@@ -251,26 +261,74 @@ class Nitpicker::Capture_root : public Root_component<Capture_session>
 		 * Constructor
 		 */
 		Capture_root(Env                      &env,
+		             Action                   &action,
 		             Allocator                &md_alloc,
 		             View_stack         const &view_stack,
 		             Capture_session::Handler &handler)
 		:
 			Root_component<Capture_session>(&env.ep().rpc_ep(), &md_alloc),
-			_env(env), _view_stack(view_stack), _handler(handler)
+			_env(env), _action(action), _view_stack(view_stack), _handler(handler)
 		{ }
 
-		/**
-		 * Determine the size of the bounding box of all capture pixel buffers
-		 */
-		Area bounding_box() const
+		void apply_config(Xml_node const &config)
 		{
-			Area result = { 0, 0 };
-			bool any_session_present = false;
-			_sessions.for_each([&] (Capture_session const &session) {
-				any_session_present = true;
-				result = max_area(result, session.buffer_size()); });
+			using Policy = Capture_session::Policy;
 
-			return any_session_present ? result : _fallback_bounding_box;
+			if (config.num_sub_nodes() == 0) {
+
+				/* if no policies are defined, mirror with no constraints */
+				_sessions.for_each([&] (Capture_session &session) {
+					session.apply_policy(Policy::unconstrained()); });
+
+			} else {
+
+				/* apply constraits per session */
+				_sessions.for_each([&] (Capture_session &session) {
+					with_matching_policy(session.label(), config,
+						[&] (Xml_node const &policy) {
+							session.apply_policy(Policy::from_xml(policy));
+						},
+						[&] { session.apply_policy(Policy::blocked()); }); });
+			}
+		}
+
+		/**
+		 * Determine the bounding box of all capture clients
+		 */
+		Rect bounding_box() const
+		{
+			Rect bb { };
+			_sessions.for_each([&] (Capture_session const &session) {
+				bb = Rect::compound(bb, session.bounding_box()); });
+
+			return bb.valid() ? bb : _fallback_bounding_box;
+		}
+
+		/**
+		 * Return true if specified position is suited as pointer position
+		 */
+		bool visible(Pointer const pointer) const
+		{
+			bool result = false;
+			pointer.with_result(
+				[&] (Point const p) {
+					_sessions.for_each([&] (Capture_session const &session) {
+						if (!result && session.bounding_box().contains(p))
+							result = true; }); },
+				[&] (Nowhere) { });
+			return result;
+		}
+
+		/**
+		 * Return position suitable for the initial pointer position
+		 */
+		Pointer any_visible_pointer_position() const
+		{
+			Pointer result = Nowhere { };
+			_sessions.for_each([&] (Capture_session const &session) {
+				if (session.bounding_box().valid())
+					result = session.bounding_box().center({ 1, 1 }); });
+			return result;
 		}
 
 		/**
@@ -290,22 +348,12 @@ class Nitpicker::Capture_root : public Root_component<Capture_session>
 
 		void report_displays(Xml_generator &xml) const
 		{
-			bool any_session_present = false;
-			_sessions.for_each([&] (Capture_session const &) {
-				any_session_present = true; });
+			Capture_session::gen_attr(xml, _view_stack.bounding_box());
 
-			if (!any_session_present)
-				return;
-
-			Area const size = bounding_box();
-
-			if (size.count() == 0)
-				return;
-
-			xml.node("display", [&] () {
-				xml.attribute("width",  size.w);
-				xml.attribute("height", size.h);
-			});
+			_sessions.for_each([&] (Capture_session const &capture) {
+				xml.node("capture", [&] {
+					xml.attribute("name", capture.label());
+					Capture_session::gen_attr(xml, capture.bounding_box()); }); });
 		}
 };
 
@@ -361,7 +409,10 @@ class Nitpicker::Event_root : public Root_component<Event_session>
 struct Nitpicker::Main : Focus_updater, Hover_updater,
                          View_stack::Damage,
                          Capture_session::Handler,
-                         Event_session::Handler
+                         Event_session::Handler,
+                         Capture_root::Action,
+                         User_state::Action
+
 {
 	Env &_env;
 
@@ -425,7 +476,7 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 
 		Canvas<PT> _screen { _fb_ds.local_addr<PT>(), Point(0, 0), _mode.area };
 
-		Area const _size = _screen.size();
+		Rect const _rect { { 0, 0 }, _screen.size() };
 
 		using Dirty_rect = Genode::Dirty_rect<Rect, 3>;
 
@@ -458,7 +509,7 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 		{
 			_fb.mode_sigh(_main._fb_screen_mode_handler);
 			_fb.sync_sigh(_sync_handler);
-			mark_as_dirty(Rect { Point { 0, 0 }, _size });
+			mark_as_dirty(_rect);
 		}
 
 		~Framebuffer_screen()
@@ -474,6 +525,10 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 			if (_main._now().ms - _previous_sync.ms > 40)
 				_handle_sync();
 		}
+
+		bool visible(Point p) const { return _rect.contains(p); }
+
+		Point anywhere() const { return _rect.center({ 1, 1 }); };
 	};
 
 	bool _request_framebuffer = false;
@@ -481,6 +536,19 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 
 	Constructible<Framebuffer_screen> _fb_screen { };
 
+	bool _visible_at_fb_screen(Pointer pointer) const
+	{
+		return pointer.convert<bool>(
+			[&] (Point p) { return _fb_screen.constructed() && _fb_screen->visible(p); },
+			[&] (Nowhere) { return false; });
+	}
+
+	Pointer _anywhere_at_fb_screen() const
+	{
+		return _fb_screen.constructed() ? Pointer { _fb_screen->anywhere() }
+		                                : Pointer { Nowhere { } };
+	}
+
 	Signal_handler<Main> _fb_screen_mode_handler {
 		_env.ep(), *this, &Main::_reconstruct_fb_screen };
 
@@ -518,7 +586,7 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 
 	Focus      _focus { };
 	View_stack _view_stack { _focus, _font, *this };
-	User_state _user_state { _focus, _global_keys, _view_stack };
+	User_state _user_state { *this, _focus, _global_keys, _view_stack };
 
 	View_owner _global_view_owner { };
 
@@ -550,7 +618,7 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 	                     _builtin_background, _sliced_heap,
 	                     _focus_reporter, *this, *this };
 
-	Capture_root _capture_root { _env, _sliced_heap, _view_stack, *this };
+	Capture_root _capture_root { _env, *this, _sliced_heap, _view_stack, *this };
 
 	Event_root _event_root { _env, _sliced_heap, *this };
 
@@ -575,7 +643,7 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 
 	void _update_input_connection()
 	{
-		bool const output_present = (_view_stack.size().count() > 0);
+		bool const output_present = (_view_stack.bounding_box().valid());
 		_input.conditional(_request_input && output_present, _env, *this);
 	}
 
@@ -589,18 +657,21 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 		 * present output back ends.
 		 */
 
-		Area new_size { 0, 0 };
+		Rect new_bb { };
 
 		if (_fb_screen.constructed())
-			new_size = max_area(new_size, _fb_screen->_size);
+			new_bb = Rect::compound(new_bb, Rect { _fb_screen->_rect });
 
-		new_size = max_area(new_size, _capture_root.bounding_box());
+		new_bb = Rect::compound(new_bb, _capture_root.bounding_box());
 
-		bool const size_changed = (new_size != _view_stack.size());
+		bool const size_changed = (new_bb != _view_stack.bounding_box());
 
 		if (size_changed) {
-			_view_stack.size(new_size);
-			_user_state.sanitize_pointer_position();
+			_view_stack.bounding_box(new_bb);
+
+			if (!_user_state.pointer().ok())
+				_user_state.pointer(_capture_root.any_visible_pointer_position());
+
 			_update_pointer_position();
 			_capture_root.screen_size_changed();
 
@@ -626,6 +697,22 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 			s->submit_sync();
 	}
 
+	/**
+	 * User_state::Action interface
+	 */
+	Pointer sanitized_pointer_position(Pointer const orig_pos, Point pos) override
+	{
+		if (_capture_root.visible(pos) || _visible_at_fb_screen(pos))
+			return pos;
+
+		if (_capture_root.visible(orig_pos) || _visible_at_fb_screen(orig_pos))
+			return orig_pos;
+
+		Pointer const captured_pos = _capture_root.any_visible_pointer_position();
+
+		return captured_pos.ok() ? captured_pos : _anywhere_at_fb_screen();
+	}
+
 	/**
 	 * Focus_updater interface
 	 *
@@ -652,15 +739,25 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 	 * manually to turn the initial configuration into effect.
 	 */
 	void _handle_config();
+	void _apply_capture_config();
 
-	Signal_handler<Main> _config_handler = { _env.ep(), *this, &Main::_handle_config };
+	Signal_handler<Main> _config_handler { _env.ep(), *this, &Main::_handle_config };
+
+	/**
+	 * Capture_root::Action interface
+	 */
+	void capture_client_appeared_or_disappeared() override
+	{
+		_apply_capture_config();
+		capture_buffer_size_changed();
+	}
 
 	/**
 	 * Signal handler for externally triggered focus changes
 	 */
 	void _handle_focus();
 
-	Signal_handler<Main> _focus_handler = { _env.ep(), *this, &Main::_handle_focus };
+	Signal_handler<Main> _focus_handler { _env.ep(), *this, &Main::_handle_focus };
 
 	/**
 	 * Event_session::Handler interface
@@ -692,7 +789,10 @@ struct Nitpicker::Main : Focus_updater, Hover_updater,
 
 	void _update_pointer_position()
 	{
-		_view_stack.geometry(_pointer_origin, Rect(_user_state.pointer_pos(), Area{}));
+		_user_state.pointer().with_result(
+			[&] (Point p) {
+				_view_stack.geometry(_pointer_origin, Rect(p, Area{})); },
+			[&] (Nowhere) { });
 	}
 
 	Main(Env &env) : _env(env)
@@ -836,6 +936,15 @@ void Nitpicker::Main::_handle_focus()
 }
 
 
+void Nitpicker::Main::_apply_capture_config()
+{
+	/* propagate capture policies */
+	_config_rom.xml().with_optional_sub_node("capture",
+		[&] (Xml_node const &capture) {
+			_capture_root.apply_config(capture); });
+}
+
+
 void Nitpicker::Main::_handle_config()
 {
 	_config_rom.update();
@@ -859,6 +968,8 @@ void Nitpicker::Main::_handle_config()
 	configure_reporter(config, _clicked_reporter);
 	configure_reporter(config, _displays_reporter);
 
+	_apply_capture_config();
+
 	/* update domain registry and session policies */
 	for (Gui_session *s = _session_list.first(); s; s = s->next())
 		s->reset_domain();
@@ -927,12 +1038,8 @@ void Nitpicker::Main::_report_displays()
 		return;
 
 	Reporter::Xml_generator xml(_displays_reporter, [&] () {
-		if (_fb_screen.constructed()) {
-			xml.node("display", [&] () {
-				xml.attribute("width",  _fb_screen->_size.w);
-				xml.attribute("height", _fb_screen->_size.h);
-			});
-		}
+		if (_fb_screen.constructed())
+			xml.node("display", [&] { gen_attr(xml, _fb_screen->_rect); });
 
 		_capture_root.report_displays(xml);
 	});
diff --git a/repos/os/src/server/nitpicker/types.h b/repos/os/src/server/nitpicker/types.h
index 9ec5042c94..2350eace1a 100644
--- a/repos/os/src/server/nitpicker/types.h
+++ b/repos/os/src/server/nitpicker/types.h
@@ -16,6 +16,7 @@
 
 /* Genode includes */
 #include <util/xml_node.h>
+#include <util/xml_generator.h>
 #include <util/color.h>
 #include <base/allocator.h>
 #include <gui_session/gui_session.h>
@@ -39,6 +40,23 @@ namespace Nitpicker {
 	{
 		return Area(max(a1.w, a2.w), max(a1.h, a2.h));
 	}
+
+	struct Nowhere { };
+
+	using Pointer = Attempt<Point, Nowhere>;
+
+	static inline void gen_attr(Xml_generator &xml, Point const point)
+	{
+		if (point.x) xml.attribute("xpos", point.x);
+		if (point.y) xml.attribute("ypos", point.y);
+	}
+
+	static inline void gen_attr(Xml_generator &xml, Rect const rect)
+	{
+		gen_attr(xml, rect.at);
+		if (rect.w()) xml.attribute("width",  rect.w());
+		if (rect.h()) xml.attribute("height", rect.h());
+	}
 }
 
 #endif /* _TYPES_H_ */
diff --git a/repos/os/src/server/nitpicker/user_state.cc b/repos/os/src/server/nitpicker/user_state.cc
index e36e0c01d2..3d12891ad2 100644
--- a/repos/os/src/server/nitpicker/user_state.cc
+++ b/repos/os/src/server/nitpicker/user_state.cc
@@ -94,23 +94,26 @@ void User_state::_handle_input_event(Input::Event ev)
 
 	/* transparently convert relative into absolute motion event */
 	ev.handle_relative_motion([&] (int x, int y) {
-
-		int const ox = _pointer_pos.x,
-		          oy = _pointer_pos.y;
-
-		int const ax = max(0, min((int)_view_stack.size().w - 1, ox + x)),
-		          ay = max(0, min((int)_view_stack.size().h - 1, oy + y));
-
-		ev = Absolute_motion{ax, ay};
+		_pointer.with_result(
+			[&] (Point orig_pos) {
+				Point const p = orig_pos + Point { x, y };
+				ev = Absolute_motion { p.x, p.y }; },
+			[&] (Nowhere) { });
 	});
 
 	/* respond to motion events by updating the pointer position */
 	ev.handle_absolute_motion([&] (int x, int y) {
-		_pointer_pos = Point(x, y); });
+		_try_move_pointer({ x, y });
+
+		/* enforce sanitized position (prevent move to invisible areas) */
+		_pointer.with_result(
+			[&] (Point p) { ev = Absolute_motion { p.x, p.y }; },
+			[&] (Nowhere) { ev = { }; });
+	});
 
 	/* let pointer position correspond to most recent touch position */
 	ev.handle_touch([&] (Input::Touch_id, float x, float y) {
-		_pointer_pos = Point((int)x, (int)y); });
+		_try_move_pointer({ int(x), int(y) }); });
 
 	/* track key states, drop double press/release events */
 	{
@@ -188,9 +191,11 @@ void User_state::_handle_input_event(Input::Event ev)
 				_focused->submit_input_event(Focus_leave());
 
 			if (_hovered) {
-				_hovered->submit_input_event(Absolute_motion{_pointer_pos.x,
-				                                             _pointer_pos.y});
-				_hovered->submit_input_event(Focus_enter());
+				_pointer.with_result(
+					[&] (Point p) {
+						_hovered->submit_input_event(Absolute_motion{p.x, p.y});
+						_hovered->submit_input_event(Focus_enter()); },
+					[&] (Nowhere) { });
 			}
 
 			if (_hovered->has_transient_focusable_domain()) {
@@ -311,7 +316,7 @@ void User_state::_handle_input_event(Input::Event ev)
 User_state::Handle_input_result
 User_state::handle_input_events(Input_batch batch)
 {
-	Point              const old_pointer_pos    = _pointer_pos;
+	Pointer            const old_pointer        = _pointer;
 	View_owner       * const old_hovered        = _hovered;
 	View_owner const * const old_focused        = _focused;
 	View_owner const * const old_input_receiver = _input_receiver;
@@ -389,13 +394,23 @@ User_state::handle_input_events(Input_batch batch)
 		_last_clicked_redeliver = false;
 	}
 
+	auto pointer_changed = [&]
+	{
+		return old_pointer.convert<bool>(
+			[&] (Point const old) {
+				return _pointer.convert<bool>(
+					[&] (Point const p) { return p != old; },
+					[&] (Nowhere)       { return true; }); },
+			[&] (Nowhere) { return _pointer.ok(); });
+	};
+
 	return {
 		.hover_changed        = _hovered != old_hovered,
 		.focus_changed        = (_focused != old_focused) ||
 		                        (_input_receiver != old_input_receiver),
 		.key_state_affected   = key_state_affected,
 		.button_activity      = button_activity,
-		.motion_activity      = (_pointer_pos != old_pointer_pos) || touch_occurred,
+		.motion_activity      = pointer_changed() || touch_occurred,
 		.key_pressed          = _key_pressed(),
 		.last_clicked_changed = last_clicked_changed
 	};
@@ -411,8 +426,8 @@ void User_state::report_keystate(Xml_generator &xml) const
 
 void User_state::report_pointer_position(Xml_generator &xml) const
 {
-	xml.attribute("xpos", _pointer_pos.x);
-	xml.attribute("ypos", _pointer_pos.y);
+	_pointer.with_result([&] (Point p) { gen_attr(xml, p); },
+	                     [&] (Nowhere) { });
 }
 
 
@@ -486,9 +501,12 @@ User_state::Update_hover_result User_state::update_hover()
 		return { .hover_changed = false };
 
 	View_owner * const old_hovered  = _hovered;
-	View const * const pointed_view = _view_stack.find_view(_pointer_pos);
 
-	_hovered = pointed_view ? &pointed_view->owner() : nullptr;
+	_hovered = _pointer.convert<View_owner *>(
+		[&] (Point const p) {
+			View const * const pointed_view = _view_stack.find_view(p);
+			return pointed_view ? &pointed_view->owner() : nullptr; },
+		[&] (Nowhere) { return nullptr; });
 
 	/*
 	 * Deliver a leave event if pointed-to session changed, notify newly
@@ -499,8 +517,10 @@ User_state::Update_hover_result User_state::update_hover()
 			old_hovered->submit_input_event(Hover_leave());
 
 		if (_hovered)
-			_hovered->submit_input_event(Absolute_motion{_pointer_pos.x,
-			                                             _pointer_pos.y});
+			_pointer.with_result(
+				[&] (Point p) {
+					_hovered->submit_input_event(Absolute_motion{p.x, p.y}); },
+				[&] (Nowhere) { });
 	}
 
 	return { .hover_changed = (_hovered != old_hovered) };
diff --git a/repos/os/src/server/nitpicker/user_state.h b/repos/os/src/server/nitpicker/user_state.h
index 821017f027..083f2f66f9 100644
--- a/repos/os/src/server/nitpicker/user_state.h
+++ b/repos/os/src/server/nitpicker/user_state.h
@@ -29,6 +29,19 @@ namespace Nitpicker { class User_state; }
 
 class Nitpicker::User_state
 {
+	public:
+
+		struct Action : Interface
+		{
+			/**
+			 * Return the pointer position when attempting to move to 'pos'
+			 *
+			 * This policy hook enables the restriction of pointer movements
+			 * to those areas that are captured.
+			 */
+			virtual Pointer sanitized_pointer_position(Pointer orig, Point pos) = 0;
+		};
+
 	private:
 
 		/*
@@ -65,6 +78,8 @@ class Nitpicker::User_state
 		 */
 		bool _focus_via_click = true;
 
+		Action &_action;
+
 		/**
 		 * Input-focus information propagated to the view stack
 		 */
@@ -80,16 +95,10 @@ class Nitpicker::User_state
 		 */
 		View_stack &_view_stack;
 
-		/**
-		 * True once the initial screen size becomes known and used as the
-		 * initial (centered) pointer position.
-		 */
-		bool _initial_pointer_position_defined = false;
-
 		/*
 		 * Current pointer position
 		 */
-		Point _pointer_pos { };
+		Pointer _pointer = Nowhere { };
 
 		/*
 		 * Currently pointed-at view owner
@@ -199,6 +208,11 @@ class Nitpicker::User_state
 			}
 		}
 
+		void _try_move_pointer(Point next)
+		{
+			_pointer = _action.sanitized_pointer_position(_pointer, next);
+		}
+
 	public:
 
 		/**
@@ -207,29 +221,16 @@ class Nitpicker::User_state
 		 * \param focus  exported focus information, to be consumed by the
 		 *               view stack to tailor its view drawing operations
 		 */
-		User_state(Focus &focus, Global_keys &global_keys, View_stack &view_stack)
+		User_state(Action &action, Focus &focus, Global_keys &global_keys,
+		           View_stack &view_stack)
 		:
-			_focus(focus), _global_keys(global_keys), _view_stack(view_stack)
+			_action(action), _focus(focus), _global_keys(global_keys),
+			_view_stack(view_stack)
 		{ }
 
-		/**
-		 * Called whenever the view-stack size has changed
-		 */
-		void sanitize_pointer_position()
-		{
-			Area const screen_size = _view_stack.size();
+		void pointer(Pointer p) { _pointer = p; }
 
-			/* center pointer initially */
-			if (!_initial_pointer_position_defined) {
-				_pointer_pos = Point(screen_size.w/2, screen_size.h/2);
-				_initial_pointer_position_defined = true;
-			}
-
-			/* ensure that pointer remains within screen boundaries */
-			if (screen_size.count() > 0)
-				_pointer_pos = Point(min((int)screen_size.w - 1, _pointer_pos.x),
-				                     min((int)screen_size.h - 1, _pointer_pos.y));
-		}
+		Pointer pointer() const { return _pointer; }
 
 
 		/****************************************
@@ -272,8 +273,6 @@ class Nitpicker::User_state
 		void report_focused_view_owner(Xml_generator &, bool button_active) const;
 		void report_last_clicked_view_owner(Xml_generator &) const;
 
-		Point pointer_pos() { return _pointer_pos; }
-
 		/**
 		 * Enable/disable direct focus changes by clicking on a client
 		 */
diff --git a/repos/os/src/server/nitpicker/view_stack.cc b/repos/os/src/server/nitpicker/view_stack.cc
index d76235655c..5012f9be34 100644
--- a/repos/os/src/server/nitpicker/view_stack.cc
+++ b/repos/os/src/server/nitpicker/view_stack.cc
@@ -145,7 +145,7 @@ void View_stack::_place_labels(Rect rect)
 			Rect old = view->label_rect(), best;
 
 			/* calculate best visible label position */
-			Rect rect = Rect::intersect(Rect(Point(), _size), view_rect);
+			Rect rect = Rect::intersect(_bounding_box, view_rect);
 			if (start) _optimize_label_rec(start, view, rect, &best);
 
 			/*
@@ -239,13 +239,13 @@ void View_stack::geometry(View &view, Rect const rect)
 	 * views. The 'refresh_view' function takes care to constrain the
 	 * refresh to the actual view geometry.
 	 */
-	refresh_view(view, Rect(Point(), _size));
+	refresh_view(view, _bounding_box);
 
 	/* change geometry */
 	view.geometry(Rect(rect));
 
 	/* refresh new view geometry */
-	refresh_view(view, Rect(Point(), _size));
+	refresh_view(view, _bounding_box);
 
 	Rect const compound = Rect::compound(old_outline, _outline(view));
 
@@ -259,7 +259,7 @@ void View_stack::buffer_offset(View &view, Point const buffer_off)
 {
 	view.buffer_off(buffer_off);
 
-	refresh_view(view, Rect(Point(), _size));
+	refresh_view(view, _bounding_box);
 }
 
 
diff --git a/repos/os/src/server/nitpicker/view_stack.h b/repos/os/src/server/nitpicker/view_stack.h
index 612c71fe1a..510846bc12 100644
--- a/repos/os/src/server/nitpicker/view_stack.h
+++ b/repos/os/src/server/nitpicker/view_stack.h
@@ -32,7 +32,7 @@ class Nitpicker::View_stack
 
 	private:
 
-		Area                   _size { };
+		Rect                   _bounding_box { };
 		Focus                 &_focus;
 		Font            const &_font;
 		List<View_stack_elem>  _views { };
@@ -99,11 +99,11 @@ class Nitpicker::View_stack
 		/**
 		 * Return size
 		 */
-		Area size() const { return _size; }
+		Rect bounding_box() const { return _bounding_box; }
 
-		void size(Area size)
+		void bounding_box(Rect rect)
 		{
-			_size = size;
+			_bounding_box = rect;
 
 			update_all_views();
 		}
@@ -128,10 +128,8 @@ class Nitpicker::View_stack
 		 */
 		void update_all_views()
 		{
-			Rect const whole_screen(Point(), _size);
-
-			_place_labels(whole_screen);
-			_damage.mark_as_damaged(whole_screen);
+			_place_labels(_bounding_box);
+			_damage.mark_as_damaged(_bounding_box);
 		}
 
 		/**