diff --git a/apps/staged/src/app.css b/apps/staged/src/app.css
index d57f79c90..f446775db 100644
--- a/apps/staged/src/app.css
+++ b/apps/staged/src/app.css
@@ -216,7 +216,18 @@ body {
background: var(--scrollbar-thumb-hover);
}
-/* Keep compositor hints limited to tiny glyph and badge surfaces. */
+/* Pin tiny glyph rasters on their own compositor layer so repaints
+ can't re-snap them (WKWebView subpixel wobble). Every lucide icon
+ is covered by default; .stable-raster(-glyphs) remain for the few
+ non-lucide raster surfaces.
+
+ Spinning icons are excluded: Tailwind's spin keyframes only define
+ `to { rotate(360deg) }`, so the implicit `from` frame would inherit
+ this translateZ(0) — mismatched transform lists then interpolate via
+ matrices, where rotate(360deg) is the identity, freezing the spinner.
+ A continuously rotating icon can't wobble and is layer-promoted while
+ animating anyway. */
+svg.lucide:not(.animate-spin),
.stable-raster,
.stable-raster-glyphs svg {
transform: translateZ(0);
diff --git a/apps/staged/src/lib/components/ui/badge/badge.svelte b/apps/staged/src/lib/components/ui/badge/badge.svelte
index 405eda89c..9634f85e5 100644
--- a/apps/staged/src/lib/components/ui/badge/badge.svelte
+++ b/apps/staged/src/lib/components/ui/badge/badge.svelte
@@ -2,7 +2,7 @@
import { type VariantProps, tv } from 'tailwind-variants';
export const badgeVariants = tv({
- base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-[color,background-color,border-color,box-shadow,opacity] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge stable-raster stable-raster-glyphs inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:ring-[3px] [&>svg]:pointer-events-none',
+ base: 'h-5 gap-1 rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium transition-[color,background-color,border-color,box-shadow,opacity] has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive group/badge stable-raster inline-flex w-fit shrink-0 items-center justify-center overflow-hidden whitespace-nowrap focus-visible:ring-[3px] [&>svg]:pointer-events-none',
variants: {
variant: {
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
diff --git a/apps/staged/src/lib/components/ui/button/button.svelte b/apps/staged/src/lib/components/ui/button/button.svelte
index 51098b4ae..27440616e 100644
--- a/apps/staged/src/lib/components/ui/button/button.svelte
+++ b/apps/staged/src/lib/components/ui/button/button.svelte
@@ -4,7 +4,7 @@
import { type VariantProps, tv } from 'tailwind-variants';
export const buttonVariants = tv({
- base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button stable-raster-glyphs inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-[color,background-color,border-color,box-shadow,opacity] outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
+ base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-md border border-transparent bg-clip-padding text-sm font-medium focus-visible:ring-3 aria-invalid:ring-3 [&_svg:not([class*='size-'])]:size-4 group/button inline-flex shrink-0 items-center justify-center whitespace-nowrap transition-[color,background-color,border-color,box-shadow,opacity] outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
diff --git a/apps/staged/src/lib/features/agents/AgentIcon.svelte b/apps/staged/src/lib/features/agents/AgentIcon.svelte
index eb3a683ed..0ae18b3f5 100644
--- a/apps/staged/src/lib/features/agents/AgentIcon.svelte
+++ b/apps/staged/src/lib/features/agents/AgentIcon.svelte
@@ -16,6 +16,10 @@
monochrome and inherit currentColor. Goose is a PNG tinted via a CSS mask
(ported from goose-internal's GooseIcon).
+ The raw brand marks aren't lucide icons, so the global svg.lucide
+ compositor pin doesn't reach them — they opt into stable-raster to
+ avoid WKWebView subpixel wobble.
+
Props:
id - provider id (the join key shared by the picker and Doctor)
size - icon dimensions in px (default 16)
@@ -40,11 +44,14 @@
{#if id === 'goose'}
-
{:else if id === 'claude'}