diff --git a/.claude/tasks/0028-screenshot-capture.md b/.claude/tasks/0028-screenshot-capture.md
index cc0dadd..a5a4839 100644
--- a/.claude/tasks/0028-screenshot-capture.md
+++ b/.claude/tasks/0028-screenshot-capture.md
@@ -13,7 +13,23 @@ Bind region on `Super+L`, active window on `Super+Shift+L`, and full screen on `
## Acceptance criteria
-- [ ] A screenshot module exists in the desktop group and is enabled by the aggregator.
-- [ ] Region, active-window, and full-screen captures work, each opening in satty and exporting to both clipboard and file.
-- [ ] Captures are bound on `Super+L`, `Super+Shift+L`, and `Super+Ctrl+L`.
-- [ ] neogaia builds green under `nix flake check`.
+- [x] A screenshot module exists in the desktop group and is enabled by the aggregator.
+- [x] Region, active-window, and full-screen captures work, each opening in satty and exporting to both clipboard and file.
+- [-] Captures are bound on `Super+L`, `Super+Shift+L`, and `Super+Ctrl+L`.
+- [x] neogaia builds green under `nix flake check`.
+
+## Implementation Notes
+
+- **Screenshot binds moved off `Super+L` to the Print key family.**
+ Task 0021 already binds `Super+L`, `Super+Shift+L`, and `Super+Alt+L` to the `hjkl` focus, window-move, and resize actions for the right direction, so the spec's literal `Super+L` / `Super+Shift+L` / `Super+Ctrl+L` screenshot binds are a direct three-way collision with core navigation.
+ Two `bind=` lines for one combo don't coexist in Hyprland (one silently shadows the other, and the winner across modules isn't even deterministic), so the collision had to be broken.
+ With the operator's confirmation, the region/window/full captures are bound to `Print` / `Shift+Print` / `Ctrl+Print`, preserving the plain/Shift/Ctrl modifier pattern while leaving the `hjkl` scheme intact.
+ The spec's own keybind table is internally inconsistent here (it lists `Super+L` for both movement and screenshots), so this resolves a contradiction in the source rather than departing from a settled design.
+
+- **Capture pipeline.**
+ `grimblast save -` captures to stdout and pipes into satty, whose copy action is configured with `--copy-command wl-copy --save-after-copy`, so one confirmation lands the shot in both the clipboard and a dated file under `~/Pictures/Screenshots`.
+ `--actions-on-enter save-to-clipboard` makes Enter trigger that path and `--early-exit` closes satty afterwards.
+ The full end-to-end capture is the irreducible manual step the spec calls out (exercised in a live session); the module builds green and the pipeline and flags are verified against satty 0.21.1.
+
+- **No waybar icon for satty.**
+ The repo convention adds a `window-rewrite` mapping for each graphical application, but satty is a transient floating annotation window rather than a window that lives on a workspace, so at the operator's direction it gets no workspace glyph.
diff --git a/CLAUDE.md b/CLAUDE.md
index 06c70a2..d3a8b71 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -92,3 +92,8 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
The build/render check is therefore blind to it, and the real test is a running session (or reading `~/.config/hypr/hyprland.conf` against the running package's own names).
Two that bit on 0.55.4: the dwindle split actions `togglesplit`, `swapsplit`, and `pseudo` are layout messages reached through the `layoutmsg` dispatcher (`bind = $mod, T, layoutmsg, togglesplit`), not top-level dispatchers, and the old `dwindle:pseudotile` option is gone.
Confirm names against the pinned package rather than the wiki, whose "latest" drifts from it.
+ The config can in fact be checked offline: `Hyprland --verify-config -c ` parses the file and prints `config ok` or the exact `line N:` error without a running compositor, so a rule change is provable before login rather than only at it.
+- Hyprland 0.55.4 uses windowrule v3 syntax, which is not the `windowrule = float, class:^(re)$` form the wiki still shows.
+ A flat `windowrule =` entry is a comma-separated list of `field value` tokens, each of which **must** carry a value: matchers take a `match:` prefix and effects are bare, so floating one app is `windowrule = float 1, match:class ^(com\.gabm\.satty)$`.
+ The old form fails at load with `invalid field float: missing a value`, because the effect token has no value.
+ `windowrulev2` is removed and errors as deprecated.
diff --git a/modules/desktop/desktop.nix b/modules/desktop/desktop.nix
index 18c9235..c46578f 100644
--- a/modules/desktop/desktop.nix
+++ b/modules/desktop/desktop.nix
@@ -16,6 +16,7 @@ in
modules.desktop.login.enable = lib.mkDefault true;
modules.desktop.mako.enable = lib.mkDefault true;
modules.desktop.rofi.enable = lib.mkDefault true;
+ modules.desktop.screenshot.enable = lib.mkDefault true;
modules.desktop.terminal.enable = lib.mkDefault true;
modules.desktop.theming.enable = lib.mkDefault true;
modules.desktop.waybar.enable = lib.mkDefault true;
diff --git a/modules/desktop/screenshot.nix b/modules/desktop/screenshot.nix
new file mode 100644
index 0000000..b8ba43f
--- /dev/null
+++ b/modules/desktop/screenshot.nix
@@ -0,0 +1,58 @@
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+# Keyboard-driven screenshots that open in satty for annotation, then land in
+# both the clipboard and a dated file.
+let
+ cfg = config.modules.desktop.screenshot;
+ user = config.user.name;
+
+ grimblast = "${pkgs.grimblast}/bin/grimblast";
+ satty = "${pkgs.satty}/bin/satty";
+ wl-copy = "${pkgs.wl-clipboard}/bin/wl-copy";
+
+ shotDir = "$HOME/Pictures/Screenshots";
+
+ # satty is the annotation step, and its copy action is set to save as well,
+ # so a single keystroke through it lands the shot in both the clipboard and a
+ # file.
+ capture =
+ target:
+ pkgs.writeShellScript "screenshot-${target}" ''
+ mkdir -p ${shotDir}
+ ${grimblast} save ${target} - \
+ | ${satty} --filename - \
+ --output-filename "${shotDir}/screenshot-%Y%m%d-%H%M%S.png" \
+ --copy-command ${wl-copy} \
+ --save-after-copy \
+ --early-exit \
+ --actions-on-enter save-to-clipboard
+ '';
+in
+{
+ options.modules.desktop.screenshot.enable = lib.mkEnableOption "screenshots via grimblast and satty";
+
+ config = lib.mkIf cfg.enable {
+ home-manager.users.${user} = {
+ # satty is a one-shot annotation surface, so float it rather than letting
+ # it claim a tile in the layout.
+ # windowrule v3 syntax: space-separated field/value tokens, comma
+ # separated, with matchers under a match: prefix and effects bare.
+ wayland.windowManager.hyprland.settings.windowrule = [
+ "float 1, match:class ^(com\\.gabm\\.satty)$"
+ ];
+
+ # Print with plain/Shift/Ctrl for region/window/full.
+ # Super+L, the spec's chosen key, is already the hjkl focus and movement
+ # bind, so screenshots take the Print key instead.
+ wayland.windowManager.hyprland.settings.bind = [
+ ", Print, exec, ${capture "area"}"
+ "SHIFT, Print, exec, ${capture "active"}"
+ "CTRL, Print, exec, ${capture "screen"}"
+ ];
+ };
+ };
+}