{
  description = "cc-ci — Co-op Cloud recipe CI server (NixOS)";

  inputs = {
    # Pinned to the nixos-26.05 channel tip (2026-08-03). Matches the orchestrator host's
    # channel so both boxes share a nixpkgs and CVEs get patched. Bump deliberately, not drift.
    # Previous pin: 50ab793 (nixos-24.11, 2025-06-30) — 24.11 was EOL; this is a 3-release jump
    # (24.11 -> 25.05 -> 25.11 -> 26.05). Notable 26.05 changes: systemd Stage 1 boot by default,
    # dbus-broker default, bash nixos-rebuild removed (Python rewrite mandatory), MySQL 8.0 removed.
    nixpkgs.url = "github:NixOS/nixpkgs/531670d871c0e29724a02f3cbcac170adc65b58c";

    # sops-nix master (buildGo125Module / Go 1.25), which builds against nixpkgs 26.05.
    # Previous pin (77c423a) held back to plain buildGoModule for nixpkgs 24.11 compat — no
    # longer needed on 26.05.
    sops-nix.url = "github:Mic92/sops-nix/f1406619a3884cd5c47992a70b8b35c9c0fcb4c9";
    sops-nix.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { self, nixpkgs, sops-nix, ... }:
    let
      system = "x86_64-linux";
      pkgs = nixpkgs.legacyPackages.${system};
      # Lint/format toolchain (Phase 1b, RL1). Same tools the `.drone.yml` lint stage and
      # `scripts/lint.sh` use, built from the pinned nixpkgs so CI and local agree byte-for-byte.
      # Nix: nixpkgs-fmt (format) · statix (lints) · deadnix (dead code).
      # Python: ruff (lint + format). Shell: shellcheck + shfmt. YAML: yamllint.
      lintTools = with pkgs; [
        nixpkgs-fmt
        statix
        deadnix
        ruff
        shellcheck
        shfmt
        yamllint
      ];
    in
    {
      # The whole CI server as one reusable module (nix/modules/default.nix): every service,
      # the harness tooling, sops wiring and acme-dns — but no hardware, networking, tailscale
      # node, root keys or stateVersion. sops-nix's module comes bundled so a consumer only has
      # to import this and set `cc-ci.publicIPv4` (+ `cc-ci.sopsFile` when it is not built from
      # a --recursive clone). A consuming flake MUST make its `cc-ci` input follow its own
      # `nixpkgs` and `sops-nix`, otherwise two sops-nix module trees collide.
      # Consumer: recipe-maintainers/cc-ci-orchestrator `#cc-ci` (CI server + orchestrator on
      # one Hetzner host, 2026-09).
      nixosModules.cc-ci-server = {
        imports = [ sops-nix.nixosModules.sops ./nix/modules ];
      };

      nixosConfigurations = {
        # Canonical live host target: the Hetzner cc-ci server.
        # Use `.#cc-ci` for the current production host.
        cc-ci = nixpkgs.lib.nixosSystem {
          inherit system;
          modules = [
            self.nixosModules.cc-ci-server
            ./nix/hosts/cc-ci-hetzner/configuration.nix
          ];
        };

        # Legacy Incus VM host definition retained only for historical comparison and fallback.
        # Do NOT use this target on the live Hetzner server.
        cc-ci-incus = nixpkgs.lib.nixosSystem {
          inherit system;
          modules = [
            sops-nix.nixosModules.sops
            ./nix/hosts/cc-ci/configuration.nix
          ];
        };

        # Explicit alias for the live Hetzner host. Kept alongside `cc-ci` so the intended host
        # target remains obvious in recovery/migration workflows.
        cc-ci-hetzner = nixpkgs.lib.nixosSystem {
          inherit system;
          modules = [
            self.nixosModules.cc-ci-server
            ./nix/hosts/cc-ci-hetzner/configuration.nix
          ];
        };
      };

      devShells.${system} = {
        # Devshell for working on the harness/bridge locally (tools + lint toolchain).
        default = pkgs.mkShell {
          packages = (with pkgs; [ git jq curl ]) ++ lintTools;
        };
        # `nix develop .#lint` — exactly the lint toolchain, nothing else. Used by
        # `scripts/lint.sh` and the `.drone.yml` lint stage.
        lint = pkgs.mkShell {
          packages = lintTools;
        };
      };

      formatter.${system} = pkgs.nixpkgs-fmt;
    };
}
