diff --git a/CHANGES.md b/CHANGES.md
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,9 +1,42 @@
 * Hackage: <http://hackage.haskell.org/package/crackNum>
 * GitHub:  <http://github.com/LeventErkok/crackNum/>
 
-* Latest Hackage released version: 3.30, 2026-08-21
+* Latest Hackage released version: 4.0, 2026-08-24
 
-### Version 3.31, Not yet released
+### Version 4.0, 2026-08-24
+
+  * The Windows GUI now carries the CrackNum icon instead of the stock WinForms
+    one. The artwork is rendered by the same generator that produces the macOS
+    AppIcon.icns, so the two platforms cannot drift apart.
+
+  * Note on numbering: 3.32 was tagged and published as a GitHub release, but never
+    uploaded to Hackage. So for anyone installing from Hackage, this is the first
+    version to support Windows -- see the 3.32 entry below for what that involves.
+
+### Version 3.32, 2026-08-24
+
+  * Windows is now a supported platform, with a prebuilt bundle
+    (`crackNum-<version>-windows-x86_64.zip`) alongside the Mac and Linux ones. It
+    carries the CLI, a native GUI, and a copy of z3, and needs nothing installed:
+    the GUI targets .NET Framework 4.8, which ships with Windows 10 and 11, and z3
+    brings its own Microsoft C runtime. The binaries are not code-signed, so
+    SmartScreen warns on first run; the bundled README says how to get past it.
+
+  * The Windows GUI (`GUI/winGUI/`) is a native WinForms application rather than a
+    port of the Tcl/Tk script that serves Linux. Reusing the Tcl would have been a
+    far smaller change, but it would have required users to install a Tcl
+    distribution before `--gui` did anything, and a GUI behind a prerequisite that
+    most people will not clear is worth less than the code it saves.
+
+  * `--version` and the usage text no longer print the executable's `.exe`
+    extension on Windows, where `getProgName` keeps it.
+
+  * The golden test suite writes its temporary files with newline translation
+    turned off, so the golds stay LF everywhere. Previously, running the suite on
+    Windows compared CRLF output against LF golds, and `--accept` there would have
+    rewritten every gold in the process.
+
+### Version 3.31, 2026-08-22
 
   * The Hackage description now shows a screenshot of the GUI, and points at the
     releases page for the prebuilt Mac and Linux bundles. The screenshot was dropped
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@
 | --- | --- | --- |
 | Linux (x86_64) | `crackNum-<version>-linux-x86_64.tar.gz` | Statically linked, so there is no glibc or distribution requirement: it runs as-is on any x86_64 Linux, old or new. |
 | macOS (Apple Silicon) | `crackNum-<version>-macos-arm64.tar.gz` | Includes `CrackNum.app`. Ad-hoc signed rather than notarized, so clear the quarantine flag as shown below. |
+| Windows (x86_64) | `crackNum-<version>-windows-x86_64.zip` | Includes `CrackNumGUI.exe`. Unsigned, so SmartScreen warns on first run; see below. |
 
 Unpack the bundle anywhere you like, then put the files somewhere on your `PATH`.
 `z3` has to go there too, since crackNum shells out to it for every operation.
@@ -53,9 +54,24 @@
 $ export PATH=$HOME/bin:$PATH        # put this in your login shell's startup file
 ```
 
-Either way, check with:
+**Windows** — keep the files together in one folder; `crackNum.exe` looks beside
+itself for the GUI and the solver before consulting your `PATH`:
 
 ```
