diff --git a/CHANGES.md b/CHANGES.md
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,7 +1,20 @@
 * Hackage: <http://hackage.haskell.org/package/crackNum>
 * GitHub:  <http://github.com/LeventErkok/crackNum/>
 
-* Latest Hackage released version: 4.3, 2026-08-27
+* Latest Hackage released version: 4.4, 2026-08-31
+
+### Version 4.4, 2026-08-31
+
+  * New web GUI: a browser front-end, alongside the existing macOS, Windows and
+    Linux ones. It runs the same unmodified `crackNum` binary and needs nothing
+    installed beyond Python 3.9. Adds permalinks and dark mode. See
+    `GUI/webGUI/README.md`.
+
+  * All four GUIs now show the crackNum version in a footer, with links to the
+    project page and to the issue tracker.
+
+  * `GUI/webGUI/deploy` has nginx and systemd files for running the web GUI on a
+    server.
 
 ### Version 4.3, 2026-08-27
 
diff --git a/GUI/tclGUI/crackNum.tcl b/GUI/tclGUI/crackNum.tcl
--- a/GUI/tclGUI/crackNum.tcl
+++ b/GUI/tclGUI/crackNum.tcl
@@ -647,6 +647,119 @@
 }
 
 # ---------------------------------------------------------------------------
+# Footer: the version, and where to report what it gets wrong
+# ---------------------------------------------------------------------------
+
+set REPO_URL   "https://github.com/LeventErkok/crackNum"
+set ISSUES_URL "$REPO_URL/issues"
+
+# Ask the binary its version rather than carrying a copy here, which would drift
+# the first time a release bumped the cabal file and not this file. Empty if
+# crackNum is missing or says something unexpected: the output pane is already
+# reporting a missing binary, and a version invented on top of that would be
+# worse than none.
+proc crackNumVersion {} {
+    global CRACKNUM
+    if {$CRACKNUM eq ""} { return "" }
+    if {[catch {exec $CRACKNUM -v 2>@1} out]} { return "" }
+    # "crackNum v4.3, (c) Levent Erkok. Released with a BSD3 license."
+    # \y, not \b: in Tcl's regexp \b is a backspace, so \b never matches here.
+    if {[regexp {\yv(\d[\w.]*)} $out -> v]} { return $v }
+    return ""
+}
+
+# Tk has no "open this in a browser", so hand off to the platform's opener.
+# Returns 1 if something was launched, 0 if nothing could be.
+#
+# macOS and Windows each have exactly one answer. Linux has none: xdg-open is
+# the convention but ships with the xdg-utils package, which a minimal install
+# may simply not have -- and its absence is the whole reason this used to do
+# nothing at all when clicked. So try the plausible openers in turn and report
+# honestly when every one is missing, rather than swallowing it.
+proc openURL {url} {
+    set cmds {}
+
+    if {$::tcl_platform(platform) eq "windows"} {
+        lappend cmds [list {*}[auto_execok start] "" $url]
+    } elseif {$::tcl_platform(os) eq "Darwin"} {
+        lappend cmds [list open $url]
+    } else {
+        # $BROWSER first: if the user has said what they want, honour it.
+        if {[info exists ::env(BROWSER)] && $::env(BROWSER) ne ""} {
+            lappend cmds [list $::env(BROWSER) $url]
+        }
+        # firefox ahead of the indirection layers: naming a browser we can see
+        # on PATH is one step, where xdg-open and gio each add a lookup that can
+        # fail for its own reasons. The desktop-integration openers stay as
+        # fallbacks for machines without firefox.
+        foreach opener {firefox xdg-open gio gnome-open kde-open5 kde-open
+                        x-www-browser sensible-browser chromium
+                        chromium-browser google-chrome} {
+            if {$opener eq "gio"} {
+                # gio wants a subcommand, and is only useful if a handler is
+                # actually registered: with none it exits non-zero *after* we
+                # have backgrounded it, which reads as success here and would
+                # stop us trying the browsers below -- another silent no-op.
+                # Ask first, and simply skip gio when the answer is no.
+                if {![catch {exec gio mime x-scheme-handler/https} reply]
+                    && [string match -nocase "*default*:*" $reply]} {
+                    lappend cmds [list gio open $url]
+                }
+            } else {
+                lappend cmds [list $opener $url]
+            }
+        }
+    }
+
+    foreach cmd $cmds {
+        # auto_execok rather than trusting exec to fail: a missing opener and a
+        # browser that launched and then exited look the same to a backgrounded
+        # exec, and only the first should send us on to the next candidate.
+        if {[auto_execok [lindex $cmd 0]] eq ""} { continue }
+        if {![catch {exec {*}$cmd &}]} { return 1 }
+    }
+    return 0
+}
+
+# Packed with -before .main: .main is packed with -expand yes and would otherwise
+# have already claimed the space this bar needs, leaving it squeezed or invisible.
+frame .footer
+pack .footer -side bottom -fill x -padx 8 -pady {0 6} -before .main
+
+set VERSION [crackNumVersion]
+if {$VERSION ne ""} {
+    label .footer.ver -text "crackNum v$VERSION" -anchor w \
+          -fg #2b62e8 -cursor hand2
+    pack  .footer.ver -side left
+}
+
+label .footer.link -text "Bugs/Feedback?" -anchor e \
+      -fg #2b62e8 -cursor hand2
+pack  .footer.link -side right
+
+# Underline both links, leaving anything else in the footer in the default font.
+set linkFont [font actual [.footer.link cget -font]]
+dict set linkFont -underline 1
+.footer.link configure -font $linkFont
+if {[winfo exists .footer.ver]} { .footer.ver configure -font $linkFont }
+
+# If no browser could be launched, say so and show the URL: a deliberate click
+# that produces nothing at all is indistinguishable from a broken widget, which
+# is exactly how this first shipped.
+proc followLink {url} {
+    if {![openURL $url]} {
+        tk_messageBox -parent . -icon info -title "crackNum" \
+            -message "Could not find a browser to open:\n\n$url" \
+            -detail "Copy the address above, or install xdg-utils."
+    }
+}
+
+bind .footer.link <Button-1> {followLink $ISSUES_URL}
+if {[winfo exists .footer.ver]} {
+    bind .footer.ver <Button-1> {followLink $REPO_URL}
+}
+
+# ---------------------------------------------------------------------------
 # Start
 # ---------------------------------------------------------------------------
 showOutput $WELCOME
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -169,6 +169,11 @@
 $ crackNum --gui 0xdeadbeef      -- open it pre-filled with a value to decode
 ```
 
+There is also a browser front-end, which runs the same binary and serves the same
+interface over HTTP. It needs nothing beyond Python 3.9, and adds permalinks that
+reproduce a result exactly. See [`GUI/webGUI`](GUI/webGUI/README.md); to run it on
+a server, see [`GUI/webGUI/deploy`](GUI/webGUI/deploy/README.md).
+
 ### Usage info
 ```
 Usage: crackNum value OR binary/hex-pattern
diff --git a/crackNum.cabal b/crackNum.cabal
--- a/crackNum.cabal
+++ b/crackNum.cabal
@@ -1,9 +1,9 @@
 Cabal-version      : 2.2
 Name               : crackNum
-Version            : 4.3
+Version            : 4.4
 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 Windows GUIs and a Tcl-based Linux GUI are available as well:
+                     Along with a command-line interface on any platform, native MacOS and Windows GUIs, a Tcl-based Linux GUI, and a browser front-end are available as well:
                      .
                      <<https://raw.githubusercontent.com/LeventErkok/crackNum/master/crackNumGUI.png>>
                      .
