{
  description = "Nomarchy — the rock-solid reproducibility of NixOS, the polish of an opinionated desktop";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";

    home-manager = {
      url = "github:nix-community/home-manager/release-26.05";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    stylix = {
      # Track the release branch matching nixpkgs/HM — master is built
      # against unstable and warns about the version skew on every
      # activation. (No home-manager input to follow anymore: stylix
      # vendors its HM integration.)
      url = "github:nix-community/stylix/release-26.05";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    # Pinned by Nomarchy so distro + hardware quirks are tested together
    # (consumed by lib.mkFlake's hardwareProfile). Its nixpkgs input is
    # only used by its own CI — follow ours to keep locks/ISO lean.
    nixos-hardware = {
      url = "github:NixOS/nixos-hardware/master";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  outputs = { self, nixpkgs, home-manager, stylix, nixos-hardware, ... }:
    let
      system = "x86_64-linux";
      username = "nomarchy"; # reference host only; downstream picks its own

      pkgs = import nixpkgs {
        inherit system;
        overlays = [ self.overlays.default ];
        # Matches nixpkgs.config.allowUnfree in modules/nixos — the
        # standalone HM generation must come from like-configured pkgs.
        config.allowUnfree = true;
      };

      # The guided installer — live-ISO only (not in the overlay): it bakes
      # in the downstream template, this checkout's flake.lock, and the
      # source identity of this very revision so installs work offline.
      gitUrl = "https://git.bemagri.xyz/bernardo/nomarchy.git";
      nomarchyInstall = pkgs.callPackage ./pkgs/nomarchy-install {
        templateDir = ./templates/downstream;
        nomarchyLock = ./flake.lock;
        hardwareModuleNames = pkgs.writeText "nixos-hardware-modules.txt"
          (nixpkgs.lib.concatStringsSep "\n"
            (builtins.attrNames nixos-hardware.nixosModules));
        nixpkgsPath = nixpkgs.outPath;
        # Branch model: `main` is the development default; `v1` is the
        # release pointer — the stable line for the NixOS 26.05 rewrite,
        # advanced to main once a batch is tested (rolling, no tags). A
        # future major bump would become `v2`. Installed machines track
        # `?ref=v1` on `nix flake update`, not the forge's default branch.
        flakeUrl = "git+${gitUrl}?ref=v1";
        # Future updates re-resolve from the forge (original); the install
        # itself resolves from the ISO store (locked = path) — fully
        # offline, even from a dirty-tree ISO.
        originalNode = { type = "git"; url = gitUrl; ref = "v1"; };
        lockedNode = {
          type = "path";
          path = self.outPath;
          narHash = self.narHash;
          lastModified = self.lastModified or 0;
        };
        rev = self.rev or null;
      };
    in
    {
      # ─── Downstream API ────────────────────────────────────────────
      # A user's machine flake imports these and layers its own
      # system.nix / home.nix on top (see templates/downstream).
      # Their theme-state.json lives in THEIR flake and is wired up via
      # the `nomarchy.stateFile` option — reading it stays pure.

      overlays.default = final: prev: {
        nomarchy-theme-sync = final.callPackage ./pkgs/nomarchy-theme-sync {
          # Presets baked into the package, so `nomarchy-theme-sync list`
          # works on machines that don't check out this repo.
          themesDir = ./themes;
        };
        nomarchy-doctor = final.callPackage ./pkgs/nomarchy-doctor { };
        nomarchy-control-center = final.callPackage ./pkgs/nomarchy-control-center { };
        nomarchy-battery-notify = final.callPackage ./pkgs/nomarchy-battery-notify { };
        nomarchy-detect-hw = final.callPackage ./pkgs/nomarchy-detect-hw { };
      };

      nixosModules.nomarchy = {
        imports = [ ./modules/nixos ];
        nixpkgs.overlays = [ self.overlays.default ];
      };

      homeModules.nomarchy = {
        # stylix's home module is injected here because it needs the
        # flake input; modules/home/stylix.nix only configures it.
        imports = [ stylix.homeModules.stylix ./modules/home ];
      };

      # One-call downstream wrapper — see templates/downstream/flake.nix.
      # The generated flake is never hand-edited; machines live in
      # system.nix / home.nix.
      lib = import ./lib.nix {
        inherit nixpkgs home-manager nixos-hardware;
        nomarchy = self;
      };

      templates.default = {
        path = ./templates/downstream;
        description = "Nomarchy machine configuration (downstream flake)";
      };

      packages.${system} = {
        nomarchy-theme-sync = pkgs.nomarchy-theme-sync;
        nomarchy-install = nomarchyInstall;
        # Overlay tools exported for `nix build .#nomarchy-doctor` etc.
        # (maintainer/CI convenience — modules install them via the overlay).
        nomarchy-doctor = pkgs.nomarchy-doctor;
        nomarchy-control-center = pkgs.nomarchy-control-center;
        nomarchy-battery-notify = pkgs.nomarchy-battery-notify;
        nomarchy-detect-hw = pkgs.nomarchy-detect-hw;
        default = pkgs.nomarchy-theme-sync;
      };

      # ─── Checks ────────────────────────────────────────────────────
      # `nix flake check` never evaluates templates/, so exercise the
      # downstream template through mkFlake directly (this bypasses the
      # template's own three-line flake.nix, which only resolves once
      # instantiated — everything else is covered here, including the
      # hardwareProfile → nixos-hardware mapping).
      checks.${system} =
        let
          downstream = self.lib.mkFlake {
            src = ./templates/downstream;
            username = "me";
            hardwareProfile = "framework-13-7040-amd"; # exercises the mapping
          };
        in
        {
          downstream-template-system =
            downstream.nixosConfigurations.default.config.system.build.toplevel;
          downstream-template-home =
            downstream.homeConfigurations.me.activationPackage;

          # Every hex-on-hex text pairing the generated swaync CSS uses
          # must contrast in EVERY palette — summer-day's body text was
          # invisible on hardware (item 25: subtext==base there). Cheap
          # (no VM); extend the script's PAIRINGS as more generated
          # surfaces move to palette-safe roles (item 27: waybar/rofi).
          theme-contrast = pkgs.runCommand "nomarchy-theme-contrast"
            { nativeBuildInputs = [ pkgs.python3 ]; }
            ''
              python3 ${./tools/check-theme-contrast.py} ${./themes}
              touch $out
            '';

          # Whole-swap waybar.css is read raw (no palette prepend) — every
          # @color it references must be @define-color'd in the same file
          # (neon-glass shipped zero defines; BACKLOG #62). Also gate
          # preview.png + non-empty backgrounds/ per preset.
          theme-wholeswap = pkgs.runCommand "nomarchy-theme-wholeswap"
            { nativeBuildInputs = [ pkgs.python3 ]; }
            ''
              python3 ${./tools/check-theme-wholeswap.py} ${./themes}
              touch $out
            '';

          # The README option tables must track the live `nomarchy.*`
          # surface. The surface is walked at eval time from the four
          # option-declaring modules (their lazy `config` halves stay
          # unevaluated), so the live side of the diff can't go stale;
          # the script wants a table row per option (inline `.leaf`
          # mentions ok for non-enable sub-knobs) and flags stale rows.
          option-docs =
            let
              lib = nixpkgs.lib;
              names = prefix: opts: lib.concatLists (lib.mapAttrsToList
                (n: v:
                  if lib.isOption v
                  then lib.optional (!(v.readOnly or false)) "${prefix}${n}"
                  else names "${prefix}${n}." v)
                opts);
              surface = lib.concatLists [
                (names "nomarchy."
                  ((import ./modules/home/options.nix { inherit lib pkgs; config = { }; }).options.nomarchy))
                (names "nomarchy.system."
                  ((import ./modules/nixos/options.nix { inherit lib; config = { }; }).options.nomarchy.system))
                (names "nomarchy.hardware."
                  ((import ./modules/nixos/hardware.nix { inherit lib pkgs; config = { }; }).options.nomarchy.hardware))
                (names "nomarchy.services."
                  ((import ./modules/nixos/services.nix { inherit lib pkgs; config = { }; }).options.nomarchy.services))
              ];
            in
            pkgs.runCommand "nomarchy-option-docs"
              { nativeBuildInputs = [ pkgs.python3 ]; }
              ''
                python3 ${./tools/check-option-docs.py} \
                  ${pkgs.writeText "option-surface.txt"
                    (lib.concatStringsSep "\n" surface + "\n")} \
                  ${./README.md}
                touch $out
              '';

          # The state-file friendly-errors contract (item 11): validate
          # reports broken fixtures by field with a fix, good state
          # passes, and an invalid `set` is refused BEFORE the write —
          # the on-disk file must be byte-identical after the attempt.
          theme-sync-validate = pkgs.runCommand "nomarchy-theme-sync-validate"
            { nativeBuildInputs = [ pkgs.python3 pkgs.jq ]; }
            ''
              set -eu
              tool=${./pkgs/nomarchy-theme-sync/nomarchy-theme-sync.py}
              export NOMARCHY_DEFAULT_THEMES=${./themes}

              mkdir good && cp ${./templates/downstream/theme-state.json} good/theme-state.json
              chmod +w good/theme-state.json
              NOMARCHY_PATH=$PWD/good python3 "$tool" validate

              mkdir syntax && printf '{ "slug": "x", }' > syntax/theme-state.json
              if NOMARCHY_PATH=$PWD/syntax python3 "$tool" validate 2>err; then
                echo "FAIL: syntax fixture accepted"; exit 1; fi
              grep -q "syntax error at line" err

              mkdir badhex && jq '.colors.accent = "7aa2f7"' \
                good/theme-state.json > badhex/theme-state.json
              if NOMARCHY_PATH=$PWD/badhex python3 "$tool" validate 2>err; then
                echo "FAIL: bad hex accepted"; exit 1; fi
              grep -q "colors.accent" err && grep -q "fix:" err

              mkdir badtype && jq '.ui.gapsOut = "12"' \
                good/theme-state.json > badtype/theme-state.json
              if NOMARCHY_PATH=$PWD/badtype python3 "$tool" validate 2>err; then
                echo "FAIL: bad type accepted"; exit 1; fi
              grep -q "ui.gapsOut" err

              mkdir unknown && jq '.extraKey = 1' \
                good/theme-state.json > unknown/theme-state.json
              NOMARCHY_PATH=$PWD/unknown python3 "$tool" validate \
                | grep -q "unknown top-level key"

              cp good/theme-state.json before.json
              if NOMARCHY_PATH=$PWD/good python3 "$tool" --quiet \
                   set ui.gapsOut '"12"' --no-switch 2>err; then
                echo "FAIL: invalid set was written"; exit 1; fi
              grep -q "refusing to write" err
              cmp good/theme-state.json before.json

              touch $out
            '';

          # Installer hardware DB ↔ nixos-hardware (item 49): every module
          # name the DB table and the CPU/GPU/chassis autodetect reference
          # must exist in the PINNED nixos-hardware.nixosModules — a lock
          # bump can rename a module upstream (the X1 Carbon → x1-Nth-gen
          # churn) and break profiled installs on just the matching DMI,
          # invisible to any VM. The available-set is generated here from
          # attrNames — the same value fed to nomarchy-install — so it
          # tracks the lock automatically.
          hardware-db-modules = pkgs.runCommand "nomarchy-hardware-db-modules"
            { nativeBuildInputs = [ pkgs.python3 ]; }
            ''
              python3 ${./tools/check-hardware-db.py} \
                ${./pkgs/nomarchy-install/hardware-db.sh} \
                ${pkgs.writeText "nixos-hardware-modules.txt"
                  (nixpkgs.lib.concatStringsSep "\n"
                    (builtins.attrNames nixos-hardware.nixosModules))}
              touch $out
            '';

          # compose-lock.py offline-composer contract (item 49): the lock
          # the installer writes on the target with no network must root at
          # nomarchy, carry every upstream node one level down with its
          # `follows` paths rebased, and fail closed on a missing narHash or
          # a pre-existing nomarchy node. Pure — runs the script on fixtures.
          installer-compose-lock = pkgs.runCommand "nomarchy-installer-compose-lock"
            { nativeBuildInputs = [ pkgs.python3 ]; }
            ''
              python3 ${./tools/check-compose-lock.py} \
                ${./pkgs/nomarchy-install/compose-lock.py}
              touch $out
            '';

          # Installer safety guards (item 54): offline compose-lock fail-closed
          # (no network flake lock), disk-signature warn regex, pre-activate
          # recovery hint path, password floors. Pure source+regex contract.
          installer-safety = pkgs.runCommand "nomarchy-installer-safety"
            { nativeBuildInputs = [ pkgs.python3 ]; }
            ''
              python3 ${./tools/check-install-safety.py} \
                ${./pkgs/nomarchy-install/nomarchy-install.sh}
              touch $out
            '';

          # Installer share ↔ templates/downstream SoT (item 72): the four
          # files default.nix copies into share/.../template/ must stay
          # byte-identical to the checkout template (single machine-seed
          # SoT; no thinner second catalog). Cheap package build + cmp.
          template-sot = pkgs.runCommand "nomarchy-template-sot"
            { nativeBuildInputs = [ pkgs.python3 ]; }
            ''
              python3 ${./tools/check-template-sot.py} \
                ${nomarchyInstall}/share/nomarchy-install/template \
                ${./templates/downstream}
              touch $out
            '';

          # The installer's disko arg contract (item 49): swapSize "0"/"0G"
          # (bare "0" is what the swap=0 install passes) must yield NO @swap
          # subvolume, a sized value a correctly-sized one, and withLuks must
          # gate the LUKS wrapper around the BTRFS root. Pure eval of
          # disko-config.nix (a plain function) — no disko, no VM.
          installer-disko =
            let
              diskoFor = args: import ./pkgs/nomarchy-install/disko-config.nix
                ({ mainDrive = "/dev/vda"; } // args);
              rootContent = args:
                (diskoFor args).disko.devices.disk.main.content.partitions.root.content;
              btrfsOf = c: if c.type == "luks" then c.content else c;
              subvols = args: (btrfsOf (rootContent args)).subvolumes;
              hasSwap = args: builtins.hasAttr "@swap" (subvols args);
              expect = cond: msg: nixpkgs.lib.assertMsg cond "installer-disko: ${msg}";
              ok =
                expect (! hasSwap { swapSize = "0"; })
                  ''swapSize "0" produced an @swap subvol''
                && expect (! hasSwap { swapSize = "0G"; })
                  ''swapSize "0G" produced an @swap subvol''
                && expect (hasSwap { swapSize = "2G"; })
                  ''swapSize "2G" produced no @swap subvol''
                && expect ((subvols { swapSize = "2G"; })."@swap".swap.swapfile.size == "2G")
                  ''the @swap swapfile size is not "2G"''
                && expect ((rootContent { withLuks = true; }).type == "luks")
                  "withLuks=true did not wrap root in luks"
                && expect ((rootContent { withLuks = true; }).name == "crypted")
                  "the luks device is not named 'crypted'"
                && expect ((rootContent { withLuks = false; }).type == "btrfs")
                  "withLuks=false did not put btrfs directly on root"
                && expect (builtins.hasAttr "@" (subvols { withLuks = false; }))
                  "the no-luks root lost its @ subvolume";
            in
            assert ok; pkgs.runCommand "nomarchy-installer-disko" { } "touch $out";

          # Hyprland windowrule dead-syntax guard (item 49): build the
          # GENERATED hyprland.conf (the ISO's HM output) and fail on a
          # regression to the pre-0.53 grammar — the `windowrulev2` keyword,
          # rule-first order (`float, class:^…$`), or a bare `<prop>:`
          # matcher. That syntax shipped a red config-error banner on every
          # boot (fixed in ed7fd93); this locks the correct form. Building
          # one config file is a pure derivation, not a VM.
          windowrule-syntax =
            let
              hyprconf = self.nixosConfigurations.nomarchy-live.config
                .home-manager.users.${username}.xdg.configFile."hypr/hyprland.conf".source;
            in
            pkgs.runCommand "nomarchy-windowrule-syntax"
              { nativeBuildInputs = [ pkgs.python3 ]; }
              ''
                python3 ${./tools/check-windowrule-syntax.py} ${hyprconf}
                touch $out
              '';

          # nomarchy-doctor's contract: an induced failed unit flips the
          # sheet to ✖/exit-1 and names the unit; with the failure
          # cleared it reports healthy/exit-0. Minimal node (just the
          # package) — the disk/flake/snapper checks self-skip in a VM.
          # Live-ISO install affordance (BACKLOG #57): desktop entry baked
          # into the live host's HM generation + installer on PATH + Tools
          # menu gate in nomarchy-menu. Builds the real nomarchy-live HM
          # package (not a full ISO) — V2 file-level smoke of the same
          # artifacts the ISO ships.
          live-install-entry =
            let
              liveHm =
                self.nixosConfigurations.nomarchy-live.config.home-manager.users.${username};
              gen = liveHm.home.activationPackage;
            in
            pkgs.runCommand "nomarchy-live-install-entry"
              {
                nativeBuildInputs = [ pkgs.gnugrep pkgs.findutils ];
                passAsFile = [ ];
              }
              ''
                set -euo pipefail
                desk=$(find ${gen}/home-path/share/applications \
                  -name nomarchy-install.desktop | head -1)
                test -n "$desk"
                grep -q '^Name=Install Nomarchy$' "$desk"
                grep -q '^Terminal=true$' "$desk"
                grep -q '^Exec=nomarchy-install$' "$desk"
                test -x ${nomarchyInstall}/bin/nomarchy-install
                # Tools › Install Nomarchy self-gate lives in nomarchy-menu
                # (rofi.nix), which is on the live HM path.
                menu=$(find ${gen}/home-path/bin -name nomarchy-menu | head -1)
                test -n "$menu"
                grep -q 'Install Nomarchy' "$menu"
                echo "live-install-entry: ok desk=$desk menu=$menu"
                touch $out
              '';

          doctor = pkgs.testers.runNixOSTest {
            name = "nomarchy-doctor";
            nodes.machine = { ... }: {
              environment.systemPackages = [ pkgs.nomarchy-doctor ];
              systemd.services.doomed = {
                description = "deliberately failing unit (doctor test)";
                serviceConfig = {
                  Type = "oneshot";
                  ExecStart = "${pkgs.coreutils}/bin/false";
                };
                wantedBy = [ "multi-user.target" ];
              };
            };
            testScript = ''
              machine.wait_for_unit("multi-user.target")
              machine.wait_until_succeeds("systemctl is-failed doomed.service")
              out = machine.fail("nomarchy-doctor 2>&1")
              assert "doomed.service" in out, f"doctor did not name the failed unit:\n{out}"
              assert "journalctl" in out, f"doctor did not print a fix:\n{out}"
              machine.succeed("systemctl reset-failed")
              # The healthy run also exercises the #44 hardware section: in a
              # bare VM every probe (nmcli/wpctl/vainfo/fprintd/fwupd) is
              # absent, so each check must self-gate to a skip row and the
              # sheet must still exit 0 — i.e. the section runs without
              # erroring under `set -euo pipefail`.
              healthy = machine.succeed("nomarchy-doctor 2>&1")
              for probe in ["NetworkManager", "fingerprint", "fwupd", "VA-API"]:
                  assert probe in healthy, f"doctor hardware section missing a self-gated '{probe}' row:\n{healthy}"
            '';
          };

          # Config-level VM assertions for the nomarchy.hardware.* toggles —
          # what they SET (kernel cmdline, fprintd, PAM). Real hardware
          # behaviour (firmware/driver/device) needs bare metal and is out of
          # scope here. Imports only hardware.nix, so the VM stays minimal.
          hardware-toggles = pkgs.testers.runNixOSTest {
            name = "nomarchy-hardware-toggles";
            nodes.machine = { ... }: {
              imports = [ ./modules/nixos/hardware.nix ];
              nomarchy.hardware = {
                intel.enable = true;
                amd.enable = true;
                fingerprint = { enable = true; pam = true; };
                npu.enable = true;
              };
            };
            testScript = ''
              machine.wait_for_unit("multi-user.target")
              cmdline = machine.succeed("cat /proc/cmdline")
              assert "amd_pstate=active" in cmdline, cmdline
              assert "i915.enable_guc=3" in cmdline, cmdline
              machine.succeed("systemctl cat fprintd.service")
              machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo")
            '';
          };

          # Runtime VM check for the battery-charge-limit re-apply: the udev
          # rule must restart the oneshot on an AC state change (firmware can
          # clear the threshold on unplug). The `test_power` module fakes a
          # Mains adapter we can toggle to emit a real power_supply uevent —
          # so this exercises the trigger, not just the config wiring. (The
          # sysfs *write* needs a real charge_control_end_threshold and stays
          # an on-hardware check.) Imports options.nix (where the power
          # options live) + power.nix, so the VM stays minimal.
          battery-charge-limit = pkgs.testers.runNixOSTest {
            name = "nomarchy-battery-charge-limit";
            nodes.machine = { ... }: {
              imports = [ ./modules/nixos/options.nix ./modules/nixos/power.nix ];
              boot.kernelModules = [ "test_power" ]; # fake Mains + battery
              nomarchy.system.power = {
                enable = true;
                laptop = true;
                batteryChargeLimit = 80;
              };
            };
            testScript = ''
              machine.wait_for_unit("multi-user.target")

              # The oneshot exists and the fake Mains adapter is present.
              machine.succeed("systemctl cat nomarchy-battery-charge-limit.service")
              machine.succeed("udevadm settle")
              machine.succeed("grep -lx Mains /sys/class/power_supply/*/type")

              # Let any add-event restart settle, then sample the invocation.
              machine.succeed("sleep 2")
              before = machine.succeed(
                  "systemctl show -p InvocationID --value "
                  "nomarchy-battery-charge-limit.service"
              ).strip()

              # Simulate an AC unplug → 'change' uevent on the Mains device.
              machine.succeed("echo off > /sys/module/test_power/parameters/ac_online")
              machine.succeed("udevadm settle")

              # The udev rule must have restarted the oneshot (new InvocationID).
              machine.wait_until_succeeds(
                  'test "$(systemctl show -p InvocationID --value '
                  'nomarchy-battery-charge-limit.service)" != "' + before + '"',
                  timeout=30,
              )
            '';
          };

          # Unit test of the monitor-rule overlay (modules/home/
          # monitor-rules.nix — pure, imported directly, no HM eval): the
          # active profile must replace base entries whole by name, leave
          # unnamed base outputs alone, and the disable-guard must stop a
          # stale menu resolution pick (settings.monitors) resurrecting a
          # panel the profile disables. Junk in settings.displayProfile
          # degrades to base config, never an eval error.
          display-profiles =
            let
              inherit (nixpkgs) lib;
              monitorLib = import ./modules/home/monitor-rules.nix { inherit lib; };
              entry = a: monitorLib.defaults // a;
              fixture = {
                base = [
                  (entry { name = "eDP-1"; position = "0x0"; })
                  (entry { name = "HDMI-A-1"; position = "auto-right"; })
                ];
                profiles.docked = [
                  (entry { name = "eDP-1"; resolution = "disable"; })
                  (entry { name = "DP-3"; position = "0x0"; })
                ];
                resOverrides."eDP-1" = "1920x1080@60"; # stale pick — must stay dead
              };
              rulesFor = active: map monitorLib.rule
                (monitorLib.resolve (fixture // { inherit active; }));
              docked = rulesFor "docked";
              junk = rulesFor 42; # hand-edited state (validator only warns)
              expect = cond: msg: lib.assertMsg cond
                "display-profiles: ${msg} (rendered: ${builtins.toJSON docked})";
              ok =
                expect (lib.elem "eDP-1, disable" docked)
                  "the profile did not disable eDP-1"
                && expect (! lib.any (lib.hasInfix "1920x1080") docked)
                  "a stale resolution pick resurrected the disabled panel"
                && expect (lib.elem "DP-3, preferred, 0x0, 1" docked)
                  "the profile-only output's rule is missing"
                && expect (lib.elem "HDMI-A-1, preferred, auto-right, 1" docked)
                  "an unnamed base output lost its rule"
                && expect (lib.elem "eDP-1, 1920x1080@60, 0x0, 1" junk)
                  "junk active profile must degrade to base config (with the pick applied)"
                && expect (monitorLib.workspaceRule "1" "DP-3" == "1, monitor:DP-3")
                  "workspace pin did not render as a Hyprland workspace rule";
            in
            assert ok; pkgs.runCommand "nomarchy-display-profiles" { } "touch $out";

          # The low-battery watcher's crossing logic: one toast per downward
          # crossing (normal at 25%, critical at 10%), re-armed by charging.
          # test_power fakes a battery whose capacity/status we script;
          # notify-send is shimmed to a log via PATH (the package resolves
          # it from PATH for exactly this reason). What stays on-hardware:
          # the real toast rendering through swaync in a session.
          battery-notify = pkgs.testers.runNixOSTest {
            name = "nomarchy-battery-notify";
            nodes.machine = { ... }: {
              boot.kernelModules = [ "test_power" ]; # fake battery + Mains
              environment.systemPackages = [ pkgs.nomarchy-battery-notify ];
            };
            testScript = ''
              machine.wait_for_unit("multi-user.target")

              # notify-send shim: log the calls instead of needing a session bus.
              machine.succeed(
                  "mkdir -p /shim && printf '#!/bin/sh\necho \"$*\" >> /tmp/notifications\n'"
                  " > /shim/notify-send && chmod +x /shim/notify-send"
              )

              # Self-gate: with the fake battery gone, the watcher exits 0 at once.
              machine.succeed("modprobe -r test_power")
              machine.succeed("timeout 5 nomarchy-battery-notify 1")
              machine.succeed("modprobe test_power")

              # Healthy level while discharging: silence.
              machine.succeed("echo discharging > /sys/module/test_power/parameters/battery_status")
              machine.succeed("echo 80 > /sys/module/test_power/parameters/battery_capacity")
              machine.succeed(
                  "systemd-run --unit=batwatch --setenv=PATH=/shim:/run/current-system/sw/bin"
                  " /run/current-system/sw/bin/nomarchy-battery-notify 1"
              )
              machine.sleep(3)
              machine.fail("test -s /tmp/notifications")

              # Crossing 25% fires the low toast exactly once, not per poll.
              machine.succeed("echo 20 > /sys/module/test_power/parameters/battery_capacity")
              machine.wait_until_succeeds("grep -q 'Battery low' /tmp/notifications")
              machine.succeed("echo 15 > /sys/module/test_power/parameters/battery_capacity")
              machine.sleep(3)
              assert machine.succeed("grep -c 'Battery low' /tmp/notifications").strip() == "1"

              # Crossing 10% escalates to a critical-urgency toast, once.
              machine.succeed("echo 5 > /sys/module/test_power/parameters/battery_capacity")
              machine.wait_until_succeeds("grep -q 'Battery critical' /tmp/notifications")
              assert "critical" in machine.succeed("grep 'Battery critical' /tmp/notifications")
              machine.sleep(3)
              assert machine.succeed("grep -c 'Battery critical' /tmp/notifications").strip() == "1"

              # Back on the charger → re-armed: the next drain notifies again.
              machine.succeed("echo charging > /sys/module/test_power/parameters/battery_status")
              machine.succeed("echo 80 > /sys/module/test_power/parameters/battery_capacity")
              machine.sleep(3)
              machine.succeed("echo discharging > /sys/module/test_power/parameters/battery_status")
              machine.succeed("echo 20 > /sys/module/test_power/parameters/battery_capacity")
              machine.wait_until_succeeds(
                  "test \"$(grep -c 'Battery low' /tmp/notifications)\" = 2"
              )
            '';
          };

          # Memory-pressure protection (oom.nix): earlyoom must kill a
          # runaway allocator BEFORE the kernel thrash point — and must not
          # take anything else with it. A bystander unit survives the kill,
          # proving the process-level granularity that motivated earlyoom
          # over cgroup-level systemd-oomd (which would kill the whole
          # Hyprland session scope). Imports only oom.nix so the VM stays
          # minimal.
          oom-protection = pkgs.testers.runNixOSTest {
            name = "nomarchy-oom-protection";
            nodes.machine = { ... }: {
              imports = [ ./modules/nixos/oom.nix ];
              environment.systemPackages = [ pkgs.python3 ];
              virtualisation.memorySize = 1024;
            };
            testScript = ''
              machine.wait_for_unit("earlyoom.service")

              # One owner: oomd (nixpkgs default-on, inert) is disabled.
              machine.fail("systemctl is-active --quiet systemd-oomd.service")
              # The session avoid-list made it into the daemon invocation.
              machine.succeed("systemctl cat earlyoom.service | grep -q -- --avoid")

              # A bystander that must survive (stand-in for the session).
              machine.succeed("systemd-run --unit=bystander sleep infinity")

              # The hog: allocate in 20 MB chunks with a short sleep, so
              # earlyoom's poll wins the race against the kernel OOM killer.
              machine.succeed(
                  "systemd-run --unit=hog python3 -c 'import time; "
                  "exec(\"a=[]\\nwhile True:\\n a.append(bytearray(20*1024*1024)); time.sleep(0.05)\")'"
              )

              # earlyoom (not the kernel) pulled the trigger…
              machine.wait_until_succeeds(
                  "journalctl -u earlyoom.service | grep -Eiq 'sending SIG(TERM|KILL)'",
                  timeout=120,
              )
              # …the hog is gone…
              machine.wait_until_succeeds(
                  'test "$(systemctl show -p ActiveState --value hog.service)" != "active"',
                  timeout=60,
              )
              # …and the bystander is untouched.
              machine.succeed("systemctl is-active --quiet bystander.service")
            '';
          };

          # Snapshot GUI canary: btrfs-assistant crashes in libbtrfsutil's
          # UNPRIVILEGED subvolume iteration (btrfs-progs 6.17.1, fixed
          # upstream after 6.17.1) but runs fine as root — which is how the
          # menu launches it (pkexec launcher). Guard the root path on a
          # real btrfs volume so a lock bump that breaks it fails here, not
          # on a user's machine. The GUI event loop is exercised offscreen
          # (survives a 5s timeout = no startup crash past the Btrfs scan).
          snapshot-gui = pkgs.testers.runNixOSTest {
            name = "nomarchy-snapshot-gui";
            nodes.machine = { pkgs, ... }: {
              environment.systemPackages = [ pkgs.btrfs-assistant pkgs.btrfs-progs ];
              virtualisation.emptyDiskImages = [ 512 ];
            };
            testScript = ''
              machine.wait_for_unit("multi-user.target")
              machine.succeed(
                  "mkfs.btrfs /dev/vdb && mkdir -p /mnt && mount /dev/vdb /mnt "
                  "&& btrfs subvolume create /mnt/sub1"
              )
              # Root (the launcher's effective context): must work.
              machine.succeed("QT_QPA_PLATFORM=offscreen btrfs-assistant-bin --version")
              # The GUI proper survives startup (timeout kill = alive → 124).
              machine.succeed(
                  "set +e; timeout 5 env QT_QPA_PLATFORM=offscreen btrfs-assistant-bin; "
                  "[ $? -eq 124 ]"
              )
            '';
          };

          # The distroId question (roadmap "Distro branding"): set
          # distroId = "nomarchy" so /etc/os-release is honest (ID=nomarchy,
          # ID_LIKE=nixos). The risk is switch-to-configuration's "is this
          # NixOS?" guard — but it builds the check from the *configured*
          # distroId (and /etc/NIXOS still exists), so a rebuild must still
          # work. This boots such a system and runs `switch-to-configuration
          # dry-activate` to prove the guard passes. Inline (not the full
          # module) so the VM stays minimal — the real wiring in default.nix
          # is covered by the downstream-template-system build.
          distro-id = pkgs.testers.runNixOSTest {
            name = "nomarchy-distro-id";
            nodes.machine = { ... }: {
              system.nixos.distroName = "Nomarchy";
              system.nixos.distroId = "nomarchy";
              # Test base omits switch-to-configuration by default; we need it
              # to exercise the "is this NixOS?" guard.
              system.switch.enable = true;
            };
            testScript = { nodes, ... }: ''
              machine.wait_for_unit("multi-user.target")

              # os-release reflects the rebrand, with the nixos lineage marker.
              machine.succeed("grep -q '^ID=nomarchy$' /etc/os-release")
              machine.succeed("grep -q '^ID_LIKE=nixos$' /etc/os-release")
              machine.succeed("grep -q '^NAME=Nomarchy$' /etc/os-release")

              # The rebuild path must still recognise this as a NixOS install
              # (the guard is built from the configured distroId, not "nixos").
              machine.succeed(
                  "${nodes.machine.system.build.toplevel}/bin/switch-to-configuration dry-activate"
              )
            '';
          };
        };

      # ─── Reference host ────────────────────────────────────────────
      # Manually wired rather than via lib.mkFlake: the repo-root
      # theme-state.json is load-bearing (live ISO + in-repo CLI dev),
      # which doesn't match mkFlake's src-layout convention.
      # Deliberately split rebuild paths:
      #   system  → sudo nixos-rebuild switch --flake .#nomarchy   (rare)
      #   desktop → home-manager switch --flake .#nomarchy         (no sudo;
      #             this is what `nomarchy-theme-sync apply` runs)
      # Theme changes never touch the system layer, so they never need
      # root and never rebuild the world.
      nixosConfigurations.nomarchy = nixpkgs.lib.nixosSystem {
        inherit system;
        specialArgs = { inherit username; };
        modules = [
          self.nixosModules.nomarchy
          { nomarchy.system.stateFile = ./theme-state.json; }
          ./hosts/default/configuration.nix
        ];
      };

      homeConfigurations.${username} = home-manager.lib.homeManagerConfiguration {
        inherit pkgs;
        modules = [
          self.homeModules.nomarchy
          {
            # The single source of truth — pure read: the file is part
            # of this flake's source.
            nomarchy.stateFile = ./theme-state.json;
            home = {
              inherit username;
              homeDirectory = "/home/${username}";
            };
          }
        ];
      };

      # ─── Live ISO ──────────────────────────────────────────────────
      # Full desktop on a bootable stick, no installation:
      #   nix build .#nixosConfigurations.nomarchy-live.config.system.build.isoImage
      # Test in QEMU with tools/test-live-iso.sh. See docs/TESTING.md.
      nixosConfigurations.nomarchy-live = nixpkgs.lib.nixosSystem {
        inherit system;
        specialArgs = { inherit username; nomarchySrc = self; };
        modules = [
          "${nixpkgs}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix"
          self.nixosModules.nomarchy
          ./hosts/live.nix

          # The ISO is a sealed artifact, so the desktop is baked in via
          # the HM NixOS module (the installed-system split into
          # nixos-rebuild vs home-manager switch doesn't apply here —
          # though the seeded ~/.nomarchy flake still allows standalone
          # `home-manager switch` for theme testing).
          home-manager.nixosModules.home-manager
          {
            home-manager = {
              useGlobalPkgs = true;
              useUserPackages = true;
              users.${username} = {
                imports = [ self.homeModules.nomarchy ];
                nomarchy.stateFile = ./theme-state.json;
              };
            };

            # The standalone CLI that `nomarchy-theme-sync apply` runs for
            # theme switching, from the same pinned input — plus the
            # guided installer.
            environment.systemPackages = [
              home-manager.packages.${system}.home-manager
              nomarchyInstall
            ];

            # Keep the locked flake inputs in the ISO store so theme
            # switching on the live system works without a network: the
            # lockfile's narHashes resolve against these paths instead
            # of fetching from GitHub. Must be *transitive* — evaluating
            # the seeded flake also fetches stylix's own inputs.
            system.extraDependencies =
              let
                collectInputs = input:
                  [ input.outPath ] ++ builtins.concatMap collectInputs
                    (builtins.attrValues (input.inputs or { }));
              in
              collectInputs self ++ (
                # Pre-build a system shaped like what nomarchy-install
                # GENERATES (snapper, auto-login, LUKS initrd, swapfile +
                # resume — not the bare template!) plus the template
                # desktop, so the offline install never builds packages
                # from source: a custom username/hostname then changes
                # only a handful of derivations. No hardware profile —
                # matches a VM install; profiled installs share most of it.
                let
                  template = self.lib.mkFlake {
                    src = ./templates/downstream;
                    username = username;
                  };
                  representativeInstall = nixpkgs.lib.nixosSystem {
                    inherit system;
                    specialArgs = { inherit username; };
                    modules = [
                      self.nixosModules.nomarchy
                      { environment.systemPackages = [ home-manager.packages.${system}.home-manager ]; }
                      # The installer's autodetection emits these on real
                      # machines (and the intel CPU module pulls the whole
                      # iGPU stack) — without them an offline install on
                      # common hardware builds graphics drivers from
                      # source. Model-specific profiles may still need
                      # network; the common ground is covered.
                      nixos-hardware.nixosModules.common-cpu-intel
                      nixos-hardware.nixosModules.common-cpu-amd
                      nixos-hardware.nixosModules.common-gpu-amd
                      nixos-hardware.nixosModules.common-pc-laptop
                      nixos-hardware.nixosModules.common-pc-ssd
                      nixos-hardware.nixosModules.common-pc
                      (./templates/downstream + "/hardware-configuration.nix")
                      (./templates/downstream + "/system.nix")
                      ({ lib, ... }: {
                        nomarchy.system.snapper.enable = true;
                        nomarchy.system.greeter.autoLogin = username;
                        # nixos-generate-config keys microcode off
                        # enableRedistributableFirmware (now on): the
                        # real machine enables exactly one vendor —
                        # pin both, or the offline install builds
                        # amd-ucode/intel-ucode from source (found the
                        # hard way: the 2700s regression hang).
                        hardware.cpu.amd.updateMicrocode = true;
                        hardware.cpu.intel.updateMicrocode = true;
                        swapDevices = [{ device = "/swap/swapfile"; }];
                        boot.resumeDevice = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000";
                        boot.kernelParams = [ "resume_offset=1" ];
                        boot.initrd.luks.devices.crypted.device = "/dev/disk/by-partlabel/disk-main-root";
                        # The real install is BTRFS with a vfat ESP — the
                        # placeholder's bare ext4 would leave fs-gated bits
                        # out of this pin (snapper via btrfs; mtools and
                        # dosfstools enter systemPackages via vfat!).
                        fileSystems."/" = lib.mkForce {
                          device = "/dev/mapper/crypted";
                          fsType = "btrfs";
                          options = [ "subvol=@" "compress=zstd" "noatime" ];
                        };
                        fileSystems."/boot" = {
                          device = "/dev/disk/by-uuid/0000-0000";
                          fsType = "vfat";
                          options = [ "umask=0077" ];
                        };
                      })
                    ];
                  };
                in [
                  representativeInstall.config.system.build.toplevel
                  template.homeConfigurations.${username}.activationPackage

                  # The live session's `home-manager switch` builds the
                  # REPO's standalone generation (the seeded ~/.nomarchy
                  # flake) — not the template's, and not the NixOS-module
                  # one baked into the ISO. It has drvs the other two
                  # variants don't (user-dbus-services et al.), so pin it
                  # and its direct build inputs too.
                  self.homeConfigurations.${username}.activationPackage
                  self.homeConfigurations.${username}.activationPackage.inputDerivation
                  # home-files is rebuilt per theme and COPIES some of its
                  # inputs (user-dbus-services) instead of referencing
                  # them — the activation closure doesn't carry those, so
                  # pin home-files' direct build inputs separately.
                  self.homeConfigurations.${username}.config.home-files.inputDerivation

                  # A real install rebuilds the drvs that embed the
                  # user's hostname/username/UUIDs (etc, initrd, units,
                  # toplevel, HM activation). inputDerivation pins each
                  # one's direct build inputs…
                  representativeInstall.config.system.build.toplevel.inputDerivation
                  representativeInstall.config.system.build.etc.inputDerivation
                  representativeInstall.config.system.build.initialRamdisk.inputDerivation
                  template.homeConfigurations.${username}.activationPackage.inputDerivation
                  # home-files COPIES user-dbus-services instead of
                  # referencing it (same story as the self pin above) —
                  # a custom-username install rebuilds home-files and
                  # needs its direct inputs present.
                  template.homeConfigurations.${username}.config.home-files.inputDerivation
                ] ++ [
                  # A theme switch re-renders stylix's base16 templates
                  # (gtk.css, kvantum, …) on the machine: the renderer and
                  # the stdenv its trivial drvs build with must be on
                  # board, or an offline `apply <theme>` cascades into
                  # building mustache-go (and eventually stdenv) from
                  # source. Found with tools/vm/gap-analysis.py.
                  pkgs.mustache-go
                  pkgs.stdenv
                ] ++ [
                  # …and the second-level build tools those rebuilds need
                  # (their parents are themselves rebuilt, so parent
                  # inputDerivations don't reach them): kernel-module
                  # shrinking, unit-dir assembly, dbus config, /etc.
                  pkgs.kmod.dev
                  pkgs.nukeReferences
                  pkgs.xorg.lndir
                  pkgs.libxslt.bin
                  pkgs.python3Minimal
                  # system-path is rebuilt on EVERY machine: buildEnv's
                  # package-list order follows module-graph order, and any
                  # hardware profile/import reshuffles it. Its builder
                  # wants these.
                  pkgs.texinfo
                  pkgs.getconf
                  # greetd.toml embeds the auto-login user and is generated
                  # by remarshal — without this pin an offline install
                  # tries to BUILD remarshal, whose test closure pulls
                  # matplotlib/ffmpeg/x265 (the ~1900-drv cascade).
                  pkgs.remarshal
                  # system-path/initrd-bin-env are buildEnvs rebuilt per
                  # machine; their shared builder.pl is build-time-only
                  # (never in any runtime closure) — an empty env's
                  # inputDerivation retains it.
                  (pkgs.buildEnv { name = "nomarchy-pin-buildenv"; paths = [ ]; }).inputDerivation
                  # /etc/dbus-1 reassembly wants this setup hook.
                  pkgs.findXMLCatalogs
                  # nixos-generate-config keys microcode off
                  # enableRedistributableFirmware (now on): exactly one
                  # vendor per real machine, so pin both ucode cpios.
                  pkgs.microcode-amd
                  pkgs.microcode-intel

                  # …and one representative disko script: its tool closure
                  # (file, which, wrapper hooks, …) isn't otherwise on the
                  # ISO, and offline nix would try to build it from source.
                  # The user's actual args produce a different script drv,
                  # but with identical inputs — built offline in seconds.
                  (import "${pkgs.disko}/share/disko/cli.nix" {
                    inherit pkgs;
                    mode = "destroy,format,mount";
                    diskoFile = ./pkgs/nomarchy-install/disko-config.nix;
                    mainDrive = "/dev/vda";
                    withLuks = true;
                    swapSize = "2G";
                  })
                ]
              );
          }
        ];
      };
    };
}