+> Expand-Archive crackNum-<version>-windows-x86_64.zip -DestinationPath .
+> cd crackNum-<version>-windows-x86_64
+> New-Item -ItemType Directory -Force $env:USERPROFILE\bin
+> Copy-Item * $env:USERPROFILE\bin
+```
+
+Then add that folder to your `PATH` and open a new terminal, since a `PATH` change
+does not reach terminals that are already running. The binaries are not code-signed,
+so the first run may raise a SmartScreen prompt; choose "More info" then "Run
+anyway", or tick "Unblock" in the .zip's Properties before extracting.
+
+Any of the three, check with:
+
+```
 $ crackNum -fsp 3.5
 $ crackNum --gui
 ```
@@ -411,7 +427,7 @@
 the same formats.
 
 If you installed from a [release bundle](#prebuilt-binaries-nothing-to-build-no-haskell-toolchain)
-the GUI is already in it, and there is nothing to build on either platform. The rest
+the GUI is already in it, and there is nothing to build on any of the three. The rest
 of this section is for installing from Hackage or from a source checkout.
 
 **macOS** — a native Swift/AppKit app (`GUI/swiftGUI/`). It is not part of the
@@ -437,7 +453,21 @@
 script, either put it on your PATH as `crackNum.tcl`, or point at it directly
 with `CRACKNUM_TCL=/path/to/crackNum.tcl`.
 
-On both platforms, launch the GUI from the command line via the `--gui` option,
+**Windows** — a native WinForms app (`GUI/winGUI/`). It targets .NET Framework 4.8,
+which ships as part of Windows 10 and 11, so the built executable needs no runtime
+install. Like the macOS app it is not part of the Hackage package; building it
+needs a clone and the .NET SDK:
+
+```
+> git clone https://github.com/LeventErkok/crackNum.git
+> cd crackNum\GUI\winGUI
+> dotnet build -c Release
+```
+
+Put the resulting `CrackNumGUI.exe` next to `crackNum.exe`, or point at it with
+`CRACKNUM_GUI=C:\path\to\CrackNumGUI.exe`.
+
+On all three platforms, launch the GUI from the command line via the `--gui` option,
 which forwards any format/rounding flags and value to the app:
 
 ```
diff --git a/crackNum.cabal b/crackNum.cabal
--- a/crackNum.cabal
+++ b/crackNum.cabal
@@ -1,15 +1,15 @@
 Cabal-version      : 2.2
 Name               : crackNum
-Version            : 3.31
+Version            : 4.0
 Synopsis           : Crack various integer and floating-point data formats
 Description        : Crack IEEE-754 and other float formats and arbitrary sized words and integers, showing the layout.
-                     Along with a command-line interface on any platform, native MacOS and Tcl-based Linux GUIs are available as well:
+                     Along with a command-line interface on any platform, native MacOS and Windows GUIs and a Tcl-based Linux GUI are available as well:
                      .
                      <<https://raw.githubusercontent.com/LeventErkok/crackNum/master/crackNumGUI.png>>
                      .
                      For details, please see: <http://github.com/LeventErkok/crackNum/>. See the
                      <https://github.com/LeventErkok/crackNum/releases releases page>
-                     for binary releases for Mac and Linux.
+                     for binary releases for Mac, Linux, and Windows.
 License            : BSD-3-Clause
 License-file       : LICENSE
 Author             : Levent Erkok
diff --git a/src/CrackNum/GUI.hs b/src/CrackNum/GUI.hs
--- a/src/CrackNum/GUI.hs
+++ b/src/CrackNum/GUI.hs
@@ -19,7 +19,7 @@
 import System.Environment (lookupEnv, getExecutablePath)
 import System.Exit        (ExitCode(..))
 import System.FilePath    (takeDirectory, (</>))
-import System.Process     (rawSystem)
+import System.Process     (rawSystem, spawnProcess)
 import qualified System.Info as Info
 
 import Paths_crackNum (getDataFileName)
@@ -31,6 +31,26 @@
 tclRelPath :: FilePath
 tclRelPath = "GUI/tclGUI/crackNum.tcl"
 
