diff --git a/docs/mocha-wallpaper-assets.md b/docs/mocha-wallpaper-assets.md
new file mode 100644
index 0000000..cb5055f
--- /dev/null
+++ b/docs/mocha-wallpaper-assets.md
@@ -0,0 +1,46 @@
+# Mocha wallpaper source configuration
+
+The Noctalia template points its default and DP-1 wallpaper to the supplied
+3840×2160 landscape PNG, and HDMI-A-1 to the separately composed 2160×3840
+portrait PNG. Per-monitor browsing directories use the same pools. These are
+source configuration and assets only; this change does not run `chezmoi apply`,
+change a live session, or start/enable a timer.
+
+The upstream template's `[wallpaper.automation] enabled = false` remains
+unchanged. With automation off and no separately selected scheduler, the
+wallpapers remain static; the supplied one-image-per-orientation pools do not
+provide distinct candidates for rotation. Users who want Noctalia's native
+automation can explicitly set `enabled = true` in their managed configuration.
+
+Rotation alternatives are separate and opt-in. PR #92's
+`ezwallpaper-rotation` integration uses the `ezwallpaper-mocha` pools. Local
+rotation B (PR #95, documented in
+[`ezwallpaper-rotation-b.md`](ezwallpaper-rotation-b.md)) has a path mismatch:
+its profile template currently points to
+`~/.local/share/wallpapers/{landscape,portrait}`, but its preflight guard is
+hard-coded to require real directories at
+`~/.local/share/wallpapers/ezwallpaper-mocha/{landscape,portrait}`. Changing
+the profile alone does not make the guard support arbitrary pool paths; both
+the profile's candidate paths and the guard's fixed paths must satisfy their
+respective checks. Neither profile nor timer is added or enabled by this
+asset/config change, and the two ezwallpaper schedulers must not run together.
+B additionally requires a compatible `~/.local/bin/ezwallpaper` executable,
+valid distinct candidates for rotation, and the session/config/unit safeguards;
+neither that private/local binary nor additional runtime images are supplied
+here. An operator selecting B must resolve the path contract, review scoped
+`chezmoi diff`, establish B as the sole rotation owner, deploy its files
+separately, verify the pools and session prerequisites, and ensure Noctalia's
+managed automation and any GUI override are off before separately enabling B.
+This change does not enable B or run its timer.
+
+Noctalia's native automation is a distinct alternative: users may explicitly
+set its managed `enabled` flag to true. That does not enable either ezwallpaper
+integration, whose preflight requires native automation to be off. If no
+rotation controller is deliberately deployed, this supplied one-image-per-
+orientation configuration remains static; native Noctalia automation can
+instead be opted into separately.
+
+The B preflight is a point-in-time guard, not a lock against later config or
+unit changes, and cannot prove the running Noctalia process reloaded settings
+or that rendered pixels are correct. No live setter, service, timer, reload,
+or apply was run while preparing this source change.
diff --git a/dotfiles/dot_config/noctalia/desktop.toml.tmpl b/dotfiles/dot_config/noctalia/desktop.toml.tmpl
index ed0cd89..0061dd3 100644
--- a/dotfiles/dot_config/noctalia/desktop.toml.tmpl
+++ b/dotfiles/dot_config/noctalia/desktop.toml.tmpl
@@ -60,21 +60,28 @@ enabled = false
[wallpaper]
enabled = true
-directory = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/omarchy"
+directory = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/ezwallpaper-mocha/landscape"
fill_mode = "crop"
transition = ["fade"]
transition_duration = 900.0
transition_on_startup = true
fill_color = "surface"
+per_monitor_directories = true
[wallpaper.default]
-path = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/omarchy/sunset-lake-4k.webp"
+path = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/ezwallpaper-mocha/landscape/mocha-flat-landscape.png"
[wallpaper.monitors.DP-1]
-path = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/omarchy/sunset-lake-4k.webp"
+path = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/ezwallpaper-mocha/landscape/mocha-flat-landscape.png"
[wallpaper.monitors.HDMI-A-1]
-path = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/omarchy/sunset-lake-portrait.webp"
+path = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/ezwallpaper-mocha/portrait/mocha-flat-portrait.png"
+
+[wallpaper.monitor.DP-1]
+directory = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/ezwallpaper-mocha/landscape"
+
+[wallpaper.monitor.HDMI-A-1]
+directory = "{{ .chezmoi.homeDir }}/.local/share/wallpapers/ezwallpaper-mocha/portrait"
[wallpaper.automation]
enabled = false
diff --git a/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/README.md b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/README.md
new file mode 100644
index 0000000..bcf0885
--- /dev/null
+++ b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/README.md
@@ -0,0 +1,12 @@
+# Mocha flat wallpaper assets
+
+This directory contains two project-authored, flat-vector wallpapers: a
+3840×2160 landscape scene and a separately composed 2160×3840 portrait scene.
+Their editable SVG sources are in `source/`; the PNGs in `landscape/` and
+`portrait/` are the files referenced by the Noctalia desktop template. They
+use no external images, fonts, or downloaded assets. The repository MIT
+license applies; no third-party artwork is bundled.
+
+Each runtime pool currently contains one image. This supplies stable desktop
+wallpapers, not a meaningful rotation pool. Existing `landscape/`, `portrait/`,
+and `omarchy/` source assets are not replaced or removed.
diff --git a/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/landscape/mocha-flat-landscape.png b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/landscape/mocha-flat-landscape.png
new file mode 100644
index 0000000..f8692ca
Binary files /dev/null and b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/landscape/mocha-flat-landscape.png differ
diff --git a/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/portrait/mocha-flat-portrait.png b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/portrait/mocha-flat-portrait.png
new file mode 100644
index 0000000..f87b58e
Binary files /dev/null and b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/portrait/mocha-flat-portrait.png differ
diff --git a/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/source/mocha-flat-landscape.svg b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/source/mocha-flat-landscape.svg
new file mode 100644
index 0000000..7175e31
--- /dev/null
+++ b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/source/mocha-flat-landscape.svg
@@ -0,0 +1,11 @@
+
diff --git a/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/source/mocha-flat-portrait.svg b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/source/mocha-flat-portrait.svg
new file mode 100644
index 0000000..bf0945f
--- /dev/null
+++ b/dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha/source/mocha-flat-portrait.svg
@@ -0,0 +1,12 @@
+
diff --git a/tests/test_mocha_wallpaper_assets.py b/tests/test_mocha_wallpaper_assets.py
new file mode 100644
index 0000000..d1b44d4
--- /dev/null
+++ b/tests/test_mocha_wallpaper_assets.py
@@ -0,0 +1,159 @@
+"""Offline contract checks for the bundled Mocha wallpaper assets/config."""
+from __future__ import annotations
+
+import re
+import struct
+import tomllib
+import unittest
+import xml.etree.ElementTree as ET
+import zlib
+from pathlib import Path
+
+
+ROOT = Path(__file__).resolve().parents[1]
+ASSETS = ROOT / "dotfiles/dot_local/share/wallpapers/ezwallpaper-mocha"
+TEMPLATE = ROOT / "dotfiles/dot_config/noctalia/desktop.toml.tmpl"
+DOC = ROOT / "docs/mocha-wallpaper-assets.md"
+B_PROFILE = ROOT / "dotfiles/dot_config/ezwallpaper/rotation.json.tmpl"
+B_PREFLIGHT = ROOT / "dotfiles/dot_local/bin/executable_ezwallpaper-rotation-preflight.py"
+EXPECTED = {
+ "landscape/mocha-flat-landscape.png": (3840, 2160),
+ "portrait/mocha-flat-portrait.png": (2160, 3840),
+}
+
+
+def inspect_png(path: Path) -> tuple[int, int]:
+ """Validate PNG chunks/CRCs and fully decompress non-interlaced RGB data."""
+ data = path.read_bytes()
+ if data[:8] != b"\x89PNG\r\n\x1a\n":
+ raise AssertionError(f"{path} is not a PNG")
+ pos = 8
+ width = height = bit_depth = color_type = interlace = None
+ compressed = bytearray()
+ saw_end = False
+ while pos < len(data):
+ if pos + 12 > len(data):
+ raise AssertionError(f"truncated PNG chunk in {path}")
+ length = struct.unpack_from(">I", data, pos)[0]
+ kind = data[pos + 4 : pos + 8]
+ end = pos + 8 + length
+ if end + 4 > len(data):
+ raise AssertionError(f"truncated PNG payload in {path}")
+ payload = data[pos + 8 : end]
+ expected_crc = struct.unpack_from(">I", data, end)[0]
+ actual_crc = zlib.crc32(kind + payload) & 0xFFFFFFFF
+ if actual_crc != expected_crc:
+ raise AssertionError(f"bad PNG CRC for {kind!r} in {path}")
+ if kind == b"IHDR":
+ if length != 13 or width is not None:
+ raise AssertionError("invalid or repeated PNG IHDR")
+ width, height, bit_depth, color_type, compression, filtering, interlace = struct.unpack(
+ ">IIBBBBB", payload
+ )
+ if compression or filtering or interlace:
+ raise AssertionError("only non-interlaced standard PNGs are supported")
+ elif kind == b"IDAT":
+ compressed.extend(payload)
+ elif kind == b"IEND":
+ if length:
+ raise AssertionError("invalid PNG IEND")
+ saw_end = True
+ pos = end + 4
+ break
+ pos = end + 4
+ if not saw_end or pos != len(data) or width is None:
+ raise AssertionError(f"missing IEND/IHDR or trailing bytes in {path}")
+ if bit_depth != 8 or color_type != 2:
+ raise AssertionError(f"expected 8-bit RGB PNG: depth={bit_depth}, type={color_type}")
+ pixels = zlib.decompress(compressed)
+ if len(pixels) != height * (1 + width * 3):
+ raise AssertionError(f"unexpected decompressed scanline size in {path}")
+ return width, height
+
+
+class MochaWallpaperContractTests(unittest.TestCase):
+ def test_assets_are_real_decodable_images_at_expected_dimensions(self) -> None:
+ for relative, dimensions in EXPECTED.items():
+ path = ASSETS / relative
+ self.assertTrue(path.is_file(), f"missing referenced asset: {relative}")
+ self.assertFalse(path.is_symlink(), f"asset must be a regular file: {relative}")
+ self.assertEqual(inspect_png(path), dimensions)
+
+ def test_svg_sources_are_self_contained_and_parse(self) -> None:
+ for relative in (
+ "source/mocha-flat-landscape.svg",
+ "source/mocha-flat-portrait.svg",
+ ):
+ root = ET.parse(ASSETS / relative).getroot()
+ self.assertEqual(root.tag.rsplit("}", 1)[-1], "svg")
+ for element in root.iter():
+ self.assertNotIn(element.tag.rsplit("}", 1)[-1], {"script", "image"})
+ for key, value in element.attrib.items():
+ if key.rsplit("}", 1)[-1] in {"href", "src"}:
+ self.assertFalse(re.match(r"^[a-z]+:", value, re.I), value)
+
+ def test_rendered_wallpaper_config_references_only_supplied_mocha_paths(self) -> None:
+ template = TEMPLATE.read_text(encoding="utf-8")
+ block = template.split("[wallpaper]\n", 1)[1].split("\n[backdrop]\n", 1)[0]
+ paths = re.findall(r'path = "\{\{ \.chezmoi\.homeDir \}\}/\.local/share/wallpapers/([^\"]+)"', block)
+ self.assertEqual(paths.count("ezwallpaper-mocha/landscape/mocha-flat-landscape.png"), 2)
+ self.assertEqual(paths.count("ezwallpaper-mocha/portrait/mocha-flat-portrait.png"), 1)
+ directories = re.findall(r'directory = "\{\{ \.chezmoi\.homeDir \}\}/\.local/share/wallpapers/([^\"]+)"', block)
+ self.assertEqual(set(directories), {"ezwallpaper-mocha/landscape", "ezwallpaper-mocha/portrait"})
+ for relative in paths:
+ self.assertTrue((ROOT / "dotfiles/dot_local/share/wallpapers" / relative).is_file())
+ self.assertIn("per_monitor_directories = true", block)
+ self.assertIn("[wallpaper.monitor.DP-1]", block)
+ self.assertIn("[wallpaper.monitor.HDMI-A-1]", block)
+ self.assertIn("[wallpaper.automation]\nenabled = false", block)
+
+ def test_docs_disclose_static_default_and_b_prerequisites(self) -> None:
+ doc = DOC.read_text(encoding="utf-8")
+ profile = B_PROFILE.read_text(encoding="utf-8")
+ preflight = B_PREFLIGHT.read_text(encoding="utf-8")
+ for path in (
+ ".local/share/wallpapers/landscape",
+ ".local/share/wallpapers/portrait",
+ ):
+ self.assertIn(path, profile)
+ for path in (
+ ".local/share/wallpapers/ezwallpaper-mocha/landscape",
+ ".local/share/wallpapers/ezwallpaper-mocha/portrait",
+ ):
+ self.assertIn(path, preflight)
+ self.assertIn("~/.local/share/wallpapers/{landscape,portrait}", doc)
+ self.assertIn(
+ "~/.local/share/wallpapers/ezwallpaper-mocha/{landscape,portrait}",
+ doc,
+ )
+ self.assertIn("hard-coded", doc)
+ self.assertIn(
+ "does not make the guard support arbitrary pool paths",
+ doc,
+ )
+ for phrase in (
+ "remain static",
+ "native automation",
+ "ezwallpaper-rotation-b.md",
+ "private/local",
+ "ezwallpaper-rotation",
+ "wallpapers/{landscape,portrait}",
+ "native automation is a distinct alternative",
+ "cannot prove",
+ ):
+ self.assertIn(phrase, doc)
+
+ def test_rendered_toml_wallpaper_tables_parse_and_defaults_stay_off(self) -> None:
+ template = TEMPLATE.read_text(encoding="utf-8")
+ rendered = template.replace("{{ .chezmoi.homeDir }}", "/home/test")
+ config = tomllib.loads(rendered)
+ wallpaper = config["wallpaper"]
+ self.assertEqual(wallpaper["directory"], "/home/test/.local/share/wallpapers/ezwallpaper-mocha/landscape")
+ self.assertTrue(wallpaper["per_monitor_directories"])
+ self.assertEqual(wallpaper["monitors"]["DP-1"]["path"], "/home/test/.local/share/wallpapers/ezwallpaper-mocha/landscape/mocha-flat-landscape.png")
+ self.assertEqual(wallpaper["monitors"]["HDMI-A-1"]["path"], "/home/test/.local/share/wallpapers/ezwallpaper-mocha/portrait/mocha-flat-portrait.png")
+ self.assertFalse(wallpaper["automation"]["enabled"])
+
+
+if __name__ == "__main__":
+ unittest.main()