diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6350,6 +6350,7 @@ dependencies = [
  "craft-ui",
  "craft-update",
  "egui",
+ "egui-wgpu",
  "egui_extras",
  "egui_kittest",
  "image",
@@ -6377,6 +6378,7 @@ dependencies = [
  "vectorcraft-tools",
  "wat",
  "web-time",
+ "wgpu",
 ]
 
 [[package]]
diff --git a/apps/vectorcraft-web/src/web.rs b/apps/vectorcraft-web/src/web.rs
--- a/apps/vectorcraft-web/src/web.rs
+++ b/apps/vectorcraft-web/src/web.rs
@@ -115,6 +115,7 @@ async fn run(host: Host, canvas: web_sys::HtmlCanvasElement, moving: Option<Movi
                 watch_canvas(&page, &loss, ctx);
                 if let Some(rs) = &cc.wgpu_render_state {
                     log::info!("vectorcraft-web: wgpu backend {:?}", rs.adapter.get_info().backend);
+                    vectorcraft_ui_egui::gpu_invert::setup(rs);
                     let (loss, ctx) = (loss.clone(), ctx.clone());
                     rs.device.set_device_lost_callback(move |reason, msg| loss.report(&ctx, format!("{reason:?}: {msg}")));
                 }
diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs
--- a/apps/vectorcraft/src/main.rs
+++ b/apps/vectorcraft/src/main.rs
@@ -1099,6 +1099,7 @@ fn main() -> std::process::ExitCode {
                     let summary = adapter_summary(&rs.adapter.get_info());
                     log::info!("rendering with {summary} (power preference {power:?})");
                     created.created(&cc.egui_ctx);
+                    vectorcraft_ui_egui::gpu_invert::setup(rs);
                     if !gpu::skipped().is_empty() {
                         // A processor chosen in Settings can be the one that can't show the window.
                         let hint = if chosen_gpu {
diff --git a/contributors/people.toml b/contributors/people.toml
--- a/contributors/people.toml
+++ b/contributors/people.toml
@@ -155,6 +155,10 @@ display_name = "Don Omondi"
 [people.alpha-rock]
 display_name = "Aditya Sisodiya"
 
+[people.MajedAlaejem]
+real_name = "Majed Alaejem"
+sync_github_name = true
+
 [people.Ronfo2013]
 real_name = "Angelo Bernardini"
 display_name = "Angelo Bernardini"
diff --git a/crates/engine/src/tests_eyedropper.rs b/crates/engine/src/tests_eyedropper.rs
--- a/crates/engine/src/tests_eyedropper.rs
+++ b/crates/engine/src/tests_eyedropper.rs
@@ -657,7 +657,7 @@ fn eyedropper_shift_color_preserves_other_characters_and_clears_on_deselect() {
     assert_eq!(t.runs[2].style, before.first_style());
     assert_eq!(t.runs[1].style.fill.color().unwrap().to_hex(), "#00ff00");
     assert_eq!(t.runs[1].style.size, before.first_style().size);
-    assert!(s.overlays(view).iter().any(|o| matches!(o, vectorcraft_tools::Overlay::Highlight { .. })), "the highlighted range remains visible");
+    assert!(s.overlays(view).iter().any(|o| matches!(o, vectorcraft_tools::Overlay::Invert { .. } | vectorcraft_tools::Overlay::Highlight { .. })), "the highlighted range remains visible");
     s.execute("select.none", &json!({})).unwrap();
     assert!(s.tool_options().get("editing").is_none_or(Value::is_null));
     s.execute("select.set", &json!({"ids": [dst.0]})).unwrap();
diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs
--- a/crates/tools/src/lib.rs
+++ b/crates/tools/src/lib.rs
@@ -469,6 +469,8 @@ pub enum Overlay {
     Distance { a: Point, b: Point, text: String, color: [u8; 3] },
     /// Translucent filled quad (text selection highlight), RGBA.
     Highlight { quad: [Point; 4], color: [u8; 4] },
+    /// Inverting quad (inverse-video text selection highlight, matching reference behaviour).
+    Invert { quad: [Point; 4] },
     /// A colour chip of fixed screen size (a gradient stop), RGBA; ringed when selected.
     Swatch { p: Point, color: [u8; 4], selected: bool },
     /// A hairline in a translucent colour (perspective gridlines), RGBA.
diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs
--- a/crates/tools/src/text.rs
+++ b/crates/tools/src/text.rs
@@ -1486,7 +1486,7 @@ impl Tool for TypeTool {
             o.push(Overlay::Line { a: t.xf * p, b: t.xf * q, color: [0, 0, 0], dashed: false });
         } else {
             for q in vectorcraft_text::selection_quads(&lay, a, b) {
-                o.push(Overlay::Highlight { quad: q.map(|p| t.xf * p), color: [60, 120, 255, 90] });
+                o.push(Overlay::Invert { quad: q.map(|p| t.xf * p) });
             }
         }
         if let Some(id) = self.editing.filter(|id| shown && ported(cx.doc, *id).is_some()) {
diff --git a/crates/tools/src/text_tests.rs b/crates/tools/src/text_tests.rs
--- a/crates/tools/src/text_tests.rs
+++ b/crates/tools/src/text_tests.rs
@@ -342,7 +342,7 @@ fn drag_selects_and_overlay_highlights() {
     tool.pointer(&c, &PointerEvent::new(PointerKind::Up, b.x + 0.1, b.y));
     assert_eq!(tool.sel(), (7, 9));
     let o = tool.overlays(&c);
-    assert!(o.iter().any(|o| matches!(o, Overlay::Highlight { .. })));
+    assert!(o.iter().any(|o| matches!(o, Overlay::Invert { .. } | Overlay::Highlight { .. })));
     let opts = tool.options();
     assert_eq!((opts["start"].as_u64(), opts["end"].as_u64()), (Some(7), Some(9)));
 }
diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -3404,6 +3404,22 @@ fn names_an_artboard(text: &str) -> bool {
 }
 
 fn draw_overlays(p: &egui::Painter, xf: &Xf, overlays: &[Overlay], t: &Tokens, look: HandleLook) {
+    let invert_quads: Vec<[egui::Pos2; 4]> = overlays
+        .iter()
+        .filter_map(|o| match o {
+            Overlay::Invert { quad } => Some([
+                xf.to_screen(quad[0]),
+                xf.to_screen(quad[1]),
+                xf.to_screen(quad[2]),
+                xf.to_screen(quad[3]),
+            ]),
+            _ => None,
+        })
+        .collect();
+    if !invert_quads.is_empty() {
+        crate::gpu_invert::paint_invert_quads(p, &invert_quads);
+    }
+
     for o in overlays {
         match o {
             // Where the tools select: the document rectangle, turned with the view.
@@ -3444,6 +3460,7 @@ fn draw_overlays(p: &egui::Painter, xf: &Xf, overlays: &[Overlay], t: &Tokens, l
                 let c = Color32::from_rgba_unmultiplied(color[0], color[1], color[2], color[3]);
                 p.add(Shape::convex_polygon(quad.iter().map(|q| xf.to_screen(*q)).collect(), c, Stroke::NONE));
             }
+            Overlay::Invert { .. } => {}
             Overlay::Measure { p: pt, text } => {
                 let sp = xf.to_screen(*pt) + vec2(14.0, 14.0);
                 let galley = p.layout(text.clone(), egui::FontId::proportional(11.0), Color32::WHITE, 200.0);
diff --git a/crates/ui-egui/src/gpu_invert.rs b/crates/ui-egui/src/gpu_invert.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/src/gpu_invert.rs
@@ -0,0 +1,274 @@
+use egui_wgpu::CallbackTrait;
+
+const MAX_QUADS: usize = 64;
+
+const INVERT_WGSL: &str = r#"
+struct VertexOutput {
+    @builtin(position) clip_position: vec4<f32>,
+};
+
+struct Quad {
+    v0: vec2<f32>,
+    v1: vec2<f32>,
+    v2: vec2<f32>,
+    v3: vec2<f32>,
+};
+
+struct QuadsUniform {
+    quads: array<Quad, 64>,
+};
+
+@group(0) @binding(0)
+var<uniform> u_data: QuadsUniform;
+
+@vertex
+fn vs_main(@builtin(vertex_index) v_idx: u32, @builtin(instance_index) i_idx: u32) -> VertexOutput {
+    let q = u_data.quads[i_idx];
+    var p = vec2<f32>(0.0, 0.0);
+    switch (v_idx) {
+        case 0u: { p = q.v0; }
+        case 1u: { p = q.v1; }
+        case 2u: { p = q.v2; }
+        case 3u: { p = q.v0; }
+        case 4u: { p = q.v2; }
+        default: { p = q.v3; }
+    }
+    var out: VertexOutput;
+    out.clip_position = vec4<f32>(p.x, p.y, 0.0, 1.0);
+    return out;
+}
+
+@fragment
+fn fs_main() -> @location(0) vec4<f32> {
+    return vec4<f32>(1.0, 1.0, 1.0, 1.0);
+}
+"#;
+
+#[repr(C)]
+#[derive(Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct GpuQuad {
+    pub v0: [f32; 2],
+    pub v1: [f32; 2],
+    pub v2: [f32; 2],
+    pub v3: [f32; 2],
+}
+
+pub struct InvertResources {
+    pub pipeline: wgpu::RenderPipeline,
+    pub uniform_buffer: wgpu::Buffer,
+    pub bind_group: wgpu::BindGroup,
+}
+
+impl InvertResources {
+    pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
+        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+            label: Some("invert_shader"),
+            source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(INVERT_WGSL)),
+        });
+
+        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+            label: Some("invert_quads_ubo"),
+            size: (MAX_QUADS * std::mem::size_of::<GpuQuad>()) as u64,
+            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
+            mapped_at_creation: false,
+        });
+
+        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+            label: Some("invert_bind_group_layout"),
+            entries: &[wgpu::BindGroupLayoutEntry {
+                binding: 0,
+                visibility: wgpu::ShaderStages::VERTEX,
+                ty: wgpu::BindingType::Buffer {
+                    ty: wgpu::BufferBindingType::Uniform,
+                    has_dynamic_offset: false,
+                    min_binding_size: None,
+                },
+                count: None,
+            }],
+        });
+
+        let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+            label: Some("invert_bind_group"),
+            layout: &bind_group_layout,
+            entries: &[wgpu::BindGroupEntry {
+                binding: 0,
+                resource: uniform_buffer.as_entire_binding(),
+            }],
+        });
+
+        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+            label: Some("invert_pipeline_layout"),
+            bind_group_layouts: &[Some(&bind_group_layout)],
+            immediate_size: 0,
+        });
+
+        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+            label: Some("invert_pipeline"),
+            layout: Some(&pipeline_layout),
+            vertex: wgpu::VertexState {
+                module: &shader,
+                entry_point: Some("vs_main"),
+                compilation_options: Default::default(),
+                buffers: &[],
+            },
+            fragment: Some(wgpu::FragmentState {
+                module: &shader,
+                entry_point: Some("fs_main"),
+                compilation_options: Default::default(),
+                targets: &[Some(wgpu::ColorTargetState {
+                    format,
+                    blend: Some(wgpu::BlendState {
+                        color: wgpu::BlendComponent {
+                            src_factor: wgpu::BlendFactor::OneMinusDst,
+                            dst_factor: wgpu::BlendFactor::Zero,
+                            operation: wgpu::BlendOperation::Add,
+                        },
+                        alpha: wgpu::BlendComponent {
+                            src_factor: wgpu::BlendFactor::Zero,
+                            dst_factor: wgpu::BlendFactor::One,
+                            operation: wgpu::BlendOperation::Add,
+                        },
+                    }),
+                    write_mask: wgpu::ColorWrites::ALL,
+                })],
+            }),
+            primitive: wgpu::PrimitiveState {
+                topology: wgpu::PrimitiveTopology::TriangleList,
+                strip_index_format: None,
+                front_face: wgpu::FrontFace::Ccw,
+                cull_mode: None,
+                unclipped_depth: false,
+                polygon_mode: wgpu::PolygonMode::Fill,
+                conservative: false,
+            },
+            depth_stencil: None,
+            multisample: wgpu::MultisampleState {
+                count: 1,
+                mask: !0,
+                alpha_to_coverage_enabled: false,
+            },
+            multiview_mask: None,
+            cache: None,
+        });
+
+        Self {
+            pipeline,
+            uniform_buffer,
+            bind_group,
+        }
+    }
+}
+
+pub struct InvertCallback {
+    pub quads: Vec<[egui::Pos2; 4]>,
+}
+
+impl CallbackTrait for InvertCallback {
+    fn prepare(
+        &self,
+        _device: &wgpu::Device,
+        queue: &wgpu::Queue,
+        screen_descriptor: &egui_wgpu::ScreenDescriptor,
+        _egui_encoder: &mut wgpu::CommandEncoder,
+        callback_resources: &mut egui_wgpu::CallbackResources,
+    ) -> Vec<wgpu::CommandBuffer> {
+        let Some(res) = callback_resources.get::<InvertResources>() else {
+            return Vec::new();
+        };
+
+        let w = screen_descriptor.size_in_pixels[0] as f32 / screen_descriptor.pixels_per_point.max(0.001);
+        let h = screen_descriptor.size_in_pixels[1] as f32 / screen_descriptor.pixels_per_point.max(0.001);
+        let to_ndc = |p: egui::Pos2| -> [f32; 2] {
+            [(p.x / w) * 2.0 - 1.0, 1.0 - (p.y / h) * 2.0]
+        };
+
+        let mut gpu_quads = [GpuQuad {
+            v0: [0.0; 2],
+            v1: [0.0; 2],
+            v2: [0.0; 2],
+            v3: [0.0; 2],
+        }; MAX_QUADS];
+
+        let count = self.quads.len().min(MAX_QUADS);
+        for i in 0..count {
+            let q = &self.quads[i];
+            gpu_quads[i] = GpuQuad {
+                v0: to_ndc(q[0]),
+                v1: to_ndc(q[1]),
+                v2: to_ndc(q[2]),
+                v3: to_ndc(q[3]),
+            };
+        }
+
+        queue.write_buffer(&res.uniform_buffer, 0, bytemuck::cast_slice(&gpu_quads[..count]));
+
+        Vec::new()
+    }
+
+    fn paint(
+        &self,
+        info: egui::PaintCallbackInfo,
+        render_pass: &mut wgpu::RenderPass<'static>,
+        callback_resources: &egui_wgpu::CallbackResources,
+    ) {
+        let count = self.quads.len().min(MAX_QUADS);
+        if count == 0 {
+            return;
+        }
+        let Some(res) = callback_resources.get::<InvertResources>() else {
+            return;
+        };
+
+        let clip = info.clip_rect_in_pixels();
+        let x = clip.left_px.max(0) as u32;
+        let y = clip.top_px.max(0) as u32;
+        let width = (clip.width_px.max(0) as u32).min(info.screen_size_px[0].saturating_sub(x));
+        let height = (clip.height_px.max(0) as u32).min(info.screen_size_px[1].saturating_sub(y));
+        if width == 0 || height == 0 {
+            return;
+        }
+
+        render_pass.set_viewport(
+            0.0,
+            0.0,
+            info.screen_size_px[0] as f32,
+            info.screen_size_px[1] as f32,
+            0.0,
+            1.0,
+        );
+        render_pass.set_scissor_rect(x, y, width, height);
+
+        render_pass.set_pipeline(&res.pipeline);
+        render_pass.set_bind_group(0, &res.bind_group, &[]);
+        render_pass.draw(0..6, 0..(count as u32));
+    }
+}
+
+pub fn setup(rs: &egui_wgpu::RenderState) {
+    let resources = InvertResources::new(&rs.device, rs.target_format);
+    rs.renderer.write().callback_resources.insert(resources);
+}
+
+pub fn paint_invert_quads(painter: &egui::Painter, quads: &[[egui::Pos2; 4]]) {
+    if quads.is_empty() {
+        return;
+    }
+    let mut min = egui::Pos2::new(f32::INFINITY, f32::INFINITY);
+    let mut max = egui::Pos2::new(f32::NEG_INFINITY, f32::NEG_INFINITY);
+    for q in quads {
+        for pt in q {
+            min.x = min.x.min(pt.x);
+            min.y = min.y.min(pt.y);
+            max.x = max.x.max(pt.x);
+            max.y = max.y.max(pt.y);
+        }
+    }
+    let rect = egui::Rect::from_min_max(min, max);
+    let cb = egui_wgpu::Callback::new_paint_callback(
+        rect,
+        InvertCallback {
+            quads: quads.to_vec(),
+        },
+    );
+    painter.add(cb);
+}
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -30,6 +30,7 @@ pub mod find_font;
 pub mod font_menu;
 mod frame_rate;
 mod free_transform;
+pub mod gpu_invert;
 pub mod graphics;
 pub mod i18n;
 pub mod icon_data;