+-- | The Windows GUI executable. Note the name: Windows file systems are
+-- case-insensitive, so calling it CrackNum.exe would make it the very same file as
+-- the crackNum.exe it drives, and the two cannot share a directory.
+winGUIExe :: FilePath
+winGUIExe = "CrackNumGUI.exe"
+
+-- | Where a file would sit if it were next to the running executable.
+--
+-- NB. getExecutablePath resolves symlinks, so this finds the file even when the
+-- binary is reached through a link from elsewhere on the PATH.
+besideExe :: FilePath -> IO FilePath
+besideExe f = do exe <- getExecutablePath
+                 pure (takeDirectory exe </> f)
+
+-- | The first candidate that exists; if none do, die with the given message.
+search :: [FilePath] -> [String] -> IO FilePath
+search []     onFail = die onFail
+search (c:cs) onFail = do ok <- doesFileExist c
+                          if ok then pure c else search cs onFail
+
 -- | Locate the Tcl/Tk GUI script. Normally it is installed together with the
 -- binary, so this just works; we look in four places, in order:
 --
@@ -50,22 +70,13 @@
                                            , ""
                                            , "    " ++ p
                                            ]
-                 Nothing -> do beside    <- besideExe
+                 Nothing -> do beside    <- besideExe "crackNum.tcl"
                                installed <- getDataFileName tclRelPath
                                mbPath    <- findExecutable "crackNum.tcl"
                                case mbPath of
                                  Just p  -> pure p
                                  Nothing -> search [beside, installed] (noTcl beside installed)
-  where -- NB. getExecutablePath resolves symlinks, so this finds the script even
-        -- when the binary is reached through a link from elsewhere on the PATH.
-        besideExe = do exe <- getExecutablePath
-                       pure (takeDirectory exe </> "crackNum.tcl")
-
-        search []     onFail = die onFail
-        search (c:cs) onFail = do ok <- doesFileExist c
-                                  if ok then pure c else search cs onFail
-
-        noTcl beside installed =
+  where noTcl beside installed =
              [ "Cannot find the CrackNum GUI script (crackNum.tcl)."
              , ""
              , "Looked in:"
@@ -87,13 +98,58 @@
              , "    git clone http://github.com/LeventErkok/crackNum.git"
              ]
 
+-- | Locate the Windows GUI executable, mirroring 'locateTcl'. We look in three
+-- places, in order:
+--
+--   1. $CRACKNUM_GUI, if set: an explicit override, as on macOS.
+--   2. Next to the executable itself, which is how the binary zip is laid out.
+--   3. The PATH, for an installation that puts the two elsewhere.
+locateWinGUI :: IO FilePath
+locateWinGUI = do mbEnv <- lookupEnv "CRACKNUM_GUI"
+                  case mbEnv of
+                    Just p  -> do ok <- doesFileExist p
+                                  if ok
+                                     then pure p
+                                     else die [ "The CRACKNUM_GUI environment variable is set, but does not name a file:"
+                                              , ""
+                                              , "    " ++ p
+                                              ]
+                    Nothing -> do beside <- besideExe winGUIExe
+                                  ok     <- doesFileExist beside
+                                  if ok
+                                     then pure beside
+                                     else do mbPath <- findExecutable winGUIExe
+                                             case mbPath of
+                                               Just p  -> pure p
+                                               Nothing -> die (noGUI beside)
+  where noGUI beside =
+             [ "Cannot find the CrackNum GUI (" ++ winGUIExe ++ ")."
+             , ""
+             , "Looked in:"
+             , "  %CRACKNUM_GUI%                (not set)"
+             , "  " ++ beside
+             , "  " ++ winGUIExe ++ " on your PATH  (not found)"
+             , ""
+             , "The GUI ships in the same zip as crackNum.exe, so seeing this means the"
+             , "copy next to the binary is missing or the binary has been moved."
+             , ""
+             , "Get the Windows bundle from:"
+             , ""
+             , "    https://github.com/LeventErkok/crackNum/releases"
+             , ""
+             , "Or point at a copy directly:"
+             , ""
+             , "    $env:CRACKNUM_GUI = \"C:\\path\\to\\" ++ winGUIExe ++ "\""
+             ]
+
 -- | Launch the graphical interface, forwarding all remaining arguments
 -- (format flags, rounding mode, and/or the value to crack) so the GUI can preselect
 -- them. The GUI itself calls back into this executable to do the actual cracking.
 --
 -- On macOS the GUI is a Swift/AppKit app; CRACKNUM_GUI can override the .app bundle
 -- location. On Linux the GUI is a Tcl/Tk script; 'wish' is located via PATH, and
--- 'crackNum.tcl' via 'locateTcl'.
+-- 'crackNum.tcl' via 'locateTcl'. On Windows it is a WinForms app located by
+-- 'locateWinGUI', with CRACKNUM_GUI overriding as it does on macOS.
 launchGUI :: [String] -> IO ()
 launchGUI vals
   | Info.os == "darwin"
@@ -134,6 +190,13 @@
                               , ""
                               , "Tried: " ++ wish ++ " " ++ tcl
                               ]
+  | Info.os == "mingw32"
+  = do gui <- locateWinGUI
+       -- Spawned rather than run through rawSystem: the GUI outlives this process,
+       -- matching what 'open -n' gives us on macOS. Waiting would pin the console
+       -- for as long as the window stayed open.
+       _   <- spawnProcess gui vals
+       pure ()
   | True
   = die [ "The --gui option is not supported on this platform (" ++ Info.os ++ ")."
         , "Use crackNum directly from the command line."
diff --git a/src/CrackNum/Main.hs b/src/CrackNum/Main.hs
--- a/src/CrackNum/Main.hs
+++ b/src/CrackNum/Main.hs
@@ -21,6 +21,7 @@
 import qualified Control.Exception as C
 
 import System.Environment    (getArgs, getProgName, withArgs)
+import System.FilePath       (dropExtension)
 import System.Console.GetOpt (ArgOrder(Permute), getOpt)
 import System.Exit           (exitFailure)
 
@@ -42,7 +43,11 @@
 -- | main entry point to crackNum
 main :: IO ()
 main = do argv <- getArgs
-          pn   <- getProgName
+
+          -- dropExtension: on Windows getProgName keeps the .exe, which would put
+          -- "crackNum.exe" in the --version banner and every usage message, where
+          -- the other platforms say "crackNum".
+          pn   <- dropExtension <$> getProgName
 
           let rt = "--runTests"
 
diff --git a/src/CrackNum/TestSuite.hs b/src/CrackNum/TestSuite.hs
--- a/src/CrackNum/TestSuite.hs
+++ b/src/CrackNum/TestSuite.hs
@@ -22,6 +22,7 @@
 import System.FilePath
 
 import System.Directory (removeFile)
+import System.IO (IOMode(WriteMode), hPutStr, hSetNewlineMode, noNewlineTranslation, withFile)
 import System.Process (readProcessWithExitCode)
 
 import Data.List (intercalate)
@@ -35,12 +36,17 @@
 
         args = words as
 
+        -- NB. Written with newline translation off, so the temp file uses LF on
+        -- every platform. The golds are LF; plain writeFile would emit CRLF on
+        -- Windows, and --accept there would rewrite every gold in the process.
+        write f s = withFile f WriteMode $ \h -> hSetNewlineMode h noNewlineTranslation >> hPutStr h s
+
         run = do (ec, so, se) <- readProcessWithExitCode "crackNum" args ""
-                 writeFile gfTmp $ intercalate "\n" $ [ "Arguments: " ++ as
-                                                      , "Exit code: " ++ show ec
-                                                      , so
-                                                      ]
-                                                      ++ concat [["STDERR:", se] | not (null se)]
+                 write gfTmp $ intercalate "\n" $ [ "Arguments: " ++ as
+                                                  , "Exit code: " ++ show ec
+                                                  , so
+                                                  ]
+                                                  ++ concat [["STDERR:", se] | not (null se)]
 
         diff ref new = ["diff", "-w", "-u", ref, new]
 
