Repository: code-golf/code-golf

Issue:
Fuzzy search text box for jumping between holes and langs.
**Description**

- Ctrl+P pulls up a little text box (shortcut based on VS Code and chrome devtools fuzzy-file-search buttons)
- Type and it fuzzy-searches other holes to go to.
- Type "rpssl" then Enter to go to rock-paper-scissors-spock-lizard.
- Type "pi # asm" then Enter to go to π#assembly
- Type "tau # c", then down arrow then Enter to pick the second result and go to tau#cpp

![typing 'play' in code-golf repo and getting play.go, play_test.go recommended](https://github.com/user-attachments/assets/5c087716-8e8c-405e-86b7-f80e47aee43b)

**Search details**

- If there is no `#`, it searches for just hole.
- If there is `#`, it splits at the `#` and uses the contents before to filter hole, and the contents after to filter lang
- Just filter by `String.includes()` for MVP, can be extended to fuzzy searching later.

**UI details**

- Ctrl+P twice should do regular print (Thanks Michal for this solution, I was thinking a 'print' button otherwise)
- Just like other modals (see solution-delete button), escape should exit, and clicking outside should exit.
- If placeholder text is used to describe the behavior, use aria-describedby to a display:none element with the same text.
- There will need to be a button for users without keyboard inputs. Magnifying glass icon? 
  - The top bar is getting cluttered. It should properly overflow (hamburger menu?) instead of triggering horizontal scroll.

**Future possible improvements:**
1. Add aliases for searching (like `π` to `pi` and `cpp` to `c++` if that doesn't exist already)
2. Fuzzy searching
3. Show which holes you've completed (and show those first when searching for lang only)
4. Show your medal / point positions in the search results (so `#c` will tell you how you're doing in C)
5. Somehow setup tags, like `[sequence]#c` will show all sequence holes you've solved in c.
6. Link to https://code.golf/rankings/holes/all/c/bytes if you write `#c`, and https://code.golf/rankings/holes/css-colors/all/bytes if you write `css`, etc.

Discussion:
@MichalMarsalek: I also saw Ctrl+F being used for this purpose on the web. The issue with both shortcuts is that it overrides the standard browser shortcuts which I saw being solved by triggering the browser action when the menu is already opened (so when I want to print the page I would hit Ctrl+P twice).

[Stripe docs do this](https://docs.stripe.com/js)@voytxt: Ctrl + K could be used instead, it's the default shortcut for switching in a lot of apps including Discord & GitHub. Yes, it is a shortcut in major browsers (for searching with your default engine), but I think it's way less used than Ctrl + F.@MichalMarsalek: `@name` searches golfers?
just `@` should show first a link to your own profile, then to all golfers you follow@MichalMarsalek: `wiki` could go to `/wiki` and `wiki#nim` to `/wiki/langs/nim`@JRaspass: I believe this is now done since #2010

Solution patch:
diff --git a/.github/workflows/lint-test.yml b/.github/workflows/lint-test.yml
index bea247e0..661e316e 100644
--- a/.github/workflows/lint-test.yml
+++ b/.github/workflows/lint-test.yml
@@ -7,6 +7,14 @@ jobs:
   build:
     runs-on: ubuntu-latest
     steps:
+
+      # FIXME Downgrade Docker to prevent "failed to compute cache key" errors.
+      - run: |
+          curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
+          sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu  $(lsb_release -cs)  stable"
+          sudo apt-get update
+          sudo apt-get install --allow-downgrades docker-ce=5:27.0.3*
+
       - uses: actions/checkout@v4
 
       - uses: actions/setup-go@v5
diff --git a/.github/workflows/merge-conflicts.yml b/.github/workflows/merge-conflicts.yml
new file mode 100644
index 00000000..7b2c422b
--- /dev/null
+++ b/.github/workflows/merge-conflicts.yml
@@ -0,0 +1,18 @@
+name: Merge Conflicts
+
+on:
+  push:
+  pull_request_target:
+    types: [synchronize]
+
+jobs:
+  main:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: eps1lon/actions-label-merge-conflict@v3
+        with:
+          commentOnDirty: >
+            Your pull request has conflicts that need to be resolved before it
+            can be reviewed and merged.
+          dirtyLabel: conflicts
+          repoToken: ${{ secrets.GITHUB_TOKEN }}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dc8170d0..e4cc2aba 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -20,8 +20,8 @@ In addition, each section may define the following fields:
 - `experiment` - ID of the issue that suggested this hole. All experimental holes need to link to an issue so that the community can vote and suggest improvements.
 - `variants` - List of names of the holes that are variants of this hole, including itself.
 - `case-fold` - A flag indicating that the output should be checked case insensitively.
-- `item-delimiter` - If set, indicates that the output should be understood as a collection of items that can appear in any order and that the items should be delimited by the provided token.
-- `multiset-delimiter` - If set, treats output as fixed order collection of multiple multisets, each separated by `item-delimiter`. Otherwise, output is treated as a single multiset.
+- `multiset-item-delimiter` - If set, indicates that the output should be understood as a collection of items that can appear in any order and that the items should be delimited by the provided token.
+- `output-delimiter` - If set, treats output as fixed order collection of multiple outputs and treats each one individually.
 
 Example:
 
@@ -49,7 +49,7 @@ If the hole has no inputs, the expected output needs to be placed in `/config/ho
 
 ### Holes with computed solutions
 
-For computed solutions, a function is defined in its own file in `/holes/`.
+For computed solutions, a function is defined and registered in its own file in `/holes/`.
 This function must return data for at least one test run. Data for each run takes form of a list of inputs and a single string containing the expected output.
 
 `/hole/hole-name.go`
@@ -62,6 +62,17 @@ var _ = answerFunc("hole-id", func() []Answer {
 }
 ```
 
+### Custom judges
+In addition to simply comparing the user output to the expected output (either from the static file or the generator) by equality, holes can employ custom judges. Given a run (list of inputs and a user output) a judge determines whether the output is correct or not. If it is, the judge must return the user output. Otherwise, it should return an output that is correct and is as close to the user output as possible. To write a custom judge, you may use the `perOutputJudge` or `oneOfPerOutputJudge` helper functions. A judge is registered in a similar way to the answer function:
+
+```go
+var _ = judge("hole-id", func(run Run) string {
+    // Return a valid answer similar to `run.Stdout`
+
+    return run.Answer.Answer;
+}
+```
+
 ### Tips for randomizing the tests
 
 The point of multiple random tests is to avoid cheating solutions that only implement some part of the problem.
diff --git a/Makefile b/Makefile
index 1fa32abb..6af7016d 100644
--- a/Makefile
+++ b/Makefile
@@ -79,7 +79,7 @@ mathjax-fonts:
 
 lint:
 	@docker run --rm -v $(CURDIR):/app -w /app \
-	    golangci/golangci-lint:v2.0.2 golangci-lint run
+	    golangci/golangci-lint:v2.3.0 golangci-lint run
 
 	@node_modules/.bin/eslint js
 
diff --git a/build-langs b/build-langs
index cef17d1f..d41345be 100755
--- a/build-langs
+++ b/build-langs
@@ -2,7 +2,7 @@
 
 use TOML::Thumb;
 
-unit sub MAIN(Bool :$no-push, *@langs);
+unit sub MAIN(Bool :$no-cache, Bool :$no-push, *@langs);
 
 chdir $*PROGRAM.dirname;
 
@@ -21,13 +21,22 @@ run <docker buildx build>,
     '--tag',    'codegolf/lang-base',
     '.';
 
-for %langs{ @langs || * }:p.sort: *.key.fc -> (:key($name), :value(%lang)) {
+# Build all languages if no arguments are specified.
+@langs ||= %langs.keys.sort: *.fc;
+
+for %langs{@langs}:p -> (:key($name), :value(%lang)) {
     my $id  = id($name);
     my $img = "codegolf/lang-$id";
 
-    # Tag the builder stage so "docker system prune" won't remove it.
-    run «docker buildx build -t "{$img}-builder" --pull --target builder "langs/$id"»;
-    run «docker buildx build -t $img "langs/$id"»;
+    if !$no-cache {
+        # Tag the builder stage so "docker system prune" won't remove it.
+        run «docker buildx build -t "{$img}-builder" --pull --target builder "langs/$id"»;
+    }
+
+    run <docker buildx build>,
+         ( '--no-cache' if $no-cache ),
+         '--tag', $img,
+         "langs/$id";
 
     # Size.
     my $proc = run :out,
@@ -44,7 +53,7 @@ for %langs{ @langs || * }:p.sort: *.key.fc -> (:key($name), :value(%lang)) {
     }
 
     # Version.
-    if $id ~~ any <05ab1e apl arturo assembly befunge brainfuck cjam erlang fish golfscript hare hexagony k scala stax vyxal> {
+    if $id ~~ any <05ab1e apl arturo assembly befunge brainfuck cjam erlang fish golfscript hare hexagony j k qore scala stax umka vyxal> {
         "langs/$id/Dockerfile".IO.slurp ~~ / ' VER=' (\S+) | ' VERSION=' (\S+) /;
 
         %lang<version> = do given $id {
@@ -85,7 +94,7 @@ for %langs{ @langs || * }:p.sort: *.key.fc -> (:key($name), :value(%lang)) {
             when 'Common Lisp' { $ver = "GNU CLISP $digits" }
             when 'D'           { $ver = "DMD $digits" }
             when 'Haskell'     { $ver = "GHC $digits" }
-            when 'Java'        { $ver = "OpenJDK 24.0" } # FIXME no decimal place.
+            when 'Java'        { $ver = "OpenJDK $digits" }
             when 'JavaScript'  { $ver = "V8 $digits" }
             when 'Pascal'      { $ver = "FPC $digits" }
             when 'Prolog'      { $ver = "SWI-Prolog $digits" }
diff --git a/config/config.go b/config/config.go
index a994a58a..98ebe27d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -15,7 +15,7 @@ var id = strings.NewReplacer(
 	"🦄 ", "", "💎 ", "", "🥇 ", "",
 
 	// Remove.
-	"!", "", "(", "", ")", "", ",", "", "’", "",
+	"!", "", "(", "", ")", "", ",", "", "’", "", "?", "",
 
 	// Hyphenate.
 	" -", "-", " ", "-", ";", "-", "–", "-",
diff --git a/config/data/cheevos.toml b/config/data/cheevos.toml
index a7b708b8..5443ec1d 100644
--- a/config/data/cheevos.toml
+++ b/config/data/cheevos.toml
@@ -1,13 +1,8 @@
 # Categories in alpha order, cheevos in difficulty then alpha order.
+# Date specific cheevos are in order of starting date.
 
 # Date Specific
 
-[['Date Specific']]
-name        = 'Happy Birthday, Code Golf'
-emoji       = '🎂'
-dates       = [2000-10-02, 2000-10-03]
-description = 'Solve any hole on <a href=//github.com/code-golf/code-golf/commit/4b44>2 Oct</a>.'
-
 [['Date Specific']]
 name        = 'Hugs and Kisses'
 emoji       = '🥰'
@@ -15,28 +10,34 @@ dates       = [2000-02-14, 2000-02-15]
 description = 'Solve <a href=/tic-tac-toe>Tic-tac-toe</a> on <a href=//www.wikipedia.org/wiki/Valentine%27s_Day>14 Feb</a>.'
 
 [['Date Specific']]
-name        = 'Independence Day'
-emoji       = '🇺🇸'
-dates       = [2000-07-04, 2000-07-05]
-description = 'Solve <a href=/united-states>United States</a> on <a href=//www.wikipedia.org/wiki/Independence_Day_(United_States)>4 Jul</a>.'
+name        = 'Pi Day'
+emoji       = '🥧'
+dates       = [2000-03-14, 2000-03-15]
+description = 'Solve <a href=/π>π</a> on <a href=//www.wikipedia.org/wiki/Pi_Day>14 Mar</a>.'
 
 [['Date Specific']]
 name        = 'May the 4ᵗʰ Be with You'
 emoji       = '🌌'
 dates       = [2000-05-04, 2000-05-05]
-description = 'Solve <a href=/star-wars-opening-crawl>Star Wars Opening Crawl</a> on <a href=//www.wikipedia.org/wiki/Star_Wars_Day>4 May</a>.'
+description = 'Solve <a href=/star-wars-gpt>Star Wars GPT</a> or <a href=/star-wars-opening-crawl>Star Wars Opening Crawl</a> on <a href=//www.wikipedia.org/wiki/Star_Wars_Day>4 May</a>.'
 
 [['Date Specific']]
-name        = 'Pi Day'
-emoji       = '🥧'
-dates       = [2000-03-14, 2000-03-15]
-description = 'Solve <a href=/π>π</a> on <a href=//www.wikipedia.org/wiki/Pi_Day>14 Mar</a>.'
+name        = 'How about Second Pi?'
+emoji       = '🍕'
+dates       = [2000-06-28, 2000-06-29]
+description = 'Solve <a href=/τ>τ</a> on <a href=//www.wikipedia.org/wiki/Pi_Day#Alternative_dates>28 Jun</a>.'
 
 [['Date Specific']]
-name        = 'Twelvetide'
-emoji       = '🎅'
-dates       = [1999-12-25, 2000-01-06, 2000-12-25, 2001-01-06]
-description = 'Solve <a href=/12-days-of-christmas>12 Days of Christmas</a> during <a href=//www.wikipedia.org/wiki/Twelvetide>25 Dec – 5 Jan</a>.'
+name        = 'Independence Day'
+emoji       = '🇺🇸'
+dates       = [2000-07-04, 2000-07-05]
+description = 'Solve <a href=/united-states>United States</a> on <a href=//www.wikipedia.org/wiki/Independence_Day_(United_States)>4 Jul</a>.'
+
+[['Date Specific']]
+name        = 'Happy Birthday, Code Golf'
+emoji       = '🎂'
+dates       = [2000-10-02, 2000-10-03]
+description = 'Solve any hole on <a href=//github.com/code-golf/code-golf/commit/4b44>2 Oct</a>.'
 
 [['Date Specific']]
 name        = 'Vampire Byte'
@@ -44,6 +45,12 @@ emoji       = '🧛'
 dates       = [2000-10-31, 2000-11-01]
 description = 'Solve <a href=/vampire-numbers>Vampire Numbers</a> on <a href=//www.wikipedia.org/wiki/Halloween>31 Oct</a>.'
 
+[['Date Specific']]
+name        = 'Twelvetide'
+emoji       = '🎅'
+dates       = [1999-12-25, 2000-01-06, 2000-12-25, 2001-01-06]
+description = 'Solve <a href=/12-days-of-christmas>12 Days of Christmas</a> during <a href=//www.wikipedia.org/wiki/Twelvetide>25 Dec – 5 Jan</a>.'
+
 # GitHub
 
 [[GitHub]]
@@ -73,6 +80,12 @@ name        = 'Alchemist'
 emoji       = '🧙'
 description = 'Solve <a href=/game-of-life#elixir>Game of Life in Elixir</a>.'
 
+[['Hole/Lang Specific']]
+name        = 'Alphabet Soup'
+emoji       = '🥣'
+description = 'Solve <a href=/scrambled-sort>Scrambled Sort</a> in any three of <a href=/scrambled-sort#c>C</a>, <a href=/scrambled-sort#d>D</a>, <a href=/scrambled-sort#j>J</a>, <a href=/scrambled-sort#k>K</a>, <a href=/scrambled-sort#r>R</a>, or <a href=/scrambled-sort#v>V</a>.'
+target      = 3
+
 [['Hole/Lang Specific']]
 name        = 'Archivist'
 emoji       = '📚'
@@ -431,7 +444,7 @@ target      = 1984
 [['Total Points']]
 name        = 'It’s Over 9000!'
 emoji       = '🐉'
-description = 'Earn over 9,000 total points in either bytes or chars scoring.'
+description = 'Earn 9,001 total points in either bytes or chars scoring.'
 target      = 9001
 
 [['Total Points']]
@@ -451,3 +464,15 @@ name        = '0xDEAD'
 emoji       = '🪦'
 description = 'Earn 57,005 total points in either bytes or chars scoring.'
 target      = 57005
+
+[['Total Points']]
+name        = 'Overflowing'
+emoji       = '⛲'
+description = 'Earn 65,536 total points in either bytes or chars scoring.'
+target      = 65536
+
+[['Total Points']]
+name        = 'Into Space!'
+emoji       = '🚀'
+description = 'Earn <a href=//en.wikipedia.org/wiki/Kármán_line>100,000</a> total points in either bytes or chars scoring.'
+target      = 100000
diff --git a/config/data/holes.toml b/config/data/holes.toml
index f74501e5..7ea0cc12 100644
--- a/config/data/holes.toml
+++ b/config/data/holes.toml
@@ -215,7 +215,7 @@ preamble = '''
 <p>
     An arithmetic number is a number for which the average of its divisors is
     an integer. For example, number <b>6</b> is arithmetic because its divisors
-    (<b>1</b>, <b>2</b>, <b>3</b> and <b>6</b>) average <b>3</b>.
+    (<b>1</b>, <b>2</b>, <b>3</b>, and <b>6</b>) average <b>3</b>.
 
 <p>
     Print all the arithmetic numbers from <b>1</b> to <b>10,000</b> inclusive,
@@ -669,8 +669,13 @@ released = 2020-06-21
 synopsis = 'Convert CSS color keywords to their hex values.'
 preamble = '''
 <p>
+{{ if eq .Name "CSS Colors" }}
     Given a list of all the CSS color keywords print the hex values in the
     corresponding order. The full mapping is as follows:
+{{ else }}
+    For each given color hex code print one of the corresponding css keywords.
+    The full mapping is as follows:
+{{ end }}
 
 <div id=colors class="tables-4col">
 {{ range .DataMap }}
@@ -696,6 +701,12 @@ preamble = '''
 </div>
 '''
 
+[['CSS Colors'.variants]]
+name             = 'CSS Colors (Inverse)'
+experiment       = 1353
+output-delimiter = "\n"
+synopsis         = 'Convert color hex values to their CSS keywords.'
+
 ['CSS Grid']
 category = 'Art'
 experiment = 1214
@@ -812,6 +823,9 @@ preamble = '''
 ['Day of Week']
 category = 'Transform'
 released = 2023-04-16
+links = [
+    { name = 'Wikipedia', url = '//en.wikipedia.org/wiki/Determination_of_the_day_of_the_week' },
+]
 synopsis = 'Given a date, output the day of the week.'
 preamble = '''
 <p>
@@ -1682,7 +1696,7 @@ experiment = 1449
 category   = 'Sequence'
 links = [
     { name = 'OEIS A002182', url = '//oeis.org/A002182' },
-    { name = 'Wikipedia', url = '//en.wikipedia.org/wiki/Highly_composite_number' },
+    { name = 'Wikipedia',    url = '//en.wikipedia.org/wiki/Highly_composite_number' },
 ]
 synopsis = 'Print all the highly composite numbers from 1 to 250,000,000.'
 preamble = '''
@@ -1704,7 +1718,8 @@ preamble = '''
 experiment = 1448
 category   = 'Art'
 links = [
-    { name = 'Wikipedia', url = '//en.wikipedia.org/wiki/Hilbert_curve' },
+    { name = 'Rosetta Code', url = '//rosettacode.org/wiki/Hilbert_curve' },
+    { name = 'Wikipedia',    url = '//en.wikipedia.org/wiki/Hilbert_curve' },
 ]
 synopsis = 'Print five iterations of the Hilbert curve.'
 preamble = '''
@@ -2126,11 +2141,11 @@ preamble = '''
 
 [Mandelbrot]
 category = 'Art'
-experiment = 813
 links = [
     { name = 'Rosetta Code', url = '//rosettacode.org/wiki/Mandelbrot_set' },
     { name = 'Wikipedia',    url = '//en.wikipedia.org/wiki/Mandelbrot_set' },
 ]
+released = 2025-07-01
 synopsis = 'Draw the Mandelbrot set.'
 preamble = '''
 <p>
@@ -2477,9 +2492,9 @@ preamble = '''
 category = 'Gaming'
 links = [
     { name = 'Rosetta Code', url = '//rosettacode.org/wiki/N-queens_problem' },
-    { name = 'Wikipedia', url = '//en.wikipedia.org/wiki/Eight_queens_puzzle' },
+    { name = 'Wikipedia',    url = '//en.wikipedia.org/wiki/Eight_queens_puzzle' },
 ]
-item-delimiter = "\n"
+multiset-item-delimiter = "\n"
 released = 2024-05-01
 synopsis = 'Print all solutions to the N queens problem.'
 preamble = '''
@@ -2913,9 +2928,26 @@ preamble = '''
 </div>
 '''
 
+['Polygon Triangulations']
+category       = 'Computing'
+multiset-item-delimiter = "\n"
+synopsis       = 'Print all triangulations of polygons with 3-8 sides.'
+links = [
+    { name = 'Wikipedia', url = '//en.wikipedia.org/wiki/Polygon_triangulation' },
+]
+experiment = 2181
+preamble = '''
+<p>
+    Print all possible triangulations of a regular polygon with 3-8 sides.
+    The vertices of the polygon are labeled alphabetically, starting with A and going clockwise.
+    Each triangle should have its vertices listed in order.
+    The triangles in a triangulation should be listed lexicographically and separated by spaces.
+    The triangulations may be printed in any order.
+'''
+
 [Polyominoes]
 category       = 'Computing'
-item-delimiter = "\n\n"
+multiset-item-delimiter = "\n\n"
 released       = 2024-08-01
 synopsis       = 'Print all polyominoes of sizes 1 to 6.'
 links = [
@@ -3442,11 +3474,12 @@ preamble = '''
 </pre>
 '''
 
-['Scrambled Alphabetization']
-experiment = 1324
-category = 'Transform'
-synopsis = 'Alphabetize words according to scrambled alphabets.'
-preamble = '''
+['Scrambled Sort']
+category  = 'Transform'
+redirects = ['scrambled-alphabetization']
+released  = 2025-06-01
+synopsis  = 'Sort words according to scrambled alphabets.'
+preamble  = '''
 <p>
     Given a scrambled alphabet and a set of words, print the words in
     alphabetical order according to the given alphabet. Separate the
@@ -3458,7 +3491,7 @@ experiment = 1154
 category = 'Sequence'
 links = [
     { name = 'OEIS A001358', url = '//oeis.org/A001358' },
-    { name = 'Wikipedia', url = '//en.wikipedia.org/wiki/Semiprime' },
+    { name = 'Wikipedia',    url = '//en.wikipedia.org/wiki/Semiprime' },
 ]
 synopsis = 'Print all the semiprime numbers from 1 to 10,000.'
 preamble = '''
@@ -3472,7 +3505,6 @@ preamble = '''
 '''
 
 [Set]
-experiment = 1058
 category = 'Gaming'
 links = [
     { name = 'Rosetta Code', url = '//rosettacode.org/wiki/Set,_the_card_game' },
@@ -3860,7 +3892,7 @@ experiment = 1154
 category = 'Sequence'
 links = [
     { name = 'OEIS A007304', url = '//oeis.org/A007304' },
-    { name = 'Wikipedia', url = '//en.wikipedia.org/wiki/Sphenic_number' },
+    { name = 'Wikipedia',    url = '//en.wikipedia.org/wiki/Sphenic_number' },
 ]
 synopsis = 'Print all the sphenic numbers from 1 to 10,000.'
 preamble = '''
@@ -3874,19 +3906,20 @@ preamble = '''
 '''
 
 ['Star Wars GPT']
-experiment = 1463
 category = 'Transform'
 links = [
     { name = 'Wookieepedia', url = '//starwars.fandom.com/wiki/Opening_crawl' },
 ]
-synopsis = 'Output the most likely next word to occur given word frequency in the Star Wars opening crawl.'
+released = 2025-05-01
+synopsis = 'Print the next most likely word in the Star Wars opening crawls.'
 preamble = '''
 <p>
-    You are given input of a text *corpus* and several *prompts* on subsequent
-    lines. For each prompt, output the most likely next word in the corpus by
-    frequency (aka *mode*). If there is more than one mode, output the first
-    encountered in the corpus. In this problem, the corpus is fixed to be the
-    Star Wars opening crawl texts.
+    You are given a series of inputs. Each input contains a text <b>corpus</b>
+    and a dozen <b>prompts</b> on subsequent lines. For each prompt, output the
+    most likely next word in the corpus by frequency (aka <b>mode</b>). If
+    there is more than one mode, output the first encountered in the corpus. In
+    this problem, the corpus is fixed to be the
+    <a href=star-wars-opening-crawl>Star Wars Opening Crawl</a> texts.
 '''
 
 ['Star Wars Opening Crawl']
@@ -4160,6 +4193,23 @@ preamble = '''
 <pre>{{ .Answer }}</pre>
 '''
 
+['Topological Sort']
+experiment = 2308
+category = 'Computing'
+synopsis = 'Perform topological sort.'
+preamble = '''
+<p>
+    Given the set of edges of a directed graph, print the linear order of its
+    nodes. The order is always unique. Each edge is given on a separate line;
+    an edge from node <b>u</b> to node <b>v</b> is given as <code>u v</code>.
+    The nodes are numbered from <b>0</b> to <b>N - 1</b>, where <b>2 ≤ N ≤
+    10</b> is the number of nodes in the graph.
+
+<p>
+    This is similar to the <code>tsort</code> utility, except that output nodes
+    are separated with spaces instead of newlines.
+'''
+
 ['Transpose Sentence']
 category = 'Transform'
 released = 2024-07-25
diff --git a/config/data/langs.toml b/config/data/langs.toml
index 12d37b4e..d5f856c5 100644
--- a/config/data/langs.toml
+++ b/config/data/langs.toml
@@ -17,7 +17,7 @@ example    = '''
 
 ['><>']
 args    = [ '/usr/bin/fish', '--no-prng', '-c', '$code', '-u' ]
-size    = '4.85 MiB'
+size    = '549 KiB'
 version = '96b1cfc'
 website = 'https://github.com/primo-ppcg/fish-jit'
 example = '''
@@ -43,7 +43,7 @@ x is a no-op.
 args       = [ '/usr/bin/algol-68', '--exec', '$code', '--' ]
 experiment = 856
 size       = '4.67 MiB'
-version    = 'Algol 68 Genie 3.5.14'
+version    = 'Algol 68 Genie 3.5.15'
 website    = 'https://jmvdveer.home.xs4all.nl/en.algol-68-genie.html'
 example    = '''
 # Printing #
@@ -82,8 +82,8 @@ example    = '''
 [Arturo]
 args       = [ '/usr/bin/arturo', '-e', '$code' ]
 experiment = 915
-size       = '5.38 MiB'
-version    = '71a4677'
+size       = '5.23 MiB'
+version    = '5f3443c'
 website    = 'https://arturo-lang.io'
 example    = '''
 ; Printing
@@ -101,7 +101,7 @@ loop arg [a] [
 
 [Assembly]
 args    = [ '/usr/bin/defasm', '--size-out=3', '-w', '-r' ]
-size    = '115 MiB'
+size    = '118 MiB'
 version = 'DefAssembler 2.4.5'
 website = 'https://www.npmjs.com/package/@defasm/core'
 example = '''
@@ -170,9 +170,9 @@ syscall
 '''
 
 [AWK]
-args    = [ '/usr/bin/gawk', '-v', 'RS=\0', '$code' ]
-size    = '1.80 MiB'
-version = 'GNU AWK 5.3.1'
+args    = [ '/usr/bin/awk', '-v', 'RS=\0', '$code' ]
+size    = '1.91 MiB'
+version = 'GNU AWK 5.3.2'
 website = 'https://www.gnu.org/software/gawk/'
 example = '''
 BEGIN {
@@ -197,8 +197,8 @@ BEGIN {
 
 [Bash]
 args    = [ '/usr/bin/bash', '-s', '-' ]
-size    = '1.20 MiB'
-version = '5.2.37'
+size    = '1.27 MiB'
+version = '5.3.3'
 website = 'https://www.gnu.org/software/bash/'
 example = '''
 # Printing
@@ -238,7 +238,7 @@ Next
 [Befunge]
 args       = [ '/usr/bin/befunge', '-' ]
 experiment = 428
-size       = '3.95 MiB'
+size       = '2.48 MiB'
 version    = 'b7bf640'
 website    = 'https://github.com/VorpalBlade/cfunge'
 example    = '''
@@ -280,7 +280,7 @@ end
 args       = [ '/usr/bin/bqn', '-' ]
 experiment = 474
 size       = '1.26 MiB'
-version    = 'CBQN 0.7.0'
+version    = 'CBQN 0.9.0'
 website    = 'https://github.com/dzaima/CBQN'
 example    = '''
 # Printing
@@ -321,7 +321,7 @@ with 65536 cells; and cells are 8 bit with wrapping;
 '''
 
 [C]
-args    = [ '/usr/bin/tcc', '-run', '-' ]
+args    = [ '/usr/bin/c', '-run', '-' ]
 size    = '1.73 MiB'
 version = 'TCC 0.9.27'
 website = 'https://bellard.org/tcc/'
@@ -347,8 +347,8 @@ int main(int argc, char* argv[]) {
 ['C#']
 args    = [ '/usr/bin/c-sharp', '-' ]
 env     = [ 'DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1' ]
-size    = '107 MiB'
-version = 'C# 13.0 on .NET 9.0.2'
+size    = '108 MiB'
+version = 'C# 13.0 on .NET 9.0.6'
 website = 'https://docs.microsoft.com/dotnet/csharp/'
 example = '''
 // Printing
@@ -368,8 +368,8 @@ foreach (String arg in args)
 
 ['C++']
 args    = [ '/usr/bin/cpp', '-' ]
-size    = '152 MiB'
-version = 'Clang 20.1.1'
+size    = '151 MiB'
+version = 'Clang 20.1.8'
 website = 'https://clang.llvm.org'
 example = '''
 #include <iostream>
@@ -396,8 +396,8 @@ int main(int argc, char* argv[]) {
 [Civet]
 args    = [ '/usr/local/dist/civet', '--comptime', '--no-config', '-' ]
 env     = [ 'NODE_ICU_DATA=/usr/share/icu' ]
-size    = '72.1 MiB'
-version = '0.10.1'
+size    = '81.3 MiB'
+version = '0.10.5'
 website = 'https://civet.dev'
 example = '''
 // Printing
@@ -413,9 +413,9 @@ for arg of process.argv[2..]
 '''
 
 [CJam]
-args       = [ '/usr/bin/cjam', '-' ]
+args       = [ '/opt/java/bin/java', '-jar', 'cjam.jar', '/proc/self/fd/0' ]
 experiment = 1493
-size       = '134 MiB'
+size       = '62.7 MiB'
 version    = '0.6.5'
 website    = 'https://sourceforge.net/projects/cjam/'
 example    = '''
@@ -431,8 +431,8 @@ eaN*
 
 [Clojure]
 args    = [ '/usr/bin/clojure', '-e', '$code' ]
-size    = '66.4 MiB'
-version = 'Babashka 1.12.197'
+size    = '67.3 MiB'
+version = 'Babashka 1.12.206'
 website = 'https://babashka.org'
 example = '''
 ; Printing
@@ -449,7 +449,7 @@ example = '''
 
 [COBOL]
 args    = [ '/usr/bin/cobol', '-' ]
-size    = '4.61 MiB'
+size    = '4.21 MiB'
 version = 'GnuCOBOL 3.2.0'
 website = 'https://gnucobol.sourceforge.io'
 example = '''
@@ -484,7 +484,7 @@ end-program.
 [Coconut]
 args    = [ '/usr/bin/coconut', '--quiet', '--target', 'sys', '--keep-lines', '--argv' ]
 env     = [ 'PYTHON_COLORS=1' ]
-size    = '87.6 MiB'
+size    = '84.5 MiB'
 version = '3.1.2'
 website = 'https://coconut-lang.org'
 example = '''
@@ -503,10 +503,10 @@ for arg in sys.argv[1:]:
 '''
 
 [CoffeeScript]
-args       = [ '/usr/bin/coffeescript', '-' ]
+args       = [ '/usr/bin/coffee', '/proc/self/fd/0' ]
 env        = [ 'NODE_ICU_DATA=/usr/share/icu' ]
 experiment = 450
-size       = '71.3 MiB'
+size       = '80.7 MiB'
 version    = '2.7.0'
 website    = 'https://coffeescript.org'
 example    = '''
@@ -523,9 +523,9 @@ for arg in process.argv[2..]
 '''
 
 ['Common Lisp']
-args      = [ '/usr/bin/lisp', '-' ]
+args      = [ '/usr/bin/common-lisp', '-' ]
 redirects = [ 'lisp' ]
-size      = '31.1 MiB'
+size      = '31.0 MiB'
 version   = 'GNU CLISP 2.49.92'
 website   = 'https://clisp.sourceforge.io'
 example   = '''
@@ -546,8 +546,8 @@ example   = '''
 [Crystal]
 args    = [ '/usr/bin/crystal', 'run', '--stdin-filename', 'code.cr', '--' ]
 env     = [ 'CRYSTAL_CACHE_DIR=/tmp', 'PATH=/usr/bin:/bin' ]
-size    = '270 MiB'
-version = '1.15.1'
+size    = '271 MiB'
+version = '1.17.1'
 website = 'https://crystal-lang.org'
 example = '''
 # Printing
@@ -567,8 +567,8 @@ end
 [D]
 args    = [ '/usr/bin/D', '-' ]
 env     = [ 'PATH=/usr/bin' ]
-size    = '93.3 MiB'
-version = 'DMD 2.110.0'
+size    = '95.1 MiB'
+version = 'DMD 2.111.0'
 website = 'https://dlang.org'
 example = '''
 import std.stdio;
@@ -590,9 +590,9 @@ void main(string[] argv) {
 '''
 
 [Dart]
-args    = [ '/usr/bin/dartwrapper', '-' ]
-size    = '246 MiB'
-version = '3.7.2'
+args    = [ '/usr/bin/dart', '/proc/self/fd/0' ]
+size    = '168 MiB'
+version = '3.8.1'
 website = 'https://dart.dev'
 example = '''
 void main(List<String> argv) {
@@ -612,9 +612,9 @@ void main(List<String> argv) {
 '''
 
 [Egel]
-args       = [ '/usr/bin/egelwrapper', '-' ]
+args       = [ '/usr/bin/egel', '-' ]
 experiment = 1434
-size       = '49.7 MiB'
+size       = '11.6 MiB'
 version    = '0.1.14'
 website    = 'https://egel-lang.github.io'
 example    = '''
@@ -636,10 +636,10 @@ def main =
 '''
 
 [Elixir]
-args    = [ '/usr/local/bin/elixir', '-e', '$code', '--' ]
+args    = [ '/usr/local/bin/elixir', '--color', '-e', '$code', '--' ]
 env     = [ 'LANG=C.UTF-8' ]
-size    = '74.3 MiB'
-version = '1.18.3'
+size    = '56.0 MiB'
+version = '1.18.4'
 website = 'https://elixir-lang.org'
 example = '''
 # Printing
@@ -655,8 +655,8 @@ for arg <- System.argv, do: IO.puts arg
 [Erlang]
 args       = [ '/usr/bin/erlang', '-' ]
 experiment = 181
-size       = '68.6 MiB'
-version    = 'OTP 27.3.1'
+size       = '49.0 MiB'
+version    = 'OTP 28.0.2'
 website    = 'https://www.erlang.org'
 example    = '''
 main(Args) ->
@@ -673,8 +673,8 @@ main(Args) ->
 ['F#']
 args    = [ '/usr/bin/f-sharp', '-' ]
 env     = [ 'DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1' ]
-size    = '109 MiB'
-version = 'F# 9.0 on .NET 9.0.2'
+size    = '110 MiB'
+version = 'F# 9.0 on .NET 9.0.6'
 website = 'https://fsharp.org'
 example = '''
 [<EntryPoint>]
@@ -730,7 +730,7 @@ example    = '''
 
 [Forth]
 args    = [ '/usr/bin/forth', '/proc/self/fd/0' ]
-size    = '2.86 MiB'
+size    = '2.85 MiB'
 version = 'GNU Forth 0.7.3'
 website = 'https://www.gnu.org/software/gforth/'
 example = '''
@@ -752,12 +752,12 @@ bye
 
 [Fortran]
 args    = [ '/usr/bin/fortran', '-' ]
-size    = '95.1 MiB'
-version = 'GNU Fortran 14.2.0'
+size    = '72.8 MiB'
+version = 'GNU Fortran 15.1.0'
 website = 'https://gcc.gnu.org/fortran/'
 example = '''
-character(len=32) :: s
 integer :: i
+character(len=2048) :: arg
 
 ! Printing
 print "(a)", "Hello, World!"
@@ -770,8 +770,8 @@ end do
 ! Accessing arguments
 ! (NOTE: getarg() and iargc() are GNU Fortran extensions)
 do i = 1, iargc()
-    call getarg(i,s)
-    print "(a)", s
+    call getarg(i, arg)
+    print "(a)", arg
 end do
 
 end
@@ -780,8 +780,8 @@ end
 [Gleam]
 args    = [ '/usr/bin/gleam', '-', '--' ]
 env     = [ 'LANG=C.UTF-8' ]
-size    = '91.6 MiB'
-version = '1.9.1'
+size    = '73.5 MiB'
+version = '1.11.1'
 website = 'https://gleam.run'
 example = '''
 import argv
@@ -806,8 +806,8 @@ pub fn main() {
 [Go]
 args    = [ '/usr/bin/gowrapper', '-' ]
 env     = [ 'GOCACHE=/tmp/cache' ]
-size    = '418 MiB'
-version = '1.24.1'
+size    = '324 MiB'
+version = '1.24.5'
 website = 'https://go.dev'
 example = '''
 package main
@@ -836,7 +836,7 @@ func main() {
 [GolfScript]
 args       = [ '/usr/bin/golfscript', '-n', '-e', '$code', '--' ]
 args-quine = [ '/usr/bin/golfscript', '-n', '-e', '$code', '-q', '--' ]
-size       = '33.2 MiB'
+size       = '33.3 MiB'
 version    = '6155e9f'
 website    = 'https://golfscript.com/golfscript/'
 example    = '''
@@ -854,8 +854,8 @@ n*
 args       = [ '/usr/bin/groovy', '-e', '$code' ]
 env        = [ 'JAVA_HOME=/opt/java' ]
 experiment = 1145
-size       = '148 MiB'
-version    = '4.0.26'
+size       = '76.5 MiB'
+version    = '4.0.27'
 website    = 'https://groovy-lang.org'
 example    = '''
 // Printing
@@ -894,10 +894,10 @@ NEXT
 '''
 
 [Hare]
-args       = [ '/usr/bin/harewrapper', '-' ]
+args       = [ '/usr/bin/hare', '-' ]
 env        = [ 'PATH=/usr/local/bin:/usr/bin' ]
 experiment = 1323
-size       = '11.4 MiB'
+size       = '11.5 MiB'
 version    = '0.24.2'
 website    = 'https://harelang.org'
 example    = '''
@@ -942,8 +942,8 @@ main = do
 args       = [ '/usr/bin/haxewrapper', '-' ]
 env        = [ 'HAXE_STD_PATH=/usr/lib/std' ]
 experiment = 1366
-size       = '39.8 MiB'
-version    = '4.3.6'
+size       = '41.1 MiB'
+version    = '4.3.7'
 website    = 'https://haxe.org'
 example    = '''
 class Main {
@@ -964,7 +964,7 @@ class Main {
 
 [Hexagony]
 args    = [ '/usr/bin/hexagony', '-d', '-' ]
-size    = '8.91 MiB'
+size    = '8.90 MiB'
 version = '770406a'
 website = 'https://github.com/SirBogman/Hexagony'
 example = '''
@@ -992,7 +992,7 @@ example = '''
 [Hush]
 args       = [ '/usr/bin/hush', '-' ]
 experiment = 656
-size       = '3.35 MiB'
+size       = '2.30 MiB'
 version    = '0.1.4'
 website    = 'https://hush-shell.github.io'
 example    = '''
@@ -1013,8 +1013,8 @@ end
 [Hy]
 args       = [ '/usr/bin/hy', '-' ]
 experiment = 586
-size       = '41.8 MiB'
-version    = '1.0.0'
+size       = '41.7 MiB'
+version    = '1.1.0'
 website    = 'https://hylang.org'
 example    = '''
 ; Printing
@@ -1032,16 +1032,12 @@ example    = '''
 [iogii]
 args       = [ '/usr/bin/iogii', '-' ]
 experiment = 1770
-size       = '34.1 MiB'
-version    = '0.3'
+size       = '33.4 MiB'
+version    = '1.1'
 website    = 'https://golfscript.com/iogii/'
 example    = '''
-# Process input in raw mode
-,
-
 # Printing
-"Hello, World!
-"
+"Hello, World!\n"
 
 # Looping
 10T
@@ -1051,9 +1047,9 @@ $
 '''
 
 [J]
-args    = [ '/usr/bin/j', '/tmp/code.ijs' ]
+args    = [ '/opt/j/bin/jconsole', '/proc/self/fd/0' ]
 size    = '11.3 MiB'
-version = '9.6.14'
+version = '9.6'
 website = 'https://www.jsoftware.com'
 example = '''
 NB.Printing
@@ -1068,7 +1064,7 @@ echo each 2}.ARGV
 
 [Janet]
 args    = [ '/usr/bin/janet', '/proc/self/fd/0' ]
-size    = '906 KiB'
+size    = '898 KiB'
 version = '1.38.0'
 website = 'https://janet-lang.org'
 example = '''
@@ -1086,8 +1082,8 @@ example = '''
 
 [Java]
 args    = [ '/usr/bin/java', '-' ]
-size    = '59.3 MiB'
-version = 'OpenJDK 24.0'
+size    = '57.3 MiB'
+version = 'OpenJDK 24.0.2'
 website = 'https://openjdk.java.net'
 example = '''
 class Main {
@@ -1108,8 +1104,8 @@ class Main {
 
 [JavaScript]
 args    = [ '/usr/bin/javascript', '-e', '$code', '--' ]
-size    = '43.2 MiB'
-version = 'V8 13.4.114.21'
+size    = '43.9 MiB'
+version = 'V8 13.8.500258'
 website = 'https://v8.dev'
 example = '''
 // Printing
@@ -1126,8 +1122,8 @@ for (let arg of arguments)
 
 [jq]
 args    = [ '/usr/bin/jq', '--args', '--null-input', '--raw-output', '--', '$code' ]
-size    = '973 KiB'
-version = '1.7.1'
+size    = '989 KiB'
+version = '1.8.1'
 website = 'https://jqlang.github.io/jq/'
 example = '''
 # Printing
@@ -1143,7 +1139,7 @@ $ARGS.positional[]
 [Julia]
 args    = [ '/usr/bin/julia', '--color=yes', '--eval', '$code', '--' ]
 size    = '300 MiB'
-version = '1.11.4'
+version = '1.11.6'
 website = 'https://julialang.org'
 example = '''
 # Printing
@@ -1161,8 +1157,8 @@ end
 '''
 
 [K]
-args    = [ '/usr/bin/kwrapper', '/tmp/code.k' ]
-size    = '631 KiB'
+args    = [ '/usr/bin/k', '/proc/self/fd/0' ]
+size    = '605 KiB'
 version = '544d014afd'
 website = 'https://codeberg.org/ngn/k'
 example = '''
@@ -1179,8 +1175,8 @@ example = '''
 [Kotlin]
 args    = [ '/usr/bin/kotlin', '-Xuse-fast-jar-file-system', '-e', '$code' ]
 env     = [ 'JAVA_HOME=/opt/java' ]
-size    = '227 MiB'
-version = '2.1.20'
+size    = '140 MiB'
+version = '2.2.0'
 website = 'https://kotlinlang.org'
 example = '''
 // Printing
@@ -1198,7 +1194,7 @@ for (arg in args)
 [Lua]
 args    = [ '/usr/bin/lua', '-' ]
 size    = '362 KiB'
-version = '5.4.7'
+version = '5.4.8'
 website = 'https://www.lua.org'
 example = '''
 -- Printing
@@ -1217,8 +1213,8 @@ end
 
 [Nim]
 args    = [ '/usr/bin/nim', '--colors:on', '-o:/tmp/code', '-r', 'c', '-' ]
-size    = '15.0 MiB'
-version = '2.2.2'
+size    = '14.9 MiB'
+version = '2.2.4'
 website = 'https://nim-lang.org'
 example = '''
 import os
@@ -1236,8 +1232,8 @@ for arg in commandLineParams():
 '''
 
 [OCaml]
-args    = [ '/usr/bin/ocaml', '/proc/self/fd/0' ]
-size    = '84.5 MiB'
+args    = [ '/usr/bin/ocaml', '-color=always', '/proc/self/fd/0' ]
+size    = '83.3 MiB'
 version = '5.3.0'
 website = 'https://ocaml.org'
 example = '''
@@ -1259,8 +1255,8 @@ Sys.argv
 [Odin]
 args       = [ '/usr/bin/odinwrapper', '-', '--' ]
 experiment = 766
-size       = '280 MiB'
-version    = '2025-03'
+size       = '299 MiB'
+version    = '2025-07'
 website    = 'https://odin-lang.org'
 example    = '''
 package main
@@ -1286,7 +1282,7 @@ main :: proc() {
 
 [Pascal]
 args    = [ '/usr/bin/pascal', '-' ]
-size    = '26.6 MiB'
+size    = '26.8 MiB'
 version = 'FPC 3.2.2'
 website = 'https://freepascal.org'
 example = '''
@@ -1308,8 +1304,8 @@ end.
 
 [Perl]
 args    = [ '/usr/bin/perl', '-E', '$code', '--' ]
-size    = '5.54 MiB'
-version = '5.40.1'
+size    = '5.66 MiB'
+version = '5.42.0'
 website = 'https://www.perl.org'
 example = '''
 # Printing
@@ -1326,8 +1322,8 @@ say for @ARGV;
 
 [PHP]
 args    = [ '/usr/bin/php', '--' ]
-size    = '8.40 MiB'
-version = '8.4.5'
+size    = '8.39 MiB'
+version = '8.4.10'
 website = 'https://www.php.net'
 example = '''
 # Printing
@@ -1377,8 +1373,8 @@ main(Args) =>
 args       = [ '/usr/bin/powershell' ]
 args-quine = [ '/usr/bin/powershell', '--explicit' ]
 env        = [ 'DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1' ]
-size       = '135 MiB'
-version    = 'PowerShell 7.5.0 on .NET 9.0.2'
+size       = '136 MiB'
+version    = 'PowerShell 7.5.2 on .NET 9.0.6'
 website    = 'https://docs.microsoft.com/powershell/scripting/overview'
 example    = '''
 # Printing
@@ -1417,8 +1413,8 @@ example = '''
 [Python]
 args    = [ '/usr/bin/python', '-u', '-' ]
 env     = [ 'PYTHON_COLORS=1' ]
-size    = '29.2 MiB'
-version = '3.13.2'
+size    = '29.1 MiB'
+version = '3.13.5'
 website = 'https://www.python.org'
 example = '''
 import sys
@@ -1438,8 +1434,8 @@ for arg in sys.argv[1:]:
 [Qore]
 args       = [ '/usr/bin/qore', '--allow-bare-refs', '-K', '--mit', '-pno-external-access', '-e', '$code', '--' ]
 experiment = 1928
-size       = '18.0 MiB'
-version    = '1.19.2'
+size       = '17.3 MiB'
+version    = '2.1.2'
 website    = 'https://qore.org'
 example    = '''
 # Printing
@@ -1455,10 +1451,10 @@ foreach string arg in (ARGV)
 '''
 
 [R]
-args       = [ '/usr/bin/Rscript', '-' ]
-args-quine = [ '/usr/bin/Rscript', '-e', "source('stdin')" ]
-size       = '30.1 MiB'
-version    = '4.4.3'
+args       = [ '/usr/bin/R', '-' ]
+args-quine = [ '/usr/bin/R', '-e', "source('stdin')" ]
+size       = '30.5 MiB'
+version    = '4.5.1'
 website    = 'https://www.r-project.org'
 example    = '''
 # Printing
@@ -1476,10 +1472,10 @@ for (arg in commandArgs(TRUE)) {
 '''
 
 [Racket]
-args       = [ '/usr/local/bin/racket', '-e', '$code', '--' ]
+args       = [ '/usr/bin/racket', '-e', '$code', '--' ]
 experiment = 1358
-size       = '29.1 MiB'
-version    = '8.16'
+size       = '29.4 MiB'
+version    = '8.17'
 website    = 'https://racket-lang.org'
 example    = '''
 ; Printing
@@ -1496,8 +1492,8 @@ example    = '''
 [Raku]
 args      = [ '/usr/bin/raku', '-' ]
 redirects = [ 'perl6' ]
-size      = '77.9 MiB'
-version   = 'Raku 6.d on Rakudo 2025.03'
+size      = '75.6 MiB'
+version   = 'Raku 6.d on Rakudo 2025.06.1'
 website   = 'https://raku.org'
 example   = '''
 # Printing
@@ -1534,8 +1530,8 @@ foreach arg system/options/args [
 [Rexx]
 args       = [ '/usr/bin/rexx', '-' ]
 experiment = 873
-size       = '3.06 MiB'
-version    = 'Regina 3.9.6'
+size       = '2.98 MiB'
+version    = 'Regina 3.9.7'
 website    = 'https://regina-rexx.sourceforge.io'
 example    = '''
 -- Printing
@@ -1556,7 +1552,7 @@ end
 args      = [ '/usr/bin/rockstarwrapper', '-' ]
 redirects = [ 'rockstar-2' ]
 size      = '4.26 MiB'
-version   = '2.0.30'
+version   = '2.0.31'
 website   = 'https://codewithrockstar.com'
 example   = '''
 (Printing)
@@ -1576,7 +1572,7 @@ end
 [Ruby]
 args    = [ '/usr/bin/ruby', '-' ]
 size    = '33.2 MiB'
-version = '3.4.2'
+version = '3.4.5'
 website = 'https://www.ruby-lang.org'
 example = '''
 # Printing
@@ -1595,8 +1591,8 @@ end
 
 [Rust]
 args    = [ '/usr/bin/rust', '-' ]
-size    = '355 MiB'
-version = '1.85.1'
+size    = '392 MiB'
+version = '1.88.0'
 website = 'https://www.rust-lang.org'
 example = '''
 fn main() {
@@ -1619,7 +1615,7 @@ fn main() {
 args       = [ '/usr/local/bin/scala', '-opt:none', '-Xsource:3-cross', '-e', '$code', '--' ]
 env        = [ 'JAVA_HOME=/opt/java' ]
 experiment = 235
-size       = '205 MiB'
+size       = '89.0 MiB'
 version    = '2.13.16'
 website    = 'https://www.scala-lang.org'
 example    = '''
@@ -1636,8 +1632,8 @@ args.foreach(println)
 
 [Scheme]
 args    = [ '/usr/bin/schemewrapper', '-' ]
-size    = '7.38 MiB'
-version = 'Chez Scheme 10.1.0'
+size    = '7.40 MiB'
+version = 'Chez Scheme 10.2.0'
 website = 'https://cisco.github.io/ChezScheme/'
 example = '''
 ; Printing
@@ -1681,8 +1677,8 @@ G
 
 [SQL]
 args    = [ '/usr/bin/sql', '-' ]
-size    = '1.21 MiB'
-version = 'SQLite 3.49.1'
+size    = '1.22 MiB'
+version = 'SQLite 3.50.3'
 website = 'https://sqlite.org'
 example = '''
 -- Printing
@@ -1707,7 +1703,7 @@ SELECT arg FROM argv;
 [Squirrel]
 args       = [ '/usr/bin/squirrel', '-' ]
 experiment = 1598
-size       = '4.08 MiB'
+size       = '4.06 MiB'
 version    = '3.2'
 website    = 'http://squirrel-lang.org'
 example    = '''
@@ -1742,10 +1738,9 @@ Lm
 '''
 
 [Swift]
-args    = [ '/usr/bin/swift', '-module-cache-path', '/tmp', '-' ]
-env     = [ 'PATH=/usr/bin:/bin' ]
-size    = '516 MiB'
-version = '6.0.3'
+args    = [ '/usr/bin/swift', '-color-diagnostics', '-module-cache-path', '/tmp', '-' ]
+size    = '543 MiB'
+version = '6.1.2'
 website = 'https://swift.org'
 example = '''
 // Printing
@@ -1765,7 +1760,7 @@ for arg in CommandLine.arguments[1...] {
 [Tcl]
 args    = [ '/usr/bin/tcl', '/proc/self/fd/0' ]
 size    = '5.68 MiB'
-version = '9.0.1'
+version = '9.0.2'
 website = 'https://www.tcl-lang.org'
 example = '''
 # Printing
@@ -1784,7 +1779,7 @@ foreach arg $argv {
 
 [TeX]
 args    = [ '/usr/bin/tex', '$code' ]
-size    = '9.67 MiB'
+size    = '7.99 MiB'
 version = '3.141592653'
 website = 'https://tug.org'
 example = '''
@@ -1809,24 +1804,49 @@ Hello, World!
 [Uiua]
 args       = [ '/usr/bin/uiua', 'eval', '--', '$code' ]
 experiment = 1115
-size       = '30.4 MiB'
-version    = '0.14.1'
+size       = '19.5 MiB'
+version    = '0.16.2'
 website    = 'https://www.uiua.org'
 example    = '''
 # Printing
 &p"Hello, World!"
 
 # Looping
-∵&p⇡10
+≡&p⇡10
 
 # Accessing arguments
-∵&p↘1&args
+≡&p↘1&args
+'''
+
+[Umka]
+args       = [ '/usr/bin/umka', '-' ]
+experiment = 1921
+size       = '1.29 MiB'
+version    = '1.5.3'
+website    = 'https://github.com/vtereshkov/umka-lang'
+example    = '''
+import "std.um"
+
+fn main() {
+    // Printing
+    std::println("Hello, World!")
+
+    // Looping
+    for i := 0; i < 10; i++ {
+        printf("%d\n", i)
+    }
+
+    // Accessing arguments
+    for i := 1; i < std::argc(); i++ {
+        std::println(std::argv(i))
+    }
+}
 '''
 
 [V]
 args    = [ '/usr/bin/V', '-' ]
-size    = '50.6 MiB'
-version = '0.4.10'
+size    = '38.3 MiB'
+version = '0.4.11'
 website = 'https://vlang.io'
 example = '''
 import os
@@ -1845,6 +1865,30 @@ for arg in os.args[1..] {
 }
 '''
 
+[Vala]
+args       = [ '/usr/bin/vala', '-' ]
+env        = [ 'PATH=/usr/bin' ]
+experiment = 1354
+size       = '99.6 MiB'
+version    = '0.56.18'
+website    = 'https://vala.dev'
+example    = '''
+int main(string[] args) {
+    // Printing
+    print("Hello, World!\n");
+
+    // Looping
+    for (int i = 0; i < 10; i++)
+        print("%d\n", i);
+
+    // Accessing arguments
+    foreach (string arg in args[1:])
+        stdout.printf("%s\n", arg);
+
+    return 0;
+}
+'''
+
 [VimL]
 args    = [ '/usr/bin/viml', '-' ]
 size    = '24.1 MiB'
@@ -1873,15 +1917,15 @@ w
 [Vyxal]
 args       = [ '/opt/java/bin/java', '-jar', 'vyxal.jar', '-O', '--code', '$code', '--' ]
 experiment = 1846
-size       = '154 MiB'
-version    = '3.7.0'
+size       = '83.0 MiB'
+version    = '3.8.0'
 website    = 'https://vyxal.github.io'
 example    = '''
 ## Printing
 "Hello, World!",
 
 ## Looping
-0,9 (n,}
+9ʁ (n,}
 
 ## Accessing arguments
 #? (,}
@@ -1906,9 +1950,9 @@ for (a in Process.arguments) System.print(a)
 '''
 
 [Zig]
-args    = [ '/usr/bin/zigwrapper', '-' ]
-size    = '301 MiB'
-version = '0.14.0'
+args    = [ '/usr/bin/zig', '-' ]
+size    = '181 MiB'
+version = '0.14.1'
 website = 'https://ziglang.org'
 example = '''
 const std = @import("std");
diff --git a/config/hole-answers/hilbert-curve.txt b/config/hole-answers/hilbert-curve.txt
index e649e723..14f5d936 100644
--- a/config/hole-answers/hilbert-curve.txt
+++ b/config/hole-answers/hilbert-curve.txt
@@ -29,4 +29,4 @@
 │ ┌───┐ ┌───┐ │ ┌─┐ ┌─┐ │ ┌───┐ ┌───┐ │ ┌─┐ ┌─┐ │ ┌───┐ ┌───┐ │
 └─┘ ┌─┘ └─┐ └─┘ │ └─┘ │ └─┘ ┌─┘ └─┐ └─┘ │ └─┘ │ └─┘ ┌─┘ └─┐ └─┘
 ┌─┐ └─┐ ┌─┘ ┌─┐ └─┐ ┌─┘ ┌─┐ └─┐ ┌─┘ ┌─┐ └─┐ ┌─┘ ┌─┐ └─┐ ┌─┘ ┌─┐
-│ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ │
+╵ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ ╵
diff --git a/config/hole-answers/polygon-triangulations.txt b/config/hole-answers/polygon-triangulations.txt
new file mode 100644
index 00000000..05e5fb85
--- /dev/null
+++ b/config/hole-answers/polygon-triangulations.txt
@@ -0,0 +1,196 @@
+ABC
+ABC ACD
+ABD BCD
+ABC ACD ADE
+ABC ACE CDE
+ABD ADE BCD
+ABE BCD BDE
+ABE BCE CDE
+ABC ACD ADE AEF
+ABC ACD ADF DEF
+ABC ACE AEF CDE
+ABC ACF CDE CEF
+ABC ACF CDF DEF
+ABD ADE AEF BCD
+ABD ADF BCD DEF
+ABE AEF BCD BDE
+ABE AEF BCE CDE
+ABF BCD BDE BEF
+ABF BCD BDF DEF
+ABF BCE BEF CDE
+ABF BCF CDE CEF
+ABF BCF CDF DEF
+ABC ACD ADE AEF AFG
+ABC ACD ADE AEG EFG
+ABC ACD ADF AFG DEF
+ABC ACD ADG DEF DFG
+ABC ACD ADG DEG EFG
+ABC ACE AEF AFG CDE
+ABC ACE AEG CDE EFG
+ABC ACF AFG CDE CEF
+ABC ACF AFG CDF DEF
+ABC ACG CDE CEF CFG
+ABC ACG CDE CEG EFG
+ABC ACG CDF CFG DEF
+ABC ACG CDG DEF DFG
+ABC ACG CDG DEG EFG
+ABD ADE AEF AFG BCD
+ABD ADE AEG BCD EFG
+ABD ADF AFG BCD DEF
+ABD ADG BCD DEF DFG
+ABD ADG BCD DEG EFG
+ABE AEF AFG BCD BDE
+ABE AEF AFG BCE CDE
+ABE AEG BCD BDE EFG
+ABE AEG BCE CDE EFG
+ABF AFG BCD BDE BEF
+ABF AFG BCD BDF DEF
+ABF AFG BCE BEF CDE
+ABF AFG BCF CDE CEF
+ABF AFG BCF CDF DEF
+ABG BCD BDE BEF BFG
+ABG BCD BDE BEG EFG
+ABG BCD BDF BFG DEF
+ABG BCD BDG DEF DFG
+ABG BCD BDG DEG EFG
+ABG BCE BEF BFG CDE
+ABG BCE BEG CDE EFG
+ABG BCF BFG CDE CEF
+ABG BCF BFG CDF DEF
+ABG BCG CDE CEF CFG
+ABG BCG CDE CEG EFG
+ABG BCG CDF CFG DEF
+ABG BCG CDG DEF DFG
+ABG BCG CDG DEG EFG
+ABC ACD ADE AEF AFG AGH
+ABC ACD ADE AEF AFH FGH
+ABC ACD ADE AEG AGH EFG
+ABC ACD ADE AEH EFG EGH
+ABC ACD ADE AEH EFH FGH
+ABC ACD ADF AFG AGH DEF
+ABC ACD ADF AFH DEF FGH
+ABC ACD ADG AGH DEF DFG
+ABC ACD ADG AGH DEG EFG
+ABC ACD ADH DEF DFG DGH
+ABC ACD ADH DEF DFH FGH
+ABC ACD ADH DEG DGH EFG
+ABC ACD ADH DEH EFG EGH
+ABC ACD ADH DEH EFH FGH
+ABC ACE AEF AFG AGH CDE
+ABC ACE AEF AFH CDE FGH
+ABC ACE AEG AGH CDE EFG
+ABC ACE AEH CDE EFG EGH
+ABC ACE AEH CDE EFH FGH
+ABC ACF AFG AGH CDE CEF
+ABC ACF AFG AGH CDF DEF
+ABC ACF AFH CDE CEF FGH
+ABC ACF AFH CDF DEF FGH
+ABC ACG AGH CDE CEF CFG
+ABC ACG AGH CDE CEG EFG
+ABC ACG AGH CDF CFG DEF
+ABC ACG AGH CDG DEF DFG
+ABC ACG AGH CDG DEG EFG
+ABC ACH CDE CEF CFG CGH
+ABC ACH CDE CEF CFH FGH
+ABC ACH CDE CEG CGH EFG
+ABC ACH CDE CEH EFG EGH
+ABC ACH CDE CEH EFH FGH
+ABC ACH CDF CFG CGH DEF
+ABC ACH CDF CFH DEF FGH
+ABC ACH CDG CGH DEF DFG
+ABC ACH CDG CGH DEG EFG
+ABC ACH CDH DEF DFG DGH
+ABC ACH CDH DEF DFH FGH
+ABC ACH CDH DEG DGH EFG
+ABC ACH CDH DEH EFG EGH
+ABC ACH CDH DEH EFH FGH
+ABD ADE AEF AFG AGH BCD
+ABD ADE AEF AFH BCD FGH
+ABD ADE AEG AGH BCD EFG
+ABD ADE AEH BCD EFG EGH
+ABD ADE AEH BCD EFH FGH
+ABD ADF AFG AGH BCD DEF
+ABD ADF AFH BCD DEF FGH
+ABD ADG AGH BCD DEF DFG
+ABD ADG AGH BCD DEG EFG
+ABD ADH BCD DEF DFG DGH
+ABD ADH BCD DEF DFH FGH
+ABD ADH BCD DEG DGH EFG
+ABD ADH BCD DEH EFG EGH
+ABD ADH BCD DEH EFH FGH
+ABE AEF AFG AGH BCD BDE
+ABE AEF AFG AGH BCE CDE
+ABE AEF AFH BCD BDE FGH
+ABE AEF AFH BCE CDE FGH
+ABE AEG AGH BCD BDE EFG
+ABE AEG AGH BCE CDE EFG
+ABE AEH BCD BDE EFG EGH
+ABE AEH BCD BDE EFH FGH
+ABE AEH BCE CDE EFG EGH
+ABE AEH BCE CDE EFH FGH
+ABF AFG AGH BCD BDE BEF
+ABF AFG AGH BCD BDF DEF
+ABF AFG AGH BCE BEF CDE
+ABF AFG AGH BCF CDE CEF
+ABF AFG AGH BCF CDF DEF
+ABF AFH BCD BDE BEF FGH
+ABF AFH BCD BDF DEF FGH
+ABF AFH BCE BEF CDE FGH
+ABF AFH BCF CDE CEF FGH
+ABF AFH BCF CDF DEF FGH
+ABG AGH BCD BDE BEF BFG
+ABG AGH BCD BDE BEG EFG
+ABG AGH BCD BDF BFG DEF
+ABG AGH BCD BDG DEF DFG
+ABG AGH BCD BDG DEG EFG
+ABG AGH BCE BEF BFG CDE
+ABG AGH BCE BEG CDE EFG
+ABG AGH BCF BFG CDE CEF
+ABG AGH BCF BFG CDF DEF
+ABG AGH BCG CDE CEF CFG
+ABG AGH BCG CDE CEG EFG
+ABG AGH BCG CDF CFG DEF
+ABG AGH BCG CDG DEF DFG
+ABG AGH BCG CDG DEG EFG
+ABH BCD BDE BEF BFG BGH
+ABH BCD BDE BEF BFH FGH
+ABH BCD BDE BEG BGH EFG
+ABH BCD BDE BEH EFG EGH
+ABH BCD BDE BEH EFH FGH
+ABH BCD BDF BFG BGH DEF
+ABH BCD BDF BFH DEF FGH
+ABH BCD BDG BGH DEF DFG
+ABH BCD BDG BGH DEG EFG
+ABH BCD BDH DEF DFG DGH
+ABH BCD BDH DEF DFH FGH
+ABH BCD BDH DEG DGH EFG
+ABH BCD BDH DEH EFG EGH
+ABH BCD BDH DEH EFH FGH
+ABH BCE BEF BFG BGH CDE
+ABH BCE BEF BFH CDE FGH
+ABH BCE BEG BGH CDE EFG
+ABH BCE BEH CDE EFG EGH
+ABH BCE BEH CDE EFH FGH
+ABH BCF BFG BGH CDE CEF
+ABH BCF BFG BGH CDF DEF
+ABH BCF BFH CDE CEF FGH
+ABH BCF BFH CDF DEF FGH
+ABH BCG BGH CDE CEF CFG
+ABH BCG BGH CDE CEG EFG
+ABH BCG BGH CDF CFG DEF
+ABH BCG BGH CDG DEF DFG
+ABH BCG BGH CDG DEG EFG
+ABH BCH CDE CEF CFG CGH
+ABH BCH CDE CEF CFH FGH
+ABH BCH CDE CEG CGH EFG
+ABH BCH CDE CEH EFG EGH
+ABH BCH CDE CEH EFH FGH
+ABH BCH CDF CFG CGH DEF
+ABH BCH CDF CFH DEF FGH
+ABH BCH CDG CGH DEF DFG
+ABH BCH CDG CGH DEG EFG
+ABH BCH CDH DEF DFG DGH
+ABH BCH CDH DEF DFH FGH
+ABH BCH CDH DEG DGH EFG
+ABH BCH CDH DEH EFG EGH
+ABH BCH CDH DEH EFH FGH
diff --git a/config/holes.go b/config/holes.go
index 3b3c7386..9e767a3b 100644
--- a/config/holes.go
+++ b/config/holes.go
@@ -60,8 +60,8 @@ type Hole struct {
 	DataMap                     ordered.Map    `json:"-"`
 	Experiment                  int            `json:"-"`
 	ID                          string         `json:"id"`
-	ItemDelimiter               string         `json:"-" toml:"item-delimiter"`
-	MultisetDelimiter           string         `json:"-" toml:"multiset-delimiter"`
+	MultisetItemDelimiter       string         `json:"-" toml:"multiset-item-delimiter"`
+	OutputDelimiter             string         `json:"-" toml:"output-delimiter"`
 	Links                       []Link         `json:"links"`
 	Name                        string         `json:"name"`
 	Preamble                    template.HTML  `json:"preamble"`
diff --git a/config/nav.go b/config/nav.go
index 77cc01bf..96e30251 100644
--- a/config/nav.go
+++ b/config/nav.go
@@ -116,6 +116,7 @@ func init() {
 					Slug: "page",
 					Links: []*NavLink{
 						{Name: "Overview", Path: "/stats"},
+						{Name: "Achievements", Slug: "cheevos"},
 						{Name: "Countries", Slug: "countries"},
 						{Name: "Golfers", Slug: "golfers"},
 						{Name: "Holes", Slug: "holes"},
diff --git a/config/settings.go b/config/settings.go
index c5a2bac5..f95a3053 100644
--- a/config/settings.go
+++ b/config/settings.go
@@ -2,7 +2,11 @@ package config
 
 import "slices"
 
-type Option struct{ ID, Name string }
+// TODO These structures are very similar to config/nav, consider sharing?
+type Option struct {
+	Group    bool
+	ID, Name string
+}
 
 type Setting struct {
 	Checkbox bool
@@ -40,11 +44,27 @@ func init() {
 					setting.Options = append(setting.Options,
 						&Option{ID: hole.ID, Name: hole.Name})
 				}
+
+				setting.Options = append(setting.Options,
+					&Option{Group: true, Name: "Experimental Holes"})
+
+				for _, hole := range ExpHoleList {
+					setting.Options = append(setting.Options,
+						&Option{ID: hole.ID, Name: hole.Name})
+				}
 			case "All Languages":
 				for _, lang := range LangList {
 					setting.Options = append(setting.Options,
 						&Option{ID: lang.ID, Name: lang.Name})
 				}
+
+				setting.Options = append(setting.Options,
+					&Option{Group: true, Name: "Experimental Languages"})
+
+				for _, lang := range ExpLangList {
+					setting.Options = append(setting.Options,
+						&Option{ID: lang.ID, Name: lang.Name})
+				}
 			}
 		}
 	}
@@ -57,7 +77,9 @@ func (s *Setting) FromFormValue(value string) any {
 	}
 
 	// TODO Consider something more effecient like a hash?
-	if slices.ContainsFunc(s.Options, func(o *Option) bool { return o.ID == value }) {
+	if slices.ContainsFunc(s.Options, func(o *Option) bool {
+		return o.ID == value && !o.Group
+	}) {
 		return value
 	}
 
diff --git a/css/common/base.css b/css/common/base.css
index bde20d5a..89cd211f 100644
--- a/css/common/base.css
+++ b/css/common/base.css
@@ -205,6 +205,46 @@ dialog > * {
     padding: 1rem;
 }
 
+dialog#search-nav-dialog {
+    translate: -50% 0;
+    top: 0;
+    width: 35rem;
+    border-top: none;
+
+    a {
+        justify-content: space-between;
+
+        .result-description {
+            color: var(--color);
+        }
+        .result-path {
+            color: var(--grey);
+        }
+    }
+    a:hover {
+        text-decoration: none;
+    }
+
+    .hint {
+        color: var(--grey);
+    }
+
+    ul {
+        margin: 0;
+        gap: 0;
+    }
+    li {
+        padding: 0.5rem;
+        border-radius: 0.25rem;
+    }
+    ul:not(:has(li:hover)):not(:has(li:focus-within)) li.current-result, li:hover, li:focus-within {
+        background-color: var(--light-grey);
+    }
+    li :focus {
+        outline: none;
+    }
+}
+
 dialog a {
     align-items: center;
     display: flex;
diff --git a/css/golfer/code-search.css b/css/golfer/code-search.css
index ab772c0a..b5f57bb8 100644
--- a/css/golfer/code-search.css
+++ b/css/golfer/code-search.css
@@ -28,6 +28,11 @@
         padding: 1rem;
     }
 
+    span {
+        overflow-x: auto;
+        overflow-y: hidden;
+    }
+
     code {
         white-space: pre;
         background-color: var(--light-grey);
diff --git a/css/hole-tabs.css b/css/hole-tabs.css
index 11ec884a..5f43e277 100644
--- a/css/hole-tabs.css
+++ b/css/hole-tabs.css
@@ -8,6 +8,8 @@ body {
 
 #details-content { padding: .375rem }
 
+.cm-editor { height: 100% }
+
 .readonly-output {
     flex: 1;
     font-family: emoji, mahjong, 'Source Code Pro', monospace;
@@ -29,7 +31,10 @@ section {
 
 section header {
     /* a little more space than default hole.css */
+    align-items: center;
+    display: flex;
     height: 2.2rem;
+    justify-content: space-between;
     padding: 0 .5rem;
 }
 
@@ -75,12 +80,6 @@ section header {
     grid-template-rows: 1fr max-content;
 }
 
-#scores-wrapper {
-    overflow-y: auto;
-    /* Avoid scroll bars from tooltip overflow */
-    overflow-x: hidden;
-}
-
 #scores-wrapper header {
     position: sticky;
     top: 0;
@@ -94,18 +93,6 @@ section header {
     height: unset;
 }
 
-#scores [data-tooltip]:after {
-    /* Override tooltip transform to avoid right-overflow */
-    transform: translate(-100%, -0.25rem);
-    left: 110%;
-    /* Override tooltip transform to avoid top-overflow */
-    bottom: 100%;
-}
-
-#scores [data-tooltip]:before {
-    bottom: calc(100% - 0.5rem);
-}
-
 /* Remove left and right borders from base.css */
 #scores tbody td {
     border-left: 0;
diff --git a/css/hole.css b/css/hole.css
index 5583fb26..936dd17d 100644
--- a/css/hole.css
+++ b/css/hole.css
@@ -51,19 +51,39 @@ main .info {
     padding: .5rem;
 }
 
-section {
-    display: grid;
-    grid-template: auto 1fr / 1fr 17rem;
-}
-
-section header {
-    align-items: center;
+#editor-and-rankings {
     display: flex;
-    justify-content: space-between;
-    padding-right: .5rem;
-}
+    gap: 1rem;
+    height: 20rem;
+
+    > div {
+        display: flex;
+        flex-direction: column;
+        overflow: auto;
+
+        > div {
+            border: 1px solid var(--color);
+            border-top-width: 2px;
+            flex: 1;
+            overflow: auto;
+        }
+
+        > header {
+            align-items: center;
+            display: flex;
+            justify-content: space-between;
+            padding-right: .5rem;
+        }
+    }
+
+    #editor { flex: 1 }
 
-.cm-editor { height: 100% }
+    #rankings {
+        width: 17rem;
+
+        > div { border-bottom: 0 }
+    }
+}
 
 .cm-editor input {
     color: inherit;
@@ -176,15 +196,6 @@ section header {
 #diff .diff-char-addition { background-color: var(--green) }
 #diff .diff-skip          { background-color: var(--blue) }
 
-#editor {
-    border: 1px solid var(--color);
-    border-right-width: 0;
-    border-top-width: 2px;
-    grid-row: span 2;
-    height: calc((2rem + 1px) * 8 + 2px);
-    overflow-x: hidden;
-}
-
 #emojify {
     display: grid;
     gap: .5rem;
@@ -212,7 +223,6 @@ section header {
 #rankingsView {
     background: var(--color);
     border: 1px solid var(--color);
-    border-top: 0;
     gap: 1px;
 }
 
@@ -242,11 +252,6 @@ section header {
     text-align: right;
 }
 
-#scores {
-    border-top: 2px solid var(--color);
-    height: 100%;
-}
-
 /* Shrink the padding compared to a normal table */
 #scores td               { padding: 0 .25rem }
 #scores td:first-child { padding-left: .5rem }
@@ -257,6 +262,11 @@ section header {
 #scores .bytes td:nth-child(4),
 #scores .chars td:nth-child(3) { color: var(--grey) }
 
+#scores td {
+    border-left: 0;
+    border-right: 0;
+}
+
 #status {
     align-items: center;
     display: grid;
@@ -265,7 +275,10 @@ section header {
     padding: 1rem;
 }
 
-#strokes { font-weight: bold }
+#strokes {
+    font-weight: bold;
+    padding: .375rem 0;
+}
 
 /* TODO Generalise the hole mapping table CSS, lose the specific IDs. */
                             .tables-4col { columns: 4 }
@@ -296,15 +309,7 @@ thead { user-select: none }
     width: 100%;
 }
 
-@media (max-width: 34rem) {
-    section, #status { grid-template-columns: 1fr }
-
-    section header { padding: 0 }
-
-    #editor { border-width: 1px }
-
-    #thirdParty:empty { display: none }
-}
+@media (max-width: 34rem) { #thirdParty:empty { display: none } }
 
 .run-result-btn {
     border: none;
@@ -397,9 +402,5 @@ h2 {
     display: none;
 }
 
-.pass.lastSubmittedCode #editor {
-    border-color: var(--green);
-}
-.fail.lastSubmittedCode #editor {
-    border-color: var(--red);
-}
+.pass.lastSubmittedCode .cm-editor { border-color: var(--green) !important }
+.fail.lastSubmittedCode .cm-editor { border-color: var(--red)   !important }
diff --git a/css/rankings/medals.css b/css/rankings/medals.css
index d895119f..01576eb8 100644
--- a/css/rankings/medals.css
+++ b/css/rankings/medals.css
@@ -1,6 +1 @@
 @import "./_rankings.css";
-
-td:nth-child(4) {
-    color: var(--grey);
-    font-size: .75rem;
-}
diff --git a/db/db.go b/db/db.go
index 14a47df8..a2ca078a 100644
--- a/db/db.go
+++ b/db/db.go
@@ -27,11 +27,10 @@ func Open() (db *sqlx.DB) {
 // Queryable includes all methods shared by sqlx.DB & sqlx.Tx, allowing either
 // type to be used interchangeably. https://github.com/jmoiron/sqlx/pull/809
 type Queryable interface {
-	sqlx.ExecerContext
 	sqlx.Ext
+	sqlx.ExtContext
 	sqlx.Preparer
 	sqlx.PreparerContext
-	sqlx.QueryerContext
 
 	Get(any, string, ...any) error
 	GetContext(context.Context, any, string, ...any) error
diff --git a/discord/bot.go b/discord/bot.go
index fb34ee35..dacdbd7b 100644
--- a/discord/bot.go
+++ b/discord/bot.go
@@ -28,6 +28,7 @@ var (
 	botToken      = os.Getenv("DISCORD_BOT_TOKEN")       // Caddie
 	chanFreshID   = os.Getenv("DISCORD_CHAN_FRESH_ID")   // 🍇・fresh-grapes
 	chanSourID    = os.Getenv("DISCORD_CHAN_SOUR_ID")    // 🍇・sour-grapes
+	chanWildID    = os.Getenv("DISCORD_CHAN_WILD_ID")    // 🍇・wild-grapes
 	guildID       = os.Getenv("DISCORD_GUILD_ID")        // Code Golf
 	roleContribID = os.Getenv("DISCORD_ROLE_CONTRIB_ID") // Contributor
 	roleSponsorID = os.Getenv("DISCORD_ROLE_SPONSOR_ID") // Sponsor
@@ -47,7 +48,8 @@ type RecAnnouncement struct {
 func init() {
 	// Ensure we have all our config.
 	switch "" {
-	case botToken, chanFreshID, chanSourID, guildID, roleContribID, roleSponsorID:
+	case botToken, chanFreshID, chanSourID, chanWildID, guildID,
+		roleContribID, roleSponsorID:
 		return
 	}
 
@@ -64,7 +66,13 @@ func init() {
 }
 
 // TODO Make this dynamic based on hole/lang age.
-func channel(_ *config.Hole, _ *config.Lang) string {
+func channel(hole *config.Hole, lang *config.Lang) string {
+	if hole.Experiment != 0 || lang.Experiment != 0 {
+		return chanWildID
+	}
+	if hole.ID == "mandelbrot" {
+		return chanFreshID
+	}
 	return chanSourID
 }
 
diff --git a/docker/core.yml b/docker/core.yml
index 93139956..0e0fca37 100644
--- a/docker/core.yml
+++ b/docker/core.yml
@@ -14,7 +14,7 @@ services:
     volumes:
       - ..:/go
   db:
-    image: postgres:16.8-alpine
+    image: postgres:16.9-alpine
     environment:
       POSTGRES_DB: code-golf
       POSTGRES_HOST_AUTH_METHOD: trust
diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile
index f53d45e1..25054d6e 100644
--- a/docker/dev.Dockerfile
+++ b/docker/dev.Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.24.1-alpine3.21
+FROM golang:1.24.5-alpine3.22
 
 ENV CGO_ENABLED=0 GOPATH= TZ=Europe/London
 
@@ -6,91 +6,93 @@ ENV CGO_ENABLED=0 GOPATH= TZ=Europe/London
 RUN apk add --no-cache build-base curl git linux-headers tzdata \
  && GOBIN=/bin go install github.com/cespare/reflex@latest
 
-COPY --from=codegolf/lang-swift        ["/", "/langs/swift/rootfs/"       ] #  516 MiB
-COPY --from=codegolf/lang-go           ["/", "/langs/go/rootfs/"          ] #  418 MiB
-COPY --from=codegolf/lang-rust         ["/", "/langs/rust/rootfs/"        ] #  355 MiB
+COPY --from=codegolf/lang-swift        ["/", "/langs/swift/rootfs/"       ] #  543 MiB
+COPY --from=codegolf/lang-rust         ["/", "/langs/rust/rootfs/"        ] #  392 MiB
 COPY --from=codegolf/lang-haskell      ["/", "/langs/haskell/rootfs/"     ] #  339 MiB
-COPY --from=codegolf/lang-zig          ["/", "/langs/zig/rootfs/"         ] #  301 MiB
+COPY --from=codegolf/lang-go           ["/", "/langs/go/rootfs/"          ] #  324 MiB
 COPY --from=codegolf/lang-julia        ["/", "/langs/julia/rootfs/"       ] #  300 MiB
-COPY --from=codegolf/lang-odin         ["/", "/langs/odin/rootfs/"        ] #  280 MiB
-COPY --from=codegolf/lang-crystal      ["/", "/langs/crystal/rootfs/"     ] #  270 MiB
-COPY --from=codegolf/lang-dart         ["/", "/langs/dart/rootfs/"        ] #  246 MiB
-COPY --from=codegolf/lang-kotlin       ["/", "/langs/kotlin/rootfs/"      ] #  227 MiB
-COPY --from=codegolf/lang-scala        ["/", "/langs/scala/rootfs/"       ] #  205 MiB
+COPY --from=codegolf/lang-odin         ["/", "/langs/odin/rootfs/"        ] #  299 MiB
+COPY --from=codegolf/lang-crystal      ["/", "/langs/crystal/rootfs/"     ] #  271 MiB
 COPY --from=codegolf/lang-basic        ["/", "/langs/basic/rootfs/"       ] #  200 MiB
+COPY --from=codegolf/lang-zig          ["/", "/langs/zig/rootfs/"         ] #  181 MiB
 COPY --from=codegolf/lang-factor       ["/", "/langs/factor/rootfs/"      ] #  173 MiB
-COPY --from=codegolf/lang-vyxal        ["/", "/langs/vyxal/rootfs/"       ] #  154 MiB
-COPY --from=codegolf/lang-cpp          ["/", "/langs/cpp/rootfs/"         ] #  152 MiB
-COPY --from=codegolf/lang-groovy       ["/", "/langs/groovy/rootfs/"      ] #  148 MiB
-COPY --from=codegolf/lang-powershell   ["/", "/langs/powershell/rootfs/"  ] #  135 MiB
-COPY --from=codegolf/lang-cjam         ["/", "/langs/cjam/rootfs/"        ] #  134 MiB
-COPY --from=codegolf/lang-assembly     ["/", "/langs/assembly/rootfs/"    ] #  115 MiB
-COPY --from=codegolf/lang-f-sharp      ["/", "/langs/f-sharp/rootfs/"     ] #  109 MiB
-COPY --from=codegolf/lang-c-sharp      ["/", "/langs/c-sharp/rootfs/"     ] #  107 MiB
-COPY --from=codegolf/lang-fortran      ["/", "/langs/fortran/rootfs/"     ] # 95.1 MiB
-COPY --from=codegolf/lang-d            ["/", "/langs/d/rootfs/"           ] # 93.3 MiB
-COPY --from=codegolf/lang-gleam        ["/", "/langs/gleam/rootfs/"       ] # 91.6 MiB
-COPY --from=codegolf/lang-coconut      ["/", "/langs/coconut/rootfs/"     ] # 87.6 MiB
-COPY --from=codegolf/lang-ocaml        ["/", "/langs/ocaml/rootfs/"       ] # 84.5 MiB
-COPY --from=codegolf/lang-raku         ["/", "/langs/raku/rootfs/"        ] # 77.9 MiB
+COPY --from=codegolf/lang-dart         ["/", "/langs/dart/rootfs/"        ] #  168 MiB
+COPY --from=codegolf/lang-cpp          ["/", "/langs/cpp/rootfs/"         ] #  151 MiB
+COPY --from=codegolf/lang-kotlin       ["/", "/langs/kotlin/rootfs/"      ] #  140 MiB
+COPY --from=codegolf/lang-powershell   ["/", "/langs/powershell/rootfs/"  ] #  136 MiB
+COPY --from=codegolf/lang-assembly     ["/", "/langs/assembly/rootfs/"    ] #  118 MiB
+COPY --from=codegolf/lang-f-sharp      ["/", "/langs/f-sharp/rootfs/"     ] #  110 MiB
+COPY --from=codegolf/lang-c-sharp      ["/", "/langs/c-sharp/rootfs/"     ] #  108 MiB
+COPY --from=codegolf/lang-vala         ["/", "/langs/vala/rootfs/"        ] # 99.6 MiB
+COPY --from=codegolf/lang-d            ["/", "/langs/d/rootfs/"           ] # 95.1 MiB
+COPY --from=codegolf/lang-scala        ["/", "/langs/scala/rootfs/"       ] # 89.0 MiB
+COPY --from=codegolf/lang-coconut      ["/", "/langs/coconut/rootfs/"     ] # 84.5 MiB
+COPY --from=codegolf/lang-ocaml        ["/", "/langs/ocaml/rootfs/"       ] # 83.3 MiB
+COPY --from=codegolf/lang-vyxal        ["/", "/langs/vyxal/rootfs/"       ] # 83.0 MiB
+COPY --from=codegolf/lang-civet        ["/", "/langs/civet/rootfs/"       ] # 81.3 MiB
+COPY --from=codegolf/lang-coffeescript ["/", "/langs/coffeescript/rootfs/"] # 80.7 MiB
+COPY --from=codegolf/lang-groovy       ["/", "/langs/groovy/rootfs/"      ] # 76.5 MiB
 COPY --from=codegolf/lang-stax         ["/", "/langs/stax/rootfs/"        ] # 76.5 MiB
-COPY --from=codegolf/lang-elixir       ["/", "/langs/elixir/rootfs/"      ] # 74.3 MiB
-COPY --from=codegolf/lang-civet        ["/", "/langs/civet/rootfs/"       ] # 72.1 MiB
-COPY --from=codegolf/lang-coffeescript ["/", "/langs/coffeescript/rootfs/"] # 71.3 MiB
-COPY --from=codegolf/lang-erlang       ["/", "/langs/erlang/rootfs/"      ] # 68.6 MiB
-COPY --from=codegolf/lang-clojure      ["/", "/langs/clojure/rootfs/"     ] # 66.4 MiB
-COPY --from=codegolf/lang-java         ["/", "/langs/java/rootfs/"        ] # 59.3 MiB
-COPY --from=codegolf/lang-v            ["/", "/langs/v/rootfs/"           ] # 50.6 MiB
-COPY --from=codegolf/lang-egel         ["/", "/langs/egel/rootfs/"        ] # 49.7 MiB
+COPY --from=codegolf/lang-raku         ["/", "/langs/raku/rootfs/"        ] # 75.6 MiB
+COPY --from=codegolf/lang-gleam        ["/", "/langs/gleam/rootfs/"       ] # 73.5 MiB
+COPY --from=codegolf/lang-fortran      ["/", "/langs/fortran/rootfs/"     ] # 72.8 MiB
+COPY --from=codegolf/lang-clojure      ["/", "/langs/clojure/rootfs/"     ] # 67.3 MiB
+COPY --from=codegolf/lang-cjam         ["/", "/langs/cjam/rootfs/"        ] # 62.7 MiB
+COPY --from=codegolf/lang-java         ["/", "/langs/java/rootfs/"        ] # 57.3 MiB
+COPY --from=codegolf/lang-elixir       ["/", "/langs/elixir/rootfs/"      ] # 56.0 MiB
 COPY --from=codegolf/lang-prolog       ["/", "/langs/prolog/rootfs/"      ] # 49.3 MiB
-COPY --from=codegolf/lang-javascript   ["/", "/langs/javascript/rootfs/"  ] # 43.2 MiB
-COPY --from=codegolf/lang-hy           ["/", "/langs/hy/rootfs/"          ] # 41.8 MiB
-COPY --from=codegolf/lang-haxe         ["/", "/langs/haxe/rootfs/"        ] # 39.8 MiB
-COPY --from=codegolf/lang-iogii        ["/", "/langs/iogii/rootfs/"       ] # 34.1 MiB
-COPY --from=codegolf/lang-golfscript   ["/", "/langs/golfscript/rootfs/"  ] # 33.2 MiB
+COPY --from=codegolf/lang-erlang       ["/", "/langs/erlang/rootfs/"      ] # 49.0 MiB
+COPY --from=codegolf/lang-javascript   ["/", "/langs/javascript/rootfs/"  ] # 43.9 MiB
+COPY --from=codegolf/lang-hy           ["/", "/langs/hy/rootfs/"          ] # 41.7 MiB
+COPY --from=codegolf/lang-haxe         ["/", "/langs/haxe/rootfs/"        ] # 41.1 MiB
+COPY --from=codegolf/lang-v            ["/", "/langs/v/rootfs/"           ] # 38.3 MiB
+COPY --from=codegolf/lang-iogii        ["/", "/langs/iogii/rootfs/"       ] # 33.4 MiB
+COPY --from=codegolf/lang-golfscript   ["/", "/langs/golfscript/rootfs/"  ] # 33.3 MiB
 COPY --from=codegolf/lang-ruby         ["/", "/langs/ruby/rootfs/"        ] # 33.2 MiB
-COPY --from=codegolf/lang-common-lisp  ["/", "/langs/common-lisp/rootfs/" ] # 31.1 MiB
-COPY --from=codegolf/lang-uiua         ["/", "/langs/uiua/rootfs/"        ] # 30.4 MiB
-COPY --from=codegolf/lang-r            ["/", "/langs/r/rootfs/"           ] # 30.1 MiB
-COPY --from=codegolf/lang-python       ["/", "/langs/python/rootfs/"      ] # 29.2 MiB
-COPY --from=codegolf/lang-racket       ["/", "/langs/racket/rootfs/"      ] # 29.1 MiB
+COPY --from=codegolf/lang-common-lisp  ["/", "/langs/common-lisp/rootfs/" ] # 31.0 MiB
+COPY --from=codegolf/lang-r            ["/", "/langs/r/rootfs/"           ] # 30.5 MiB
+COPY --from=codegolf/lang-racket       ["/", "/langs/racket/rootfs/"      ] # 29.4 MiB
+COPY --from=codegolf/lang-python       ["/", "/langs/python/rootfs/"      ] # 29.1 MiB
 COPY --from=codegolf/lang-05ab1e       ["/", "/langs/05ab1e/rootfs/"      ] # 28.2 MiB
-COPY --from=codegolf/lang-pascal       ["/", "/langs/pascal/rootfs/"      ] # 26.6 MiB
+COPY --from=codegolf/lang-pascal       ["/", "/langs/pascal/rootfs/"      ] # 26.8 MiB
 COPY --from=codegolf/lang-viml         ["/", "/langs/viml/rootfs/"        ] # 24.1 MiB
-COPY --from=codegolf/lang-qore         ["/", "/langs/qore/rootfs/"        ] # 18.0 MiB
+COPY --from=codegolf/lang-uiua         ["/", "/langs/uiua/rootfs/"        ] # 19.5 MiB
+COPY --from=codegolf/lang-qore         ["/", "/langs/qore/rootfs/"        ] # 17.3 MiB
 COPY --from=codegolf/lang-apl          ["/", "/langs/apl/rootfs/"         ] # 15.0 MiB
-COPY --from=codegolf/lang-nim          ["/", "/langs/nim/rootfs/"         ] # 15.0 MiB
+COPY --from=codegolf/lang-nim          ["/", "/langs/nim/rootfs/"         ] # 14.9 MiB
 COPY --from=codegolf/lang-harbour      ["/", "/langs/harbour/rootfs/"     ] # 12.2 MiB
-COPY --from=codegolf/lang-hare         ["/", "/langs/hare/rootfs/"        ] # 11.4 MiB
+COPY --from=codegolf/lang-egel         ["/", "/langs/egel/rootfs/"        ] # 11.6 MiB
+COPY --from=codegolf/lang-hare         ["/", "/langs/hare/rootfs/"        ] # 11.5 MiB
 COPY --from=codegolf/lang-j            ["/", "/langs/j/rootfs/"           ] # 11.3 MiB
 COPY --from=codegolf/lang-picat        ["/", "/langs/picat/rootfs/"       ] # 11.1 MiB
-COPY --from=codegolf/lang-tex          ["/", "/langs/tex/rootfs/"         ] # 9.67 MiB
-COPY --from=codegolf/lang-hexagony     ["/", "/langs/hexagony/rootfs/"    ] # 8.91 MiB
-COPY --from=codegolf/lang-php          ["/", "/langs/php/rootfs/"         ] # 8.40 MiB
-COPY --from=codegolf/lang-scheme       ["/", "/langs/scheme/rootfs/"      ] # 7.38 MiB
+COPY --from=codegolf/lang-hexagony     ["/", "/langs/hexagony/rootfs/"    ] # 8.90 MiB
+COPY --from=codegolf/lang-php          ["/", "/langs/php/rootfs/"         ] # 8.39 MiB
+COPY --from=codegolf/lang-tex          ["/", "/langs/tex/rootfs/"         ] # 7.99 MiB
+COPY --from=codegolf/lang-scheme       ["/", "/langs/scheme/rootfs/"      ] # 7.40 MiB
 COPY --from=codegolf/lang-tcl          ["/", "/langs/tcl/rootfs/"         ] # 5.68 MiB
-COPY --from=codegolf/lang-perl         ["/", "/langs/perl/rootfs/"        ] # 5.54 MiB
-COPY --from=codegolf/lang-arturo       ["/", "/langs/arturo/rootfs/"      ] # 5.38 MiB
-COPY --from=codegolf/lang-fish         ["/", "/langs/fish/rootfs/"        ] # 4.85 MiB
+COPY --from=codegolf/lang-perl         ["/", "/langs/perl/rootfs/"        ] # 5.66 MiB
+COPY --from=codegolf/lang-arturo       ["/", "/langs/arturo/rootfs/"      ] # 5.23 MiB
 COPY --from=codegolf/lang-algol-68     ["/", "/langs/algol-68/rootfs/"    ] # 4.67 MiB
-COPY --from=codegolf/lang-cobol        ["/", "/langs/cobol/rootfs/"       ] # 4.61 MiB
 COPY --from=codegolf/lang-rockstar     ["/", "/langs/rockstar/rootfs/"    ] # 4.26 MiB
-COPY --from=codegolf/lang-squirrel     ["/", "/langs/squirrel/rootfs/"    ] # 4.08 MiB
-COPY --from=codegolf/lang-befunge      ["/", "/langs/befunge/rootfs/"     ] # 3.95 MiB
-COPY --from=codegolf/lang-hush         ["/", "/langs/hush/rootfs/"        ] # 3.35 MiB
-COPY --from=codegolf/lang-rexx         ["/", "/langs/rexx/rootfs/"        ] # 3.06 MiB
-COPY --from=codegolf/lang-forth        ["/", "/langs/forth/rootfs/"       ] # 2.86 MiB
-COPY --from=codegolf/lang-awk          ["/", "/langs/awk/rootfs/"         ] # 1.80 MiB
+COPY --from=codegolf/lang-cobol        ["/", "/langs/cobol/rootfs/"       ] # 4.21 MiB
+COPY --from=codegolf/lang-squirrel     ["/", "/langs/squirrel/rootfs/"    ] # 4.06 MiB
+COPY --from=codegolf/lang-rexx         ["/", "/langs/rexx/rootfs/"        ] # 2.98 MiB
+COPY --from=codegolf/lang-forth        ["/", "/langs/forth/rootfs/"       ] # 2.85 MiB
+COPY --from=codegolf/lang-befunge      ["/", "/langs/befunge/rootfs/"     ] # 2.48 MiB
+COPY --from=codegolf/lang-hush         ["/", "/langs/hush/rootfs/"        ] # 2.30 MiB
+COPY --from=codegolf/lang-awk          ["/", "/langs/awk/rootfs/"         ] # 1.91 MiB
 COPY --from=codegolf/lang-c            ["/", "/langs/c/rootfs/"           ] # 1.73 MiB
+COPY --from=codegolf/lang-umka         ["/", "/langs/umka/rootfs/"        ] # 1.29 MiB
+COPY --from=codegolf/lang-bash         ["/", "/langs/bash/rootfs/"        ] # 1.27 MiB
 COPY --from=codegolf/lang-bqn          ["/", "/langs/bqn/rootfs/"         ] # 1.26 MiB
 COPY --from=codegolf/lang-rebol        ["/", "/langs/rebol/rootfs/"       ] # 1.23 MiB
-COPY --from=codegolf/lang-sql          ["/", "/langs/sql/rootfs/"         ] # 1.21 MiB
-COPY --from=codegolf/lang-bash         ["/", "/langs/bash/rootfs/"        ] # 1.20 MiB
+COPY --from=codegolf/lang-sql          ["/", "/langs/sql/rootfs/"         ] # 1.22 MiB
+COPY --from=codegolf/lang-jq           ["/", "/langs/jq/rootfs/"          ] #  989 KiB
 COPY --from=codegolf/lang-berry        ["/", "/langs/berry/rootfs/"       ] #  986 KiB
-COPY --from=codegolf/lang-jq           ["/", "/langs/jq/rootfs/"          ] #  973 KiB
-COPY --from=codegolf/lang-janet        ["/", "/langs/janet/rootfs/"       ] #  906 KiB
+COPY --from=codegolf/lang-janet        ["/", "/langs/janet/rootfs/"       ] #  898 KiB
 COPY --from=codegolf/lang-fennel       ["/", "/langs/fennel/rootfs/"      ] #  681 KiB
-COPY --from=codegolf/lang-k            ["/", "/langs/k/rootfs/"           ] #  631 KiB
+COPY --from=codegolf/lang-k            ["/", "/langs/k/rootfs/"           ] #  605 KiB
+COPY --from=codegolf/lang-fish         ["/", "/langs/fish/rootfs/"        ] #  549 KiB
 COPY --from=codegolf/lang-wren         ["/", "/langs/wren/rootfs/"        ] #  505 KiB
 COPY --from=codegolf/lang-lua          ["/", "/langs/lua/rootfs/"         ] #  362 KiB
 COPY --from=codegolf/lang-sed          ["/", "/langs/sed/rootfs/"         ] #  244 KiB
diff --git a/docker/dev.yml b/docker/dev.yml
index d12a790f..c1f3927f 100644
--- a/docker/dev.yml
+++ b/docker/dev.yml
@@ -4,7 +4,7 @@ services:
       - 80:80
       - 443:443
   assets:
-    image: node:23.5.0-alpine3.21
+    image: node:24.4.1-alpine3.22
     command:
       - ./esbuild
       - --watch=forever
diff --git a/docker/firefox.Dockerfile b/docker/firefox.Dockerfile
index 85cfabcf..78f69a65 100644
--- a/docker/firefox.Dockerfile
+++ b/docker/firefox.Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.21
+FROM alpine:3.22
 
 ENV VER=0.36.0
 
diff --git a/docker/font.Dockerfile b/docker/font.Dockerfile
index 5d6a316f..26d601d4 100644
--- a/docker/font.Dockerfile
+++ b/docker/font.Dockerfile
@@ -1,138 +1,142 @@
-FROM node:23.5.0-bookworm-slim
+FROM node:24.4.1-bookworm-slim
 
-RUN apt-get update && apt-get install --no-install-recommends -y \
-    ca-certificates fontforge fonttools git make python3-fontforge unzip woff2 zip
-
-RUN git clone -b v0.7.0 https://github.com/mozilla/twemoji-colr.git
+RUN apt-get update                                \
+ && DEBIAN_FRONTEND='noninteractive'              \
+    apt-get install --no-install-recommends --yes \
+    ca-certificates curl fontforge fonttools make \
+    python3-fontforge unzip woff2 zip
 
 WORKDIR /twemoji-colr
 
-# https://github.com/mozilla/twemoji-colr/pull/72
-COPY font.patch /tmp/
-RUN git apply /tmp/font.patch
+RUN curl -#L https://github.com/matrix-org/twemoji-colr/tarball/397dec7 \
+  | tar xz --strip-components 1
 
 RUN npm install
 
 RUN echo [] > extras/ligatures.json                      \
- && unzip -q twe-svg.zip                                 \
-    svg/1f1*-*          `# Flags`                        \
-    svg/1f0cf.svg       `# Joker`                        \
-    svg/1f308.svg       `# Rainbow`                      \
-    svg/1f30c.svg       `# Milky Way`                    \
-    svg/1f340.svg       `# Four Leaf Clover`             \
-    svg/1f356.svg       `# Meat on Bone`                 \
-    svg/1f35e.svg       `# Bread`                        \
-    svg/1f36f.svg       `# Honey Pot`                    \
-    svg/1f371.svg       `# Bento Box`                    \
-    svg/1f377.svg       `# Wine Glass`                   \
-    svg/1f37a.svg       `# Beer Mug`                     \
-    svg/1f382.svg       `# Birthday Cake`                \
-    svg/1f385.svg       `# Santa Claus`                  \
-    svg/1f388.svg       `# Balloon`                      \
-    svg/1f3ae.svg       `# Video Game Controller`        \
-    svg/1f3af.svg       `# Direct Hit`                   \
-    svg/1f3b3.svg       `# Bowling`                      \
-    svg/1f3b8.svg       `# Guitar`                       \
-    svg/1f3ba.svg       `# Trumpet`                      \
-    svg/1f3c3.svg       `# Person Running`               \
-    svg/1f3c6.svg       `# Trophy`                       \
-    svg/1f3cc.svg       `# Person Golfing`               \
-    svg/1f3de.svg       `# National Park`                \
-    svg/1f3e5.svg       `# Hospital`                     \
-    svg/1f409.svg       `# Dragon`                       \
-    svg/1f40b.svg       `# Whale`                        \
-    svg/1f40d.svg       `# Sanke`                        \
-    svg/1f411.svg       `# Sheep`                        \
-    svg/1f418.svg       `# Elephant`                     \
-    svg/1f41f.svg       `# Fish`                         \
-    svg/1f426.svg       `# Bird`                         \
-    svg/1f42a.svg       `# Camel`                        \
-    svg/1f441.svg       `# Eye`                          \
-    svg/1f444.svg       `# Mouth`                        \
-    svg/1f445.svg       `# Tongue`                       \
-    svg/1f44b.svg       `# Waving Hand`                  \
-    svg/1f44d.svg       `# Thumbs Up`                    \
-    svg/1f47f.svg       `# Angry Face with Horns`        \
-    svg/1f48d.svg       `# Ring`                         \
-    svg/1f48e.svg       `# Gem Stone`                    \
-    svg/1f4af.svg       `# Hundred Points`               \
-    svg/1f4b0.svg       `# Money Bag`                    \
-    svg/1f4bc.svg       `# Briefcase`                    \
-    svg/1f4be.svg       `# Floppy Disk`                  \
-    svg/1f4c4.svg       `# Page Facing Up`               \
-    svg/1f4c5.svg       `# Calendar`                     \
-    svg/1f4d0.svg       `# Triangular Ruler`             \
-    svg/1f4d5.svg       `# Closed Book`                  \
-    svg/1f4da.svg       `# Books`                        \
-    svg/1f4e3.svg       `# Megaphone`                    \
-    svg/1f4f4.svg       `# Mobile Phone Off`             \
-    svg/1f511.svg       `# Key`                          \
-    svg/1f51e.svg       `# No One Under Eighteen`        \
-    svg/1f520.svg       `# Input Latin Uppercase`        \
-    svg/1f523.svg       `# Input Symbols`                \
-    svg/1f526.svg       `# Flashlight`                   \
-    svg/1f549.svg       `# Om`                           \
-    svg/1f596.svg       `# Vulcan Salute`                \
-    svg/1f5a5.svg       `# Desktop Computer`             \
-    svg/1f5d1.svg       `# Wastebasket`                  \
-    svg/1f5dc.svg       `# Compression`                  \
-    svg/1f5f3.svg       `# Ballot Box with Ballot`       \
-    svg/1f600.svg       `# Grinning Face`                \
-    svg/1f602.svg       `# Face with Tears of Joy`       \
-    svg/1f605.svg       `# Grinning Face with Sweat`     \
-    svg/1f606.svg       `# Grinning Squinting Face`      \
-    svg/1f607.svg       `# Smiling Face with Halo`       \
-    svg/1f608.svg       `# Smiling Face with Horns`      \
-    svg/1f609.svg       `# Winking Face`                 \
-    svg/1f60e.svg       `# Smiling Face with Sunglasses` \
-    svg/1f60f.svg       `# Smirking Face`                \
-    svg/1f610.svg       `# Neutral Face`                 \
-    svg/1f613.svg       `# Downcast Face with Sweat`     \
-    svg/1f615.svg       `# Confused Face`                \
-    svg/1f617.svg       `# Kissing Face`                 \
-    svg/1f61b.svg       `# Face with Tongue`             \
-    svg/1f61c.svg       `# Winking Face with Tongue`     \
-    svg/1f61d.svg       `# Squinting Face with Tongue`   \
-    svg/1f621.svg       `# Pouting Face`                 \
-    svg/1f622.svg       `# Crying Face`                  \
-    svg/1f62e.svg       `# Face with Open Mouth`         \
-    svg/1f633.svg       `# Flushed Face`                 \
-    svg/1f634.svg       `# Sleeping Face`                \
-    svg/1f636.svg       `# Face Without Mouth`           \
-    svg/1f641.svg       `# Slightly Frowning Face`       \
-    svg/1f642.svg       `# Slightly Smiling Face`        \
-    svg/1f697.svg       `# Automobile`                   \
-    svg/1f6a2.svg       `# Ship`                         \
-    svg/1f910.svg       `# Zipper-Mouth Face`            \
-    svg/1f947.svg       `# 1st Place Medal`              \
-    svg/1f948.svg       `# 2nd Place Medal`              \
-    svg/1f949.svg       `# 3rd Place Medal`              \
-    svg/1f961.svg       `# Takeout Box`                  \
-    svg/1f967.svg       `# Pie`                          \
-    svg/1f96a.svg       `# Sandwich`                     \
-    svg/1f970.svg       `# Smiling Face with Hearts`     \
-    svg/1f984.svg       `# Unicorn`                      \
-    svg/1f98b.svg       `# Butterfly`                    \
-    svg/1f98e.svg       `# Lizard`                       \
-    svg/1f9a5.svg       `# Sloth`                        \
-    svg/1f9ab.svg       `# Beaver`                       \
-    svg/1f9d9.svg       `# Mage`                         \
-    svg/1f9db.svg       `# Vampire`                      \
-    svg/1f9e0.svg       `# Brain`                        \
-    svg/1f9ea.svg       `# Test Tube`                    \
-    svg/1f9f6.svg       `# Yarn`                         \
-    svg/1fa84.svg       `# Magic Wand`                   \
-    svg/1fa9b.svg       `# Screwdriver`                  \
-    svg/1fa9e.svg       `# Mirror`                       \
-    svg/1faa6.svg       `# Headstone`                    \
-    svg/23f1.svg        `# Stopwatch`                    \
-    svg/2615.svg        `# Hot Beverage`                 \
-    svg/2623.svg        `# Biohazard`                    \
-    svg/2639.svg        `# Frowning Face`                \
-    svg/26f3.svg        `# Flag in Hole`                 \
-    svg/2702.svg        `# Scissors`                     \
-    svg/274c.svg        `# Ballot X`                     \
-    svg/2b50.svg        `# Star`                         \
+ && unzip -q -j -d svg twe-svg.zip                       \
+    '*/1f1*-1f1*.svg'   `# Flags`                        \
+    '*/1f0cf.svg'       `# Joker`                        \
+    '*/1f308.svg'       `# Rainbow`                      \
+    '*/1f30c.svg'       `# Milky Way`                    \
+    '*/1f340.svg'       `# Four Leaf Clover`             \
+    '*/1f355.svg'       `# Pizza`                        \
+    '*/1f356.svg'       `# Meat on Bone`                 \
+    '*/1f35e.svg'       `# Bread`                        \
+    '*/1f36f.svg'       `# Honey Pot`                    \
+    '*/1f371.svg'       `# Bento Box`                    \
+    '*/1f377.svg'       `# Wine Glass`                   \
+    '*/1f37a.svg'       `# Beer Mug`                     \
+    '*/1f382.svg'       `# Birthday Cake`                \
+    '*/1f385.svg'       `# Santa Claus`                  \
+    '*/1f388.svg'       `# Balloon`                      \
+    '*/1f3ae.svg'       `# Video Game Controller`        \
+    '*/1f3af.svg'       `# Direct Hit`                   \
+    '*/1f3b3.svg'       `# Bowling`                      \
+    '*/1f3b8.svg'       `# Guitar`                       \
+    '*/1f3ba.svg'       `# Trumpet`                      \
+    '*/1f3c3.svg'       `# Person Running`               \
+    '*/1f3c6.svg'       `# Trophy`                       \
+    '*/1f3cc.svg'       `# Person Golfing`               \
+    '*/1f3de.svg'       `# National Park`                \
+    '*/1f3e5.svg'       `# Hospital`                     \
+    '*/1f409.svg'       `# Dragon`                       \
+    '*/1f40b.svg'       `# Whale`                        \
+    '*/1f40d.svg'       `# Sanke`                        \
+    '*/1f411.svg'       `# Sheep`                        \
+    '*/1f418.svg'       `# Elephant`                     \
+    '*/1f41f.svg'       `# Fish`                         \
+    '*/1f426.svg'       `# Bird`                         \
+    '*/1f42a.svg'       `# Camel`                        \
+    '*/1f441.svg'       `# Eye`                          \
+    '*/1f444.svg'       `# Mouth`                        \
+    '*/1f445.svg'       `# Tongue`                       \
+    '*/1f44b.svg'       `# Waving Hand`                  \
+    '*/1f44d.svg'       `# Thumbs Up`                    \
+    '*/1f47f.svg'       `# Angry Face with Horns`        \
+    '*/1f48d.svg'       `# Ring`                         \
+    '*/1f48e.svg'       `# Gem Stone`                    \
+    '*/1f4af.svg'       `# Hundred Points`               \
+    '*/1f4b0.svg'       `# Money Bag`                    \
+    '*/1f4bc.svg'       `# Briefcase`                    \
+    '*/1f4be.svg'       `# Floppy Disk`                  \
+    '*/1f4c4.svg'       `# Page Facing Up`               \
+    '*/1f4c5.svg'       `# Calendar`                     \
+    '*/1f4d0.svg'       `# Triangular Ruler`             \
+    '*/1f4d5.svg'       `# Closed Book`                  \
+    '*/1f4da.svg'       `# Books`                        \
+    '*/1f4e3.svg'       `# Megaphone`                    \
+    '*/1f4f4.svg'       `# Mobile Phone Off`             \
+    '*/1f511.svg'       `# Key`                          \
+    '*/1f51e.svg'       `# No One Under Eighteen`        \
+    '*/1f520.svg'       `# Input Latin Uppercase`        \
+    '*/1f523.svg'       `# Input Symbols`                \
+    '*/1f526.svg'       `# Flashlight`                   \
+    '*/1f549.svg'       `# Om`                           \
+    '*/1f596.svg'       `# Vulcan Salute`                \
+    '*/1f5a5.svg'       `# Desktop Computer`             \
+    '*/1f5d1.svg'       `# Wastebasket`                  \
+    '*/1f5dc.svg'       `# Compression`                  \
+    '*/1f5f3.svg'       `# Ballot Box with Ballot`       \
+    '*/1f600.svg'       `# Grinning Face`                \
+    '*/1f602.svg'       `# Face with Tears of Joy`       \
+    '*/1f605.svg'       `# Grinning Face with Sweat`     \
+    '*/1f606.svg'       `# Grinning Squinting Face`      \
+    '*/1f607.svg'       `# Smiling Face with Halo`       \
+    '*/1f608.svg'       `# Smiling Face with Horns`      \
+    '*/1f609.svg'       `# Winking Face`                 \
+    '*/1f60e.svg'       `# Smiling Face with Sunglasses` \
+    '*/1f60f.svg'       `# Smirking Face`                \
+    '*/1f610.svg'       `# Neutral Face`                 \
+    '*/1f613.svg'       `# Downcast Face with Sweat`     \
+    '*/1f615.svg'       `# Confused Face`                \
+    '*/1f617.svg'       `# Kissing Face`                 \
+    '*/1f61b.svg'       `# Face with Tongue`             \
+    '*/1f61c.svg'       `# Winking Face with Tongue`     \
+    '*/1f61d.svg'       `# Squinting Face with Tongue`   \
+    '*/1f621.svg'       `# Pouting Face`                 \
+    '*/1f622.svg'       `# Crying Face`                  \
+    '*/1f62e.svg'       `# Face with Open Mouth`         \
+    '*/1f633.svg'       `# Flushed Face`                 \
+    '*/1f634.svg'       `# Sleeping Face`                \
+    '*/1f636.svg'       `# Face Without Mouth`           \
+    '*/1f641.svg'       `# Slightly Frowning Face`       \
+    '*/1f642.svg'       `# Slightly Smiling Face`        \
+    '*/1f680.svg'       `# Rocket`                       \
+    '*/1f697.svg'       `# Automobile`                   \
+    '*/1f6a2.svg'       `# Ship`                         \
+    '*/1f910.svg'       `# Zipper-Mouth Face`            \
+    '*/1f947.svg'       `# 1st Place Medal`              \
+    '*/1f948.svg'       `# 2nd Place Medal`              \
+    '*/1f949.svg'       `# 3rd Place Medal`              \
+    '*/1f961.svg'       `# Takeout Box`                  \
+    '*/1f963.svg'       `# Bowl with Spoon`              \
+    '*/1f967.svg'       `# Pie`                          \
+    '*/1f96a.svg'       `# Sandwich`                     \
+    '*/1f970.svg'       `# Smiling Face with Hearts`     \
+    '*/1f984.svg'       `# Unicorn`                      \
+    '*/1f98b.svg'       `# Butterfly`                    \
+    '*/1f98e.svg'       `# Lizard`                       \
+    '*/1f9a5.svg'       `# Sloth`                        \
+    '*/1f9ab.svg'       `# Beaver`                       \
+    '*/1f9d9.svg'       `# Mage`                         \
+    '*/1f9db.svg'       `# Vampire`                      \
+    '*/1f9e0.svg'       `# Brain`                        \
+    '*/1f9ea.svg'       `# Test Tube`                    \
+    '*/1f9f6.svg'       `# Yarn`                         \
+    '*/1fa84.svg'       `# Magic Wand`                   \
+    '*/1fa9b.svg'       `# Screwdriver`                  \
+    '*/1fa9e.svg'       `# Mirror`                       \
+    '*/1faa6.svg'       `# Headstone`                    \
+    '*/23f1.svg'        `# Stopwatch`                    \
+    '*/2615.svg'        `# Hot Beverage`                 \
+    '*/2623.svg'        `# Biohazard`                    \
+    '*/2639.svg'        `# Frowning Face`                \
+    '*/26f2.svg'        `# Fountain`                     \
+    '*/26f3.svg'        `# Flag in Hole`                 \
+    '*/2702.svg'        `# Scissors`                     \
+    '*/274c.svg'        `# Ballot X`                     \
+    '*/2b50.svg'        `# Star`                         \
  && rm twe-svg.zip                                       \
     svg/1f1ea-1f1fa.svg `# Flag: European Union`         \
     svg/1f1fa-1f1f3.svg `# Flag: United Nations`         \
diff --git a/docker/font.patch b/docker/font.patch
deleted file mode 100644
index 395b1403..00000000
--- a/docker/font.patch
+++ /dev/null
@@ -1,67 +0,0 @@
-From e63e4fa60b80053b87f32b7089c2a1debe7beec9 Mon Sep 17 00:00:00 2001
-From: SpacingBat3 <git@spacingbat3.anonaddy.com>
-Date: Sat, 4 Nov 2023 17:48:55 +0100
-Subject: [PATCH] Fix deps for modern Node.js versions.
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-• Replace `unzip` package with `unzipper`.
-• Remove `rmdir` package (literally once-liner in Node API can replace,
-  maybe in the past it was useful but now it's literally junk library).
-• Overall update dependencies to the latest version available (except
-  devDependencies for now).
----
- layerize.js  |  5 ++---
- package.json | 10 +++-------
- 2 files changed, 5 insertions(+), 10 deletions(-)
-
-diff --git a/layerize.js b/layerize.js
-index 2fe5dc9..277a2ac 100644
---- a/layerize.js
-+++ b/layerize.js
-@@ -1,6 +1,5 @@
- var fs = require('fs'),
--    rmdir = require('rmdir'),
--    unzip = require('unzip'),
-+    unzip = require('unzipper'),
-     xmlbuilder = require('xmlbuilder'),
-     xml2js = require('xml2js');
- 
-@@ -718,7 +717,7 @@ function generateTTX() {
- }
- 
- // Delete and re-create target directory, to remove any pre-existing junk
--rmdir(targetDir, function () {
-+fs.rm(targetDir, {recursive:true,force:true}, function () {
-     fs.mkdirSync(targetDir);
-     fs.mkdirSync(targetDir + "/glyphs");
-     fs.mkdirSync(targetDir + "/colorGlyphs");
-diff --git a/package.json b/package.json
-index 406e81f..6855044 100644
---- a/package.json
-+++ b/package.json
-@@ -21,11 +21,10 @@
-   "dependencies": {
-     "grunt": "^1.4.1",
-     "grunt-cli": "^1.4.3",
--    "grunt-webfonts": "^4.0.2",
-+    "grunt-webfonts": "^5.0.0",
-     "load-grunt-tasks": "^5.1.0",
--    "rmdir": "^1.2.0",
--    "unzip": "^0.1.11",
--    "xml2js": "^0.4.23",
-+    "unzipper": "^0.10.14",
-+    "xml2js": "^0.6.2",
-     "xmlbuilder": "^15.1.1"
-   },
-   "keywords": [
-@@ -41,8 +40,5 @@
-   },
-   "scripts": {
-     "grunt": "grunt"
--  },
--  "resolutions": {
--    "graceful-fs": "^4.2.9"
-   }
- }
diff --git a/docker/lang-base.Dockerfile b/docker/lang-base.Dockerfile
index 1864ebd0..90605328 100644
--- a/docker/lang-base.Dockerfile
+++ b/docker/lang-base.Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.21
+FROM alpine:3.22
 
 WORKDIR /scratch
 
diff --git a/docker/live.Dockerfile b/docker/live.Dockerfile
index 0717cded..e50b8dfa 100644
--- a/docker/live.Dockerfile
+++ b/docker/live.Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.24.1-alpine3.21
+FROM golang:1.24.5-alpine3.22
 
 ENV CGO_ENABLED=0 GOAMD64=v4 GOPATH=
 
@@ -35,91 +35,93 @@ RUN go build -ldflags -s -trimpath \
 
 FROM scratch
 
-COPY --from=codegolf/lang-swift        ["/", "/langs/swift/rootfs/"       ] #  516 MiB
-COPY --from=codegolf/lang-go           ["/", "/langs/go/rootfs/"          ] #  418 MiB
-COPY --from=codegolf/lang-rust         ["/", "/langs/rust/rootfs/"        ] #  355 MiB
+COPY --from=codegolf/lang-swift        ["/", "/langs/swift/rootfs/"       ] #  543 MiB
+COPY --from=codegolf/lang-rust         ["/", "/langs/rust/rootfs/"        ] #  392 MiB
 COPY --from=codegolf/lang-haskell      ["/", "/langs/haskell/rootfs/"     ] #  339 MiB
-COPY --from=codegolf/lang-zig          ["/", "/langs/zig/rootfs/"         ] #  301 MiB
+COPY --from=codegolf/lang-go           ["/", "/langs/go/rootfs/"          ] #  324 MiB
 COPY --from=codegolf/lang-julia        ["/", "/langs/julia/rootfs/"       ] #  300 MiB
-COPY --from=codegolf/lang-odin         ["/", "/langs/odin/rootfs/"        ] #  280 MiB
-COPY --from=codegolf/lang-crystal      ["/", "/langs/crystal/rootfs/"     ] #  270 MiB
-COPY --from=codegolf/lang-dart         ["/", "/langs/dart/rootfs/"        ] #  246 MiB
-COPY --from=codegolf/lang-kotlin       ["/", "/langs/kotlin/rootfs/"      ] #  227 MiB
-COPY --from=codegolf/lang-scala        ["/", "/langs/scala/rootfs/"       ] #  205 MiB
+COPY --from=codegolf/lang-odin         ["/", "/langs/odin/rootfs/"        ] #  299 MiB
+COPY --from=codegolf/lang-crystal      ["/", "/langs/crystal/rootfs/"     ] #  271 MiB
 COPY --from=codegolf/lang-basic        ["/", "/langs/basic/rootfs/"       ] #  200 MiB
+COPY --from=codegolf/lang-zig          ["/", "/langs/zig/rootfs/"         ] #  181 MiB
 COPY --from=codegolf/lang-factor       ["/", "/langs/factor/rootfs/"      ] #  173 MiB
-COPY --from=codegolf/lang-vyxal        ["/", "/langs/vyxal/rootfs/"       ] #  154 MiB
-COPY --from=codegolf/lang-cpp          ["/", "/langs/cpp/rootfs/"         ] #  152 MiB
-COPY --from=codegolf/lang-groovy       ["/", "/langs/groovy/rootfs/"      ] #  148 MiB
-COPY --from=codegolf/lang-powershell   ["/", "/langs/powershell/rootfs/"  ] #  135 MiB
-COPY --from=codegolf/lang-cjam         ["/", "/langs/cjam/rootfs/"        ] #  134 MiB
-COPY --from=codegolf/lang-assembly     ["/", "/langs/assembly/rootfs/"    ] #  115 MiB
-COPY --from=codegolf/lang-f-sharp      ["/", "/langs/f-sharp/rootfs/"     ] #  109 MiB
-COPY --from=codegolf/lang-c-sharp      ["/", "/langs/c-sharp/rootfs/"     ] #  107 MiB
-COPY --from=codegolf/lang-fortran      ["/", "/langs/fortran/rootfs/"     ] # 95.1 MiB
-COPY --from=codegolf/lang-d            ["/", "/langs/d/rootfs/"           ] # 93.3 MiB
-COPY --from=codegolf/lang-gleam        ["/", "/langs/gleam/rootfs/"       ] # 91.6 MiB
-COPY --from=codegolf/lang-coconut      ["/", "/langs/coconut/rootfs/"     ] # 87.6 MiB
-COPY --from=codegolf/lang-ocaml        ["/", "/langs/ocaml/rootfs/"       ] # 84.5 MiB
-COPY --from=codegolf/lang-raku         ["/", "/langs/raku/rootfs/"        ] # 77.9 MiB
+COPY --from=codegolf/lang-dart         ["/", "/langs/dart/rootfs/"        ] #  168 MiB
+COPY --from=codegolf/lang-cpp          ["/", "/langs/cpp/rootfs/"         ] #  151 MiB
+COPY --from=codegolf/lang-kotlin       ["/", "/langs/kotlin/rootfs/"      ] #  140 MiB
+COPY --from=codegolf/lang-powershell   ["/", "/langs/powershell/rootfs/"  ] #  136 MiB
+COPY --from=codegolf/lang-assembly     ["/", "/langs/assembly/rootfs/"    ] #  118 MiB
+COPY --from=codegolf/lang-f-sharp      ["/", "/langs/f-sharp/rootfs/"     ] #  110 MiB
+COPY --from=codegolf/lang-c-sharp      ["/", "/langs/c-sharp/rootfs/"     ] #  108 MiB
+COPY --from=codegolf/lang-vala         ["/", "/langs/vala/rootfs/"        ] # 99.6 MiB
+COPY --from=codegolf/lang-d            ["/", "/langs/d/rootfs/"           ] # 95.1 MiB
+COPY --from=codegolf/lang-scala        ["/", "/langs/scala/rootfs/"       ] # 89.0 MiB
+COPY --from=codegolf/lang-coconut      ["/", "/langs/coconut/rootfs/"     ] # 84.5 MiB
+COPY --from=codegolf/lang-ocaml        ["/", "/langs/ocaml/rootfs/"       ] # 83.3 MiB
+COPY --from=codegolf/lang-vyxal        ["/", "/langs/vyxal/rootfs/"       ] # 83.0 MiB
+COPY --from=codegolf/lang-civet        ["/", "/langs/civet/rootfs/"       ] # 81.3 MiB
+COPY --from=codegolf/lang-coffeescript ["/", "/langs/coffeescript/rootfs/"] # 80.7 MiB
+COPY --from=codegolf/lang-groovy       ["/", "/langs/groovy/rootfs/"      ] # 76.5 MiB
 COPY --from=codegolf/lang-stax         ["/", "/langs/stax/rootfs/"        ] # 76.5 MiB
-COPY --from=codegolf/lang-elixir       ["/", "/langs/elixir/rootfs/"      ] # 74.3 MiB
-COPY --from=codegolf/lang-civet        ["/", "/langs/civet/rootfs/"       ] # 72.1 MiB
-COPY --from=codegolf/lang-coffeescript ["/", "/langs/coffeescript/rootfs/"] # 71.3 MiB
-COPY --from=codegolf/lang-erlang       ["/", "/langs/erlang/rootfs/"      ] # 68.6 MiB
-COPY --from=codegolf/lang-clojure      ["/", "/langs/clojure/rootfs/"     ] # 66.4 MiB
-COPY --from=codegolf/lang-java         ["/", "/langs/java/rootfs/"        ] # 59.3 MiB
-COPY --from=codegolf/lang-v            ["/", "/langs/v/rootfs/"           ] # 50.6 MiB
-COPY --from=codegolf/lang-egel         ["/", "/langs/egel/rootfs/"        ] # 49.7 MiB
+COPY --from=codegolf/lang-raku         ["/", "/langs/raku/rootfs/"        ] # 75.6 MiB
+COPY --from=codegolf/lang-gleam        ["/", "/langs/gleam/rootfs/"       ] # 73.5 MiB
+COPY --from=codegolf/lang-fortran      ["/", "/langs/fortran/rootfs/"     ] # 72.8 MiB
+COPY --from=codegolf/lang-clojure      ["/", "/langs/clojure/rootfs/"     ] # 67.3 MiB
+COPY --from=codegolf/lang-cjam         ["/", "/langs/cjam/rootfs/"        ] # 62.7 MiB
+COPY --from=codegolf/lang-java         ["/", "/langs/java/rootfs/"        ] # 57.3 MiB
+COPY --from=codegolf/lang-elixir       ["/", "/langs/elixir/rootfs/"      ] # 56.0 MiB
 COPY --from=codegolf/lang-prolog       ["/", "/langs/prolog/rootfs/"      ] # 49.3 MiB
-COPY --from=codegolf/lang-javascript   ["/", "/langs/javascript/rootfs/"  ] # 43.2 MiB
-COPY --from=codegolf/lang-hy           ["/", "/langs/hy/rootfs/"          ] # 41.8 MiB
-COPY --from=codegolf/lang-haxe         ["/", "/langs/haxe/rootfs/"        ] # 39.8 MiB
-COPY --from=codegolf/lang-iogii        ["/", "/langs/iogii/rootfs/"       ] # 34.1 MiB
-COPY --from=codegolf/lang-golfscript   ["/", "/langs/golfscript/rootfs/"  ] # 33.2 MiB
+COPY --from=codegolf/lang-erlang       ["/", "/langs/erlang/rootfs/"      ] # 49.0 MiB
+COPY --from=codegolf/lang-javascript   ["/", "/langs/javascript/rootfs/"  ] # 43.9 MiB
+COPY --from=codegolf/lang-hy           ["/", "/langs/hy/rootfs/"          ] # 41.7 MiB
+COPY --from=codegolf/lang-haxe         ["/", "/langs/haxe/rootfs/"        ] # 41.1 MiB
+COPY --from=codegolf/lang-v            ["/", "/langs/v/rootfs/"           ] # 38.3 MiB
+COPY --from=codegolf/lang-iogii        ["/", "/langs/iogii/rootfs/"       ] # 33.4 MiB
+COPY --from=codegolf/lang-golfscript   ["/", "/langs/golfscript/rootfs/"  ] # 33.3 MiB
 COPY --from=codegolf/lang-ruby         ["/", "/langs/ruby/rootfs/"        ] # 33.2 MiB
-COPY --from=codegolf/lang-common-lisp  ["/", "/langs/common-lisp/rootfs/" ] # 31.1 MiB
-COPY --from=codegolf/lang-uiua         ["/", "/langs/uiua/rootfs/"        ] # 30.4 MiB
-COPY --from=codegolf/lang-r            ["/", "/langs/r/rootfs/"           ] # 30.1 MiB
-COPY --from=codegolf/lang-python       ["/", "/langs/python/rootfs/"      ] # 29.2 MiB
-COPY --from=codegolf/lang-racket       ["/", "/langs/racket/rootfs/"      ] # 29.1 MiB
+COPY --from=codegolf/lang-common-lisp  ["/", "/langs/common-lisp/rootfs/" ] # 31.0 MiB
+COPY --from=codegolf/lang-r            ["/", "/langs/r/rootfs/"           ] # 30.5 MiB
+COPY --from=codegolf/lang-racket       ["/", "/langs/racket/rootfs/"      ] # 29.4 MiB
+COPY --from=codegolf/lang-python       ["/", "/langs/python/rootfs/"      ] # 29.1 MiB
 COPY --from=codegolf/lang-05ab1e       ["/", "/langs/05ab1e/rootfs/"      ] # 28.2 MiB
-COPY --from=codegolf/lang-pascal       ["/", "/langs/pascal/rootfs/"      ] # 26.6 MiB
+COPY --from=codegolf/lang-pascal       ["/", "/langs/pascal/rootfs/"      ] # 26.8 MiB
 COPY --from=codegolf/lang-viml         ["/", "/langs/viml/rootfs/"        ] # 24.1 MiB
-COPY --from=codegolf/lang-qore         ["/", "/langs/qore/rootfs/"        ] # 18.0 MiB
+COPY --from=codegolf/lang-uiua         ["/", "/langs/uiua/rootfs/"        ] # 19.5 MiB
+COPY --from=codegolf/lang-qore         ["/", "/langs/qore/rootfs/"        ] # 17.3 MiB
 COPY --from=codegolf/lang-apl          ["/", "/langs/apl/rootfs/"         ] # 15.0 MiB
-COPY --from=codegolf/lang-nim          ["/", "/langs/nim/rootfs/"         ] # 15.0 MiB
+COPY --from=codegolf/lang-nim          ["/", "/langs/nim/rootfs/"         ] # 14.9 MiB
 COPY --from=codegolf/lang-harbour      ["/", "/langs/harbour/rootfs/"     ] # 12.2 MiB
-COPY --from=codegolf/lang-hare         ["/", "/langs/hare/rootfs/"        ] # 11.4 MiB
+COPY --from=codegolf/lang-egel         ["/", "/langs/egel/rootfs/"        ] # 11.6 MiB
+COPY --from=codegolf/lang-hare         ["/", "/langs/hare/rootfs/"        ] # 11.5 MiB
 COPY --from=codegolf/lang-j            ["/", "/langs/j/rootfs/"           ] # 11.3 MiB
 COPY --from=codegolf/lang-picat        ["/", "/langs/picat/rootfs/"       ] # 11.1 MiB
-COPY --from=codegolf/lang-tex          ["/", "/langs/tex/rootfs/"         ] # 9.67 MiB
-COPY --from=codegolf/lang-hexagony     ["/", "/langs/hexagony/rootfs/"    ] # 8.91 MiB
-COPY --from=codegolf/lang-php          ["/", "/langs/php/rootfs/"         ] # 8.40 MiB
-COPY --from=codegolf/lang-scheme       ["/", "/langs/scheme/rootfs/"      ] # 7.38 MiB
+COPY --from=codegolf/lang-hexagony     ["/", "/langs/hexagony/rootfs/"    ] # 8.90 MiB
+COPY --from=codegolf/lang-php          ["/", "/langs/php/rootfs/"         ] # 8.39 MiB
+COPY --from=codegolf/lang-tex          ["/", "/langs/tex/rootfs/"         ] # 7.99 MiB
+COPY --from=codegolf/lang-scheme       ["/", "/langs/scheme/rootfs/"      ] # 7.40 MiB
 COPY --from=codegolf/lang-tcl          ["/", "/langs/tcl/rootfs/"         ] # 5.68 MiB
-COPY --from=codegolf/lang-perl         ["/", "/langs/perl/rootfs/"        ] # 5.54 MiB
-COPY --from=codegolf/lang-arturo       ["/", "/langs/arturo/rootfs/"      ] # 5.38 MiB
-COPY --from=codegolf/lang-fish         ["/", "/langs/fish/rootfs/"        ] # 4.85 MiB
+COPY --from=codegolf/lang-perl         ["/", "/langs/perl/rootfs/"        ] # 5.66 MiB
+COPY --from=codegolf/lang-arturo       ["/", "/langs/arturo/rootfs/"      ] # 5.23 MiB
 COPY --from=codegolf/lang-algol-68     ["/", "/langs/algol-68/rootfs/"    ] # 4.67 MiB
-COPY --from=codegolf/lang-cobol        ["/", "/langs/cobol/rootfs/"       ] # 4.61 MiB
 COPY --from=codegolf/lang-rockstar     ["/", "/langs/rockstar/rootfs/"    ] # 4.26 MiB
-COPY --from=codegolf/lang-squirrel     ["/", "/langs/squirrel/rootfs/"    ] # 4.08 MiB
-COPY --from=codegolf/lang-befunge      ["/", "/langs/befunge/rootfs/"     ] # 3.95 MiB
-COPY --from=codegolf/lang-hush         ["/", "/langs/hush/rootfs/"        ] # 3.35 MiB
-COPY --from=codegolf/lang-rexx         ["/", "/langs/rexx/rootfs/"        ] # 3.06 MiB
-COPY --from=codegolf/lang-forth        ["/", "/langs/forth/rootfs/"       ] # 2.86 MiB
-COPY --from=codegolf/lang-awk          ["/", "/langs/awk/rootfs/"         ] # 1.80 MiB
+COPY --from=codegolf/lang-cobol        ["/", "/langs/cobol/rootfs/"       ] # 4.21 MiB
+COPY --from=codegolf/lang-squirrel     ["/", "/langs/squirrel/rootfs/"    ] # 4.06 MiB
+COPY --from=codegolf/lang-rexx         ["/", "/langs/rexx/rootfs/"        ] # 2.98 MiB
+COPY --from=codegolf/lang-forth        ["/", "/langs/forth/rootfs/"       ] # 2.85 MiB
+COPY --from=codegolf/lang-befunge      ["/", "/langs/befunge/rootfs/"     ] # 2.48 MiB
+COPY --from=codegolf/lang-hush         ["/", "/langs/hush/rootfs/"        ] # 2.30 MiB
+COPY --from=codegolf/lang-awk          ["/", "/langs/awk/rootfs/"         ] # 1.91 MiB
 COPY --from=codegolf/lang-c            ["/", "/langs/c/rootfs/"           ] # 1.73 MiB
+COPY --from=codegolf/lang-umka         ["/", "/langs/umka/rootfs/"        ] # 1.29 MiB
+COPY --from=codegolf/lang-bash         ["/", "/langs/bash/rootfs/"        ] # 1.27 MiB
 COPY --from=codegolf/lang-bqn          ["/", "/langs/bqn/rootfs/"         ] # 1.26 MiB
 COPY --from=codegolf/lang-rebol        ["/", "/langs/rebol/rootfs/"       ] # 1.23 MiB
-COPY --from=codegolf/lang-sql          ["/", "/langs/sql/rootfs/"         ] # 1.21 MiB
-COPY --from=codegolf/lang-bash         ["/", "/langs/bash/rootfs/"        ] # 1.20 MiB
+COPY --from=codegolf/lang-sql          ["/", "/langs/sql/rootfs/"         ] # 1.22 MiB
+COPY --from=codegolf/lang-jq           ["/", "/langs/jq/rootfs/"          ] #  989 KiB
 COPY --from=codegolf/lang-berry        ["/", "/langs/berry/rootfs/"       ] #  986 KiB
-COPY --from=codegolf/lang-jq           ["/", "/langs/jq/rootfs/"          ] #  973 KiB
-COPY --from=codegolf/lang-janet        ["/", "/langs/janet/rootfs/"       ] #  906 KiB
+COPY --from=codegolf/lang-janet        ["/", "/langs/janet/rootfs/"       ] #  898 KiB
 COPY --from=codegolf/lang-fennel       ["/", "/langs/fennel/rootfs/"      ] #  681 KiB
-COPY --from=codegolf/lang-k            ["/", "/langs/k/rootfs/"           ] #  631 KiB
+COPY --from=codegolf/lang-k            ["/", "/langs/k/rootfs/"           ] #  605 KiB
+COPY --from=codegolf/lang-fish         ["/", "/langs/fish/rootfs/"        ] #  549 KiB
 COPY --from=codegolf/lang-wren         ["/", "/langs/wren/rootfs/"        ] #  505 KiB
 COPY --from=codegolf/lang-lua          ["/", "/langs/lua/rootfs/"         ] #  362 KiB
 COPY --from=codegolf/lang-sed          ["/", "/langs/sed/rootfs/"         ] #  244 KiB
diff --git a/fonts/twemoji.woff2 b/fonts/twemoji.woff2
index 8192decd..742e5c86 100644
Binary files a/fonts/twemoji.woff2 and b/fonts/twemoji.woff2 differ
diff --git a/github/wiki.go b/github/wiki.go
index ff5b7ebc..2882a6ee 100644
--- a/github/wiki.go
+++ b/github/wiki.go
@@ -72,6 +72,8 @@ func Wiki(db *sqlx.DB) error {
 			name = "Other Sites"
 		} else if slug == "spigot-algorithms" {
 			name = "Spigot Algorithms"
+		} else if slug == "tutorial" {
+			name = "Tutorial"
 		} else {
 			continue
 		}
diff --git a/go.mod b/go.mod
index 2250f1f6..a7f57bec 100644
--- a/go.mod
+++ b/go.mod
@@ -4,43 +4,47 @@ go 1.24
 
 require (
 	github.com/DATA-DOG/go-sqlmock v1.5.2
-	github.com/SeerUK/assert v1.0.0
 	github.com/agnivade/levenshtein v1.2.1
 	github.com/buildkite/terminal-to-html/v3 v3.16.8
-	github.com/bwmarrin/discordgo v0.28.1
-	github.com/go-chi/chi/v5 v5.2.1
+	github.com/bwmarrin/discordgo v0.29.0
+	github.com/go-chi/chi/v5 v5.2.2
 	github.com/go-git/go-billy/v5 v5.6.2
-	github.com/go-git/go-git/v5 v5.14.0
+	github.com/go-git/go-git/v5 v5.16.2
 	github.com/gofrs/uuid/v5 v5.3.2
 	github.com/gorilla/feeds v1.2.0
 	github.com/jmoiron/sqlx v1.4.0
 	github.com/lib/pq v1.10.9
 	github.com/oddg/hungarian-algorithm v0.0.0-20170809162819-9567cbc363de
-	github.com/pelletier/go-toml/v2 v2.2.3
+	github.com/pelletier/go-toml/v2 v2.2.4
 	github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
 	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
-	github.com/yuin/goldmark v1.7.8
-	golang.org/x/oauth2 v0.28.0
+	github.com/stretchr/testify v1.10.0
+	github.com/yuin/goldmark v1.7.13
+	golang.org/x/oauth2 v0.30.0
+	golang.org/x/sync v0.16.0
 )
 
 require (
-	dario.cat/mergo v1.0.1 // indirect
+	dario.cat/mergo v1.0.2 // indirect
 	github.com/Microsoft/go-winio v0.6.2 // indirect
-	github.com/ProtonMail/go-crypto v1.1.6 // indirect
-	github.com/cloudflare/circl v1.6.0 // indirect
+	github.com/ProtonMail/go-crypto v1.3.0 // indirect
+	github.com/cloudflare/circl v1.6.1 // indirect
 	github.com/cyphar/filepath-securejoin v0.4.1 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
 	github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/gorilla/websocket v1.5.3 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
 	github.com/kevinburke/ssh_config v1.2.0 // indirect
-	github.com/pjbgf/sha1cd v0.3.2 // indirect
-	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
+	github.com/pjbgf/sha1cd v0.4.0 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/sergi/go-diff v1.4.0 // indirect
 	github.com/skeema/knownhosts v1.3.1 // indirect
 	github.com/xanzy/ssh-agent v0.3.3 // indirect
-	golang.org/x/crypto v0.36.0 // indirect
-	golang.org/x/net v0.38.0 // indirect
-	golang.org/x/sys v0.31.0 // indirect
+	golang.org/x/crypto v0.40.0 // indirect
+	golang.org/x/net v0.42.0 // indirect
+	golang.org/x/sys v0.34.0 // indirect
 	gopkg.in/warnings.v0 v0.1.2 // indirect
+	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 8d6a838d..f95218c3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
-dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
 filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
 filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
 github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
@@ -7,10 +7,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ
 github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
 github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
 github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
-github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
-github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
-github.com/SeerUK/assert v1.0.0 h1:UuhBwrK3UxJ3xT+3+k8qfriUEqfgiOCerFKE746TqEI=
-github.com/SeerUK/assert v1.0.0/go.mod h1:gYQnFIWikwvM2F3SoZpBdnbwf/XKaUSeG+Lx1GzSS9E=
+github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
 github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
 github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
 github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -21,10 +19,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
 github.com/buildkite/terminal-to-html/v3 v3.16.8 h1:QN/daUob6cmK8GcdKnwn9+YTlPr1vNj+oeAIiJK6fPc=
 github.com/buildkite/terminal-to-html/v3 v3.16.8/go.mod h1:+k1KVKROZocrTLsEQ9PEf9A+8+X8uaVV5iO1ZIOwKYM=
-github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
-github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
-github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
-github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/bwmarrin/discordgo v0.29.0 h1:FmWeXFaKUwrcL3Cx65c20bTRW+vOb6k8AnaP+EgjDno=
+github.com/bwmarrin/discordgo v0.29.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
+github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
+github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
 github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
 github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -38,16 +36,16 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc
 github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
 github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
-github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
-github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
+github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
 github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
 github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
 github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.14.0 h1:/MD3lCrGjCen5WfEAzKg00MJJffKhC8gzS80ycmCi60=
-github.com/go-git/go-git/v5 v5.14.0/go.mod h1:Z5Xhoia5PcWA3NF8vRLURn9E5FRhSl7dGj9ItW3Wk5k=
+github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM=
+github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
 github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
 github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
 github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
@@ -83,18 +81,18 @@ github.com/oddg/hungarian-algorithm v0.0.0-20170809162819-9567cbc363de h1:kuqx+Z
 github.com/oddg/hungarian-algorithm v0.0.0-20170809162819-9567cbc363de/go.mod h1:dv3Q0yoeN8DwXGhZiv8Vi6/rr9mPtf4ylV60eLTGjUo=
 github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
 github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
-github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
-github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
-github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
-github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
+github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
-github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
-github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
+github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
+github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
 github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE=
 github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
@@ -109,35 +107,37 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
 github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
-github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
-github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
+github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
-golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
-golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
+golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
+golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
-golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
+golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
+golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/hole/hole.go b/hole/0hole.go
similarity index 79%
rename from hole/hole.go
rename to hole/0hole.go
index ff353e5c..667e9804 100644
--- a/hole/hole.go
+++ b/hole/0hole.go
@@ -27,6 +27,9 @@ var fixedTestsFS embed.FS
 var wordsTxt string
 var words = strings.Fields(wordsTxt)
 
+var holeJudges = make(map[string]Judge)
+
+// Other init blocks in this package rely on the fact that this init runs first, hence the file name.
 func init() {
 	if err := fs.WalkDir(fixedTestsFS, ".", func(path string, d fs.DirEntry, err error) error {
 		if d.IsDir() || err != nil {
@@ -70,6 +73,18 @@ func init() {
 	}); err != nil {
 		panic(err)
 	}
+
+	for _, hole := range config.AllHoleList {
+		// Use multiset judge for holes that have configured `MultisetItemDelimiter`
+		if holeJudges[hole.ID] == nil && hole.MultisetItemDelimiter != "" {
+			holeJudges[hole.ID] = multisetJudge(hole.CaseFold)
+		}
+
+		// All other holes use the default judge which compares by equality (trimming the line endings)
+		if holeJudges[hole.ID] == nil {
+			holeJudges[hole.ID] = defaultJudge
+		}
+	}
 }
 
 // Return a copy so holes are free to append, shuffle, etc.
@@ -83,6 +98,12 @@ func answerFunc(holeID string, f config.HoleAnswerFunc) any {
 	return nil
 }
 
+// Set the judge on the Hole by ID, return any to allow top-level call.
+func judge(holeID string, judge Judge) any {
+	holeJudges[holeID] = judge
+	return nil
+}
+
 func outputTests(tests ...[]test) []Answer { return outputTestsWithSep("\n", tests...) }
 
 func outputTestsWithSep(sep string, testRuns ...[]test) []Answer {
@@ -113,6 +134,9 @@ func outputMultirunTests(tests []test) []Answer {
 	return outputTests(tests, tests[:mid], tests[mid:])
 }
 
+// Return a random bool.
+func randBool() bool { return rand.IntN(2) == 0 }
+
 // Return a random element from the given slice. Panics on empty slice.
 func randChoice[E any](x []E) E { return x[rand.IntN(len(x))] }
 
diff --git a/hole/calendar.go b/hole/calendar.go
index bba62687..0e4f8a3c 100644
--- a/hole/calendar.go
+++ b/hole/calendar.go
@@ -37,7 +37,7 @@ func generate(s string) string {
 }
 
 func isLeap(year int) bool {
-	return (year%4 != 0 && year%100 == 0) || year%400 != 0
+	return year%4 == 0 && (year%100 != 0 || year%400 == 0)
 }
 
 var _ = answerFunc("calendar", func() []Answer {
@@ -49,17 +49,25 @@ var _ = answerFunc("calendar", func() []Answer {
 		month, year := rand.IntN(12), randInt(1800, 2400)
 
 		if i < 12 {
-			// Enforce at least one calendar for every month of the year, in random years.
+			// Enforce at least one calendar for every month of the year in random non-leap years.
 			month = i
+
+			if isLeap(year) {
+				continue
+			}
 		} else if i < 18 {
-			// Enforce at least six calendars for months (five random, one February) in random leap years.
-			if i < 13 {
+			// Enforce at least six calendars for months (four random, two February) in random leap years.
+			if i < 14 {
 				month = 1
 			}
 
 			if !isLeap(year) {
 				continue
 			}
+		} else if i < 25 {
+			// Enforce February in each century
+			month = 1
+			year = 1800 + 100*(i-18)
 		}
 
 		i++
diff --git a/hole/css-colors-inverse.go b/hole/css-colors-inverse.go
new file mode 100644
index 00000000..9897016e
--- /dev/null
+++ b/hole/css-colors-inverse.go
@@ -0,0 +1,20 @@
+package hole
+
+func init() {
+	cssCodesToNames := make(map[string][]string)
+	for _, nameInCodeOut := range fixedTests("css-colors") {
+		cssCodesToNames[nameInCodeOut.out] = append(cssCodesToNames[nameInCodeOut.out], nameInCodeOut.in)
+	}
+
+	answerFunc("css-colors-inverse", func() []Answer {
+		var tests = []test{}
+		for code := range cssCodesToNames {
+			tests = append(tests, test{code, ""})
+		}
+		return outputMultirunTests(tests)
+	})
+
+	judge("css-colors-inverse", oneOfPerOutputJudge(func(code string) []string {
+		return cssCodesToNames[code]
+	}, true))
+}
diff --git a/hole/dfa-simulator.go b/hole/dfa-simulator.go
index 8ede7828..950ea564 100644
--- a/hole/dfa-simulator.go
+++ b/hole/dfa-simulator.go
@@ -82,7 +82,7 @@ func generateDFA() string {
 			inputDFA.WriteByte(' ')
 		}
 
-		if rand.IntN(2) == 0 {
+		if randBool() {
 			inputDFA.WriteByte('F')
 		} else {
 			inputDFA.WriteByte(' ')
diff --git a/hole/fractions.go b/hole/fractions.go
index 68860219..e0cd4f62 100644
--- a/hole/fractions.go
+++ b/hole/fractions.go
@@ -38,7 +38,7 @@ func largeFracGen() fraction {
 	// larger number.
 	if f.numerator <= 15 && f.denominator <= 15 {
 		newVal := rand.IntN(235) + 16
-		if rand.IntN(2) == 0 {
+		if randBool() {
 			f.numerator = newVal
 		} else {
 			f.denominator = newVal
diff --git a/hole/intersection.go b/hole/intersection.go
index e0fe2c60..1320ff0f 100644
--- a/hole/intersection.go
+++ b/hole/intersection.go
@@ -139,11 +139,11 @@ var _ = answerFunc("intersection", func() []Answer {
 						(y == 5 && h > 1) || (y == 6 && h > 1) {
 						continue
 					}
-					if rand.Float32() > 0.5 { // randomly add test ?
+					if randBool() { // randomly add test ?
 						b := bbox{x: x, y: y, w: w, h: h}
 
 						var in string
-						if rand.Float32() > 0.5 { // randomly flip input
+						if randBool() { // randomly flip input
 							in = b.String() + " " + strbigbox
 						} else {
 							in = strbigbox + " " + b.String()
diff --git a/hole/judges.go b/hole/judges.go
new file mode 100644
index 00000000..74c1caff
--- /dev/null
+++ b/hole/judges.go
@@ -0,0 +1,192 @@
+package hole
+
+import (
+	"strings"
+
+	"github.com/agnivade/levenshtein"
+	hungarianAlgorithm "github.com/oddg/hungarian-algorithm"
+)
+
+// An ultimate task of a Judge is to tell whether a run is passing or not.
+// However, it does more. When the output is not recognised as correct,
+// it returns a similar output that is.
+type Judge func(run Run) string
+
+func defaultJudge(run Run) string {
+	return trimPerLine([]byte(run.Answer))
+}
+
+// A judge for a single output (corresponding to either a single input or a single preset output item)
+type SingleOutputJudge func(arg, userOutput, rawExpectedOutput string) string
+
+// Creates a judge by treating an output as a sequence of
+// delimited outputs, each corresponding to a respective input.
+// The final expected output length's is the bigger of input length & the rawExpectedOutput length.
+// This allows two different ways of building the expected output:
+//  1. purely from the args
+//  2. by transforming the preset raw expected output (from the runs generator)
+func perOutputJudge(singleOutputJudge SingleOutputJudge) Judge {
+	return func(run Run) string {
+		args := run.Args
+		userOutputs := strings.Split(run.Stdout, run.OutputDelimiter)
+		rawExpectedOutputs := strings.Split(run.Answer, run.OutputDelimiter)
+		var expectedOutputs []string
+		for i := range max(len(args), len(rawExpectedOutputs)) {
+			arg := ""
+			if i < len(args) {
+				arg = args[i]
+			}
+			userOutput := ""
+			if i < len(userOutputs) {
+				userOutput = userOutputs[i]
+			}
+			rawExpectedOutput := ""
+			if i < len(rawExpectedOutputs) {
+				rawExpectedOutput = rawExpectedOutputs[i]
+			}
+			expectedOutput := singleOutputJudge(arg, userOutput, rawExpectedOutput)
+			if expectedOutput != "" {
+				expectedOutputs = append(expectedOutputs, expectedOutput)
+			}
+		}
+		return strings.Join(expectedOutputs, run.OutputDelimiter)
+	}
+}
+
+// Creates a judge which checks whether each user output
+// corresponds to one of preset outputs corresponding to the respective arg.
+func oneOfPerOutputJudge(getAllSolutions func(arg string) []string, caseFold bool) Judge {
+	return perOutputJudge(func(arg, userOutput, rawExpectedOutput string) string {
+		solutions := getAllSolutions(arg)
+
+		// Output is correct - output the match.
+		for _, solution := range solutions {
+			if caseFold && strings.EqualFold(solution, userOutput) || !caseFold && solution == userOutput {
+				return userOutput
+			}
+		}
+
+		// There's a single solution - output it.
+		if len(solutions) == 1 {
+			return solutions[0]
+		}
+
+		// There are multiple solutions - output the closest to the user output.
+		closestSolution := ""
+		minDistance := 1 << 24
+		userOutput = toLowerConditionally(userOutput, caseFold)
+		for _, solution := range solutions {
+			distance := levenshtein.ComputeDistance(toLowerConditionally(solution, caseFold), userOutput)
+			if distance < minDistance {
+				minDistance = distance
+				closestSolution = solution
+			}
+		}
+		return closestSolution
+	})
+}
+
+func toLowerConditionally(text string, caseFold bool) string {
+	if caseFold {
+		return strings.ToLower(text)
+	}
+	return text
+}
+
+func getClosestMultiset(anyAnswer, stdout, multisetItemDelimiter string, caseFold bool) string {
+	expectedItems := strings.Split(toLowerConditionally(anyAnswer, caseFold), multisetItemDelimiter)
+	expectedItemsReordered := make([]string, len(expectedItems))
+	userItems := strings.Split(stdout, multisetItemDelimiter)
+
+	expectedItemsMap := make(map[string]int)
+	for _, expected := range expectedItems {
+		expectedItemsMap[expected]++
+	}
+
+	// Match items that are correct
+	matches := 0
+	for i, user := range userItems {
+		if i < len(expectedItems) && expectedItemsMap[toLowerConditionally(user, caseFold)] > 0 {
+			expectedItemsReordered[i] = user
+			expectedItemsMap[user]--
+			userItems[i] = ""
+			matches++
+		}
+	}
+
+	// Process mismatched items
+	if matches < len(expectedItems) {
+
+		// Calculate indices of expected & user items that couldn't be matched be equality
+		unmatchedExpectedIndices := []int{}
+		unmatchedUserIndices := []int{}
+
+		for i, expected := range expectedItems {
+			if expectedItemsMap[expected] > 0 {
+				unmatchedExpectedIndices = append(unmatchedExpectedIndices, i)
+				expectedItemsMap[expected]--
+			}
+		}
+
+		for i, user := range userItems {
+			if user != "" {
+				unmatchedUserIndices = append(unmatchedUserIndices, i)
+			}
+		}
+
+		n := max(len(unmatchedExpectedIndices), len(unmatchedUserIndices))
+
+		permutation := make([]int, n)
+		for i := range permutation {
+			permutation[i] = i
+		}
+
+		// If there are not many wrong items, try to match them
+		// otherwise, use the above identity permutation
+		if n <= 32 {
+			dist := make([][]int, n)
+			for i := range dist {
+				dist[i] = make([]int, n)
+				for j := range dist {
+					if j >= len(unmatchedExpectedIndices) {
+						dist[i][j] = len(userItems[unmatchedUserIndices[i]])
+					} else if i >= len(unmatchedUserIndices) {
+						dist[i][j] = len(expectedItems[unmatchedExpectedIndices[j]])
+					} else {
+						dist[i][j] = levenshtein.ComputeDistance(expectedItems[unmatchedExpectedIndices[j]], toLowerConditionally(userItems[unmatchedUserIndices[i]], caseFold))
+					}
+				}
+			}
+
+			permutation, _ = hungarianAlgorithm.Solve(dist)
+		}
+
+		k := 0
+		for _, i := range permutation {
+			if k >= len(expectedItemsReordered) {
+				break
+			}
+			if i < len(unmatchedExpectedIndices) {
+				for expectedItemsReordered[k] != "" {
+					k++
+				}
+				expectedItemsReordered[k] = expectedItems[unmatchedExpectedIndices[i]]
+			}
+		}
+	}
+
+	return strings.Join(expectedItemsReordered, multisetItemDelimiter)
+}
+
+func multisetJudge(caseFold bool) Judge {
+	return func(run Run) string {
+		run.Answer = defaultJudge(run)
+		if run.OutputDelimiter != "" {
+			return perOutputJudge(func(input, userOutput, rawExpectedOutput string) string {
+				return getClosestMultiset(rawExpectedOutput, userOutput, run.MultisetItemDelimiter, caseFold)
+			})(run)
+		} else {
+			return getClosestMultiset(run.Answer, run.Stdout, run.MultisetItemDelimiter, caseFold)
+		}
+	}
+}
diff --git a/hole/nfa-simulator.go b/hole/nfa-simulator.go
index 4c7b3d79..10753125 100644
--- a/hole/nfa-simulator.go
+++ b/hole/nfa-simulator.go
@@ -166,7 +166,7 @@ func generateNFA() string {
 			inputNFA.WriteByte(' ')
 		}
 
-		if rand.IntN(2) == 0 {
+		if randBool() {
 			inputNFA.WriteByte('F')
 		} else {
 			inputNFA.WriteByte(' ')
diff --git a/hole/palindromemordnilap.go b/hole/palindromemordnilap.go
index 73e6a4a7..965cdfef 100644
--- a/hole/palindromemordnilap.go
+++ b/hole/palindromemordnilap.go
@@ -28,7 +28,7 @@ var _ = answerFunc("palindromemordnilap", func() []Answer {
 		"123456", "8989a",
 
 		"a", "aA", "aa", "aaaaaaa", "ab", "aba", "abaaaba", "abb", "abc",
-		"abca", "abcdc", "abcdcc",
+		"abca", "abcdc", "abcdcc", "sesphase",
 
 		"better", "mississippi", "Palindrome",
 
diff --git a/hole/play.go b/hole/play.go
index 0b830b3e..8f54af97 100644
--- a/hole/play.go
+++ b/hole/play.go
@@ -6,21 +6,18 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/exec"
 	"regexp"
 	"slices"
 	"strings"
-	"sync"
 	"syscall"
 	"time"
 	"unicode"
 
-	"github.com/agnivade/levenshtein"
 	"github.com/buildkite/terminal-to-html/v3"
 	"github.com/code-golf/code-golf/config"
-	hungarianAlgorithm "github.com/oddg/hungarian-algorithm"
+	"golang.org/x/sync/errgroup"
 )
 
 var timeout = 5 * time.Second
@@ -42,138 +39,31 @@ func trimPerLine(bytesSlice []byte) string {
 
 // Run holds the results of running a given solution once.
 type Run struct {
-	Answer            string        `json:"answer"`
-	ItemDelimiter     string        `json:"item_delimiter"`
-	MultisetDelimiter string        `json:"multiset_delimiter"`
-	Args              []string      `json:"args"`
-	ExitCode          int           `json:"exit_code"`
-	Pass              bool          `json:"pass"`
-	Stderr            string        `json:"stderr"`
-	Stdout            string        `json:"stdout"`
-	Time              time.Duration `json:"time_ns"`
-	Timeout           bool          `json:"timeout"`
+	Answer                string        `json:"answer"`
+	Code                  string        `json:"-"`
+	MultisetItemDelimiter string        `json:"multiset_item_delimiter"`
+	OutputDelimiter       string        `json:"output_delimiter"`
+	Args                  []string      `json:"args"`
+	ExitCode              int           `json:"exit_code"`
+	Pass                  bool          `json:"pass"`
+	Stderr                string        `json:"stderr"`
+	Stdout                string        `json:"stdout"`
+	Time                  time.Duration `json:"time_ns"`
+	Timeout               bool          `json:"timeout"`
 
 	// This is a bit hacky, the only way to discover how long an assembly
 	// solution is is to compile it so we store it here but don't JSON it.
 	ASMBytes int `json:"-"`
 }
 
-func getClosestAnswer(anyAnswer, stdout, itemDelimiter, multisetDelimiter string) string {
-	answerMultisets := []string{anyAnswer}
-	stdoutMultisets := []string{stdout}
-	if multisetDelimiter != "" {
-		answerMultisets = strings.Split(anyAnswer, multisetDelimiter)
-		stdoutMultisets = strings.Split(stdout, multisetDelimiter)
-	}
-	closestMultisets := make([]string, len(answerMultisets))
-
-	for i, answerMultiset := range answerMultisets {
-		stdoutMultiset := ""
-		if i < len(stdoutMultisets) {
-			stdoutMultiset = stdoutMultisets[i]
-		}
-		closestMultisets[i] = getClosestMultiset(answerMultiset, stdoutMultiset, itemDelimiter)
-	}
-	return strings.Join(closestMultisets, multisetDelimiter)
-}
-
-func getClosestMultiset(anyAnswer, stdout, itemDelimiter string) string {
-	expectedItems := strings.Split(anyAnswer, itemDelimiter)
-	expectedItemsReordered := make([]string, len(expectedItems))
-	userItems := strings.Split(stdout, itemDelimiter)
-
-	expectedItemsMap := make(map[string]int)
-	for _, expected := range expectedItems {
-		expectedItemsMap[expected]++
-	}
-
-	// Match items that are correct
-	matches := 0
-	for i, user := range userItems {
-		if i < len(expectedItems) && expectedItemsMap[user] > 0 {
-			expectedItemsReordered[i] = user
-			expectedItemsMap[user]--
-			userItems[i] = ""
-			matches++
-		}
-	}
-
-	// Process mismatched items
-	if matches < len(expectedItems) {
-
-		// Calculate indices of expected & user items that couldn't be matched be equality
-		unmatchedExpectedIndices := []int{}
-		unmatchedUserIndices := []int{}
-
-		for i, expected := range expectedItems {
-			if expectedItemsMap[expected] > 0 {
-				unmatchedExpectedIndices = append(unmatchedExpectedIndices, i)
-				expectedItemsMap[expected]--
-			}
-		}
-
-		for i, user := range userItems {
-			if user != "" {
-				unmatchedUserIndices = append(unmatchedUserIndices, i)
-			}
-		}
-
-		n := max(len(unmatchedExpectedIndices), len(unmatchedUserIndices))
-
-		permutation := make([]int, n)
-		for i := range permutation {
-			permutation[i] = i
-		}
-
-		// If there are not many wrong items, try to match them
-		// otherwise, use the above identity permutation
-		if n <= 32 {
-			dist := make([][]int, n)
-			for i := range dist {
-				dist[i] = make([]int, n)
-				for j := range dist {
-					if j >= len(unmatchedExpectedIndices) {
-						dist[i][j] = len(userItems[unmatchedUserIndices[i]])
-					} else if i >= len(unmatchedUserIndices) {
-						dist[i][j] = len(expectedItems[unmatchedExpectedIndices[j]])
-					} else {
-						dist[i][j] = levenshtein.ComputeDistance(expectedItems[unmatchedExpectedIndices[j]], userItems[unmatchedUserIndices[i]])
-					}
-				}
-			}
-
-			permutation, _ = hungarianAlgorithm.Solve(dist)
-		}
-
-		k := 0
-		for _, i := range permutation {
-			if k >= len(expectedItemsReordered) {
-				break
-			}
-			if i < len(unmatchedExpectedIndices) {
-				for expectedItemsReordered[k] != "" {
-					k++
-				}
-				expectedItemsReordered[k] = expectedItems[unmatchedExpectedIndices[i]]
-			}
-		}
-	}
-
-	return strings.Join(expectedItemsReordered, itemDelimiter)
-}
-
 // Play a given hole, in a given lang, with given code and return the runs.
 func Play(
 	ctx context.Context, hole *config.Hole, lang *config.Lang, code string,
-) []Run {
+) ([]Run, error) {
 	var answers []Answer
 
 	switch hole.ID {
 
-	// Quine is special as the answer depends on the given code.
-	case "quine":
-		answers = []Answer{{Args: []string{}, Answer: code}}
-
 	// Holes with fixed test cases.
 	case "css-colors":
 		answers = outputTests(shuffle(fixedTests(hole.ID)))
@@ -194,30 +84,50 @@ func Play(
 	runs := make([]Run, len(answers))
 
 	// Run all the runs in parallel to reduce the wall clock time.
-	var wg sync.WaitGroup
-	wg.Add(len(answers))
-
+	var g errgroup.Group
 	for i, answer := range answers {
 		runs[i] = Run{Args: answer.Args, Answer: answer.Answer}
 
-		go func(run *Run) {
-			if err := play(ctx, hole, lang, code, run); err != nil {
-				log.Println(err)
-			}
+		g.Go(func() error { return play(ctx, hole, lang, code, &runs[i]) })
+	}
+
+	return runs, g.Wait()
+}
 
-			wg.Done()
-		}(&runs[i])
+func play(
+	ctx context.Context, hole *config.Hole, lang *config.Lang, code string, run *Run,
+) error {
+	err := runCode(ctx, hole, lang, code, run)
+	if err != nil {
+		return err
 	}
 
-	wg.Wait()
+	run.Code = code
+	run.OutputDelimiter = hole.OutputDelimiter
+	run.MultisetItemDelimiter = hole.MultisetItemDelimiter
+	run.Answer = holeJudges[hole.ID](*run)
 
-	return runs
+	// Timeouts and whitespace only output never pass.
+	if !run.Timeout && len(strings.TrimSpace(run.Stdout)) != 0 {
+		if hole.CaseFold {
+			run.Pass = strings.EqualFold(run.Answer, run.Stdout)
+		} else {
+			run.Pass = run.Answer == run.Stdout
+		}
+	}
+
+	return nil
 }
 
-func play(
+func runCode(
 	ctx context.Context, hole *config.Hole, lang *config.Lang, code string, run *Run,
 ) error {
 	// Preprocess code.
+	if strings.Contains(code, "\x00") {
+		run.Stderr = "Solutions must not contain a literal null byte."
+		return nil
+	}
+
 	switch lang.ID {
 	case "05ab1e":
 		// Prevent trivial quines. Error out and return early.
@@ -328,14 +238,17 @@ func play(
 		if asmBytesRead, asmBytesWrite, err = os.Pipe(); err != nil {
 			return err
 		}
+		defer asmBytesRead.Close()
+		defer asmBytesWrite.Close()
 
 		cmd.ExtraFiles = []*os.File{asmBytesWrite}
 	}
 
 	// Language arguments. Clone because we intend to mutate.
-	cmd.Args = slices.Clone(lang.Args)
 	if hole.ID == "quine" && lang.ArgsQuine != nil {
 		cmd.Args = slices.Clone(lang.ArgsQuine)
+	} else {
+		cmd.Args = slices.Clone(lang.Args)
 	}
 
 	// Pass code via args or stdin.
@@ -382,6 +295,10 @@ func play(
 
 	// Actual byte count is printed by the assembler.
 	if lang.ID == "assembly" {
+		// Explicitly close the writer in case defasm died before it could.
+		// This prevents a very long wait in the upcoming fscanf.
+		asmBytesWrite.Close()
+
 		if _, err := fmt.Fscanf(asmBytesRead, "%d", &run.ASMBytes); err != nil {
 			return err
 		}
@@ -423,24 +340,5 @@ func play(
 		run.Stdout = trimPerLine(stdoutBytes)
 	}
 
-	// Timeouts and whitespace only output never pass.
-	if !run.Timeout && len(strings.TrimSpace(run.Stdout)) != 0 {
-		if hole.ID != "quine" {
-			run.Answer = trimPerLine([]byte(run.Answer))
-		}
-		if hole.ItemDelimiter != "" {
-			run.Answer = getClosestAnswer(run.Answer, run.Stdout, hole.ItemDelimiter, hole.MultisetDelimiter)
-		}
-
-		if hole.CaseFold {
-			run.Pass = strings.EqualFold(run.Answer, run.Stdout)
-		} else {
-			run.Pass = run.Answer == run.Stdout
-		}
-	}
-
-	run.MultisetDelimiter = hole.MultisetDelimiter
-	run.ItemDelimiter = hole.ItemDelimiter
-
 	return nil
 }
diff --git a/hole/quine.go b/hole/quine.go
new file mode 100644
index 00000000..fa029de8
--- /dev/null
+++ b/hole/quine.go
@@ -0,0 +1,9 @@
+package hole
+
+var _ = answerFunc("quine", func() []Answer {
+	return []Answer{{Args: []string{}, Answer: ""}}
+})
+
+var _ = judge("quine", func(run Run) string {
+	return run.Code
+})
diff --git a/hole/reverse-polish-notation.go b/hole/reverse-polish-notation.go
index 7cc74336..01b263cc 100644
--- a/hole/reverse-polish-notation.go
+++ b/hole/reverse-polish-notation.go
@@ -40,7 +40,7 @@ func expand(node *Node) {
 			left = randChoice(factors)
 			right = val / left
 		}
-		if rand.IntN(2) == 1 {
+		if randBool() {
 			left, right = right, left
 		}
 	case '/':
diff --git a/hole/scrambled-alphabetization.go b/hole/scrambled-alphabetization.go
deleted file mode 100644
index b9f8ef34..00000000
--- a/hole/scrambled-alphabetization.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package hole
-
-import (
-	"math/rand/v2"
-	"sort"
-	"strings"
-)
-
-func alphabetizeWords(s string) string {
-	fields := strings.Fields(s)
-	order, words := make(map[rune]int), fields[1:]
-
-	for i, ch := range fields[0] {
-		order[ch] = i
-	}
-
-	sort.Slice(words, func(i, j int) bool {
-		for k := 0; k < len(words[i]) && k < len(words[j]); k++ {
-			if order[rune(words[i][k])] < order[rune(words[j][k])] {
-				return true
-			} else if order[rune(words[i][k])] > order[rune(words[j][k])] {
-				return false
-			}
-		}
-
-		return len(words[i]) < len(words[j])
-	})
-
-	return strings.Join(words, " ")
-}
-
-func generateArgument() string {
-	var argument strings.Builder
-
-	argument.WriteString(scrambleAlphabet())
-
-	for range randInt(3, 9) {
-		argument.WriteByte(' ')
-		argument.WriteString(randWord())
-	}
-
-	return argument.String()
-}
-
-func scrambleAlphabet() string {
-	alphabet := strings.Split("abcdefghijklmnopqrstuvwxyz", "")
-
-	rand.Shuffle(len(alphabet), func(i, j int) {
-		alphabet[i], alphabet[j] = alphabet[j], alphabet[i]
-	})
-
-	return strings.Join(alphabet, "")
-}
-
-var _ = answerFunc("scrambled-alphabetization", func() []Answer {
-	tests := make([]test, 0, 100)
-
-	for range 100 {
-		argument := generateArgument()
-
-		tests = append(tests, test{
-			argument,
-			alphabetizeWords(argument),
-		})
-	}
-
-	return outputTests(shuffle(tests))
-})
diff --git a/hole/scrambled-sort.go b/hole/scrambled-sort.go
new file mode 100644
index 00000000..bc814d0a
--- /dev/null
+++ b/hole/scrambled-sort.go
@@ -0,0 +1,69 @@
+package hole
+
+import (
+	"cmp"
+	"slices"
+	"strings"
+)
+
+var _ = answerFunc("scrambled-sort", func() []Answer {
+	tests := make([]test, 100)
+
+	fixedTests := []struct {
+		alphabet []byte
+		words    []string
+	}{
+		// One word, "eerie", sorts before the alphabet itself
+		{
+			[]byte("eyxmrgjbiunlwczqpsdtkhavfo"),
+			shuffle([]string{"eerie", randWord(), randWord(), randWord(), randWord()}),
+		},
+		// One word is a prefix of another
+		{
+			shuffle([]byte("abcdefghijklmnopqrstuvwxyz")),
+			shuffle([]string{"the", "then", "there", "they", randWord(), randWord()}),
+		},
+	}
+
+	for i, t := range fixedTests {
+		tests[i] = scrambledSortTest(t.alphabet, t.words)
+	}
+
+	for i := len(fixedTests); i < len(tests); i++ {
+		// Generate a random alphabet and words.
+		alphabet := shuffle([]byte("abcdefghijklmnopqrstuvwxyz"))
+
+		words := make([]string, randInt(3, 9))
+		for i := range words {
+			words[i] = randWord()
+		}
+
+		tests[i] = scrambledSortTest(alphabet, words)
+	}
+
+	return outputTests(shuffle(tests))
+})
+
+func scrambledSortTest(alphabet []byte, words []string) test {
+	var t test
+	t.in = string(alphabet) + " " + strings.Join(words, " ")
+
+	// Sort words according to the random alphabet.
+	order := map[byte]int{}
+	for i, b := range alphabet {
+		order[b] = i
+	}
+
+	slices.SortFunc(words, func(a, b string) int {
+		for i := range min(len(a), len(b)) {
+			if c := cmp.Compare(order[a[i]], order[b[i]]); c != 0 {
+				return c
+			}
+		}
+
+		return cmp.Compare(len(a), len(b))
+	})
+
+	t.out = strings.Join(words, " ")
+	return t
+}
diff --git a/hole/star-wars-gpt.go b/hole/star-wars-gpt.go
index 929984b4..958fcd05 100644
--- a/hole/star-wars-gpt.go
+++ b/hole/star-wars-gpt.go
@@ -1,19 +1,32 @@
 package hole
 
 import (
+	"regexp"
 	"slices"
 	"strings"
 )
 
+var crawls []string
+
+func init() {
+	// Munge the data from "Star Wars Opening Crawl" into our format.
+	// Drop first line, join the rest, lowercase, strip punctuation.
+	punct := regexp.MustCompile("[.,;!?]")
+	for _, t := range fixedTests("star-wars-opening-crawl") {
+		crawl := strings.Join(strings.Split(t.in, "\n")[1:], " ")
+		crawl = punct.ReplaceAllString(strings.ToLower(crawl), "")
+		crawls = append(crawls, crawl)
+	}
+}
+
 func solveSWGPT(corpusSanitized string, promptWords map[string]bool) map[string]string {
 	markovFirst := map[string][]string{}
 	markovCount := map[string]map[string]int{}
 
 	words := strings.Split(corpusSanitized, " ")
 
-	for i := 0; i < len(words)-1; i++ {
-		_, ok := markovCount[words[i]]
-		if !ok {
+	for i := range len(words) - 1 {
+		if _, ok := markovCount[words[i]]; !ok {
 			markovCount[words[i]] = make(map[string]int)
 		}
 		markovFirst[words[i]] = append(markovFirst[words[i]], words[i+1])
@@ -38,41 +51,24 @@ func solveSWGPT(corpusSanitized string, promptWords map[string]bool) map[string]
 }
 
 var _ = answerFunc("star-wars-gpt", func() []Answer {
-	corpora := shuffle([]string{
-		`Turmoil has engulfed the Galactic Republic. The taxation of trade routes to outlying star systems is in dispute. Hoping to resolve the matter with a blockade of deadly battleships, the greedy Trade Federation has stopped all shipping to the small planet of Naboo. While the Congress of the Republic endlessly debates this alarming chain of events, the Supreme Chancellor has secretly dispatched two Jedi Knights, the guardians of peace and justice in the galaxy, to settle the conflict....`,
-		`War! The Republic is crumbling under attacks by the ruthless Sith Lord, Count Dooku. There are heroes on both sides. Evil is everywhere. In a stunning move, the fiendish droid leader, General Grievous, has swept into the Republic capital and kidnapped Chancellor Palpatine, leader of the Galactic Senate. As the Separatist Droid Army attempts to flee the besieged capital with their valuable hostage, two Jedi Knights lead a desperate mission to rescue the captive Chancellor....`,
-		`There is unrest in the Galactic Senate. Several thousand solar systems have declared their intentions to leave the Republic. This separatist movement, under the leadership of the mysterious Count Dooku, has made it difficult for the limited number of Jedi Knights to maintain peace and order in the galaxy. Senator Amidala, the former Queen of Naboo, is returning to the Galactic Senate to vote on the critical issue of creating an ARMY OF THE REPUBLIC to assist the overwhelmed Jedi....`,
-		`It is a period of civil war. Rebel spaceships, striking from a hidden base, have won their first victory against the evil Galactic Empire. During the battle, Rebel spies managed to steal secret plans to the Empire's ultimate weapon, the DEATH STAR, an armored space station with enough power to destroy an entire planet. Pursued by the Empire's sinister agents, Princess Leia races home aboard her starship, custodian of the stolen plans that can save her people and restore freedom to the galaxy....`,
-		`It is a dark time for the Rebellion. Although the Death Star has been destroyed, Imperial troops have driven the Rebel forces from their hidden base and pursued them across the galaxy. Evading the dreaded Imperial Starfleet, a group of freedom fighters led by Luke Skywalker has established a new secret base on the remote ice world of Hoth. The evil lord Darth Vader, obsessed with finding young Skywalker, has dispatched thousands of remote probes into the far reaches of space....`,
-		`Luke Skywalker has returned to his home planet of Tatooine in an attempt to rescue his friend Han Solo from the clutches of the vile gangster Jabba the Hutt. Little does Luke know that the GALACTIC EMPIRE has secretly begun construction on a new armored space station even more powerful than the first dreaded Death Star. When completed, this ultimate weapon will spell certain doom for the small band of rebels struggling to restore freedom to the galaxy...`,
-		`Luke Skywalker has vanished. In his absence, the sinister FIRST ORDER has risen from the ashes of the Empire and will not rest until Skywalker, the last Jedi, has been destroyed. With the support of the REPUBLIC, General Leia Organa leads a brave RESISTANCE. She is desperate to find her brother Luke and gain his help in restoring peace and justice to the galaxy. Leia has sent her most daring pilot on a secret mission to Jakku, where an old ally has discovered a clue to Luke's whereabouts....`,
-		`The FIRST ORDER reigns. Having decimated the peaceful Republic, Supreme Leader Snoke now deploys his merciless legions to seize military control of the galaxy. Only General Leia Organa's band of RESISTANCE fighters stand against the rising tyranny, certain that Jedi Master Luke Skywalker will return and restore a spark of hope to the fight. But the Resistance has been exposed. As the First Order speeds toward the rebel base, the brave heroes mount a desperate escape....`,
-		`The dead speak! The galaxy has heard a mysterious broadcast, a threat of REVENGE in the sinister voice of the late EMPEROR PALPATINE. GENERAL LEIA ORGANA dispatches secret agents to gather intelligence, while REY, the last hope of the Jedi, trains for battle against the diabolical FIRST ORDER. Meanwhile, Supreme Leader KYLO REN rages in search of the phantom Emperor, determined to destroy any threat to his power....`,
-	})
-
-	answers := make([]Answer, 10)
+	corpora := shuffle(slices.Clone(crawls))
+
+	tests := make([]test, 13)
 	testWordCount := 12
 
 	for i := 0; i < 10; i += 2 {
-		corpus := corpora[i] + " " + corpora[(i+1)%9]
-		corpusSanitized := corpus
-		for _, c := range ".,;!?" {
-			corpusSanitized = strings.ReplaceAll(corpusSanitized, string(c), "")
-		}
-		corpusSanitized = strings.ToLower(corpusSanitized)
+		corpusSanitized := corpora[i] + " " + corpora[(i+1)%9]
 
 		chosenWords := map[string]bool{}
 		splitWords := strings.Split(corpusSanitized, " ")
 		shuffledWords := shuffle(splitWords[:len(splitWords)-1]) // omit last word
 		for j := 0; len(chosenWords) < testWordCount; j++ {
-			_, ok := chosenWords[shuffledWords[j]]
-			if !ok {
+			if _, ok := chosenWords[shuffledWords[j]]; !ok {
 				chosenWords[shuffledWords[j]] = true
 			}
 		}
 
-		var inputSWGPT strings.Builder
-		var outputSWGPT strings.Builder
+		var inputSWGPT, outputSWGPT strings.Builder
 
 		inputSWGPT.WriteString(corpusSanitized)
 		inputSWGPT.WriteByte('\n')
@@ -90,24 +86,33 @@ var _ = answerFunc("star-wars-gpt", func() []Answer {
 			counter++
 		}
 
-		args := []string{inputSWGPT.String()}
-		answers[i/2] = Answer{Args: args, Answer: outputSWGPT.String()}
+		tests[i/2] = test{inputSWGPT.String(), outputSWGPT.String()}
 	}
-	staticTest1 := "the first order reigns having decimated the peaceful republic supreme leader snoke now deploys his merciless legions to seize military control of the galaxy only general leia organa's band of resistance fighters stand against the rising tyranny certain that jedi master luke skywalker will return and restore a spark of hope to the fight but the resistance has been exposed as the first order speeds toward the rebel base the brave heroes mount a desperate escape turmoil has engulfed the galactic republic the taxation of trade routes to outlying star systems is in dispute hoping to resolve the matter with a blockade of deadly battleships the greedy trade federation has stopped all shipping to the small planet of naboo while the congress of the republic endlessly debates this alarming chain of events the supreme chancellor has secretly dispatched two jedi knights the guardians of peace and justice in the galaxy to settle the conflict\nof\nto\nthe\ndispatched\nfirst\nmilitary\nengulfed\ngalaxy\nspark\ncertain\nevents\nseize"
+
+	staticTest1 := crawls[7] + " " + crawls[0] + "\nof\nto\nthe\ndispatched\nfirst\nmilitary\nengulfed\ngalaxy\nspark\ncertain\nevents\nseize"
 	staticResult1 := "the\nthe\nfirst\ntwo\norder\ncontrol\nthe\nonly\nof\nthat\nthe\nmilitary"
-	staticTest2 := "it is a period of civil war rebel spaceships striking from a hidden base have won their first victory against the evil galactic empire during the battle rebel spies managed to steal secret plans to the empire's ultimate weapon the death star an armored space station with enough power to destroy an entire planet pursued by the empire's sinister agents princess leia races home aboard her starship custodian of the stolen plans that can save her people and restore freedom to the galaxy luke skywalker has vanished in his absence the sinister first order has risen from the ashes of the empire and will not rest until skywalker the last jedi has been destroyed with the support of the republic general leia organa leads a brave resistance she is desperate to find her brother luke and gain his help in restoring peace and justice to the galaxy leia has sent her most daring pilot on a secret mission to jakku where an old ally has discovered a clue to luke's whereabouts\nenough\nin\nempire\nthe\nsave\nto\ndestroy\nand\nsinister\na\nis\nultimate"
+	staticTest2 := crawls[3] + " " + crawls[6] + "\nenough\nin\nempire\nthe\nsave\nto\ndestroy\nand\nsinister\na\nis\nultimate"
 	staticResult2 := "power\nhis\nduring\nempire's\nher\nthe\nan\nrestore\nagents\nperiod\na\nweapon"
-	staticTest3 := "war the republic is crumbling under attacks by the ruthless sith lord count dooku there are heroes on both sides evil is everywhere in a stunning move the fiendish droid leader general grievous has swept into the republic capital and kidnapped chancellor palpatine leader of the galactic senate as the separatist droid army attempts to flee the besieged capital with their valuable hostage two jedi knights lead a desperate mission to rescue the captive chancellor luke skywalker has vanished in his absence the sinister first order has risen from the ashes of the empire and will not rest until skywalker the last jedi has been destroyed with the support of the republic general leia organa leads a brave resistance she is desperate to find her brother luke and gain his help in restoring peace and justice to the galaxy leia has sent her most daring pilot on a secret mission to jakku where an old ally has discovered a clue to luke's whereabouts\nwith\nin\nashes\ntheir\nrest\nof\nare\nthe\nknights\nfind\ncrumbling\nthere"
+	staticTest3 := crawls[2] + " " + crawls[6] + "\nwith\nin\nashes\ntheir\nrest\nof\nare\nthe\nknights\nfind\ncrumbling\nthere"
 	staticResult3 := "their\na\nof\nvaluable\nuntil\nthe\nheroes\nrepublic\nlead\nher\nunder\nare"
-	staticTest4 := "there is unrest in the galactic senate several thousand solar systems have declared their intentions to leave the republic this separatist movement under the leadership of the mysterious count dooku has made it difficult for the limited number of jedi knights to maintain peace and order in the galaxy senator amidala the former queen of naboo is returning to the galactic senate to vote on the critical issue of creating an army of the republic to assist the overwhelmed jedi war the republic is crumbling under attacks by the ruthless sith lord count dooku there are heroes on both sides evil is everywhere in a stunning move the fiendish droid leader general grievous has swept into the republic capital and kidnapped chancellor palpatine leader of the galactic senate as the separatist droid army attempts to flee the besieged capital with their valuable hostage two jedi knights lead a desperate mission to rescue the captive chancellor\nthousand\nto\ncaptive\nin\nthe\nsith\njedi\ntwo\nrepublic\ngalaxy\non\nof"
+	staticTest4 := crawls[1] + " " + crawls[2] + "\nthousand\nto\ncaptive\nin\nthe\nsith\njedi\ntwo\nrepublic\ngalaxy\non\nof"
 	staticResult4 := "solar\nleave\nchancellor\nthe\nrepublic\nlord\nknights\njedi\nthis\nsenator\nthe\nthe"
-	staticTest5 := "there is unrest in the galactic senate several thousand solar systems have declared their intentions to leave the republic this separatist movement under the leadership of the mysterious count dooku has made it difficult for the limited number of jedi knights to maintain peace and order in the galaxy senator amidala the former queen of naboo is returning to the galactic senate to vote on the critical issue of creating an army of the republic to assist the overwhelmed jedi luke skywalker has vanished in his absence the sinister first order has risen from the ashes of the empire and will not rest until skywalker the last jedi has been destroyed with the support of the republic general leia organa leads a brave resistance she is desperate to find her brother luke and gain his help in restoring peace and justice to the galaxy leia has sent her most daring pilot on a secret mission to jakku where an old ally has discovered a clue to luke's whereabouts\nand\ngain\nsenate\nbrother\na\npeace\nassist\nqueen\nhis\njedi\npilot\ngalactic"
+	staticTest5 := crawls[1] + " " + crawls[6] + "\nand\ngain\nsenate\nbrother\na\npeace\nassist\nqueen\nhis\njedi\npilot\ngalactic"
 	staticResult5 := "order\nhis\nseveral\nluke\nbrave\nand\nthe\nof\nabsence\nknights\non\nsenate"
-	answers[5] = Answer{Args: []string{staticTest1}, Answer: staticResult1}
-	answers[6] = Answer{Args: []string{staticTest2}, Answer: staticResult2}
-	answers[7] = Answer{Args: []string{staticTest3}, Answer: staticResult3}
-	answers[8] = Answer{Args: []string{staticTest4}, Answer: staticResult4}
-	answers[9] = Answer{Args: []string{staticTest5}, Answer: staticResult5}
-
-	return answers
+	staticTest6 := crawls[1] + " " + crawls[2] + "\nchancellor\ntheir\na\nreturning\nthe\nthousand\nassist\ngalactic\nin\nqueen\non\nsolar"
+	staticResult6 := "palpatine\nintentions\nstunning\nto\nrepublic\nsolar\nthe\nsenate\nthe\nof\nthe\nsystems"
+	staticTest7 := crawls[3] + " " + crawls[8] + "\nintelligence\nthe\nultimate\nevil\nfor\nthreat\njedi\nwon\nempire\npower\ndead\nrebel"
+	staticResult7 := "while\nempire's\nweapon\ngalactic\nbattle\nof\ntrains\ntheir\nduring\nto\nspeak\nspaceships"
+	staticTest8 := crawls[6] + " " + crawls[3] + "\nthe\nstolen\nspace\nbattle\nvictory\nweapon\njustice\nto\nresistance\nin\nof\njedi"
+	staticResult8 := "galaxy\nplans\nstation\nrebel\nagainst\nthe\nto\nthe\nshe\nhis\nthe\nhas"
+	tests[5] = test{staticTest1, staticResult1}
+	tests[6] = test{staticTest2, staticResult2}
+	tests[7] = test{staticTest3, staticResult3}
+	tests[8] = test{staticTest4, staticResult4}
+	tests[9] = test{staticTest5, staticResult5}
+	tests[10] = test{staticTest6, staticResult6}
+	tests[11] = test{staticTest7, staticResult7}
+	tests[12] = test{staticTest8, staticResult8}
+
+	return outputTests(shuffle(tests))
 })
diff --git a/hole/ten-pin-bowling.go b/hole/ten-pin-bowling.go
index f0353b3e..2091eecb 100644
--- a/hole/ten-pin-bowling.go
+++ b/hole/ten-pin-bowling.go
@@ -54,7 +54,7 @@ func randReplacements(gFrames []rune) []rune {
 			continue
 		}
 
-		if rand.IntN(2) == 0 {
+		if randBool() {
 			frames[i] = replacement
 		}
 	}
diff --git a/hole/topological-sort.go b/hole/topological-sort.go
new file mode 100644
index 00000000..65122b03
--- /dev/null
+++ b/hole/topological-sort.go
@@ -0,0 +1,88 @@
+package hole
+
+import (
+	"fmt"
+	"math/rand/v2"
+	"slices"
+	"strings"
+)
+
+// does not support isolated nodes
+// cycles are insurmountable obstructions
+func tsort(in string) string {
+	children, inDegrees := make(map[string][]string), make(map[string]int)
+	for pair := range slices.Chunk(strings.Fields(in), 2) {
+		children[pair[0]] = append(children[pair[0]], pair[1])
+		if _, ok := inDegrees[pair[0]]; !ok {
+			inDegrees[pair[0]] = 0
+		}
+		if pair[0] != pair[1] {
+			inDegrees[pair[1]]++
+		}
+	}
+	var stack []string
+	for node, deg := range inDegrees {
+		if deg == 0 {
+			stack = append(stack, node)
+		}
+	}
+	var result []string
+	for len(stack) > 0 {
+		next := stack[len(stack)-1]
+		stack = slices.Delete(stack, len(stack)-1, len(stack))
+		result = append(result, next)
+		for _, child := range children[next] {
+			inDegrees[child]--
+			if inDegrees[child] == 0 {
+				stack = append(stack, child)
+			}
+		}
+	}
+	return strings.Join(result, " ")
+}
+
+// guaranteed Hamiltonian path, so the output is unique
+func generateTsortTest(length, additional int) string {
+	init := rand.Perm(length)
+	var (
+		seq  []int
+		rows int
+	)
+	arcChoices, arc := rand.Perm((length-2)*(length-1)/2), 0
+	for i, x := range init {
+		for j, y := range init[i+1:] {
+			if j == 0 || arcChoices[arc] < additional {
+				seq = append(seq, x, y)
+				rows++
+			}
+			if j > 0 {
+				arc++
+			}
+		}
+	}
+	var test string
+	for i, x := range rand.Perm(rows) {
+		if i > 0 {
+			test += "\n"
+		}
+		test += fmt.Sprint(seq[2*x], seq[2*x+1])
+	}
+	return test
+}
+
+// single-digit numbers
+var _ = answerFunc("topological-sort", func() []Answer {
+	var tests []test
+	for n := range 10 - 2 + 1 {
+		n += 2
+		for a := range (n-2)*(n-1)/2 + 1 {
+			for range 3 {
+				tcase := generateTsortTest(n, a)
+				tests = append(tests, test{tcase, tsort(tcase)})
+			}
+		}
+	}
+	shuffle(tests)
+	mid := len(tests) / 2
+	return outputTests(tests[:mid], tests[mid:])
+})
diff --git a/js/_codemirror.ts b/js/_codemirror.ts
index 0f0155c5..f78390da 100644
--- a/js/_codemirror.ts
+++ b/js/_codemirror.ts
@@ -120,8 +120,15 @@ export const extensions : { [key: string]: any } = {
             { key: 'Mod-/', run: toggleComment },
         ]),
         highlightWhitespace(),
-        lineNumbers(),
     ],
+    'lineNumbers': lineNumbers(),
+    'zeroIndexedLineNumbers': lineNumbers(
+        {
+            formatNumber(num: number) {
+                return `${num - 1}`;
+            },
+        },
+    ),
     'bracketMatching': bracketMatching(),
     'vim': vim({ status: true }),
 
@@ -211,7 +218,9 @@ export const extensions : { [key: string]: any } = {
     'tcl':           StreamLanguage.define(tcl),
     'tex':           StreamLanguage.define(stex),
     // TODO uiua
+    // TODO umka
     // TODO v
+    'vala':          StreamLanguage.define(csharp),
     // TODO viml
     // TODO vyxal
     'wren':          wren,
diff --git a/js/_diff.tsx b/js/_diff.tsx
index 7b014b71..18369d7f 100644
--- a/js/_diff.tsx
+++ b/js/_diff.tsx
@@ -8,8 +8,8 @@ interface DiffPos {
     isLastDiff: boolean;
 }
 
-export default (hole: string, exp: string, out: string, argv: string[], ignoreCase: boolean, multisetDelimiter: string, itemDelimiter: string) => {
-    const provideItemwiseDiff = !multisetDelimiter && itemDelimiter;
+export default (hole: string, exp: string, out: string, argv: string[], ignoreCase: boolean, outputDelimiter: string, multisetItemDelimiter: string) => {
+    const provideItemwiseDiff = !outputDelimiter && multisetItemDelimiter;
     const isLinesDiffChecked = Boolean($('#diffKindSettings input[value="lines"]:checked'));
     return provideItemwiseDiff ?
         <div>
@@ -18,17 +18,17 @@ export default (hole: string, exp: string, out: string, argv: string[], ignoreCa
                 <label><input type="radio" name="diff_kind" value="lines" checked={isLinesDiffChecked ? true : undefined}></input> Lines</label>
             </div>
             {linesDiff(hole, exp, out, argv, ignoreCase)}
-            {itemsDiff(exp, out, itemDelimiter)}
+            {itemsDiff(exp, out, multisetItemDelimiter)}
         </div>
         : linesDiff(hole, exp, out, argv, ignoreCase);
 };
 
-function itemsDiff(exp: string, out: string, itemDelimiter: string) {
+function itemsDiff(exp: string, out: string, multisetItemDelimiter: string) {
     const diff = new Map<string, number>();
-    for (const x of exp.split(itemDelimiter)){
+    for (const x of exp.split(multisetItemDelimiter)){
         diff.set(x, (diff.get(x) ?? 0) - 1);
     }
-    for (const x of out.split(itemDelimiter)){
+    for (const x of out.split(multisetItemDelimiter)){
         diff.set(x, (diff.get(x) ?? 0) + 1);
     }
     const diffItems = [...diff.entries()].map(([x, count]) => ({x, count, absCount: Math.abs(count)}));
diff --git a/js/_hole-common.tsx b/js/_hole-common.tsx
index c2d801ed..b43b0a80 100644
--- a/js/_hole-common.tsx
+++ b/js/_hole-common.tsx
@@ -118,7 +118,7 @@ export function initCopyButtons(buttons: NodeListOf<HTMLElement>) {
             navigator.clipboard.writeText(btn.dataset.copy!);
 }
 
-export const langs = JSON.parse($('#langs').innerText);
+export const langs = JSON.parse($('#languages').innerText);
 const sortedLangs  =
     Object.values(langs).sort((a: any, b: any) => a.name.localeCompare(b.name));
 let lang: string = '';
@@ -198,7 +198,7 @@ export function setState(code: string, editor: EditorView) {
                 extensions[lang as keyof typeof extensions] ?? [],
                 // These languages shouldn't match brackets.
                 ['fish', 'hexagony'].includes(lang)
-                    ? [] : extensions.bracketMatching,
+                    ? extensions.zeroIndexedLineNumbers : [extensions.lineNumbers, extensions.bracketMatching],
                 // These languages shouldn't wrap lines.
                 ['assembly', 'fish', 'hexagony'].includes(lang)
                     ? [] : EditorView.lineWrapping,
@@ -360,8 +360,8 @@ export interface RankUpdate {
 
 export interface Run {
     answer: string,
-    multiset_delimiter: string,
-    item_delimiter: string,
+    output_delimiter: string,
+    multiset_item_delimiter: string,
     args: string[],
     exit_code: number,
     pass: boolean,
@@ -377,8 +377,8 @@ export interface ReadonlyPanelsData {
     Exp: string,
     Err: string,
     Argv: string[],
-    MultisetDelimiter: string,
-    ItemDelimiter: string
+    OutputDelimiter: string,
+    MultisetItemDelimiter: string
 }
 
 export interface SubmitResponse {
@@ -629,8 +629,8 @@ export async function submit(
             Exp: run.answer,
             Err: run.stderr,
             Out: run.stdout,
-            MultisetDelimiter: run.multiset_delimiter,
-            ItemDelimiter: run.item_delimiter,
+            OutputDelimiter: run.output_delimiter,
+            MultisetItemDelimiter: run.multiset_item_delimiter,
         });
 
         const ms = Math.round(run.time_ns / 10**6);
@@ -776,31 +776,27 @@ export async function populateScores(editor: any) {
                     <span>{r.golfer.name}</span>
                 </a>
             </td>
-            <td data-tooltip={tooltip(r, 'Bytes')}>
+            <td title={tooltip(r, 'Bytes')}>
                 {scoringID != 'bytes' ? comma(r.bytes) :
                     <a href={`/golfers/${r.golfer.name}/${hole}/${lang}/bytes`}>
                         <span>{comma(r.bytes)}</span>
                     </a>}
             </td>
-            {lang == 'assembly' ? '' : <td data-tooltip={tooltip(r, 'Chars')}>
+            {lang == 'assembly' ? '' : <td title={tooltip(r, 'Chars')}>
                 {scoringID != 'chars' ? comma(r.chars) :
                     <a href={`/golfers/${r.golfer.name}/${hole}/${lang}/chars`}>
                         <span>{comma(r.chars)}</span>
                     </a>}
             </td>}
         </tr>): <tr><td colspan={colspan}>(Empty)</td></tr>
-    }{
-        // Padding.
-        tabLayout ? [] : [...Array(7 - rows.length).keys()].map(() =>
-            <tr><td colspan={colspan}>&nbsp;</td></tr>)
     }</tbody>);
 
-    if (tabLayout) {
-        if (view === 'me')
-            $('.me')?.scrollIntoView({block: 'center'});
-        else
-            $('#scores-wrapper').scrollTop = 0;
-    }
+    // Scroll the rankings to the top or the "me" row if applicable.
+    const me            = $('.me');
+    const scoresWrapper = $('#scores-wrapper');
+    scoresWrapper.scrollTop = (view === 'me' && me)
+        ? me.offsetTop + (me.offsetHeight / 2) - (scoresWrapper.offsetHeight / 2)
+        : 0;
 
     $$<HTMLAnchorElement>('#scoringTabs a').forEach((tab, i) => {
         if (tab.innerText == scorings[scoring]) {
diff --git a/js/_search-nav.ts b/js/_search-nav.ts
new file mode 100644
index 00000000..29143fab
--- /dev/null
+++ b/js/_search-nav.ts
@@ -0,0 +1,114 @@
+import { $ } from './_util';
+
+export interface SearchNavResult {
+    path: string;
+    description: string;
+}
+
+interface SearchNavResultInternal extends SearchNavResult {
+    priority: number;
+}
+
+const langs: Record<string, string[]> = JSON.parse($('#langs').innerText);
+const holes: Record<string, string[]> = JSON.parse($('#holes').innerText);
+
+function defaultMatchPriority(haystack: string, needle: string) {
+    needle = needle.toLowerCase();
+    haystack = haystack.toLowerCase();
+    if (needle === haystack) return 1e10;
+    if (haystack.startsWith(needle)) return 1e5 - haystack.length;
+    if (haystack.includes(needle)) return 1e3 - haystack.length - needle.length;
+    return 0;
+}
+
+let lastSearch: string | undefined = undefined;
+// eslint-disable-next-line no-unused-vars
+export function requestResults(search: string, updateResults: (results: SearchNavResult[]) => void) {
+    if (!search || lastSearch === search) return;
+    lastSearch = search;
+
+    if (search.startsWith('@')) {
+        requestAtResults(search, updateResults);
+        return;
+    }
+
+    const [holeSearch, langSearch] = search.includes('#') ? search.split('#', 2) : [search, undefined];
+    const currentHole = location.pathname.slice(1) in holes ? location.pathname.slice(1) : undefined;
+
+    let matches: SearchNavResultInternal[] = [];
+
+    const holesMatches = Object.entries(holes)
+        .map(([k,v]) => ({
+            path: `/${k}`,
+            description: v[0],
+            priority: Math.max(...v.map(name => defaultMatchPriority(name, holeSearch))),
+        })).filter(x => x.priority > 0);
+
+    if (langSearch) {
+        const langsMatches = langSearch ? Object.entries(langs)
+            .map(([k,v]) => ({
+                path: `#${k}`,
+                description: v[0],
+                priority: Math.max(...v.map(name => defaultMatchPriority(name, langSearch))),
+            })).filter(x => x.priority > 0) : [];
+
+        if (holeSearch) {
+            matches = holesMatches.flatMap(hole => langsMatches.map(lang => ({
+                path: hole.path + lang.path,
+                description: hole.description + ' in ' + lang.description,
+                priority: hole.priority * lang.priority,
+            })));
+        }
+        if (currentHole) {
+            matches.push(...langsMatches.map(lang => (
+                {
+                    path: '/' + currentHole + lang.path,
+                    description: holes[currentHole][0] + ' in ' + lang.description,
+                    priority: lang.priority,
+                }
+            )));
+        }
+    }
+    else {
+        matches = holesMatches;
+    }
+
+    updateResults(processResults(matches));
+}
+
+function processResults(results: SearchNavResultInternal[]) {
+    results = results.filter(x => x.priority > 0 && new URL(x.path, location.origin).href !== location.href);
+    results.sort((a,b) => b.priority - a.priority);
+    return results.slice(0, 10);
+}
+
+// eslint-disable-next-line no-unused-vars
+function requestAtResults(search: string, updateResults: (results: SearchNavResult[]) => void) {
+    if (search === '@') {
+        const currentGolferPath = $<HTMLAnchorElement>('#site-header [title=Profile]')?.href;
+        updateResults(currentGolferPath ? processResults([{path: new URL(currentGolferPath).href, description: 'My Profile', priority: 0}]) : []);
+        return;
+    }
+    requestGolferResults(search.slice(1), updateResults);
+}
+
+const requestGolferResults = debounce(fetchGolferResults);
+
+// eslint-disable-next-line no-unused-vars
+async function fetchGolferResults(search: string, updateResults: (results: SearchNavResult[]) => void) {
+    const resp  = await fetch(`/api/suggestions/golfers?${new URLSearchParams({q: search})}`);
+    const golfers = await resp.json() as string[];
+    updateResults(processResults(golfers.map(x => ({
+        path: `/golfers/${x}`,
+        description: x,
+        priority: defaultMatchPriority(x, search),
+    }))));
+}
+
+function debounce(func: Function, timeout = 500) {
+    let timer: number | undefined;
+    return (...args: any[]) => {
+        clearTimeout(timer);
+        timer = setTimeout(() => func(...args), timeout);
+    };
+}
\ No newline at end of file
diff --git a/js/_util.ts b/js/_util.ts
index 024c6cbd..ffd4c5d4 100644
--- a/js/_util.ts
+++ b/js/_util.ts
@@ -45,12 +45,12 @@ export function $$<MatchType extends HTMLElement>(selector: string) {
 export const comma = (i: number | undefined) => i?.toLocaleString('en');
 
 /**
- * Debounce in the following sense:
+ * Throttle in the following sense:
  *  - never fire more than once in any consecutive `interval` ms
  *  - always fire at least once within the first `interval` ms after the call
  *  - always fire as soon as possible, subject to these restrictions
  */
-export function debounce(fn: () => void, interval: number) {
+export function throttle(fn: () => void, interval: number) {
     let pendingTimeout: number | undefined;
     let needsAnother = false;
     return () => {
@@ -67,3 +67,14 @@ export function debounce(fn: () => void, interval: number) {
         }
     };
 }
+
+/**
+ * Fire once when `timeout` ms has passed since the last input.
+ */
+export function debounce(func: () => void, timeout = 500) {
+    let timer: number | undefined;
+    return () => {
+        clearTimeout(timer);
+        timer = setTimeout(func, timeout);
+    };
+}
diff --git a/js/base.tsx b/js/base.tsx
index 78488b92..3258269c 100644
--- a/js/base.tsx
+++ b/js/base.tsx
@@ -1,3 +1,4 @@
+import { requestResults, SearchNavResult } from './_search-nav';
 import { $, $$ } from './_util';
 
 const { timeZone } = Intl.DateTimeFormat().resolvedOptions();
@@ -52,3 +53,60 @@ for (const btn of $$<HTMLElement>('[data-dialog]'))
         dialog.querySelector('form')?.reset();
         dialog.showModal();
     };
+
+// Search navigation dialog
+document.addEventListener('keydown', e => {
+    if (e.ctrlKey && e.key === 'p') {
+        const dialog = $<HTMLDialogElement>('#search-nav-dialog');
+        if (!dialog) return;
+
+        if (dialog.open) {
+            dialog.close();
+        }
+        else {
+            dialog.showModal();
+            e.preventDefault();
+        }
+    }
+});
+
+window.addEventListener('hashchange', () => {
+    const dialog = $<HTMLDialogElement>('#search-nav-dialog');
+    dialog?.close();
+});
+
+$('#search-nav-input').onkeyup = () => requestResults($<HTMLInputElement>('#search-nav-input').value, updateResults);
+
+$('#search-nav-form').onsubmit = e => {
+    $<HTMLAnchorElement>('li.current-result a')?.click();
+    e.preventDefault();
+};
+
+let currentIndex = 0;
+let currentResults: SearchNavResult[] = [];
+
+$<HTMLDialogElement>('#search-nav-dialog').onkeydown = (e: KeyboardEvent) => {
+    if (e.key === 'ArrowDown') {
+        currentIndex++;
+        renderResults();
+        e.preventDefault();
+    }
+    else if (e.key === 'ArrowUp') {
+        currentIndex--;
+        renderResults();
+        e.preventDefault();
+    }
+};
+
+function updateResults(results: SearchNavResult[]) {
+    currentResults = results;
+    currentIndex = 0;
+    renderResults();
+}
+
+const mod = (x: number, modulus: number) => (x % modulus + modulus) % modulus;
+
+function renderResults() {
+    const resultNodes = currentResults.map((r,i) => (<li class={i === mod(currentIndex, currentResults.length) ? 'current-result' : ''}><a href={r.path}><span class='result-description'>{r.description}</span> <span class='result-path'>{r.path}</span></a></li>));
+    $('#search-nav-results').replaceChildren(...resultNodes);
+}
\ No newline at end of file
diff --git a/js/golfer/code-search.tsx b/js/golfer/code-search.tsx
index f27b43f9..6bb4eb99 100644
--- a/js/golfer/code-search.tsx
+++ b/js/golfer/code-search.tsx
@@ -1,29 +1,21 @@
-import { $ } from '../_util';
+import { $, debounce } from '../_util';
 
 interface Match {
     before: string, match: string, after: string, count: number, hole: string, lang: string, scoring: string | null
 }
-const langs: Record<string, string> = JSON.parse($('#langs').innerText);
-const holes: Record<string, string> = JSON.parse($('#holes').innerText);
+const langs: Record<string, string[]> = JSON.parse($('#langs').innerText);
+const holes: Record<string, string[]> = JSON.parse($('#holes').innerText);
 
 $('#searchInput').onkeyup = onSearch;
 $('#isRegexInput').onchange = onSearch;
 $('#languageInput').onchange = onSearch;
 
-$('#languageInput').replaceChildren(<option value=''>All languages</option>, ...Object.entries(langs).map(([id,name]) => <option value={id}>{name}</option>));
+$('#languageInput').replaceChildren(<option value=''>All languages</option>, ...Object.entries(langs).map(([id,names]) => <option value={id}>{names[0]}</option>));
 
 const amount = (n: number, singular: string, plural?: string) => `${n} ${n === 1 ? singular : plural ?? singular + 's'}`;
 
 let searchParams = '';
 
-function debounce(func: () => void, timeout = 500) {
-    let timer: number | undefined;
-    return () => {
-        clearTimeout(timer);
-        timer = setTimeout(func, timeout);
-    };
-}
-
 async function onSearch() {
     let pattern = $<HTMLInputElement>('#searchInput').value;
     if (!pattern) {
@@ -57,20 +49,32 @@ async function onSearch() {
 
 const fetchSolutionsDebounced = debounce(fetchSolutions, 500);
 
+async function fetchSupporting408InFirefox(path: string) {
+    try {
+        return await fetch(path);
+    }
+    catch (e) {
+        if (`${e}`.includes('NetworkError')) return {status: 408} as const;
+        throw e;
+    }
+}
+
 async function fetchSolutions() {
-    const resp = await fetch(`/api/solutions-search?${searchParams}`);
+    const resp = await fetchSupporting408InFirefox(`/api/solutions-search?${searchParams}`);
     if (resp.status !== 200) {
-        $<HTMLInputElement>('#searchInput').setCustomValidity('Bad request');
+        $<HTMLInputElement>('#searchInput').setCustomValidity(resp.status === 408 ? 'Timeout. Try single language or raw text search.' : `Error ${resp.status}`);
         $<HTMLInputElement>('#searchInput').reportValidity();
         return;
     }
     const results = await resp.json() as Match[];
     const totalCount = results.map(x => x.count).reduce((a,b)=>a+b, 0);
+    const holesCount = [...new Set(results.map(x => x.hole))].length;
+    const resultsLangs = [...new Set(results.map(x => x.lang))].map(x => langs[x]);
     $('#resultsOverview').innerText = results.length === 0
         ? '0 matches'
-        : `${amount(totalCount, 'match', 'matches')} across ${amount(results.length, 'solution')}`;
+        : `${amount(totalCount, 'match', 'matches')} across ${amount(results.length, 'solution')} (${amount(holesCount, 'hole')} in ${resultsLangs.length > 5 ? `${resultsLangs.length} languages` : resultsLangs.join(', ')})`;
     const resultNodes = results.map(r => (<a href={'/' + r.hole + '#' + r.lang}>
-        <h2>{holes[r.hole]} in {langs[r.lang]}{r.scoring ? ` (${r.scoring})` : ''}</h2>
+        <h2>{holes[r.hole]?.[0]} in {langs[r.lang]?.[0]}{r.scoring ? ` (${r.scoring})` : ''}</h2>
         <span>
             <code>{r.before.split(/\n|\r/).at(-1)}<span class='match'>{r.match}</span>{r.after.split(/\n|\r/)[0]}</code>
             {r.count > 1 ? ' and ' + amount(r.count-1, 'more match', 'more matches') : ''}
diff --git a/js/hole-tabs.tsx b/js/hole-tabs.tsx
index 45fc677f..38a8edcd 100644
--- a/js/hole-tabs.tsx
+++ b/js/hole-tabs.tsx
@@ -6,7 +6,7 @@ import {
 } from 'golden-layout';
 import { EditorView }   from './_codemirror';
 import diffView         from './_diff';
-import { $, $$, comma, debounce } from './_util';
+import { $, $$, comma, throttle } from './_util';
 import {
     init, langs, hole, setSolution,
     setCode, refreshScores, getHideDeleteBtn, submit, ReadonlyPanelsData,
@@ -90,7 +90,7 @@ function updateReadonlyPanel(name: string) {
         break;
     case 'diff':
         const ignoreCase = JSON.parse($('#case-fold').innerText);
-        const diff = diffView(hole, subRes.Exp, subRes.Out, subRes.Argv, ignoreCase, subRes.MultisetDelimiter, subRes.ItemDelimiter);
+        const diff = diffView(hole, subRes.Exp, subRes.Out, subRes.Argv, ignoreCase, subRes.OutputDelimiter, subRes.MultisetItemDelimiter);
         output.replaceChildren(diff);
     }
 }
@@ -462,7 +462,7 @@ function getViewState(): ViewState {
     };
 }
 
-const saveLayout = debounce(() => {
+const saveLayout = throttle(() => {
     const state = getViewState();
     if (!state.config.root) return;
     localStorage.setItem('lastViewState', JSON.stringify(state));
diff --git a/js/hole.tsx b/js/hole.tsx
index f40e22d4..fb19e7a4 100644
--- a/js/hole.tsx
+++ b/js/hole.tsx
@@ -88,7 +88,7 @@ function updateReadonlyPanels(data: ReadonlyPanelsData) {
     $('#out div').replaceChildren(UnprintableElement.escape(data.Out));
 
     const ignoreCase = JSON.parse($('#case-fold').innerText);
-    const diff = diffView(hole, data.Exp, data.Out, data.Argv, ignoreCase, data.MultisetDelimiter, data.ItemDelimiter);
+    const diff = diffView(hole, data.Exp, data.Out, data.Argv, ignoreCase, data.OutputDelimiter, data.MultisetItemDelimiter);
     $('#diff-content').replaceChildren(diff);
     $('#diff').classList.toggle('hide', !diff);
 }
diff --git a/js/typings/_intrinsicJSX.ts b/js/typings/_intrinsicJSX.ts
index 147ae6b3..69c18d42 100644
--- a/js/typings/_intrinsicJSX.ts
+++ b/js/typings/_intrinsicJSX.ts
@@ -30,6 +30,7 @@ declare global {
       img:      any;
       input:    any;
       label:    any;
+      li:       any;
       optgroup: any;
       option:   any;
       p:        any;
diff --git a/langs/algol-68/Dockerfile b/langs/algol-68/Dockerfile
index 8638b0ac..a9fb64ce 100644
--- a/langs/algol-68/Dockerfile
+++ b/langs/algol-68/Dockerfile
@@ -4,7 +4,7 @@ RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
     apt-get install --yes curl gcc make
 
-ENV VER=3.5.14
+ENV VER=3.5.15
 
 RUN curl -#L https://jmvdveer.home.xs4all.nl/algol68g-$VER.tar.gz \
   | tar xz --strip-components 1
diff --git a/langs/arturo/Dockerfile b/langs/arturo/Dockerfile
index ed2bb8d8..d1d0a66a 100644
--- a/langs/arturo/Dockerfile
+++ b/langs/arturo/Dockerfile
@@ -2,20 +2,12 @@ FROM nimlang/nim:2.2.2-alpine AS builder
 
 RUN apk add --no-cache pcre
 
-ENV VER=71a4677
+ENV VER=5f3443c
 
 RUN curl -#L https://github.com/arturo-lang/arturo/tarball/$VER \
   | tar xz --strip-components 1
 
-# Drop an "l" to make Nim int -> C long int. Both are 64 bit, not sure why
-# this is needed on some systems and not others, seems to affect alpine more.
-# See https://github.com/arturo-lang/arturo/issues/1896
-# And https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html
-# $ echo 'echo sizeof(int)' | docker run --rm -i nimlang/nim:2.2.2-alpine nim c -r -
-# 8
-RUN sed -i -E 's/(__builtin_s)(add|sub|mul)l(l_overflow)/\1\2\3/g' \
-    src/helpers/intrinsics.nim                                     \
- && ./build.nims --install --mode mini --release
+RUN ./build.nims --install --mode mini --release
 
 FROM codegolf/lang-base
 
diff --git a/langs/assembly/Dockerfile b/langs/assembly/Dockerfile
index 51e9a12b..8405ec62 100644
--- a/langs/assembly/Dockerfile
+++ b/langs/assembly/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:23.10.0-alpine3.21 AS builder
+FROM node:24.4.1-alpine3.22 AS builder
 
 RUN apk add --no-cache binutils
 
diff --git a/langs/awk/Dockerfile b/langs/awk/Dockerfile
index 4445fad4..79c57d88 100644
--- a/langs/awk/Dockerfile
+++ b/langs/awk/Dockerfile
@@ -1,22 +1,24 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl
 
-RUN curl https://ftp.gnu.org/gnu/gawk/gawk-5.3.1.tar.xz \
+ENV VER=5.3.2
+
+RUN curl -#L https://ftp.gnu.org/gnu/gawk/gawk-$VER.tar.xz \
   | tar xJ --strip-components 1
 
 RUN ./configure    \
     --disable-lint \
-    --prefix /usr  \
+    --prefix=/usr  \
  && make install   \
  && strip /usr/bin/gawk
 
 FROM codegolf/lang-base
 
 COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /usr/bin/gawk            /usr/bin/
+COPY --from=0 /usr/bin/gawk            /usr/bin/awk
 COPY --from=0 /usr/lib/gawk            /usr/lib/gawk
 
-ENTRYPOINT ["gawk"]
+ENTRYPOINT ["awk"]
 
 CMD ["--version"]
diff --git a/langs/bash/Dockerfile b/langs/bash/Dockerfile
index cbc40e4f..551fec59 100644
--- a/langs/bash/Dockerfile
+++ b/langs/bash/Dockerfile
@@ -1,32 +1,32 @@
-FROM alpine:3.20 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl
+RUN apk add --no-cache curl gcc make musl-dev patch
 
-WORKDIR /lang
+ENV URL=https://ftpmirror.gnu.org/bash VER=5.3
 
-RUN curl https://ftp.gnu.org/gnu/bash/bash-5.2.tar.gz \
-  | tar xz --strip-components 1
+RUN curl -#L $URL/bash-$VER.tar.gz | tar xz --strip-components 1
 
-RUN curl https://ftp.gnu.org/gnu/bash/bash-5.2-patches/bash52-0[01-37] \
-  | patch -p0
+RUN curl -#L $URL/bash-$VER-patches/bash53-00[1-3] | patch -p0
 
-RUN ./configure                \
-    --disable-command-timing   \
-    --disable-debugger         \
-    --disable-directory-stack  \
-    --disable-help-builtin     \
-    --disable-job-control      \
-    --disable-net-redirections \
-    --disable-progcomp         \
-    --disable-select           \
-    --enable-static-link       \
-    --without-bash-malloc      \
- && make                       \
- && strip bash
+RUN ./configure                      \
+    --disable-command-timing         \
+    --disable-debugger               \
+    --disable-directory-stack        \
+    --disable-help-builtin           \
+    --disable-job-control            \
+    --disable-net-redirections       \
+    --disable-progcomp               \
+    --disable-prompt-string-decoding \
+    --disable-select                 \
+    --enable-static-link             \
+    --prefix=/usr                    \
+    --without-bash-malloc            \
+ && make install                     \
+ && strip /usr/bin/bash
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lang/bash /usr/bin/
+COPY --from=0 /usr/bin/bash /usr/bin/
 
 ENTRYPOINT ["bash"]
 
diff --git a/langs/befunge/Dockerfile b/langs/befunge/Dockerfile
index b937d064..cdde5174 100644
--- a/langs/befunge/Dockerfile
+++ b/langs/befunge/Dockerfile
@@ -4,7 +4,7 @@ RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
     apt-get install --yes cmake curl
 
-ENV VER=b7bf640
+ENV LDFLAGS='-static' VER=b7bf640
 
 RUN curl -#L https://github.com/VorpalBlade/cfunge/tarball/$VER \
   | tar xz --strip-components 1
@@ -16,18 +16,11 @@ RUN cmake .. && make install \
 
 COPY befunge.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/befunge -s /befunge.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/befunge -s -static /befunge.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6       \
-              /lib/x86_64-linux-gnu/libdl.so.2      \
-              /lib/x86_64-linux-gnu/libm.so.6       \
-              /lib/x86_64-linux-gnu/libncurses.so.6 \
-              /lib/x86_64-linux-gnu/libtinfo.so.6   /lib/
-COPY --from=0 /lib64/ld-linux-x86-64.so.2           /lib64/
-COPY --from=0 /usr/bin/befunge                      \
-              /usr/local/bin/cfunge                 /usr/bin/
+COPY --from=0 /usr/bin/befunge /usr/local/bin/cfunge /usr/bin/
 
 ENTRYPOINT ["befunge"]
 
diff --git a/langs/berry/Dockerfile b/langs/berry/Dockerfile
index b714c286..4ed29113 100644
--- a/langs/berry/Dockerfile
+++ b/langs/berry/Dockerfile
@@ -1,18 +1,20 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache curl gcc make musl-dev python3 readline-dev
+RUN apk add --no-cache build-base curl python3 readline-dev
 
-RUN curl -L https://github.com/berry-lang/berry/archive/97e0fca.tar.gz \
-  | tar xz && mv berry-* berry
+ENV VER=97e0fca
 
-COPY berry.c berry/default/
+RUN curl -#L https://github.com/berry-lang/berry/archive/$VER.tar.gz \
+  | tar xz --strip-components 1
 
-RUN make -C berry
+COPY berry.c /default/
+
+RUN make && make install
 
 FROM codegolf/lang-base
 
 COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /berry/berry             /usr/bin/
+COPY --from=0 /usr/local/bin/berry     /usr/bin/
 
 ENTRYPOINT ["berry"]
 
diff --git a/langs/bqn/Dockerfile b/langs/bqn/Dockerfile
index 39719499..6a115056 100644
--- a/langs/bqn/Dockerfile
+++ b/langs/bqn/Dockerfile
@@ -1,13 +1,14 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache bash build-base clang git
 
-ENV VER=0.7.0
+ENV VER=0.9.0
 
-RUN git clone https://github.com/dzaima/CBQN.git --branch v$VER
+RUN git clone --branch v$VER https://github.com/dzaima/CBQN.git
 
-RUN cd CBQN       \
- && make          \
+WORKDIR /CBQN
+
+RUN make          \
     REPLXX=0      \
     singeli=0     \
     static-bin    \
diff --git a/langs/brainfuck/Dockerfile b/langs/brainfuck/Dockerfile
index ce5be45b..8cc3e8f4 100644
--- a/langs/brainfuck/Dockerfile
+++ b/langs/brainfuck/Dockerfile
@@ -1,16 +1,16 @@
-FROM alpine:3.21 AS builder
-
-ENV VERSION=077cc7f
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache curl gcc musl-dev
 
-RUN curl -L https://github.com/primo-ppcg/bfci/tarball/$VERSION | tar xz \
- && mv primo-ppcg-bfci-* bfci
+ENV VER=077cc7f
+
+RUN curl -#L https://github.com/primo-ppcg/bfci/tarball/$VER \
+  | tar xz --strip-components 1
 
-RUN cd bfci && cc --std=c99 -static -O3 -s -o bfci bfci.c src/*.c
+RUN cc --std=c99 -O3 -o /usr/bin/brainfuck -s -static bfci.c src/*.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /bfci/bfci /usr/bin/brainfuck
+COPY --from=0 /usr/bin/brainfuck /usr/bin/
 
 ENTRYPOINT ["brainfuck"]
diff --git a/langs/c-sharp/Dockerfile b/langs/c-sharp/Dockerfile
index e6b11380..27dcb8c9 100644
--- a/langs/c-sharp/Dockerfile
+++ b/langs/c-sharp/Dockerfile
@@ -1,6 +1,6 @@
-FROM mcr.microsoft.com/dotnet/sdk:9.0.200-alpine3.21 AS builder
+FROM mcr.microsoft.com/dotnet/sdk:9.0.301-alpine3.22-amd64 AS builder
 
-WORKDIR /work
+WORKDIR /c-sharp
 
 COPY c-sharp.cs c-sharp.csproj ./
 
diff --git a/langs/c-sharp/c-sharp.csproj b/langs/c-sharp/c-sharp.csproj
index ae305a80..27f72924 100644
--- a/langs/c-sharp/c-sharp.csproj
+++ b/langs/c-sharp/c-sharp.csproj
@@ -1,6 +1,6 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <ItemGroup>
-        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0"/>
+        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0"/>
     </ItemGroup>
     <PropertyGroup>
         <ImplicitUsings>true</ImplicitUsings>
diff --git a/langs/c/Dockerfile b/langs/c/Dockerfile
index 436172e6..7631a10a 100644
--- a/langs/c/Dockerfile
+++ b/langs/c/Dockerfile
@@ -1,26 +1,27 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl
 
-RUN curl -L https://download.savannah.nongnu.org/releases/tinycc/tcc-0.9.27.tar.bz2 \
-  | tar xj
+ENV VER=0.9.27
+
+RUN curl -#L https://download.savannah.nongnu.org/releases/tinycc/tcc-$VER.tar.bz2 \
+  | tar xj --strip-components 1
 
 COPY setvbuf.patch /
 
-RUN mv tcc-* tcc             \
- && patch -p0 <setvbuf.patch \
- && cd tcc                   \
- && ./configure              \
-    --config-musl            \
-    --prefix=/usr            \
-    --strip-binaries         \
- && make                     \
+RUN patch -p0 < setvbuf.patch
+
+RUN ./configure      \
+    --config-musl    \
+    --prefix=/usr    \
+    --strip-binaries \
+ && make             \
  && make install
 
 FROM codegolf/lang-base
 
 COPY --from=0 /lib/ld-musl-x86_64.so.1   /lib/
-COPY --from=0 /usr/bin/tcc               /usr/bin/
+COPY --from=0 /usr/bin/tcc               /usr/bin/c
 COPY --from=0 /usr/include/alloca.h      \
               /usr/include/assert.h      \
               /usr/include/complex.h     \
@@ -52,10 +53,10 @@ COPY --from=0 /usr/include/alloca.h      \
               /usr/include/uchar.h       \
               /usr/include/wchar.h       \
               /usr/include/wctype.h      /usr/include/
-COPY --from=0 /usr/include/bits          /usr/include/bits/
+COPY --from=0 /usr/include/bits          /usr/include/bits
 COPY --from=0 /usr/lib/libc.so           /usr/lib/
 COPY --from=0 /usr/lib/tcc/libtcc1.a     /usr/lib/tcc/
 
-ENTRYPOINT ["tcc"]
+ENTRYPOINT ["c"]
 
 CMD ["-v"]
diff --git a/langs/c/setvbuf.patch b/langs/c/setvbuf.patch
index 7b7410c1..f771acdf 100644
--- a/langs/c/setvbuf.patch
+++ b/langs/c/setvbuf.patch
@@ -1,5 +1,5 @@
---- /tcc/tcc.c
-+++ /tcc/tcc.c
+--- tcc.c
++++ tcc.c
 @@ -252,6 +252,9 @@
      int argc; char **argv;
      FILE *ppfp = stdout;
diff --git a/langs/civet/Dockerfile b/langs/civet/Dockerfile
index eb45f292..28ad91bb 100644
--- a/langs/civet/Dockerfile
+++ b/langs/civet/Dockerfile
@@ -1,8 +1,8 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache bash curl icu-data npm
 
-ENV VER=0.10.1
+ENV VER=0.10.5
 
 RUN curl -#L https://github.com/DanielXMoore/Civet/archive/refs/tags/v$VER.tar.gz \
   | tar xz --strip-components 1
@@ -21,16 +21,17 @@ COPY --from=0 /usr/lib/libada.so.2                                       \
               /usr/lib/libcares.so.2                                     \
               /usr/lib/libcrypto.so.3                                    \
               /usr/lib/libgcc_s.so.1                                     \
-              /usr/lib/libicudata.so.74                                  \
-              /usr/lib/libicui18n.so.74                                  \
-              /usr/lib/libicuuc.so.74                                    \
+              /usr/lib/libicudata.so.76                                  \
+              /usr/lib/libicui18n.so.76                                  \
+              /usr/lib/libicuuc.so.76                                    \
               /usr/lib/libnghttp2.so.14                                  \
-              /usr/lib/libsimdjson.so.23                                 \
-              /usr/lib/libsimdutf.so.11                                  \
+              /usr/lib/libsimdjson.so.25                                 \
+              /usr/lib/libsimdutf.so.24                                  \
               /usr/lib/libsqlite3.so.0                                   \
               /usr/lib/libssl.so.3                                       \
               /usr/lib/libstdc++.so.6                                    \
-              /usr/lib/libz.so.1                                         /usr/lib/
+              /usr/lib/libz.so.1                                         \
+              /usr/lib/libzstd.so.1                                      /usr/lib/
 COPY --from=0 /package.json /register.js                                 /usr/local/
 COPY --from=0 /dist/civet                                                \
               /dist/config.js                                            \
@@ -42,6 +43,12 @@ COPY --from=0 /node_modules/@typescript/vfs/dist/index.js                \
               /node_modules/@typescript/vfs/dist/vfs.cjs.development.js  /usr/local/node_modules/@typescript/vfs/dist/
 COPY --from=0 /node_modules/acorn/package.json                           /usr/local/node_modules/acorn/
 COPY --from=0 /node_modules/acorn/dist/acorn.js                          /usr/local/node_modules/acorn/dist/
+COPY --from=0 /node_modules/picomatch/index.js                           /usr/local/node_modules/picomatch/
+COPY --from=0 /node_modules/picomatch/lib/constants.js                   \
+              /node_modules/picomatch/lib/parse.js                       \
+              /node_modules/picomatch/lib/picomatch.js                   \
+              /node_modules/picomatch/lib/scan.js                        \
+              /node_modules/picomatch/lib/utils.js                       /usr/local/node_modules/picomatch/lib/
 COPY --from=0 /node_modules/unplugin/package.json                        /usr/local/node_modules/unplugin/
 COPY --from=0 /node_modules/unplugin/dist/context-*.cjs                  \
               /node_modules/unplugin/dist/index.js                       \
@@ -51,7 +58,7 @@ COPY --from=0 /node_modules/unplugin/dist/context-*.cjs                  \
 COPY --from=0 /node_modules/webpack-virtual-modules/package.json         /usr/local/node_modules/webpack-virtual-modules/
 COPY --from=0 /node_modules/webpack-virtual-modules/lib/index.js         \
               /node_modules/webpack-virtual-modules/lib/virtual-stats.js /usr/local/node_modules/webpack-virtual-modules/lib/
-COPY --from=0 /usr/share/icu/74.2/icudt74l.dat                           /usr/share/icu/
+COPY --from=0 /usr/share/icu/76.1/icudt76l.dat                           /usr/share/icu/
 
 ENTRYPOINT ["/usr/local/dist/civet"]
 
diff --git a/langs/cjam/Dockerfile b/langs/cjam/Dockerfile
index 480d949d..d9d05b66 100644
--- a/langs/cjam/Dockerfile
+++ b/langs/cjam/Dockerfile
@@ -1,35 +1,19 @@
-FROM eclipse-temurin:24_36-jre-alpine-3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache gcc musl-dev
+RUN apk add --no-cache coreutils
 
 ENV VER=0.6.5
 
-RUN wget -O cjam.jar https://sourceforge.net/projects/cjam/files/cjam-$VER/cjam-$VER.jar/download
+RUN wget -qO cjam.jar https://sourceforge.net/projects/cjam/files/cjam-$VER/cjam-$VER.jar/download
 
-COPY cjam.c /
+FROM codegolf/lang-java
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/cjam -s -static cjam.c
-
-FROM codegolf/lang-base
-
-COPY --from=0 /cjam.jar                              /
-COPY --from=0 /lib/ld-musl-x86_64.so.1               /lib/
-COPY --from=0 /opt/java/openjdk/bin/java             /opt/java/bin/
-COPY --from=0 /opt/java/openjdk/lib/jvm.cfg          \
-              /opt/java/openjdk/lib/libjava.so       \
-              /opt/java/openjdk/lib/libjimage.so     \
-              /opt/java/openjdk/lib/libjli.so        \
-              /opt/java/openjdk/lib/libnet.so        \
-              /opt/java/openjdk/lib/libnio.so        \
-              /opt/java/openjdk/lib/libzip.so        \
-              /opt/java/openjdk/lib/modules          /opt/java/lib/
-COPY --from=0 /opt/java/openjdk/lib/server/libjvm.so /opt/java/lib/server/
-COPY --from=0 /usr/bin/cjam                          /usr/bin/
-COPY --from=0 /usr/lib/libacl.so.1                   \
-              /usr/lib/libattr.so.1                  \
-              /usr/lib/libcrypto.so.3                \
-              /usr/lib/libskarnet.so.2.14            \
-              /usr/lib/libutmps.so.0.1               /usr/lib/
+COPY --from=0 /cjam.jar                   /
+COPY --from=0 /usr/lib/libacl.so.1        \
+              /usr/lib/libattr.so.1       \
+              /usr/lib/libcrypto.so.3     \
+              /usr/lib/libskarnet.so.2.14 \
+              /usr/lib/libutmps.so.0.1    /usr/lib/
 
 ENTRYPOINT ["cjam"]
 
diff --git a/langs/clojure/Dockerfile b/langs/clojure/Dockerfile
index 7dc94a20..416d9c4d 100644
--- a/langs/clojure/Dockerfile
+++ b/langs/clojure/Dockerfile
@@ -1,12 +1,13 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-ENV VER=1.12.197
+ENV VER=1.12.206
 
-RUN wget -O- https://github.com/babashka/babashka/releases/download/v$VER/babashka-$VER-linux-amd64-static.tar.gz | tar xz
+RUN wget -O- https://github.com/babashka/babashka/releases/download/v$VER/babashka-$VER-linux-amd64-static.tar.gz \
+  | tar xz --directory /usr/bin
 
 FROM codegolf/lang-base
 
-COPY --from=0 /bb /usr/bin/clojure
+COPY --from=0 /usr/bin/bb /usr/bin/clojure
 
 ENTRYPOINT ["clojure"]
 
diff --git a/langs/cobol/Dockerfile b/langs/cobol/Dockerfile
index 38b47d81..248b5e91 100644
--- a/langs/cobol/Dockerfile
+++ b/langs/cobol/Dockerfile
@@ -1,33 +1,38 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl gmp-dev
+RUN apk add --no-cache curl gcc make mpir-dev musl-dev patch
 
-RUN curl https://ftp.gnu.org/gnu/gnucobol/gnucobol-3.2.tar.xz | tar xJ
+ENV VER=3.2
 
-COPY patch /
-RUN cd gnucobol-3.2                        \
- && patch -p0 < /patch                     \
- && ./configure --prefix=/usr --without-db \
- && make -j`nproc` install                 \
- && strip /usr/bin/cobc /usr/lib/libcob.so.4.2.0
+RUN curl -#L https://ftp.gnu.org/gnu/gnucobol/gnucobol-$VER.tar.xz \
+  | tar xJ --strip-components 1
 
-COPY cobol.c /
+COPY codegen.patch /
+
+RUN patch -p0 < codegen.patch
+
+RUN ./configure               \
+    --without-db              \
+ && make -j`nproc` install    \
+ && strip /usr/local/bin/cobc \
+          /usr/local/lib/libcob.so
 
-RUN gcc -s -o cobol cobol.c
+RUN find /usr/local/lib -not -name 'libcob.so*' -delete
+
+COPY cobol.c /
 
-# HACK Put in a dir so COPY will preserve the symlinks.
-RUN mkdir /usr/lib/libcob \
- && mv /usr/lib/libcob.so* /usr/lib/libcob
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/cobol -s cobol.c
 
 FROM codegolf/lang-c
 
-COPY --from=0 /cobol /usr/bin/cobc  /usr/bin/
-COPY --from=0 /usr/lib/libcob       /usr/lib
-COPY --from=0 /usr/lib/libgmp.so.10 /usr/lib/
-COPY --from=0 /usr/include/gmp.h    \
-              /usr/include/libcob.h /usr/include/
-COPY --from=0 /usr/include/libcob   /usr/include/libcob
-COPY --from=0 /usr/share/gnucobol   /usr/share/gnucobol
+COPY --from=0 /usr/bin/cobol                                /usr/bin/
+COPY --from=0 /usr/include/mpir.h                           /usr/include/
+COPY --from=0 /usr/lib/libmpir.so.23                        /usr/lib/
+COPY --from=0 /usr/local/bin/cobc                           /usr/local/bin/
+COPY --from=0 /usr/local/include/libcob.h                   /usr/local/include/
+COPY --from=0 /usr/local/include/libcob                     /usr/local/include/libcob
+COPY --from=0 /usr/local/lib                                /usr/local/lib
+COPY --from=0 /usr/local/share/gnucobol/config/default.conf /usr/local/share/gnucobol/config/
 
 ENTRYPOINT ["cobol"]
 
diff --git a/langs/cobol/cobol.c b/langs/cobol/cobol.c
index 91a6e54e..b8d15d18 100644
--- a/langs/cobol/cobol.c
+++ b/langs/cobol/cobol.c
@@ -4,39 +4,45 @@
 #include <sys/wait.h>
 #include <unistd.h>
 
-int main (int argc, char *argv[]) {
-    if (argc > 1 && strcmp(argv[1], "--version") == 0) {
-        execv("/usr/bin/cobc", argv);
-        perror("execv");
-        return 0;
+#define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
+
+const char* cobol = "/usr/local/bin/cobc", *C = "/usr/bin/c", *code = "code.c";
+
+int main(int argc, char* argv[]) {
+    if (!strcmp(argv[1], "--version")) {
+        execv(cobol, argv);
+        ERR_AND_EXIT("execv");
     }
 
-    pid_t pid = fork();
-    if (!pid) {
-        execl("/usr/bin/cobc", "/usr/bin/cobc", "-CFxo", "/tmp/code.c", "-", NULL);
-        perror("execl");
-        return 1;
+    if (chdir("/tmp"))
+        ERR_AND_EXIT("chdir");
+
+    pid_t pid;
+
+    if (!(pid = fork())) {
+        execl(cobol, cobol, "-CFxo", code, "-", NULL);
+        ERR_AND_EXIT("execl");
     }
 
     int status;
+
     waitpid(pid, &status, 0); 
 
     if (!WIFEXITED(status))
-        return 2;
+        exit(EXIT_FAILURE);
 
     if (WEXITSTATUS(status))
         return WEXITSTATUS(status);
 
-    int ntcc = argc + 3;
-    char **tcc = malloc(ntcc * sizeof(char*));
-    tcc[0] = "/usr/bin/tcc";
-    tcc[1] = "-lcob";
-    tcc[2] = "-run";
-    tcc[3] = "/tmp/code.c";
-    memcpy(&tcc[4], &argv[2], (argc - 2) * sizeof(char*));
-    tcc[ntcc - 1] = NULL;
-
-    execv("/usr/bin/tcc", tcc);
-    perror("execv");
-    return 6;
+    int cargc = argc + 3;
+    char** cargv = malloc(cargc * sizeof(char*));
+    cargv[0] = (char*) C;
+    cargv[1] = "-lcob";
+    cargv[2] = "-run";
+    cargv[3] = (char*) code;
+    memcpy(&cargv[4], &argv[2], (argc - 2) * sizeof(char*));
+    cargv[cargc - 1] = NULL;
+
+    execv(C, cargv);
+    ERR_AND_EXIT("execv");
 }
diff --git a/langs/cobol/patch b/langs/cobol/codegen.patch
similarity index 100%
rename from langs/cobol/patch
rename to langs/cobol/codegen.patch
diff --git a/langs/coconut/Dockerfile b/langs/coconut/Dockerfile
index cb1bfccf..22283303 100644
--- a/langs/coconut/Dockerfile
+++ b/langs/coconut/Dockerfile
@@ -1,17 +1,16 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl libffi-dev openssl-dev zlib-dev
+RUN apk add --no-cache curl gcc libffi-dev make musl-dev openssl-dev zlib-dev
 
-ENV PYVER=3.13.2 VER=3.1.2
+ENV PYVER=3.13.5 VER=3.1.2
 
-RUN curl -# https://www.python.org/ftp/python/$PYVER/Python-$PYVER.tar.xz | tar xJ
+RUN curl -#L https://www.python.org/ftp/python/$PYVER/Python-$PYVER.tar.xz \
+  | tar xJ --strip-components 1
 
-WORKDIR /Python-$PYVER
-
-RUN ./configure                \
+RUN CC='cc' ./configure        \
     --disable-test-modules     \
     --prefix=/usr              \
-    --with-lto                 \
+    --with-lto=thin            \
     --without-static-libpython \
  && make -j`nproc` install     \
  && strip /usr/bin/python3.13  \
diff --git a/langs/coffeescript/Dockerfile b/langs/coffeescript/Dockerfile
index 8c817230..0a6e87c6 100644
--- a/langs/coffeescript/Dockerfile
+++ b/langs/coffeescript/Dockerfile
@@ -1,6 +1,6 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache curl gcc icu-data musl-dev npm
+RUN apk add --no-cache curl icu-data npm
 
 ENV VER=2.7.0
 
@@ -11,16 +11,11 @@ RUN curl -#L https://github.com/jashkenas/coffeescript/tarball/$VER \
 
 RUN npm install --global coffeescript
 
-COPY coffeescript.c /
-
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/coffeescript -s -static /coffeescript.c
-
 FROM codegolf/lang-base
 
 COPY --from=0 /lib/ld-musl-x86_64.so.1              /lib/
 COPY --from=0 /usr/package.json                     /usr/
 COPY --from=0 /usr/bin/coffee                       \
-              /usr/bin/coffeescript                 \
               /usr/bin/env                          \
               /usr/bin/node                         /usr/bin/
 COPY --from=0 /usr/lib/libada.so.2                  \
@@ -30,16 +25,17 @@ COPY --from=0 /usr/lib/libada.so.2                  \
               /usr/lib/libcares.so.2                \
               /usr/lib/libcrypto.so.3               \
               /usr/lib/libgcc_s.so.1                \
-              /usr/lib/libicudata.so.74             \
-              /usr/lib/libicui18n.so.74             \
-              /usr/lib/libicuuc.so.74               \
+              /usr/lib/libicudata.so.76             \
+              /usr/lib/libicui18n.so.76             \
+              /usr/lib/libicuuc.so.76               \
               /usr/lib/libnghttp2.so.14             \
-              /usr/lib/libsimdjson.so.23            \
-              /usr/lib/libsimdutf.so.11             \
+              /usr/lib/libsimdjson.so.25            \
+              /usr/lib/libsimdutf.so.24             \
               /usr/lib/libsqlite3.so.0              \
               /usr/lib/libssl.so.3                  \
               /usr/lib/libstdc++.so.6               \
-              /usr/lib/libz.so.1                    /usr/lib/
+              /usr/lib/libz.so.1                    \
+              /usr/lib/libzstd.so.1                 /usr/lib/
 COPY --from=0 /usr/lib/coffeescript/coffeescript.js \
               /usr/lib/coffeescript/command.js      \
               /usr/lib/coffeescript/helpers.js      \
@@ -52,8 +48,8 @@ COPY --from=0 /usr/lib/coffeescript/coffeescript.js \
               /usr/lib/coffeescript/rewriter.js     \
               /usr/lib/coffeescript/scope.js        \
               /usr/lib/coffeescript/sourcemap.js    /usr/lib/coffeescript/
-COPY --from=0 /usr/share/icu/74.2/icudt74l.dat      /usr/share/icu/
+COPY --from=0 /usr/share/icu/76.1/icudt76l.dat      /usr/share/icu/
 
-ENTRYPOINT ["coffeescript"]
+ENTRYPOINT ["coffee"]
 
 CMD ["--version"]
diff --git a/langs/coffeescript/coffeescript.c b/langs/coffeescript/coffeescript.c
deleted file mode 100644
index 8a9f719a..00000000
--- a/langs/coffeescript/coffeescript.c
+++ /dev/null
@@ -1,43 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
-
-const char* coffeescript = "/usr/bin/coffee", *code = "code.coffee";
-
-int main(int argc, char* argv[]) {
-    if (!strcmp(argv[1], "--version")) {
-        execv(coffeescript, argv);
-        ERR_AND_EXIT("execv");
-    }
-
-    if (chdir("/tmp"))
-        ERR_AND_EXIT("chdir");
-
-    FILE* fp;
-
-    if (!(fp = fopen(code, "w")))
-        ERR_AND_EXIT("fopen");
-
-    char buffer[4096];
-    ssize_t nbytes;
-
-    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))))
-        if (fwrite(buffer, sizeof(char), nbytes, fp) != (size_t) nbytes)
-            ERR_AND_EXIT("fwrite");
-
-    if (fclose(fp))
-        ERR_AND_EXIT("fclose");
-
-    int cargc = argc + 1;
-    char** cargv = malloc(cargc * sizeof(char*));
-    cargv[0] = (char*) coffeescript;
-    cargv[1] = (char*) code;
-    memcpy(&cargv[2], &argv[2], (argc - 2) * sizeof(char*));
-    cargv[cargc - 1] = NULL;
-
-    execv(coffeescript, cargv);
-    ERR_AND_EXIT("execv");
-}
diff --git a/langs/common-lisp/Dockerfile b/langs/common-lisp/Dockerfile
index 0334e180..882a06a3 100644
--- a/langs/common-lisp/Dockerfile
+++ b/langs/common-lisp/Dockerfile
@@ -1,23 +1,30 @@
-FROM alpine:3.21 AS builder
+ARG VER=2.49.92
 
-ENV FORCE_UNSAFE_CONFIGURE=1
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl
+ARG VER
 
-RUN curl https://haible.de/bruno/gnu/clisp-2.49.92.tar.bz2 | tar xj
+RUN apk add --no-cache curl gcc make musl-dev
 
-RUN cd clisp-2.49.92/src                                      \
- && ../configure --ignore-absence-of-libsigsegv --prefix=/usr \
- && ./makemake --prefix=/usr > Makefile                       \
- && make                                                      \
- && make install
+RUN curl -#L https://haible.de/bruno/gnu/clisp-$VER.tar.bz2 \
+  | tar xj --strip-components 1
+
+WORKDIR /src
+
+RUN FORCE_UNSAFE_CONFIGURE=1       \
+    ../configure                   \
+    --ignore-absence-of-libsigsegv \
+    --prefix=/usr                  \
+ && make -j`nproc` install
 
 FROM codegolf/lang-base
 
+ARG VER
+
 COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /usr/bin/clisp           /usr/bin/lisp
-COPY --from=0 /usr/lib/clisp-2.49.92   /usr/lib/clisp-2.49.92
+COPY --from=0 /usr/bin/clisp           /usr/bin/common-lisp
+COPY --from=0 /usr/lib/clisp-$VER      /usr/lib/clisp-$VER
 
-ENTRYPOINT ["lisp"]
+ENTRYPOINT ["common-lisp"]
 
 CMD ["--version"]
diff --git a/langs/cpp/Dockerfile b/langs/cpp/Dockerfile
index 8251f268..831644dc 100644
--- a/langs/cpp/Dockerfile
+++ b/langs/cpp/Dockerfile
@@ -1,11 +1,11 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base cmake curl linux-headers ninja python3
+RUN apk add --no-cache cmake curl g++ linux-headers ninja python3
 
-ENV VER=20.1.1
+ENV VER=20.1.8
 
-RUN curl -#L https://github.com/llvm/llvm-project/releases/download/llvmorg-$VER/llvm-project-$VER.src.tar.xz \
-  | tar xJ --strip-components 1
+RUN curl -#L https://github.com/llvm/llvm-project/archive/refs/tags/llvmorg-$VER.tar.gz \
+  | tar xz --strip-components 1
 
 WORKDIR /llvm/build
 
@@ -24,7 +24,7 @@ RUN cmake -G Ninja                    \
 
 RUN ninja install        \
  && strip /usr/bin/clang \
-          /usr/lib/libLLVM-20.so
+          /usr/lib/libLLVM.so
 
 COPY cpp.c /
 
@@ -32,31 +32,33 @@ RUN gcc -Wall -Werror -Wextra -o /usr/bin/cpp -s -static /cpp.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1                /lib/
-COPY --from=0 /usr/bin/clang /usr/bin/cpp /usr/bin/ld /usr/bin/
-COPY --from=0 /usr/include/*.h                        /usr/include/
-COPY --from=0 /usr/include/bits/*.h                   /usr/include/bits/
-COPY --from=0 /usr/include/c++/14.2.0                 /usr/include/c++/14.2.0
-COPY --from=0 /usr/include/sys/*.h                    /usr/include/sys/
-COPY --from=0 /usr/lib/Scrt1.o                        \
-              /usr/lib/crti.o                         \
-              /usr/lib/crtn.o                         \
-              /usr/lib/libLLVM-20.so                  \
-              /usr/lib/libbfd-2.43.1.so               \
-              /usr/lib/libc.so                        \
-              /usr/lib/libctf.so.0                    \
-              /usr/lib/libgcc_s.so                    \
-              /usr/lib/libgcc_s.so.1                  \
-              /usr/lib/libjansson.so.4                \
-              /usr/lib/libsframe.so.1                 \
-              /usr/lib/libstdc++.so                   \
-              /usr/lib/libstdc++.so.6                 \
-              /usr/lib/libz.so.1                      \
-              /usr/lib/libzstd.so.1                   /usr/lib/
-COPY --from=0 /usr/lib/gcc/x86_64*/14.2.0/crtbegin.o  \
-              /usr/lib/gcc/x86_64*/14.2.0/crtbeginS.o \
-              /usr/lib/gcc/x86_64*/14.2.0/crtendS.o   \
-              /usr/lib/gcc/x86_64*/14.2.0/libgcc.a    /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/
+COPY --from=0 /lib/ld-musl-x86_64.so.1          /lib/
+COPY --from=0 /usr/bin/clang                    \
+              /usr/bin/cpp                      \
+              /usr/bin/ld                       /usr/bin/
+COPY --from=0 /usr/include/*.h                  /usr/include/
+COPY --from=0 /usr/include/bits/*.h             /usr/include/bits/
+COPY --from=0 /usr/include/c++/14.2.0           /usr/include/c++/14.2.0
+COPY --from=0 /usr/include/sys/*.h              /usr/include/sys/
+COPY --from=0 /usr/lib/Scrt1.o                  \
+              /usr/lib/crti.o                   \
+              /usr/lib/crtn.o                   \
+              /usr/lib/libLLVM.so               \
+              /usr/lib/libbfd-2.44.so           \
+              /usr/lib/libc.so                  \
+              /usr/lib/libctf.so.0              \
+              /usr/lib/libgcc_s.so              \
+              /usr/lib/libgcc_s.so.1            \
+              /usr/lib/libjansson.so.4          \
+              /usr/lib/libsframe.so.1           \
+              /usr/lib/libstdc++.so             \
+              /usr/lib/libstdc++.so.6           \
+              /usr/lib/libz.so.1                \
+              /usr/lib/libzstd.so.1             /usr/lib/
+COPY --from=0 /usr/lib/gcc/*/14.2.0/crtbegin.o  \
+              /usr/lib/gcc/*/14.2.0/crtbeginS.o \
+              /usr/lib/gcc/*/14.2.0/crtendS.o   \
+              /usr/lib/gcc/*/14.2.0/libgcc.a    /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/
 
 COPY unbuffered.cpp /
 
diff --git a/langs/cpp/cpp.c b/langs/cpp/cpp.c
index 84d0226e..0a066424 100644
--- a/langs/cpp/cpp.c
+++ b/langs/cpp/cpp.c
@@ -4,64 +4,64 @@
 #include <sys/wait.h>
 #include <unistd.h>
 
-const char* clang = "/usr/bin/clang";
-const char* code = "/tmp/code.cpp";
-const char* bin = "/tmp/code";
+#define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
+
+const char* cpp = "/usr/bin/clang", *code = "code.cpp";
 
 int main(int argc, char* argv[]) {
-    if (argc > 1 && strcmp(argv[1], "--version") == 0) {
-        execv(clang, argv);
-        perror("execv");
-        return 0;
+    if (!strcmp(argv[1], "--version")) {
+        execv(cpp, argv);
+        ERR_AND_EXIT("execv");
     }
 
-    FILE* fp = fopen(code, "w");
+    if (chdir("/tmp"))
+        ERR_AND_EXIT("chdir");
+
+    FILE* fp;
 
-    if (!fp)
-        return 1;
+    if (!(fp = fopen(code, "w")))
+        ERR_AND_EXIT("fopen");
 
     char buffer[4096];
     ssize_t nbytes;
 
-    while ((nbytes = fread(buffer, sizeof(char), sizeof(buffer), stdin)) > 0)
+    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))))
         if (fwrite(buffer, sizeof(char), nbytes, fp) != (size_t) nbytes)
-            return 2;
+            ERR_AND_EXIT("fwrite");
+
+    if (fclose(fp))
+        ERR_AND_EXIT("fclose");
 
-    fclose(fp);
+    pid_t pid;
 
-    pid_t pid = fork();
-    if (!pid) {
-        // See https://clang.llvm.org/cxx_status.html for valid -std values.
+    if (!(pid = fork())) {
         execl(
-            clang, clang, "-std=c++23", "-target", "x86_64-alpine-linux-musl",
-            "-O2", "-lstdc++", "-fcolor-diagnostics", "-o", bin, code,
+            cpp, cpp, "-std=c++23", "-target", "x86_64-alpine-linux-musl",
+            "-O2", "-lstdc++", "-fcolor-diagnostics", "-o", "code", code,
             "/unbuffered.cpp", NULL
         );
-        perror("execl");
-        return 3;
+        ERR_AND_EXIT("execl");
     }
 
     int status;
+
     waitpid(pid, &status, 0);
 
     if (!WIFEXITED(status))
-        return 4;
+        exit(EXIT_FAILURE);
 
     if (WEXITSTATUS(status))
         return WEXITSTATUS(status);
 
-    if (remove(code)) {
-        perror("Error deleting file");
-        return 5;
-    }
+    if (remove(code))
+        ERR_AND_EXIT("remove");
 
     int cargc = argc;
     char** cargv = malloc(cargc * sizeof(char*));
-    cargv[0] = (char*) bin;
+    cargv[0] = "code";
     memcpy(&cargv[1], &argv[2], (argc - 2) * sizeof(char*));
     cargv[cargc - 1] = NULL;
 
-    execv(bin, cargv);
-    perror("execv");
-    return 6;
+    execv("code", cargv);
+    ERR_AND_EXIT("execv");
 }
diff --git a/langs/crystal/Dockerfile b/langs/crystal/Dockerfile
index 52166536..631f92ca 100644
--- a/langs/crystal/Dockerfile
+++ b/langs/crystal/Dockerfile
@@ -1,7 +1,7 @@
-FROM crystallang/crystal:1.15.1-alpine AS builder
+FROM crystallang/crystal:1.17.1-alpine AS builder
 
-RUN apk add --no-cache dash
-RUN apk del --no-cache libxml2-static python3
+RUN apk add --no-cache dash \
+ && apk del --no-cache libxml2-static python3
 
 RUN strip /usr/bin/crystal
 
diff --git a/langs/d/Dockerfile b/langs/d/Dockerfile
index ae8b6986..13a30d28 100644
--- a/langs/d/Dockerfile
+++ b/langs/d/Dockerfile
@@ -4,7 +4,7 @@ RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
     apt-get install --yes curl dpkg gcc libc6-dev
 
-ENV VER=2.110.0
+ENV VER=2.111.0
 
 RUN curl -o dmd.deb https://downloads.dlang.org/releases/2.x/$VER/dmd_$VER-0_amd64.deb \
  && dpkg -i dmd.deb
diff --git a/langs/d/d.c b/langs/d/d.c
index bf7a9326..a05c8214 100644
--- a/langs/d/d.c
+++ b/langs/d/d.c
@@ -5,11 +5,11 @@
 
 #define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
-const char* dmd = "/usr/bin/dmd";
+const char* D = "/usr/bin/dmd", *code = "code.d";
 
 int main(int argc, char* argv[]) {
     if (!strcmp(argv[1], "--version")) {
-        execv(dmd, argv);
+        execv(D, argv);
         ERR_AND_EXIT("execv");
     }
 
@@ -18,7 +18,7 @@ int main(int argc, char* argv[]) {
 
     FILE* fp;
 
-    if (!(fp = fopen("code.d", "w")))
+    if (!(fp = fopen(code, "w")))
         ERR_AND_EXIT("fopen");
 
     char buffer[4096];
@@ -33,13 +33,13 @@ int main(int argc, char* argv[]) {
 
     int dargc = argc + 3;
     char** dargv = malloc(dargc * sizeof(char*));
-    dargv[0] = (char*) dmd;
+    dargv[0] = (char*) D;
     dargv[1] = "-color=on";
     dargv[2] = "-run";
-    dargv[3] = "code.d";
+    dargv[3] = (char*) code;
     memcpy(&dargv[4], &argv[2], (argc - 2) * sizeof(char*));
     dargv[dargc - 1] = NULL;
 
-    execv(dmd, dargv);
+    execv(D, dargv);
     ERR_AND_EXIT("execv");
 }
diff --git a/langs/dart/Dockerfile b/langs/dart/Dockerfile
index 310c6292..ba723ec2 100644
--- a/langs/dart/Dockerfile
+++ b/langs/dart/Dockerfile
@@ -2,17 +2,13 @@ FROM debian:bookworm-slim AS builder
 
 RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
-    apt-get install --yes curl dpkg gcc
+    apt-get install --yes curl dpkg
 
-ENV VER=3.7.2
+ENV VER=3.8.1
 
 RUN curl -#o dart.deb https://storage.googleapis.com/dart-archive/channels/stable/release/latest/linux_packages/dart_$VER-1_amd64.deb \
  && dpkg -i dart.deb
 
-COPY dartwrapper.c /
-
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/dartwrapper -s dartwrapper.c
-
 FROM codegolf/lang-base
 
 COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6                          \
@@ -20,13 +16,11 @@ COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6                          \
               /lib/x86_64-linux-gnu/libm.so.6                          \
               /lib/x86_64-linux-gnu/libpthread.so.0                    /lib/
 COPY --from=0 /lib64/ld-linux-x86-64.so.2                              /lib64/
-COPY --from=0 /usr/bin/dartwrapper /usr/lib/dart/bin/dart              /usr/bin/
-COPY --from=0 /usr/lib/dart/bin/snapshots/dartdev.dart.snapshot        \
-              /usr/lib/dart/bin/snapshots/kernel-service.dart.snapshot \
-              /usr/lib/dart/bin/snapshots/kernel_worker.dart.snapshot  /usr/bin/snapshots/
+COPY --from=0 /usr/lib/dart/bin/dart                                   /usr/bin/
+COPY --from=0 /usr/lib/dart/bin/snapshots/kernel-service.dart.snapshot /usr/bin/snapshots/
 COPY --from=0 /usr/lib/dart/include                                    /usr/include
 COPY --from=0 /usr/lib/dart/lib                                        /usr/lib
 
-ENTRYPOINT ["dartwrapper"]
+ENTRYPOINT ["dart"]
 
 CMD ["--version"]
diff --git a/langs/dart/dartwrapper.c b/langs/dart/dartwrapper.c
deleted file mode 100644
index 0e281494..00000000
--- a/langs/dart/dartwrapper.c
+++ /dev/null
@@ -1,40 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
-
-const char* dart = "/usr/bin/dart", *code = "/tmp/code.dart";
-
-int main(int argc, char* argv[]) {
-    if (!strcmp(argv[1], "--version")) {
-        execv(dart, argv);
-        ERR_AND_EXIT("execv");
-    }
-
-    FILE* fp;
-
-    if (!(fp = fopen(code, "w")))
-        ERR_AND_EXIT("fopen");
-
-    char buffer[4096];
-    ssize_t nbytes;
-
-    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))))
-        if (fwrite(buffer, sizeof(char), nbytes, fp) != (size_t) nbytes)
-            ERR_AND_EXIT("fwrite");
-
-    if (fclose(fp))
-        ERR_AND_EXIT("fclose");
-
-    int dargc = argc + 1;
-    char** dargv = malloc(dargc * sizeof(char*));
-    dargv[0] = (char*) dart;
-    dargv[1] = (char*) code;
-    memcpy(&dargv[2], &argv[2], (argc - 2) * sizeof(char*));
-    dargv[dargc - 1] = NULL;
-
-    execv(dart, dargv);
-    ERR_AND_EXIT("execv");
-}
diff --git a/langs/egel/Dockerfile b/langs/egel/Dockerfile
index f45808e0..a1010a54 100644
--- a/langs/egel/Dockerfile
+++ b/langs/egel/Dockerfile
@@ -2,52 +2,41 @@ FROM debian:bookworm-slim AS builder
 
 RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
-    apt-get install --yes binutils-dev clang cmake curl libffi-dev libfmt-dev libiberty-dev libicu-dev ninja-build pkg-config zlib1g-dev
+    apt-get install --yes autoconf automake bash cmake g++ git libtool make texinfo
 
-ENV VER=0.1.14
-   
-WORKDIR /lightning
+ENV MAKEFLAGS=-j8 VER=0.1.14
 
-RUN curl -#L https://ftp.gnu.org/gnu/lightning/lightning-2.2.3.tar.gz \
-  | tar xz --strip-components 1
-
-RUN ./configure && make install
+RUN git clone --progress --recurse-submodules -j4 -b v$VER \
+    --shallow-submodules https://github.com/egel-lang/egel
 
 WORKDIR /egel
 
-RUN curl -#L https://github.com/egel-lang/egel/archive/refs/tags/v$VER.tar.gz \
-  | tar xz --strip-components 1
+RUN cp CMakeLists.static CMakeLists.txt
+
+WORKDIR /egel/vendor
+
+RUN bash makeall.sh
 
 WORKDIR /egel/build
 
-RUN cmake -G Ninja .. && ninja install
+RUN cmake -DCMAKE_BUILD_TYPE='MinSizeRel' .. \
+ && make install && strip /usr/local/bin/egel
 
-COPY egelwrapper.c /
+COPY egel.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/egelwrapper -s /egelwrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/egel -s /egel.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/x86_64-linux-gnu/libbfd-2.40-*.so     \
-              /lib/x86_64-linux-gnu/libc.so.6            \
-              /lib/x86_64-linux-gnu/libffi.so.8          \
-              /lib/x86_64-linux-gnu/libfmt.so.9          \
-              /lib/x86_64-linux-gnu/libgcc_s.so.1        \
-              /lib/x86_64-linux-gnu/libicudata.so.72     \
-              /lib/x86_64-linux-gnu/libicui18n.so.72     \
-              /lib/x86_64-linux-gnu/libicuio.so.72       \
-              /lib/x86_64-linux-gnu/libicuuc.so.72       \
-              /lib/x86_64-linux-gnu/libm.so.6            \
-              /lib/x86_64-linux-gnu/libopcodes-2.40-*.so \
-              /lib/x86_64-linux-gnu/libsframe.so.0       \
-              /lib/x86_64-linux-gnu/libstdc++.so.6       \
-              /lib/x86_64-linux-gnu/libz.so.1            \
-              /lib/x86_64-linux-gnu/libzstd.so.1         /lib/
-COPY --from=0 /lib64/ld-linux-x86-64.so.2                /lib64/
-COPY --from=0 /usr/bin/egelwrapper /usr/local/bin/egel   /usr/bin/
-COPY --from=0 /usr/local/lib/liblightning.so.2           /usr/lib/
-COPY --from=0 /usr/local/lib/egel                        /usr/lib/egel
-
-ENTRYPOINT ["egelwrapper"]
+COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6      \
+              /lib/x86_64-linux-gnu/libgcc_s.so.1  \
+              /lib/x86_64-linux-gnu/libm.so.6      \
+              /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/
+COPY --from=0 /lib64/ld-linux-x86-64.so.2          /lib64/
+COPY --from=0 /usr/bin/egel                        /usr/bin/
+COPY --from=0 /usr/local/bin/egel                  /usr/local/bin/
+COPY --from=0 /usr/local/lib/egel                  /usr/local/lib/egel
+
+ENTRYPOINT ["egel"]
 
 CMD ["--version"]
diff --git a/langs/egel/egelwrapper.c b/langs/egel/egel.c
similarity index 94%
rename from langs/egel/egelwrapper.c
rename to langs/egel/egel.c
index 7ed872e0..d58e7a0a 100644
--- a/langs/egel/egelwrapper.c
+++ b/langs/egel/egel.c
@@ -5,7 +5,7 @@
 
 #define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
-const char* egel = "/usr/bin/egel", *code = "code.eg";
+const char* egel = "/usr/local/bin/egel", *code = "code.eg";
 
 int main(int argc, char* argv[]) {
     if (!strcmp(argv[1], "--version")) {
diff --git a/langs/elixir/Dockerfile b/langs/elixir/Dockerfile
index 5f56a936..52cfd8be 100644
--- a/langs/elixir/Dockerfile
+++ b/langs/elixir/Dockerfile
@@ -1,51 +1,28 @@
+FROM codegolf/lang-erlang
+
 FROM debian:bookworm-slim AS builder
 
 RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
-    apt-get install --yes build-essential curl ncurses-dev
-
-ENV ERLVER=27.3.1 VER=1.18.3
-
-RUN curl -#L https://github.com/erlang/otp/releases/download/OTP-$ERLVER/otp_src_$ERLVER.tar.gz | tar xz
+    apt-get install --yes curl make
 
-WORKDIR /otp_src_$ERLVER
+ENV LANG='C.UTF-8' VER=1.18.4
 
-RUN ./configure   \
-    --prefix=/usr \
- && make -j`nproc` install
+COPY --from=0 /usr /usr
 
 WORKDIR /usr/local
 
 RUN curl -#L https://github.com/elixir-lang/elixir/archive/v$VER.tar.gz \
   | tar xz --strip-components 1
 
-RUN LANG='C.UTF-8' make clean compile
+RUN make
 
 FROM codegolf/lang-base
 
-ENV ELIXIR_ERL_OPTIONS='+fnu'
-
-COPY --from=0 /bin/dash                                       /bin/sh
-COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6                 \
-              /lib/x86_64-linux-gnu/libgcc_s.so.1             \
-              /lib/x86_64-linux-gnu/libm.so.6                 \
-              /lib/x86_64-linux-gnu/libstdc++.so.6            \
-              /lib/x86_64-linux-gnu/libtinfo.so.6             /lib/
-COPY --from=0 /lib64/ld-linux-x86-64.so.2                     /lib64/
-COPY --from=0 /usr/bin/basename                               \
-              /usr/bin/dirname                                \
-              /usr/bin/erl                                    /usr/bin/
-COPY --from=0 /usr/lib/erlang/bin/start.boot                  /usr/lib/erlang/bin/
-COPY --from=0 /usr/lib/erlang/erts-15.2.4/bin/beam.smp        \
-              /usr/lib/erlang/erts-15.2.4/bin/erl_child_setup \
-              /usr/lib/erlang/erts-15.2.4/bin/erlexec         \
-              /usr/lib/erlang/erts-15.2.4/bin/inet_gethost    /usr/lib/erlang/erts-15.2.4/bin/
-COPY --from=0 /usr/lib/erlang/lib/compiler-8.6/ebin           /usr/lib/erlang/lib/compiler-8.6/ebin
-COPY --from=0 /usr/lib/erlang/lib/kernel-10.2.4/ebin          /usr/lib/erlang/lib/kernel-10.2.4/ebin
-COPY --from=0 /usr/lib/erlang/lib/stdlib-6.2.2/ebin           /usr/lib/erlang/lib/stdlib-6.2.2/ebin
-COPY --from=0 /usr/lib/locale                                 /usr/lib/locale
-COPY --from=0 /usr/local/bin/elixir                           /usr/local/bin/
-COPY --from=0 /usr/local/lib/elixir/ebin                      /usr/local/lib/elixir/ebin
+COPY --from=0 /                          /
+COPY --from=1 /usr/lib/locale            /usr/lib/locale
+COPY --from=1 /usr/local/bin/elixir      /usr/local/bin/
+COPY --from=1 /usr/local/lib/elixir/ebin /usr/local/lib/elixir/ebin
 
 ENTRYPOINT ["elixir"]
 
diff --git a/langs/erlang/Dockerfile b/langs/erlang/Dockerfile
index fe65b3b6..47ef90a3 100644
--- a/langs/erlang/Dockerfile
+++ b/langs/erlang/Dockerfile
@@ -2,49 +2,63 @@ FROM debian:bookworm-slim AS builder
 
 RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
-    apt-get install --yes build-essential curl ncurses-dev
+    apt-get install --yes curl gcc make perl
 
-# Also bump Elixir, Gleam.
-ENV VER=27.3.1
+# Also rebuild Elixir, Gleam.
+ENV VER=28.0.2
 
-RUN curl -#L https://github.com/erlang/otp/releases/download/OTP-$VER/otp_src_$VER.tar.gz | tar xz
+WORKDIR /erlang
 
-WORKDIR /otp_src_$VER
+RUN curl -#L https://github.com/erlang/otp/releases/download/OTP-$VER/otp_src_$VER.tar.gz \
+  | tar xz --strip-components 1
 
 RUN ./configure            \
+    --enable-m64-build     \
     --prefix=/usr          \
- && make -j`nproc` install \
+    --without-termcap      \
+ && make -j`nproc` release \
+ && make install           \
  && strip /usr/bin/escript
 
+WORKDIR /usr/lib/erlang/lib
+
+RUN mv ../erts* /usr/local          \
+ && find /usr/local -type f -not \( \
+    -name 'beam.smp'        -or     \
+    -name 'erl_child_setup' -or     \
+    -name 'erlexec'         -or     \
+    -name 'inet_gethost' \) -delete
+
+RUN mv compiler*                        \
+       kernel*                          \
+       parsetools*                      \
+       stdlib*                          \
+       tools* /usr/local/lib            \
+ && find /usr/local/lib -type f -not \( \
+    -name '*.*a*'     -or               \
+    -name 'file.hrl'  -or               \
+    -name 'make.beam' -or               \
+    -name 'yecc*' \) -delete
+
 COPY erlang.c /
 
 RUN gcc -Wall -Werror -Wextra -o /usr/bin/erlang -s /erlang.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /bin/dash                                       /bin/sh
-COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6                 \
-              /lib/x86_64-linux-gnu/libgcc_s.so.1             \
-              /lib/x86_64-linux-gnu/libm.so.6                 \
-              /lib/x86_64-linux-gnu/libncursesw.so.6          \
-              /lib/x86_64-linux-gnu/libstdc++.so.6            \
-              /lib/x86_64-linux-gnu/libtinfo.so.6             /lib/
-COPY --from=0 /lib64/ld-linux-x86-64.so.2                     /lib64/
-COPY --from=0 /usr/bin/basename                               \
-              /usr/bin/dirname                                \
-              /usr/bin/erl                                    \
-              /usr/bin/erlang                                 \
-              /usr/bin/erlc                                   \
-              /usr/bin/escript                                /usr/bin/
-COPY --from=0 /usr/lib/erlang/bin/no_dot_erlang.boot          \
-              /usr/lib/erlang/bin/start.boot                  /usr/lib/erlang/bin/
-COPY --from=0 /usr/lib/erlang/erts-15.2.4/bin/beam.smp        \
-              /usr/lib/erlang/erts-15.2.4/bin/erl_child_setup \
-              /usr/lib/erlang/erts-15.2.4/bin/erlexec         \
-              /usr/lib/erlang/erts-15.2.4/bin/inet_gethost    /usr/lib/erlang/erts-15.2.4/bin/
-COPY --from=0 /usr/lib/erlang/lib/compiler-8.6/ebin           /usr/lib/erlang/lib/compiler-8.6/ebin
-COPY --from=0 /usr/lib/erlang/lib/kernel-10.2.4/ebin          /usr/lib/erlang/lib/kernel-10.2.4/ebin
-COPY --from=0 /usr/lib/erlang/lib/stdlib-6.2.2/ebin           /usr/lib/erlang/lib/stdlib-6.2.2/ebin
+COPY --from=0 /bin/dash                              /bin/sh
+COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6        \
+              /lib/x86_64-linux-gnu/libm.so.6        /lib/
+COPY --from=0 /lib64/ld-linux-x86-64.so.2            /lib64/
+COPY --from=0 /usr/bin/basename                      \
+              /usr/bin/dirname                       \
+              /usr/bin/erl                           \
+              /usr/bin/erlang                        \
+              /usr/bin/erlc                          \
+              /usr/bin/escript                       /usr/bin/
+COPY --from=0 /usr/local                             /usr/lib/erlang
+COPY --from=0 /usr/lib/erlang/bin/no_dot_erlang.boot \
+              /usr/lib/erlang/bin/start.boot         /usr/lib/erlang/bin/
 
 ENTRYPOINT ["erlang"]
 
diff --git a/langs/erlang/erlang.c b/langs/erlang/erlang.c
index 856a58ef..8ce07d78 100644
--- a/langs/erlang/erlang.c
+++ b/langs/erlang/erlang.c
@@ -6,7 +6,7 @@
 
 #define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
-const char* erlang = "/usr/bin/escript", *code = "main.erl";
+const char* erlang = "/usr/bin/escript", *erlc = "/usr/bin/erlc", *code = "main.erl";
 
 int main(int argc, char* argv[]) {
     if (!strcmp(argv[1], "--version"))
@@ -39,7 +39,7 @@ int main(int argc, char* argv[]) {
         if (!dup2(STDERR_FILENO, STDOUT_FILENO))
             ERR_AND_EXIT("dup2");
 
-        execl("/usr/bin/erlc", "erlc", code, NULL);
+        execl(erlc, erlc, code, NULL);
         ERR_AND_EXIT("execl");
     }
 
diff --git a/langs/f-sharp/Dockerfile b/langs/f-sharp/Dockerfile
index f41f6fee..880f5517 100644
--- a/langs/f-sharp/Dockerfile
+++ b/langs/f-sharp/Dockerfile
@@ -1,6 +1,6 @@
-FROM mcr.microsoft.com/dotnet/sdk:9.0.200-alpine3.21 AS builder
+FROM mcr.microsoft.com/dotnet/sdk:9.0.301-alpine3.22-amd64 AS builder
 
-WORKDIR /work
+WORKDIR /f-sharp
 
 COPY f-sharp.fs f-sharp.fsproj ./
 
@@ -9,7 +9,7 @@ RUN dotnet publish -c Release -o /out
 # Trim additional things to improve F# compiler startup performance.
 WORKDIR /trimmer
 
-COPY Trimmer.csproj Trimmer.cs ExtraTrimmingList.txt ./
+COPY trimmer.cs trimmer.csproj trimmer.txt ./
 
 RUN dotnet run
 
diff --git a/langs/f-sharp/f-sharp.fsproj b/langs/f-sharp/f-sharp.fsproj
index 9a7da19a..9dae9549 100644
--- a/langs/f-sharp/f-sharp.fsproj
+++ b/langs/f-sharp/f-sharp.fsproj
@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
     <ItemGroup>
         <Compile Include="f-sharp.fs"/>
-        <PackageReference Include="FSharp.Compiler.Service" Version="43.9.201"/>
+        <PackageReference Include="FSharp.Compiler.Service" Version="43.9.300"/>
     </ItemGroup>
     <PropertyGroup>
         <OutputType>Exe</OutputType>
diff --git a/langs/f-sharp/Trimmer.cs b/langs/f-sharp/trimmer.cs
similarity index 97%
rename from langs/f-sharp/Trimmer.cs
rename to langs/f-sharp/trimmer.cs
index 807e9f08..7228cfcd 100644
--- a/langs/f-sharp/Trimmer.cs
+++ b/langs/f-sharp/trimmer.cs
@@ -6,7 +6,7 @@ using Newtonsoft.Json.Linq;
 var _removeSet = new HashSet<string>();
 var _removeCount = 0;
 
-var listPath = "ExtraTrimmingList.txt";
+var listPath = "trimmer.txt";
 var lines = File.ReadAllLines(listPath);
 
 foreach (var filename in lines)
diff --git a/langs/f-sharp/Trimmer.csproj b/langs/f-sharp/trimmer.csproj
similarity index 100%
rename from langs/f-sharp/Trimmer.csproj
rename to langs/f-sharp/trimmer.csproj
diff --git a/langs/f-sharp/ExtraTrimmingList.txt b/langs/f-sharp/trimmer.txt
similarity index 100%
rename from langs/f-sharp/ExtraTrimmingList.txt
rename to langs/f-sharp/trimmer.txt
diff --git a/langs/fennel/Dockerfile b/langs/fennel/Dockerfile
index 32a568f0..88046ec4 100644
--- a/langs/fennel/Dockerfile
+++ b/langs/fennel/Dockerfile
@@ -1,8 +1,8 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 ENV BIN='/usr/bin/fennel' VER=1.5.3
 
-RUN wget -cO $BIN https://fennel-lang.org/downloads/fennel-$VER \
+RUN wget -qO $BIN https://fennel-lang.org/downloads/fennel-$VER \
  && chmod +x $BIN
 
 # Rewrite shebang to bypass /usr/bin/env and go directly to /usr/bin/lua.
diff --git a/langs/fish/Dockerfile b/langs/fish/Dockerfile
index c84f93be..16560f6c 100644
--- a/langs/fish/Dockerfile
+++ b/langs/fish/Dockerfile
@@ -1,24 +1,27 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-ENV PY_VERSION=3.10-v7.3.17 VERSION=96b1cfc
+RUN apk add -X https://dl-cdn.alpinelinux.org/alpine/edge/testing \
+    --no-cache curl gcc libffi-dev linux-headers make musl-dev pypy
 
-RUN echo @edge https://dl-cdn.alpinelinux.org/alpine/edge/testing \
- >> /etc/apk/repositories                                         \
- && apk add --no-cache curl gcc libffi-dev linux-headers make musl-dev pypy@edge
+ENV LDFLAGS='-static' PYVER=3.11-v7.3.19 VER=96b1cfc
 
-RUN curl https://downloads.python.org/pypy/pypy$PY_VERSION-src.tar.bz2 \
-  | tar xj
+RUN curl -#L https://downloads.python.org/pypy/pypy$PYVER-src.tar.bz2 \
+  | tar xj --strip-components 1
 
-RUN curl -L https://github.com/primo-ppcg/fish-jit/tarball/$VERSION \
-  | tar xz
+WORKDIR /usr/bin
 
-RUN LDFLAGS=-static pypy pypy$PY_VERSION-src/rpython/bin/rpython \
-    --lto --opt=jit primo-ppcg-fish-jit-$VERSION/fish-jit.py     \
+RUN curl -#L https://github.com/primo-ppcg/fish-jit/tarball/$VER \
+  | tar xz --strip-components 1
+
+RUN pypy /rpython fish-jit \
+    --lto                  \
+    --make-jobs=`nproc`    \
+    --opt=jit              \
  && strip fish-jit-c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /fish-jit-c /usr/bin/fish
+COPY --from=0 /usr/bin/fish-jit-c /usr/bin/fish
 
 ENTRYPOINT ["fish"]
 
diff --git a/langs/forth/Dockerfile b/langs/forth/Dockerfile
index 80f46106..8fb6093f 100644
--- a/langs/forth/Dockerfile
+++ b/langs/forth/Dockerfile
@@ -1,21 +1,25 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl m4
+RUN apk add --no-cache curl gcc m4 make musl-dev
 
-RUN curl https://ftp.gnu.org/gnu/gforth/gforth-0.7.3.tar.gz | tar xz
+ENV VER=0.7.3
 
-RUN cd gforth-* \
- && ./configure --prefix=/usr --without-check \
- && make \
+RUN curl -#L https://ftp.gnu.org/gnu/gforth/gforth-$VER.tar.gz \
+  | tar xz --strip-components 1
+
+RUN ./configure     \
+    --prefix=/usr   \
+    --without-check \
+ && make            \
  && make install
 
 FROM codegolf/lang-base
 
-COPY --from=builder /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=builder /usr/bin/gforth          /usr/bin/forth
-COPY --from=builder /usr/include/gforth      /usr/include/gforth
-COPY --from=builder /usr/lib/gforth          /usr/lib/gforth
-COPY --from=builder /usr/share/gforth        /usr/share/gforth
+COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
+COPY --from=0 /usr/bin/gforth          /usr/bin/forth
+COPY --from=0 /usr/include/gforth      /usr/include/gforth
+COPY --from=0 /usr/lib/gforth          /usr/lib/gforth
+COPY --from=0 /usr/share/gforth        /usr/share/gforth
 
 ENTRYPOINT ["forth"]
 
diff --git a/langs/fortran/Dockerfile b/langs/fortran/Dockerfile
index 6929fc0a..677c29e7 100644
--- a/langs/fortran/Dockerfile
+++ b/langs/fortran/Dockerfile
@@ -1,79 +1,95 @@
-ARG VER=14.2.0
+FROM alpine:3.22 AS builder
 
-FROM alpine:3.21 AS builder
+RUN apk add --no-cache curl g++ m4 make
 
-ARG VER
+ENV LDFLAGS='-static' VER=15.1.0
 
-RUN apk add --no-cache curl g++ linux-headers m4 make patch tar xz
+WORKDIR /gmp
 
-WORKDIR /src
+RUN curl -#L ftp://gcc.gnu.org/pub/gcc/infrastructure/gmp-6.2.1.tar.bz2 \
+  | tar xj --strip-components 1
 
-RUN curl ftp://ftp.gnu.org/gnu/gcc/gcc-$VER/gcc-$VER.tar.xz          | tar xJ \
- && curl ftp://gcc.gnu.org/pub/gcc/infrastructure/gmp-6.2.1.tar.bz2  | tar xj \
- && curl ftp://gcc.gnu.org/pub/gcc/infrastructure/mpfr-4.1.0.tar.bz2 | tar xj \
- && curl ftp://gcc.gnu.org/pub/gcc/infrastructure/mpc-1.2.1.tar.gz   | tar xz
-
-RUN cd /src/gmp-6.2.1 \
- && ./configure --disable-shared --enable-static --prefix=/tmp/gcc \
+RUN ./configure       \
+    --disable-shared  \
+    --prefix=/tmp/gmp \
  && make -j`nproc` install
 
-RUN cd /src/mpfr-4.1.0 \
- && ./configure --disable-shared --enable-static --prefix=/tmp/gcc --with-gmp=/tmp/gcc \
+WORKDIR /mpfr
+
+RUN curl -#L ftp://gcc.gnu.org/pub/gcc/infrastructure/mpfr-4.1.0.tar.bz2 \
+  | tar xj --strip-components 1
+
+RUN ./configure         \
+    --disable-shared    \
+    --prefix=/tmp/mpfr  \
+    --with-gmp=/tmp/gmp \
  && make -j`nproc` install
 
-RUN cd /src/mpc-1.2.1 \
- && ./configure --disable-shared --enable-static --prefix=/tmp/gcc --with-gmp=/tmp/gcc --with-mpfr=/tmp/gcc \
+WORKDIR /mpc
+
+RUN curl -#L ftp://gcc.gnu.org/pub/gcc/infrastructure/mpc-1.2.1.tar.gz \
+  | tar xz --strip-components 1
+
+RUN ./configure           \
+    --disable-shared      \
+    --prefix=/tmp/mpc     \
+    --with-gmp=/tmp/gmp   \
+    --with-mpfr=/tmp/mpfr \
  && make -j`nproc` install
 
-COPY fortran.c .
-
-RUN gcc -o fortran fortran.c
-
-WORKDIR /scratch
-
-RUN /src/gcc-$VER/configure       \
-    --disable-bootstrap           \
-    --disable-gcov                \
-    --disable-libgomp             \
-    --disable-libstdcxx-pch       \
-    --disable-lto                 \
-    --disable-multilib            \
-    --disable-nls                 \
-    --disable-shared              \
-    --enable-languages=fortran    \
-    --enable-static               \
-    --prefix=/usr/local           \
-    --with-gmp=/tmp/gcc           \
-    --with-mpc=/tmp/gcc           \
-    --with-mpfr=/tmp/gcc          \
- && make -j`nproc`                \
- && make install                  \
- && strip /usr/local/bin/gfortran \
-    /usr/local/libexec/gcc/x86_64-pc-linux-musl/$VER/f951
+WORKDIR /fortran
+
+RUN curl -#L ftp://ftp.gnu.org/gnu/gcc/gcc-$VER/gcc-$VER.tar.xz \
+  | tar xJ --strip-components 1
+
+RUN ./configure                      \
+    --disable-bootstrap              \
+    --disable-gcov                   \
+    --disable-libgomp                \
+    --disable-libstdcxx-pch          \
+    --disable-lto                    \
+    --disable-multilib               \
+    --disable-nls                    \
+    --disable-shared                 \
+    --enable-languages=fortran       \
+    --with-gmp=/tmp/gmp              \
+    --with-mpc=/tmp/mpc              \
+    --with-mpfr=/tmp/mpfr            \
+    --with-static-standard-libraries \
+ && make -j`nproc`                   \
+ && make install                     \
+ && strip /usr/local/bin/gfortran    \
+          /usr/local/libexec/gcc/x86_64-pc-linux-musl/$VER/f951
+
+COPY fortran.c /
+
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/fortran -s -static /fortran.c
 
 FROM codegolf/lang-base
 
-ARG VER
-
-COPY --from=0 /lib/ld-musl-x86_64.so.1             /lib/
-COPY --from=0 /src/fortran /usr/bin/as /usr/bin/ld /usr/bin/
-COPY --from=0 /usr/lib/crt1.o                      \
-              /usr/lib/crti.o                      \
-              /usr/lib/crtn.o                      \
-              /usr/lib/libbfd-2.43.1.so            \
-              /usr/lib/libc.so                     \
-              /usr/lib/libctf.so.0                 \
-              /usr/lib/libjansson.so.4             \
-              /usr/lib/libm.a                      \
-              /usr/lib/libopcodes-2.43.1.so        \
-              /usr/lib/libsframe.so.1              \
-              /usr/lib/libz.so.1                   \
-              /usr/lib/libzstd.so.1                /usr/lib/
-COPY --from=0 /usr/local/bin/gfortran              /usr/local/bin/
-COPY --from=0 /usr/local/lib                       /usr/local/lib
-COPY --from=0 /usr/local/lib64                     /usr/local/lib64
-COPY --from=0 /usr/local/libexec/gcc/x86_64-pc-linux-musl/$VER/f951 \
-              /usr/local/libexec/gcc/x86_64-pc-linux-musl/$VER/
+COPY --from=0 /lib/ld-musl-x86_64.so.1          /lib/
+COPY --from=0 /usr/bin/as                       \
+              /usr/bin/fortran                  \
+              /usr/bin/ld                       /usr/bin/
+COPY --from=0 /usr/lib/crt1.o                   \
+              /usr/lib/crti.o                   \
+              /usr/lib/crtn.o                   \
+              /usr/lib/libbfd-2.44.so           \
+              /usr/lib/libc.so                  \
+              /usr/lib/libctf.so.0              \
+              /usr/lib/libjansson.so.4          \
+              /usr/lib/libm.a                   \
+              /usr/lib/libsframe.so.1           \
+              /usr/lib/libz.so.1                \
+              /usr/lib/libzstd.so.1             /usr/lib/
+COPY --from=0 /usr/local/bin/gfortran           /usr/local/bin/
+COPY --from=0 /usr/local/lib/gcc/*/*/crtbegin.o \
+              /usr/local/lib/gcc/*/*/crtend.o   \
+              /usr/local/lib/gcc/*/*/libgcc.a   /usr/local/lib/gcc/x86_64-pc-linux-musl/15.1.0/
+COPY --from=0 /usr/local/lib64/libgfortran.a    \
+              /usr/local/lib64/libgfortran.spec \
+              /usr/local/lib64/libquadmath.a    /usr/local/lib64/
+COPY --from=0 /usr/local/libexec/gcc/*/*/f951   /usr/local/libexec/gcc/x86_64-pc-linux-musl/
 
 ENTRYPOINT ["fortran"]
 
diff --git a/langs/fortran/fortran.c b/langs/fortran/fortran.c
index 2992d1db..43b1b017 100644
--- a/langs/fortran/fortran.c
+++ b/langs/fortran/fortran.c
@@ -4,60 +4,63 @@
 #include <sys/wait.h>
 #include <unistd.h>
 
-const char* fortran = "/usr/local/bin/gfortran";
-const char* code = "/tmp/code.f90";
-const char* bin = "/tmp/code";
+#define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
+
+const char* fortran = "/usr/local/bin/gfortran", *code = "code.f90";
 
 int main(int argc, char* argv[]) {
-    if (argc > 1 && strcmp(argv[1], "--version") == 0) {
+    if (!strcmp(argv[1], "--version")) {
         execv(fortran, argv);
-        perror("execv");
-        return 0;
+        ERR_AND_EXIT("execv");
     }
 
-    FILE* fp = fopen(code, "w");
+    if (chdir("/tmp"))
+        ERR_AND_EXIT("chdir");
+
+    FILE* fp;
 
-    if (!fp)
-        return 1;
+    if (!(fp = fopen(code, "w")))
+        ERR_AND_EXIT("fopen");
 
     char buffer[4096];
     ssize_t nbytes;
 
-    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0)
-        if (fwrite(buffer, sizeof(char), nbytes, fp) != nbytes)
-            return 2;
+    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))))
+        if (fwrite(buffer, sizeof(char), nbytes, fp) != (size_t) nbytes)
+            ERR_AND_EXIT("fwrite");
+
+    if (fclose(fp))
+        ERR_AND_EXIT("fclose");
 
-    fclose(fp);
+    pid_t pid;
 
-    pid_t pid = fork();
-    if (!pid) {
-        dup2(2, 1);
-        execl(fortran, fortran, "-o", bin, code, NULL);
-        perror("execl");
-        return 3;
+    if (!(pid = fork())) {
+        if (!dup2(STDERR_FILENO, STDOUT_FILENO))
+            ERR_AND_EXIT("dup2");
+
+        execl(fortran, fortran, "-o", "code", code, NULL);
+        ERR_AND_EXIT("execl");
     }
 
     int status;
+
     waitpid(pid, &status, 0);
 
     if (!WIFEXITED(status))
-        return 4;
+        exit(EXIT_FAILURE);
 
     if (WEXITSTATUS(status))
         return WEXITSTATUS(status);
 
-    if(remove(code)) {
-        perror("Error deleting file");
-        return 5;
-    }
+    if (remove(code))
+        ERR_AND_EXIT("remove");
 
     int fargc = argc;
     char** fargv = malloc(fargc * sizeof(char*));
-    fargv[0] = (char*)bin;
+    fargv[0] = "code";
     memcpy(&fargv[1], &argv[2], (argc - 2) * sizeof(char*));
     fargv[fargc - 1] = NULL;
 
-    execv(bin, fargv);
-    perror("execv");
-    return 6;
+    execv("code", fargv);
+    ERR_AND_EXIT("execv");
 }
diff --git a/langs/gleam/Dockerfile b/langs/gleam/Dockerfile
index 9c9c8305..76523b71 100644
--- a/langs/gleam/Dockerfile
+++ b/langs/gleam/Dockerfile
@@ -1,66 +1,45 @@
-FROM rust:1.85.1-slim-bookworm AS builder
+FROM codegolf/lang-erlang
+
+FROM rust:1.88.0-slim-bookworm AS builder
 
 RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
-    apt-get install --yes build-essential curl ncurses-dev
-
-ENV ERLVER=27.3.1 VER=1.9.1
+    apt-get install --yes curl make
 
-RUN curl -#L https://github.com/erlang/otp/releases/download/OTP-$ERLVER/otp_src_$ERLVER.tar.gz | tar xz
+ENV VER=1.11.1
 
-WORKDIR /otp_src_$ERLVER
-
-RUN ./configure            \
-    --prefix=/usr          \
- && make -j`nproc` install \
- && strip /usr/bin/escript
+WORKDIR /gleam
 
 RUN curl -#L https://github.com/gleam-lang/gleam/archive/refs/tags/v$VER.tar.gz \
   | tar xz --strip-components 1
 
 RUN make -j`nproc` install
 
-WORKDIR /gleam
-
-RUN gleam new --name main --skip-git .
+WORKDIR /usr/local
 
-RUN gleam add argv \
+RUN gleam new --name main . \
+ && gleam add argv          \
  && gleam remove gleeunit
 
-RUN chmod -R 777 .
-
 COPY gleam.c /
 
 RUN gcc -Wall -Werror -Wextra -o /usr/bin/gleam -s /gleam.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /bin/dash                                       /bin/sh
-COPY --from=0 /gleam/gleam.toml /gleam/manifest.toml          /gleam/
-COPY --from=0 /gleam/build/packages                           /gleam/build/packages
-COPY --from=0 /gleam/src                                      /gleam/src
-COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6                 \
-              /lib/x86_64-linux-gnu/libgcc_s.so.1             \
-              /lib/x86_64-linux-gnu/libm.so.6                 \
-              /lib/x86_64-linux-gnu/libstdc++.so.6            \
-              /lib/x86_64-linux-gnu/libtinfo.so.6             /lib/
-COPY --from=0 /lib64/ld-linux-x86-64.so.2                     /lib64/
-COPY --from=0 /usr/bin/basename                               \
-              /usr/bin/dirname                                \
-              /usr/bin/erl                                    \
-              /usr/bin/escript                                \
-              /usr/bin/gleam                                  /usr/bin/
-COPY --from=0 /usr/lib/erlang/bin/no_dot_erlang.boot          \
-              /usr/lib/erlang/bin/start.boot                  /usr/lib/erlang/bin/
-COPY --from=0 /usr/lib/erlang/erts-15.2.4/bin/beam.smp        \
-              /usr/lib/erlang/erts-15.2.4/bin/erl_child_setup \
-              /usr/lib/erlang/erts-15.2.4/bin/erlexec         \
-              /usr/lib/erlang/erts-15.2.4/bin/inet_gethost    /usr/lib/erlang/erts-15.2.4/bin/
-COPY --from=0 /usr/lib/erlang/lib/compiler-8.6/ebin           /usr/lib/erlang/lib/compiler-8.6/ebin
-COPY --from=0 /usr/lib/erlang/lib/kernel-10.2.4/ebin          /usr/lib/erlang/lib/kernel-10.2.4/ebin
-COPY --from=0 /usr/lib/erlang/lib/stdlib-6.2.2/ebin           /usr/lib/erlang/lib/stdlib-6.2.2/ebin
-COPY --from=0 /usr/lib/locale                                 /usr/lib/locale
-COPY --from=0 /usr/local/cargo/bin/gleam                      /usr/local/bin/
+COPY --from=0 /                                   /
+COPY --from=1 /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/
+COPY --from=1 /usr/bin/gleam                      /usr/bin/
+COPY --from=1 /usr/lib/locale                     /usr/lib/locale
+COPY --chown=nobody                               \
+     --from=1 /usr/local/gleam.toml               \
+              /usr/local/manifest.toml            /usr/local/
+COPY --chown=nobody                               \
+     --from=1 /usr/local/cargo/bin/gleam          /usr/local/bin/
+COPY --chown=nobody                               \
+     --from=1 /usr/local/build/packages           /usr/local/build/packages
+COPY --chown=nobody                               \
+     --from=1 /usr/local/src                      /usr/local/src
 
 ENTRYPOINT ["gleam"]
 
diff --git a/langs/gleam/gleam.c b/langs/gleam/gleam.c
index abb347bf..ed483728 100644
--- a/langs/gleam/gleam.c
+++ b/langs/gleam/gleam.c
@@ -19,7 +19,7 @@ int main(int argc, char* argv[]) {
         ERR_AND_EXIT("execv");
     }
 
-    copy_folder("/gleam", "/tmp");
+    copy_folder("/usr/local", "/tmp");
 
     if (chdir("/tmp"))
         ERR_AND_EXIT("chdir");
@@ -42,7 +42,6 @@ int main(int argc, char* argv[]) {
     pid_t pid;
 
     if (!(pid = fork())) {
-        // Print errors and/or warnings on STDERR.
         if (!dup2(STDERR_FILENO, STDOUT_FILENO))
             ERR_AND_EXIT("dup2");
 
diff --git a/langs/go/Dockerfile b/langs/go/Dockerfile
index b7fbb3fe..153e6448 100644
--- a/langs/go/Dockerfile
+++ b/langs/go/Dockerfile
@@ -1,20 +1,26 @@
-FROM golang:1.24.1-alpine3.21 AS builder
+ARG VER=1.24.5
+
+FROM golang:$VER-alpine3.22 AS builder
 
 RUN apk add --no-cache gcc musl-dev
 
 # Precompile the standard library.
 RUN GOCACHE=/cache go install std
 
-COPY gowrapper.c /
+COPY go.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/gowrapper -s -static /gowrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/gowrapper -s -static /go.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /cache                   /cache
-COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /usr/bin/gowrapper       /usr/bin/
-COPY --from=0 /usr/local/go            /usr/local/
+COPY --from=0 /cache                                     /cache
+COPY --from=0 /lib/ld-musl-x86_64.so.1                   /lib/
+COPY --from=0 /usr/bin/gowrapper                         /usr/bin/
+COPY --from=0 /usr/local/go/bin/go                       /usr/local/bin/
+COPY --from=0 /usr/local/go/pkg/tool/linux_amd64/asm     \
+              /usr/local/go/pkg/tool/linux_amd64/compile \
+              /usr/local/go/pkg/tool/linux_amd64/link    /usr/local/pkg/tool/linux_amd64/
+COPY --from=0 /usr/local/go/src                          /usr/local/src
 
 ENTRYPOINT ["gowrapper"]
 
diff --git a/langs/go/gowrapper.c b/langs/go/go.c
similarity index 100%
rename from langs/go/gowrapper.c
rename to langs/go/go.c
diff --git a/langs/golfscript/Dockerfile b/langs/golfscript/Dockerfile
index 6f0cac4b..967b46fa 100644
--- a/langs/golfscript/Dockerfile
+++ b/langs/golfscript/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 ENV VER=6155e9f
 
diff --git a/langs/groovy/Dockerfile b/langs/groovy/Dockerfile
index 82ca95a9..350482d6 100644
--- a/langs/groovy/Dockerfile
+++ b/langs/groovy/Dockerfile
@@ -1,46 +1,35 @@
-ARG VER=4.0.26
+ARG VER=4.0.27
 
-FROM eclipse-temurin:24_36-jre-alpine-3.21 AS builder
+FROM alpine:3.22 AS builder
 
 ARG VER
 
-RUN wget -cO groovy.zip https://groovy.jfrog.io/artifactory/dist-release-local/groovy-zips/apache-groovy-binary-$VER.zip \
- && unzip -j groovy.zip
+RUN apk add --no-cache coreutils dash
 
-FROM codegolf/lang-base
+RUN wget -qO groovy.zip https://groovy.jfrog.io/artifactory/dist-release-local/groovy-zips/apache-groovy-binary-$VER.zip \
+ && unzip -j groovy
+
+FROM codegolf/lang-java
 
 ARG VER
 
-COPY --from=0 /bin/sh /bin/uname                            /bin/
-COPY --from=0 /lib/ld-musl-x86_64.so.1                      /lib/
-COPY --from=0 /opt/java/openjdk/bin/java                    /opt/java/bin/
-COPY --from=0 /opt/java/openjdk/conf/security/java.security /opt/java/conf/security/
-COPY --from=0 /opt/java/openjdk/lib/jvm.cfg                 \
-              /opt/java/openjdk/lib/libjava.so              \
-              /opt/java/openjdk/lib/libjimage.so            \
-              /opt/java/openjdk/lib/libjli.so               \
-              /opt/java/openjdk/lib/libnet.so               \
-              /opt/java/openjdk/lib/libnio.so               \
-              /opt/java/openjdk/lib/libverify.so            \
-              /opt/java/openjdk/lib/libzip.so               \
-              /opt/java/openjdk/lib/modules                 \
-              /opt/java/openjdk/lib/tzdb.dat                /opt/java/lib/
-COPY --from=0 /opt/java/openjdk/lib/server/libjvm.so        /opt/java/lib/server/
-COPY --from=0 /groovy                                       \
-              /startGroovy                                  \
-              /usr/bin/awk                                  \
-              /usr/bin/basename                             \
-              /usr/bin/cut                                  \
-              /usr/bin/dirname                              \
-              /usr/bin/env                                  \
-              /usr/bin/expr                                 /usr/bin/
-COPY --from=0 /groovy-starter.conf                          /usr/conf/
-COPY --from=0 /groovy-$VER.jar                              \
-              /usr/lib/libacl.so.1                          \
-              /usr/lib/libattr.so.1                         \
-              /usr/lib/libcrypto.so.3                       \
-              /usr/lib/libskarnet.so.2.14                   \
-              /usr/lib/libutmps.so.0.1                      /usr/lib/
+COPY --from=0 /bin/uname                  /bin/
+COPY --from=0 /usr/bin/dash               /bin/sh
+COPY --from=0 /groovy                     \
+              /startGroovy                \
+              /usr/bin/awk                \
+              /usr/bin/basename           \
+              /usr/bin/cut                \
+              /usr/bin/dirname            \
+              /usr/bin/env                \
+              /usr/bin/expr               /usr/bin/
+COPY --from=0 /groovy-starter.conf        /usr/conf/
+COPY --from=0 /groovy-$VER.jar            \
+              /usr/lib/libacl.so.1        \
+              /usr/lib/libattr.so.1       \
+              /usr/lib/libcrypto.so.3     \
+              /usr/lib/libskarnet.so.2.14 \
+              /usr/lib/libutmps.so.0.1    /usr/lib/
 
 ENV JAVA_HOME='/opt/java'
 
diff --git a/langs/harbour/harbour.c b/langs/harbour/harbour.c
old mode 100755
new mode 100644
diff --git a/langs/hare/Dockerfile b/langs/hare/Dockerfile
index 2349957c..2d7a33f2 100644
--- a/langs/hare/Dockerfile
+++ b/langs/hare/Dockerfile
@@ -1,33 +1,34 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl qbe scdoc
 
 ENV VER=0.24.2
 
-RUN curl https://git.sr.ht/~sircmpwn/harec/archive/$VER.tar.gz | tar xz
+RUN curl -#L https://git.sr.ht/~sircmpwn/harec/archive/$VER.tar.gz \
+  | tar xz --strip-components 1
 
-RUN cd harec-$VER                 \
- && mv configs/linux.mk config.mk \
+RUN mv configs/linux.mk config.mk \
  && make install
 
-RUN curl https://git.sr.ht/~sircmpwn/hare/archive/$VER.tar.gz | tar xz
+RUN curl -# https://git.sr.ht/~sircmpwn/hare/archive/$VER.tar.gz | tar xz
 
-RUN cd hare-$VER                  \
- && mv configs/linux.mk config.mk \
+WORKDIR /hare-$VER
+
+RUN mv configs/linux.mk config.mk \
  && make && make install
 
-COPY harewrapper.c /
+COPY hare.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/harewrapper -s -static harewrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/hare -s /hare.c
 
 FROM codegolf/lang-base
 
 COPY --from=0 /lib/ld-musl-x86_64.so.1   /lib/
 COPY --from=0 /usr/bin/as                \
-              /usr/bin/harewrapper       \
+              /usr/bin/hare              \
               /usr/bin/ld                \
               /usr/bin/qbe               /usr/bin/
-COPY --from=0 /usr/lib/libbfd-2.43.1.so  \
+COPY --from=0 /usr/lib/libbfd-2.44.so    \
               /usr/lib/libctf.so.0       \
               /usr/lib/libjansson.so.4   \
               /usr/lib/libsframe.so.1    \
@@ -37,6 +38,6 @@ COPY --from=0 /usr/local/bin/hare        \
               /usr/local/bin/harec       /usr/local/bin/
 COPY --from=0 /usr/local/src/hare/stdlib /usr/local/src/hare/stdlib
 
-ENTRYPOINT ["harewrapper"]
+ENTRYPOINT ["hare"]
 
 CMD ["--version"]
diff --git a/langs/hare/harewrapper.c b/langs/hare/hare.c
similarity index 100%
rename from langs/hare/harewrapper.c
rename to langs/hare/hare.c
diff --git a/langs/haskell/Dockerfile b/langs/haskell/Dockerfile
index bd3bcd42..891d4a34 100644
--- a/langs/haskell/Dockerfile
+++ b/langs/haskell/Dockerfile
@@ -1,18 +1,18 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 ENV VER=9.8.2
 
 RUN apk add --no-cache build-base ghc=$VER-r2
 
-RUN echo "#define VERSION \"$VER\"" > haskell.h
+RUN echo "#define VER \"$VER\"" > haskell.h
 
-COPY haskell.c .
+COPY haskell.c /
 
-RUN gcc -Wall -Werror -Wextra -o haskell -s haskell.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/haskell -s haskell.c
 
 RUN find /usr/lib -name '*.a' -delete
 
-# Delete all libs except ffi, ghc, gmp, ncursesw
+# Delete all libraries except 'ffi', 'ghc', 'gmp', and 'ncursesw'.
 RUN find /usr/lib -not \(                    \
             -path '/usr/lib'                 \
         -or -path '/usr/lib/ghc-*'           \
@@ -21,7 +21,7 @@ RUN find /usr/lib -not \(                    \
         -or -path '/usr/lib/libncursesw.so*' \
     \) -delete
 
-# Delete unused libs and binaries.
+# Delete unused binaries and libraries.
 RUN rm -r /usr/lib/ghc-$VER/lib/bin                           \
           /usr/lib/ghc-$VER/lib/x86_64-linux-ghc-$VER/Cabal-* \
           /usr/lib/ghc-$VER/lib/x86_64-linux-ghc-$VER/ghci-*  \
@@ -31,8 +31,8 @@ RUN rm -r /usr/lib/ghc-$VER/lib/bin                           \
 FROM codegolf/lang-base
 
 COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /haskell                 \
-              /usr/bin/gcc             /usr/bin/
+COPY --from=0 /usr/bin/cc              \
+              /usr/bin/haskell         /usr/bin/
 COPY --from=0 /usr/lib                 /usr/lib
 
 ENTRYPOINT ["haskell"]
diff --git a/langs/haskell/haskell.c b/langs/haskell/haskell.c
index 11379cec..afd42084 100644
--- a/langs/haskell/haskell.c
+++ b/langs/haskell/haskell.c
@@ -7,35 +7,38 @@
 
 #define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
+const char* haskell = "/usr/lib/ghc-" VER "/bin/runghc-" VER, *code = "/tmp/code.hs";
+
 int main(int argc, char* argv[]) {
-    if (argc > 1 && strcmp(argv[1], "--version") == 0) {
-        puts(VERSION);
-        return 0;
+    if (!strcmp(argv[1], "--version")) {
+        execv(haskell, argv);
+        ERR_AND_EXIT("execv");
     }
 
-    FILE* fp = fopen("/tmp/code.hs", "w");
-    if (!fp)
+    FILE* fp;
+
+    if (!(fp = fopen(code, "w")))
         ERR_AND_EXIT("fopen");
 
     char buffer[4096];
     ssize_t nbytes;
 
-    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0)
-        if (fwrite(buffer, sizeof(char), nbytes, fp) != (size_t)nbytes)
+    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))))
+        if (fwrite(buffer, sizeof(char), nbytes, fp) != (size_t) nbytes)
             ERR_AND_EXIT("fwrite");
 
-    if (fclose(fp) < 0)
+    if (fclose(fp))
         ERR_AND_EXIT("fclose");
 
     int hargc = argc + 3;
-    char **hargv = malloc(hargc * sizeof(char*));
-    hargv[0] = "runghc-" VERSION;
+    char** hargv = malloc(hargc * sizeof(char*));
+    hargv[0] = (char*) haskell;
     hargv[1] = "--ghc-arg=-fdiagnostics-color=always";
-    hargv[2] = "/tmp/code.hs";
+    hargv[2] = (char*) code;
     hargv[3] = "--";
     memcpy(&hargv[4], &argv[2], (argc - 2) * sizeof(char*));
     hargv[hargc - 1] = NULL;
 
-    execv("/usr/lib/ghc-" VERSION "/bin/runghc-" VERSION, hargv);
+    execv(haskell, hargv);
     ERR_AND_EXIT("execv");
 }
diff --git a/langs/haxe/Dockerfile b/langs/haxe/Dockerfile
index 9cbb44f3..294f53b4 100644
--- a/langs/haxe/Dockerfile
+++ b/langs/haxe/Dockerfile
@@ -4,16 +4,16 @@ RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
     apt-get install --yes curl gcc
 
-ENV VER=4.3.6
+ENV VER=4.3.7
 
 WORKDIR /haxe
 
 RUN curl -#L https://github.com/HaxeFoundation/haxe/releases/download/$VER/haxe-$VER-linux64.tar.gz \
   | tar xz --strip-components 1
 
-COPY haxewrapper.c /
+COPY haxe.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/haxewrapper -s /haxewrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/haxewrapper -s /haxe.c
 
 FROM codegolf/lang-base
 
diff --git a/langs/haxe/haxewrapper.c b/langs/haxe/haxe.c
similarity index 100%
rename from langs/haxe/haxewrapper.c
rename to langs/haxe/haxe.c
diff --git a/langs/hexagony/Dockerfile b/langs/hexagony/Dockerfile
index 740d671b..4ef94835 100644
--- a/langs/hexagony/Dockerfile
+++ b/langs/hexagony/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:9.0.200-alpine3.21 AS builder
+FROM mcr.microsoft.com/dotnet/sdk:9.0.301-alpine3.22-amd64 AS builder
 
 RUN apk add --no-cache build-base zlib-dev
 
diff --git a/langs/hush/Dockerfile b/langs/hush/Dockerfile
index 6c2f1b25..86f3bdfe 100644
--- a/langs/hush/Dockerfile
+++ b/langs/hush/Dockerfile
@@ -1,13 +1,18 @@
-FROM rust:1.85.1-alpine3.21 AS builder
+FROM rust:1.88.0-slim-bookworm AS builder
 
-RUN apk add --no-cache musl-dev
+ENV CRT='x86_64-unknown-linux-musl' VER=0.1.4
 
-RUN cargo install hush --root /usr --version 0.1.4
+RUN rustup target add $CRT
+
+RUN cargo install hush \
+    --root /usr        \
+    --target $CRT      \
+    --version $VER     \
+ && strip /usr/bin/hush
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /usr/bin/hush            /usr/bin/
+COPY --from=0 /usr/bin/hush /usr/bin/
 
 ENTRYPOINT ["hush"]
 
diff --git a/langs/hy/Dockerfile b/langs/hy/Dockerfile
index ba9a3ffb..6c00a3f0 100644
--- a/langs/hy/Dockerfile
+++ b/langs/hy/Dockerfile
@@ -1,17 +1,16 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl libffi-dev openssl-dev zlib-dev
 
-ENV PYVER=3.13.2 VER=1.0.0
+ENV PYVER=3.13.5 VER=1.1.0
 
-RUN curl -# https://www.python.org/ftp/python/$PYVER/Python-$PYVER.tar.xz | tar xJ
+RUN curl -#L https://www.python.org/ftp/python/$PYVER/Python-$PYVER.tar.xz \
+  | tar xJ --strip-components 1
 
-WORKDIR /Python-$PYVER
-
-RUN ./configure                \
+RUN CC='cc' ./configure        \
     --disable-test-modules     \
     --prefix=/usr              \
-    --with-lto                 \
+    --with-lto=thin            \
     --without-static-libpython \
  && make -j`nproc` install     \
  && strip /usr/bin/python3.13  \
diff --git a/langs/iogii/Dockerfile b/langs/iogii/Dockerfile
index d100341b..57004958 100644
--- a/langs/iogii/Dockerfile
+++ b/langs/iogii/Dockerfile
@@ -1,26 +1,24 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache gcc musl-dev
 
-ENV BIN='/usr/local/bin/iogii' VER=0.3
+ENV BIN='/usr/local/bin/iogii' VER=1.1
 
-RUN wget -cO $BIN https://golfscript.com/iogii/iogii-$VER-beta \
+RUN wget -cO $BIN https://golfscript.com/iogii/iogii-$VER \
  && chmod +x $BIN
 
+# Rewrite shebang to bypass /usr/bin/env and go directly to /usr/bin/ruby.
+RUN sed -i '1c#!/usr/bin/ruby' $BIN
+
 COPY iogii.c /
 
 RUN gcc -Wall -Werror -Wextra -o /usr/bin/iogii -s -static iogii.c
 
 FROM codegolf/lang-ruby
 
-COPY --from=0 /usr/bin/env         \
-              /usr/bin/iogii       /usr/bin/
+COPY --from=0 /usr/bin/iogii       /usr/bin/
 COPY --from=0 /usr/local/bin/iogii /usr/local/bin/
 
-FROM codegolf/lang-base
-
-COPY --from=1 . /
-
 ENTRYPOINT ["iogii"]
 
 CMD ["--version"]
diff --git a/langs/j/Dockerfile b/langs/j/Dockerfile
index 6d0a0575..9e9fcb1e 100644
--- a/langs/j/Dockerfile
+++ b/langs/j/Dockerfile
@@ -1,34 +1,35 @@
+ARG VER=9.6
+
 FROM debian:bookworm-slim AS builder
 
-RUN apt-get update && apt-get install -y curl gcc
+ARG VER
+
+RUN apt-get update                   \
+ && DEBIAN_FRONTEND='noninteractive' \
+    apt-get install --yes curl
 
-RUN curl https://www.jsoftware.com/download/j9.6/install/j9.6.1_linux64.tar.gz \
+RUN curl -# https://www.jsoftware.com/download/j$VER/install/j$VER.3_linux64.tar.gz \
   | tar xz
 
 # FIXME Don't run updatejs.sh as it tries to update to AVX512 because my build
 # laptop supports it. But prod servers do not. Is there an override flag?
-# RUN cd j9.6 && ./updateje.sh
-
-COPY j.c /
-
-RUN gcc -s -o j j.c
+# RUN cd j$VER && ./updateje.sh
 
 FROM codegolf/lang-base
 
+ARG VER
+
 COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6       \
               /lib/x86_64-linux-gnu/libdl.so.2      \
               /lib/x86_64-linux-gnu/libgcc_s.so.1   \
               /lib/x86_64-linux-gnu/libm.so.6       \
               /lib/x86_64-linux-gnu/libpthread.so.0 /lib/
 COPY --from=0 /lib64/ld-linux-x86-64.so.2           /lib64/
-COPY --from=0 /j                                    /usr/bin/
-COPY --from=0 /j9.6/addons/dev                      /opt/j/addons/dev
-COPY --from=0 /j9.6/bin/jconsole                    \
-              /j9.6/bin/libgmp.so                   \
-              /j9.6/bin/libj.so                     \
-              /j9.6/bin/profile.ijs                 /opt/j/bin/
-COPY --from=0 /j9.6/system                          /opt/j/system
-
-ENTRYPOINT ["j"]
-
-CMD ["-v"]
+COPY --from=0 /j$VER/addons/dev                     /opt/j/addons/dev
+COPY --from=0 /j$VER/bin/jconsole                   \
+              /j$VER/bin/libgmp.so                  \
+              /j$VER/bin/libj.so                    \
+              /j$VER/bin/profile.ijs                /opt/j/bin/
+COPY --from=0 /j$VER/system                         /opt/j/system
+
+ENTRYPOINT ["/opt/j/bin/jconsole"]
diff --git a/langs/j/j.c b/langs/j/j.c
deleted file mode 100644
index 5b7febdc..00000000
--- a/langs/j/j.c
+++ /dev/null
@@ -1,27 +0,0 @@
-#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
-
-int main (int argc, char *argv[]) {
-    char buffer[4096];
-    ssize_t nbytes;
-    FILE *fp = fopen("/tmp/code.ijs", "w");
-
-    if (fp == 0)
-        return 1;
-
-    if (argc > 1 && strcmp(argv[1], "-v") == 0) {
-        argv[1] = "/tmp/code.ijs";
-        fputs("echo JLIB", fp);
-    }
-    else {
-        while ((nbytes = fread(buffer, sizeof(char), sizeof(buffer), stdin)) > 0)
-            if (fwrite(buffer, sizeof(char), nbytes, fp) != nbytes)
-                return 2;
-    }
-
-    fclose(fp);
-
-    execv("/opt/j/bin/jconsole", argv);
-    perror("execv");
-}
diff --git a/langs/janet/Dockerfile b/langs/janet/Dockerfile
index 7c438a25..50892ea6 100644
--- a/langs/janet/Dockerfile
+++ b/langs/janet/Dockerfile
@@ -1,18 +1,18 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl
+RUN apk add --no-cache curl gcc make musl-dev
 
-ENV VER=1.38.0
+ENV LDFLAGS='-static' VER=1.38.0
 
 RUN curl -#L https://github.com/janet-lang/janet/archive/refs/tags/v$VER.tar.gz \
   | tar xz --strip-components 1
 
-RUN LDFLAGS='-static' make build/janet \
- && strip /build/janet
+RUN make build/janet install \
+ && strip /usr/local/bin/janet
 
 FROM codegolf/lang-base
 
-COPY --from=0 /build/janet /usr/bin/
+COPY --from=0 /usr/local/bin/janet /usr/bin/
 
 ENTRYPOINT ["janet"]
 
diff --git a/langs/java/Dockerfile b/langs/java/Dockerfile
index c69194a4..ac8fb7f1 100644
--- a/langs/java/Dockerfile
+++ b/langs/java/Dockerfile
@@ -1,13 +1,33 @@
-# Also bump CJam, Groovy, Kotlin, Scala, Vyxal.
-FROM eclipse-temurin:24_36-jdk-alpine-3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base gmp-dev
+RUN apk add --no-cache alsa-lib-dev autoconf bash build-base cups-dev curl fontconfig-dev \
+                       libxrandr-dev libxrender-dev libxt-dev libxtst-dev musl-locales zip
 
-RUN jlink                                 \
-    --add-modules java.base,jdk.compiler  \
-    --compress zip-9                      \
-    --module-path /opt/java/openjdk/jmods \
-    --output /opt/jdk
+# Also rebuild CJam, Groovy, Kotlin, Scala, Vyxal.
+ENV VER=24.0.2 BUILD=12
+
+RUN curl -#L https://github.com/adoptium/temurin24-binaries/releases/download/jdk-$VER+$BUILD/OpenJDK24U-jdk_x64_alpine-linux_hotspot_${VER}_$BUILD.tar.gz \
+  | tar xz --directory /tmp --strip-components 1
+
+RUN curl -#L https://github.com/adoptium/temurin24-binaries/releases/download/jdk-$VER+$BUILD/OpenJDK24U-jdk-sources_${VER}_$BUILD.tar.gz \
+  | tar xz --strip-components 1
+
+RUN bash configure                     \
+    --enable-headless-only             \
+    --enable-jvm-feature-link-time-opt \
+    --enable-jvm-feature-management    \
+    --enable-jvm-feature-opt-size      \
+    --with-boot-jdk=/tmp               \
+    --with-conf-name=release           \
+    --with-jobs=`nproc`                \
+ && make images
+
+RUN /build/release/images/jdk/bin/jlink           \
+    --add-modules java.base,java.se               \
+    --add-modules jdk.compiler,jdk.unsupported    \
+    --compress zip-9                              \
+    --module-path /build/release/images/jdk/jmods \
+    --output /opt/java
 
 COPY java.c /
 
@@ -15,10 +35,25 @@ RUN gcc -Wall -Werror -Wextra -o /usr/bin/java -s -static java.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /opt/jdk                 /opt/jdk
-COPY --from=0 /usr/bin/java            /usr/bin/
-COPY --from=0 /usr/lib/libz.so.1       /usr/lib/
+COPY --from=0 /lib/ld-musl-x86_64.so.1              /lib/
+COPY --from=0 /opt/java/bin/java                    \
+              /opt/java/bin/javac                   /opt/java/bin/
+COPY --from=0 /opt/java/conf/security/java.security /opt/java/conf/security/
+COPY --from=0 /opt/java/lib/jrt-fs.jar              \
+              /opt/java/lib/jvm.cfg                 \
+              /opt/java/lib/libjava.so              \
+              /opt/java/lib/libjimage.so            \
+              /opt/java/lib/libjli.so               \
+              /opt/java/lib/libmanagement.so        \
+              /opt/java/lib/libnet.so               \
+              /opt/java/lib/libnio.so               \
+              /opt/java/lib/libverify.so            \
+              /opt/java/lib/libzip.so               \
+              /opt/java/lib/modules                 \
+              /opt/java/lib/tzdb.dat                /opt/java/lib/
+COPY --from=0 /opt/java/lib/server/libjvm.so        /opt/java/lib/server/
+COPY --from=0 /usr/bin/java                         /usr/bin/
+COPY --from=0 /usr/lib/libz.so.1                    /usr/lib/
 
 ENTRYPOINT ["java"]
 
diff --git a/langs/java/java.c b/langs/java/java.c
index 832b281c..6c5aa53e 100644
--- a/langs/java/java.c
+++ b/langs/java/java.c
@@ -8,7 +8,7 @@
 
 #define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
-const char* java = "/opt/jdk/bin/java", *javac = "/opt/jdk/bin/javac", *code = "code.java";
+const char* java = "/opt/java/bin/java", *javac = "/opt/java/bin/javac", *code = "code.java";
 
 int main(int argc, char* argv[]) {
     if (!strcmp(argv[1], "--version")) {
diff --git a/langs/javascript/Dockerfile b/langs/javascript/Dockerfile
index 349648c3..35a225ab 100644
--- a/langs/javascript/Dockerfile
+++ b/langs/javascript/Dockerfile
@@ -4,7 +4,7 @@ RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
     apt-get install --yes curl g++ git pkg-config procps python3
 
-ENV PATH=/depot_tools:$PATH VER=13.4.114.21
+ENV PATH=/depot_tools:$PATH VER=13.8.500258
 
 RUN git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git \
  && fetch v8
diff --git a/langs/jq/Dockerfile b/langs/jq/Dockerfile
index 822b64af..85ebcb7b 100644
--- a/langs/jq/Dockerfile
+++ b/langs/jq/Dockerfile
@@ -1,20 +1,21 @@
-FROM alpine:3.21 AS builder
-
-ENV VER=1.7.1
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl
 
-RUN curl -L https://github.com/jqlang/jq/releases/download/jq-$VER/jq-$VER.tar.gz \
+ENV VER=1.8.1
+
+RUN curl -#L https://github.com/jqlang/jq/releases/download/jq-$VER/jq-$VER.tar.gz \
   | tar xz --strip-components 1
 
-RUN ./configure              \
-    --with-oniguruma=builtin \
- && make LDFLAGS=-all-static \
- && strip jq
+RUN ./configure                        \
+    --prefix=/usr                      \
+    --with-oniguruma=builtin           \
+ && make LDFLAGS='-all-static' install \
+ && strip /usr/bin/jq
 
 FROM codegolf/lang-base
 
-COPY --from=0 ./jq /usr/bin/
+COPY --from=0 /usr/bin/jq /usr/bin/
 
 ENTRYPOINT ["jq"]
 
diff --git a/langs/julia/Dockerfile b/langs/julia/Dockerfile
index 17cefa10..ae4e6f60 100644
--- a/langs/julia/Dockerfile
+++ b/langs/julia/Dockerfile
@@ -1,13 +1,17 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache autoconf build-base cmake curl gfortran linux-headers python3
 
-ENV CXXFLAGS='-flto' LDFLAGS='-flto' VER=1.11.4
+ENV CXXFLAGS='-flto' LDFLAGS='-flto' VER=1.11.6
 
 RUN curl -#L https://github.com/JuliaLang/julia/releases/download/v$VER/julia-$VER.tar.gz | tar xz
 
-RUN make -C julia-$VER -j`nproc` prefix=/usr MARCH=x86-64-v3 install \
- && strip -s /usr/bin/julia /usr/lib/julia/*.so
+WORKDIR /julia-$VER
+
+RUN make -j`nproc` prefix=/usr MARCH=x86-64-v3 install
+
+RUN strip -s /usr/bin/julia \
+             /usr/lib/julia/*.so
 
 WORKDIR /usr/lib/julia
 # Delete unused libraries.
diff --git a/langs/k/Dockerfile b/langs/k/Dockerfile
index d68c9913..78e5594a 100644
--- a/langs/k/Dockerfile
+++ b/langs/k/Dockerfile
@@ -1,19 +1,18 @@
-FROM alpine:3.21 AS builder
-
-ENV VERSION=544d014afd
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl
 
-RUN curl -L https://codeberg.org/ngn/k/archive/$VERSION.tar.gz | tar xz \
- && sed -i s/march=native/march=x86-64-v3/ k/makefile                   \
- && make -C k CC="gcc -static"
+ENV CC='gcc -static' VER=544d014afd
+
+WORKDIR /usr/bin
 
-COPY kwrapper.c /
+RUN curl -#L https://codeberg.org/ngn/k/archive/$VER.tar.gz \
+  | tar xz --strip-components 1
 
-RUN gcc -static -s -o kwrapper kwrapper.c
+RUN sed -i s/march=native/march=x86-64-v3/ makefile | make
 
 FROM codegolf/lang-base
 
-COPY --from=0 /k/k /kwrapper /usr/bin/
+COPY --from=0 /usr/bin/k /usr/bin/
 
-ENTRYPOINT ["kwrapper"]
+ENTRYPOINT ["k"]
diff --git a/langs/k/kwrapper.c b/langs/k/kwrapper.c
deleted file mode 100644
index 60b9114b..00000000
--- a/langs/k/kwrapper.c
+++ /dev/null
@@ -1,21 +0,0 @@
-#include <stdio.h>
-#include <string.h>
-#include <unistd.h>
-
-int main (int argc, char *argv[]) {
-    char buffer[4096];
-    ssize_t nbytes;
-    FILE *fp = fopen("/tmp/code.k", "w");
-
-    if (fp == 0)
-        return 1;
-
-    while ((nbytes = fread(buffer, sizeof(char), sizeof(buffer), stdin)) > 0)
-        if (fwrite(buffer, sizeof(char), nbytes, fp) != nbytes)
-            return 2;
-
-    fclose(fp);
-
-    execv("/usr/bin/k", argv);
-    perror("execv");
-}
diff --git a/langs/kotlin/Dockerfile b/langs/kotlin/Dockerfile
index 19e8b886..a7aacb93 100644
--- a/langs/kotlin/Dockerfile
+++ b/langs/kotlin/Dockerfile
@@ -1,49 +1,30 @@
-FROM eclipse-temurin:24_36-jre-alpine-3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache bash curl
 
-ENV VER=2.1.20
+ENV VER=2.2.0
 
-RUN curl -#Lo kotlin.zip https://github.com/JetBrains/kotlin/releases/download/v$VER/kotlin-compiler-$VER.zip
-
-RUN unzip kotlin.zip && cp -r /kotlinc/* /usr/
+RUN curl -#Lo kotlin.zip https://github.com/JetBrains/kotlin/releases/download/v$VER/kotlin-compiler-$VER.zip \
+ && unzip kotlin
 
+WORKDIR /kotlinc
 # Remove some unneeded libraries.
-RUN rm -f /usr/lib/android*.jar \
-          /usr/lib/*sources.jar \
-          /usr/lib/*test*.jar
-
-FROM codegolf/lang-base
-
-COPY --from=0 /bin/bash /bin/uname                          /bin/
-COPY --from=0 /lib/ld-musl-x86_64.so.1                      /lib/
-COPY --from=0 /opt/java/openjdk/bin/java                    /opt/java/bin/
-COPY --from=0 /opt/java/openjdk/conf/security/java.security /opt/java/conf/security/
-COPY --from=0 /opt/java/openjdk/lib/jrt-fs.jar              \
-              /opt/java/openjdk/lib/jvm.cfg                 \
-              /opt/java/openjdk/lib/libjava.so              \
-              /opt/java/openjdk/lib/libjimage.so            \
-              /opt/java/openjdk/lib/libjli.so               \
-              /opt/java/openjdk/lib/libnet.so               \
-              /opt/java/openjdk/lib/libnio.so               \
-              /opt/java/openjdk/lib/libverify.so            \
-              /opt/java/openjdk/lib/libzip.so               \
-              /opt/java/openjdk/lib/modules                 /opt/java/lib/
-COPY --from=0 /opt/java/openjdk/lib/server/libjvm.so        /opt/java/lib/server/
-COPY --from=0 /usr/build.txt                                /usr/
-COPY --from=0 /usr/bin/dirname                              \
-              /usr/bin/env                                  \
-              /usr/bin/kotlin                               \
-              /usr/bin/kotlinc                              /usr/bin/
-COPY --from=0 /usr/lib/*.jar                                \
-              /usr/lib/libacl.so.1                          \
-              /usr/lib/libattr.so.1                         \
-              /usr/lib/libcrypto.so.3                       \
-              /usr/lib/libncursesw.so.6                     \
-              /usr/lib/libreadline.so.8                     \
-              /usr/lib/libskarnet.so.2.14                   \
-              /usr/lib/libutmps.so.0.1                      \
-              /usr/lib/libz.so.1                            /usr/lib/
+RUN rm -f lib/android*.* \
+          lib/*sources.* \
+          lib/*test*.*   \
+ && cp -r . /usr/
+
+FROM codegolf/lang-java
+
+COPY --from=0 /bin/bash /bin/uname      /bin/
+COPY --from=0 /usr/build.txt            /usr/
+COPY --from=0 /usr/bin/dirname          \
+              /usr/bin/env              \
+              /usr/bin/kotlin           \
+              /usr/bin/kotlinc          /usr/bin/
+COPY --from=0 /usr/lib/*.jar            \
+              /usr/lib/libncursesw.so.6 \
+              /usr/lib/libreadline.so.8 /usr/lib/
 
 ENV JAVA_HOME='/opt/java'
 
diff --git a/langs/lua/Dockerfile b/langs/lua/Dockerfile
index e8bbdd50..19cb1e85 100644
--- a/langs/lua/Dockerfile
+++ b/langs/lua/Dockerfile
@@ -1,21 +1,23 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl
+RUN apk add --no-cache curl gcc make musl-dev
+
+ENV VER=5.4.8
 
 # Also rebuild Fennel.
-RUN curl https://www.lua.org/ftp/lua-5.4.7.tar.gz | tar xz
+RUN curl -#L https://www.lua.org/ftp/lua-$VER.tar.gz \
+  | tar xz --strip-components 1
 
-RUN cd lua-*          \
- && make generic      \
-    AR='gcc-ar rcu'   \
-    MYCFLAGS=-flto    \
-    MYLDFLAGS=-static \
-    RANLIB=gcc-ranlib \
- && strip src/lua
+RUN make generic install \
+    AR='gcc-ar rcu'      \
+    MYCFLAGS='-flto'     \
+    MYLDFLAGS='-static'  \
+    RANLIB='gcc-ranlib'  \
+ && strip /usr/local/bin/lua
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lua-*/src/lua /usr/bin/
+COPY --from=0 /usr/local/bin/lua /usr/bin/
 
 ENTRYPOINT ["lua"]
 
diff --git a/langs/nim/Dockerfile b/langs/nim/Dockerfile
index c4798da5..18f26c85 100644
--- a/langs/nim/Dockerfile
+++ b/langs/nim/Dockerfile
@@ -1,9 +1,8 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add -X https://dl-cdn.alpinelinux.org/alpine/edge/testing \
-    --no-cache curl dash gcc musl-dev pcre tcc tcc-libs-static
+RUN apk add --no-cache curl dash gcc musl-dev pcre tcc tcc-libs-static
 
-ENV VER=2.2.2
+ENV VER=2.2.4
 
 RUN curl -#L https://nim-lang.org/download/nim-$VER.tar.xz \
   | tar xJ --strip-components 1
@@ -35,7 +34,7 @@ COPY --from=0 /usr/lib/crt1.o          \
               /usr/lib/librt.a         \
               /usr/lib/libtcc.so       /usr/lib/
 COPY --from=0 /nim/lib                 /usr/lib/nim/lib
-COPY --from=0 /usr/lib/tcc             /usr/lib/tcc
+COPY --from=0 /usr/lib/tcc/libtcc1.a   /usr/lib/tcc/
 
 COPY nim.cfg /etc/nim/
 
diff --git a/langs/ocaml/Dockerfile b/langs/ocaml/Dockerfile
index dc5848e8..986361ee 100644
--- a/langs/ocaml/Dockerfile
+++ b/langs/ocaml/Dockerfile
@@ -1,12 +1,11 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl
+RUN apk add --no-cache curl gcc make musl-dev
 
 ENV VER=5.3.0
 
-RUN curl -#L https://github.com/ocaml/ocaml/archive/refs/tags/$VER.tar.gz | tar xz
-
-WORKDIR /ocaml-$VER
+RUN curl -#L https://github.com/ocaml/ocaml/archive/refs/tags/$VER.tar.gz \
+  | tar xz --strip-components 1
 
 RUN ./configure              \
     --prefix=/usr            \
diff --git a/langs/odin/Dockerfile b/langs/odin/Dockerfile
index c9a32237..1a089535 100644
--- a/langs/odin/Dockerfile
+++ b/langs/odin/Dockerfile
@@ -1,8 +1,8 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache clang curl dash llvm-dev make
 
-ENV VER=2025-03
+ENV CPPFLAGS='-march=x86-64-v3' VER=2025-07
 
 RUN curl -#L https://github.com/odin-lang/Odin/archive/refs/tags/dev-$VER.tar.gz \
   | tar xz --directory /usr/local/lib --strip-components 1
@@ -16,46 +16,46 @@ RUN ln -s /usr/local/lib/base \
 WORKDIR /usr/local/lib
 
 RUN make -j`nproc` release \
- && strip /usr/local/lib/odin
+ && strip odin
 
-COPY odinwrapper.c /
+COPY odin.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/odinwrapper -s -static /odinwrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/odinwrapper -s /odin.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /usr/bin/dash                          /bin/sh
-COPY --from=0 /lib/ld-musl-x86_64.so.1               /lib/
-COPY --from=0 /usr/bin/clang                         \
-              /usr/bin/ld                            \
-              /usr/bin/odinwrapper                   /usr/bin/
-COPY --from=0 /usr/lib/crt1.o                        \
-              /usr/lib/crti.o                        \
-              /usr/lib/crtn.o                        \
-              /usr/lib/libbfd-2.43.1.so              \
-              /usr/lib/libc.so                       \
-              /usr/lib/libclang-cpp.so.19.1          \
-              /usr/lib/libctf.so.0                   \
-              /usr/lib/libffi.so.8                   \
-              /usr/lib/libgcc_s.so                   \
-              /usr/lib/libgcc_s.so.1                 \
-              /usr/lib/libjansson.so.4               \
-              /usr/lib/libLLVM.so.19.1               \
-              /usr/lib/liblzma.so.5                  \
-              /usr/lib/libm.a                        \
-              /usr/lib/libsframe.so.1                \
-              /usr/lib/libssp_nonshared.a            \
-              /usr/lib/libstdc++.so.6                \
-              /usr/lib/libxml2.so.2                  \
-              /usr/lib/libz.so.1                     \
-              /usr/lib/libzstd.so.1                  /usr/lib/
-COPY --from=0 /usr/local/lib/odin                    /usr/local/bin/
-COPY --from=0 /usr/local/bin/base                    /usr/local/bin/base
-COPY --from=0 /usr/local/bin/core                    /usr/local/bin/core
-COPY --from=0 /usr/local/bin/vendor                  /usr/local/bin/vendor
-COPY --from=0 /usr/lib/gcc/x86_64*/14.2.0/crtbegin.o \
-              /usr/lib/gcc/x86_64*/14.2.0/crtend.o   \
-              /usr/lib/gcc/x86_64*/14.2.0/libgcc.a   /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/
+COPY --from=0 /usr/bin/dash                    /bin/sh
+COPY --from=0 /lib/ld-musl-x86_64.so.1         /lib/
+COPY --from=0 /usr/bin/clang                   \
+              /usr/bin/ld                      \
+              /usr/bin/odinwrapper             /usr/bin/
+COPY --from=0 /usr/lib/crt1.o                  \
+              /usr/lib/crti.o                  \
+              /usr/lib/crtn.o                  \
+              /usr/lib/libLLVM.so.20.1         \
+              /usr/lib/libbfd-2.44.so          \
+              /usr/lib/libc.so                 \
+              /usr/lib/libclang-cpp.so.20.1    \
+              /usr/lib/libctf.so.0             \
+              /usr/lib/libffi.so.8             \
+              /usr/lib/libgcc_s.so             \
+              /usr/lib/libgcc_s.so.1           \
+              /usr/lib/libjansson.so.4         \
+              /usr/lib/liblzma.so.5            \
+              /usr/lib/libm.a                  \
+              /usr/lib/libsframe.so.1          \
+              /usr/lib/libssp_nonshared.a      \
+              /usr/lib/libstdc++.so.6          \
+              /usr/lib/libxml2.so.2            \
+              /usr/lib/libz.so.1               \
+              /usr/lib/libzstd.so.1            /usr/lib/
+COPY --from=0 /usr/lib/gcc/*/14.2.0/crtbegin.o \
+              /usr/lib/gcc/*/14.2.0/crtend.o   \
+              /usr/lib/gcc/*/14.2.0/libgcc.a   /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/
+COPY --from=0 /usr/local/lib/odin              /usr/local/bin/
+COPY --from=0 /usr/local/bin/base              /usr/local/bin/base
+COPY --from=0 /usr/local/bin/core              /usr/local/bin/core
+COPY --from=0 /usr/local/bin/vendor            /usr/local/bin/vendor
 
 ENTRYPOINT ["odinwrapper"]
 
diff --git a/langs/odin/odinwrapper.c b/langs/odin/odin.c
similarity index 100%
rename from langs/odin/odinwrapper.c
rename to langs/odin/odin.c
diff --git a/langs/pascal/Dockerfile b/langs/pascal/Dockerfile
index 78d61b8b..ca2ff099 100644
--- a/langs/pascal/Dockerfile
+++ b/langs/pascal/Dockerfile
@@ -1,6 +1,6 @@
 ARG VER=3.2.2
 
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 ARG VER
 
@@ -17,20 +17,20 @@ FROM codegolf/lang-base
 
 ARG VER
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1                         /lib/
-COPY --from=0 /usr/bin/fpc                                     \
-              /usr/bin/ld                                      \
-              /usr/bin/pascal                                  \
-              /usr/bin/ppcx64                                  /usr/bin/
-COPY --from=0 /usr/lib/libbfd-2.43.1.so                        \
-              /usr/lib/libctf.so.0                             \
-              /usr/lib/libjansson.so.4                         \
-              /usr/lib/libsframe.so.1                          \
-              /usr/lib/libz.so.1                               \
-              /usr/lib/libzstd.so.1                            /usr/lib/
-COPY --from=0 /usr/lib/fpc/$VER/units/x86_64-linux/fcl-base    \
-              /usr/lib/fpc/$VER/units/x86_64-linux/rtl         \
-              /usr/lib/fpc/$VER/units/x86_64-linux/rtl-objpas  /usr/lib/fpc/$VER/units/x86_64-linux/rtl/
+COPY --from=0 /lib/ld-musl-x86_64.so.1                        /lib/
+COPY --from=0 /usr/bin/fpc                                    \
+              /usr/bin/ld                                     \
+              /usr/bin/pascal                                 \
+              /usr/bin/ppcx64                                 /usr/bin/
+COPY --from=0 /usr/lib/libbfd-2.44.so                         \
+              /usr/lib/libctf.so.0                            \
+              /usr/lib/libjansson.so.4                        \
+              /usr/lib/libsframe.so.1                         \
+              /usr/lib/libz.so.1                              \
+              /usr/lib/libzstd.so.1                           /usr/lib/
+COPY --from=0 /usr/lib/fpc/$VER/units/x86_64-linux/fcl-base   \
+              /usr/lib/fpc/$VER/units/x86_64-linux/rtl        \
+              /usr/lib/fpc/$VER/units/x86_64-linux/rtl-objpas /usr/lib/fpc/$VER/units/x86_64-linux/rtl/
 
 ENTRYPOINT ["pascal"]
 
diff --git a/langs/perl/Dockerfile b/langs/perl/Dockerfile
index a84b8b5d..f873476d 100644
--- a/langs/perl/Dockerfile
+++ b/langs/perl/Dockerfile
@@ -1,12 +1,13 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl
+RUN apk add --no-cache curl gcc make musl-dev
 
-RUN curl https://cpan.metacpan.org/authors/id/S/SH/SHAY/perl-5.40.1.tar.gz \
-  | tar xz && mv perl-* perl
+ENV VER=5.42.0
 
-RUN cd perl                           \
- && ./Configure                       \
+RUN curl -#L https://cpan.metacpan.org/authors/id/B/BO/BOOK/perl-$VER.tar.gz \
+  | tar xz --strip-components 1
+
+RUN ./Configure                       \
     -Accflags='                       \
         -DNO_LOCALE                   \
         -DNO_MATHOMS                  \
@@ -22,41 +23,45 @@ RUN cd perl                           \
     -Dsitearch=/usr/lib/perl          \
     -Dsitelib=/usr/lib/perl           \
     -Dvendorarch=/usr/lib/perl        \
- && make -j`nproc`                    \
- && strip -s perl
+ && make -j`nproc` install            \
+ && strip /usr/bin/perl
+
+# Downgrade Math::BigInt for now to fix some holes.
+RUN curl -#L https://cpan.metacpan.org/authors/id/P/PJ/PJACKLAM/Math-BigInt-2.003004.tar.gz \
+  | tar --strip-components 2 -xzC /usr/lib/perl Math-BigInt-2.003004/lib
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1         /lib/
-COPY --from=0 /perl/perl                       /usr/bin/
-COPY --from=0 /perl/lib/Carp.pm                \
-              /perl/lib/Config.pm              \
-              /perl/lib/DynaLoader.pm          \
-              /perl/lib/Exporter.pm            \
-              /perl/lib/XSLoader.pm            \
-              /perl/lib/bigint.pm              \
-              /perl/lib/bignum.pm              \
-              /perl/lib/builtin.pm             \
-              /perl/lib/constant.pm            \
-              /perl/lib/feature.pm             \
-              /perl/lib/integer.pm             \
-              /perl/lib/overload.pm            \
-              /perl/lib/overloading.pm         \
-              /perl/lib/strict.pm              \
-              /perl/lib/utf8.pm                \
-              /perl/lib/vars.pm                \
-              /perl/lib/warnings.pm            /usr/lib/perl/
-COPY --from=0 /perl/lib/Exporter/Heavy.pm      /usr/lib/perl/Exporter/
-COPY --from=0 /perl/lib/File/Glob.pm           /usr/lib/perl/File/
-COPY --from=0 /perl/lib/List/Util.pm           /usr/lib/perl/List/
-COPY --from=0 /perl/lib/Math/BigFloat.pm       \
-              /perl/lib/Math/BigInt.pm         /usr/lib/perl/Math/
-COPY --from=0 /perl/lib/Math/BigInt/Calc.pm    \
-              /perl/lib/Math/BigInt/Lib.pm     /usr/lib/perl/Math/BigInt/
-COPY --from=0 /perl/lib/Scalar/Util.pm         /usr/lib/perl/Scalar/
-COPY --from=0 /perl/lib/auto/File/Glob/Glob.so /usr/lib/perl/auto/File/Glob/
-COPY --from=0 /perl/lib/auto/List/Util/Util.so /usr/lib/perl/auto/List/Util/
-COPY --from=0 /perl/lib/warnings/register.pm   /usr/lib/perl/warnings/
+COPY --from=0 /lib/ld-musl-x86_64.so.1             /lib/
+COPY --from=0 /usr/bin/perl                        /usr/bin/
+COPY --from=0 /usr/lib/perl/Carp.pm                \
+              /usr/lib/perl/Config.pm              \
+              /usr/lib/perl/DynaLoader.pm          \
+              /usr/lib/perl/Exporter.pm            \
+              /usr/lib/perl/XSLoader.pm            \
+              /usr/lib/perl/bigint.pm              \
+              /usr/lib/perl/bignum.pm              \
+              /usr/lib/perl/builtin.pm             \
+              /usr/lib/perl/constant.pm            \
+              /usr/lib/perl/feature.pm             \
+              /usr/lib/perl/integer.pm             \
+              /usr/lib/perl/overload.pm            \
+              /usr/lib/perl/overloading.pm         \
+              /usr/lib/perl/strict.pm              \
+              /usr/lib/perl/utf8.pm                \
+              /usr/lib/perl/vars.pm                \
+              /usr/lib/perl/warnings.pm            /usr/lib/perl/
+COPY --from=0 /usr/lib/perl/Exporter/Heavy.pm      /usr/lib/perl/Exporter/
+COPY --from=0 /usr/lib/perl/File/Glob.pm           /usr/lib/perl/File/
+COPY --from=0 /usr/lib/perl/List/Util.pm           /usr/lib/perl/List/
+COPY --from=0 /usr/lib/perl/Math/BigFloat.pm       \
+              /usr/lib/perl/Math/BigInt.pm         /usr/lib/perl/Math/
+COPY --from=0 /usr/lib/perl/Math/BigInt/Calc.pm    \
+              /usr/lib/perl/Math/BigInt/Lib.pm     /usr/lib/perl/Math/BigInt/
+COPY --from=0 /usr/lib/perl/Scalar/Util.pm         /usr/lib/perl/Scalar/
+COPY --from=0 /usr/lib/perl/auto/File/Glob/Glob.so /usr/lib/perl/auto/File/Glob/
+COPY --from=0 /usr/lib/perl/auto/List/Util/Util.so /usr/lib/perl/auto/List/Util/
+COPY --from=0 /usr/lib/perl/warnings/register.pm   /usr/lib/perl/warnings/
 
 ENTRYPOINT ["perl"]
 
diff --git a/langs/php/Dockerfile b/langs/php/Dockerfile
index 92624106..832fa2a5 100644
--- a/langs/php/Dockerfile
+++ b/langs/php/Dockerfile
@@ -1,12 +1,11 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl
+RUN apk add --no-cache curl gcc make musl-dev
 
-ENV CFLAGS='-O2 -flto' LDFLAGS='-O2 -flto -all-static' VER=8.4.5
+ENV CFLAGS='-O2 -flto' LDFLAGS='-O2 -flto -all-static' VER=8.4.10
 
-RUN curl -#L https://php.net/distributions/php-$VER.tar.xz | tar xJ
-
-WORKDIR /php-$VER
+RUN curl -#L https://php.net/distributions/php-$VER.tar.xz \
+  | tar xJ --strip-components 1
 
 RUN LDFLAGS= ./configure       \
     --disable-all              \
diff --git a/langs/picat/Dockerfile b/langs/picat/Dockerfile
index ec875b3f..64b2c93c 100644
--- a/langs/picat/Dockerfile
+++ b/langs/picat/Dockerfile
@@ -4,7 +4,9 @@ RUN apt-get update                   \
  && DEBIAN_FRONTEND='noninteractive' \
     apt-get install --yes curl g++ make
 
-RUN curl -# https://picat-lang.org/download/picat38_src.tar.gz | tar xz
+ENV VER=387
+
+RUN curl -# https://picat-lang.org/download/picat${VER}_src.tar.gz | tar xz
 
 WORKDIR /Picat/emu
 
diff --git a/langs/powershell/Dockerfile b/langs/powershell/Dockerfile
index 80a33ca9..c188d1c0 100644
--- a/langs/powershell/Dockerfile
+++ b/langs/powershell/Dockerfile
@@ -1,6 +1,6 @@
-FROM mcr.microsoft.com/dotnet/sdk:9.0.200-alpine3.21 AS builder
+FROM mcr.microsoft.com/dotnet/sdk:9.0.301-alpine3.22-amd64 AS builder
 
-WORKDIR /work
+WORKDIR /powershell
 
 COPY powershell.cs powershell.csproj ./
 
diff --git a/langs/powershell/powershell.csproj b/langs/powershell/powershell.csproj
index 7a2727c1..0733df6c 100644
--- a/langs/powershell/powershell.csproj
+++ b/langs/powershell/powershell.csproj
@@ -8,11 +8,11 @@
         <TargetFramework>net9.0</TargetFramework>
     </PropertyGroup>
     <ItemGroup>
-        <PackageReference Include="Microsoft.PowerShell.Commands.Diagnostics" Version="7.5.0"/>
-        <PackageReference Include="Microsoft.PowerShell.Commands.Management"  Version="7.5.0"/>
-        <PackageReference Include="Microsoft.PowerShell.Commands.Utility"     Version="7.5.0"/>
-        <PackageReference Include="Microsoft.PowerShell.ConsoleHost"          Version="7.5.0"/>
-        <PackageReference Include="Microsoft.WSMan.Management"                Version="7.5.0"/>
-        <PackageReference Include="System.Management.Automation"              Version="7.5.0"/>
+        <PackageReference Include="Microsoft.PowerShell.Commands.Diagnostics" Version="7.5.2"/>
+        <PackageReference Include="Microsoft.PowerShell.Commands.Management"  Version="7.5.2"/>
+        <PackageReference Include="Microsoft.PowerShell.Commands.Utility"     Version="7.5.2"/>
+        <PackageReference Include="Microsoft.PowerShell.ConsoleHost"          Version="7.5.2"/>
+        <PackageReference Include="Microsoft.WSMan.Management"                Version="7.5.2"/>
+        <PackageReference Include="System.Management.Automation"              Version="7.5.2"/>
     </ItemGroup>
 </Project>
diff --git a/langs/prolog/prolog.c b/langs/prolog/prolog.c
index 789202bb..e8b39fc2 100644
--- a/langs/prolog/prolog.c
+++ b/langs/prolog/prolog.c
@@ -1,21 +1,39 @@
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 #include <unistd.h>
 
-int main (int argc, char *argv[]) {
+#define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
+
+const char* prolog = "/usr/bin/swipl", *code = "/tmp/code.pl";
+
+int main(int argc, char* argv[]) {
+    if (!strcmp(argv[1], "--version")) {
+        execv(prolog, argv);
+        ERR_AND_EXIT("execv");
+    }
+
+    FILE* fp;
+
+    if (!(fp = fopen(code, "w")))
+        ERR_AND_EXIT("fopen");
+
     char buffer[4096];
     ssize_t nbytes;
-    FILE *fp = fopen("/tmp/code.pl", "w");
 
-    if (fp == 0)
-        return 1;
+    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))))
+        if (fwrite(buffer, sizeof(char), nbytes, fp) != (size_t) nbytes)
+            ERR_AND_EXIT("fwrite");
 
-    while ((nbytes = fread(buffer, sizeof(char), sizeof(buffer), stdin)) > 0)
-        if (fwrite(buffer, sizeof(char), nbytes, fp) != nbytes)
-            return 2;
+    if (fclose(fp))
+        ERR_AND_EXIT("fclose");
 
-    fclose(fp);
+    int pargc = argc;
+    char** pargv = malloc(pargc * sizeof(char*));
+    pargv[0] = (char*) prolog;
+    memcpy(&pargv[1], &argv[2], (argc - 2) * sizeof(char*));
+    pargv[pargc - 1] = NULL;
 
-    execv("/usr/bin/swipl", argv);
-    perror("execv");
+    execv(prolog, argv);
+    ERR_AND_EXIT("execv");
 }
diff --git a/langs/python/Dockerfile b/langs/python/Dockerfile
index c147b1e3..8522cc4e 100644
--- a/langs/python/Dockerfile
+++ b/langs/python/Dockerfile
@@ -1,18 +1,17 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl libffi-dev zlib-dev
 
 # Also bump Coconut, Hy.
-ENV VER=3.13.2
+ENV VER=3.13.5
 
-RUN curl -# https://www.python.org/ftp/python/$VER/Python-$VER.tar.xz | tar xJ
+RUN curl -#L https://www.python.org/ftp/python/$VER/Python-$VER.tar.xz \
+  | tar xJ --strip-components 1
 
-WORKDIR /Python-$VER
-
-RUN ./configure                \
+RUN CC='cc' ./configure        \
     --disable-test-modules     \
     --prefix=/usr              \
-    --with-lto                 \
+    --with-lto=thin            \
     --without-ensurepip        \
     --without-static-libpython \
  && make -j`nproc` install     \
diff --git a/langs/qore/Dockerfile b/langs/qore/Dockerfile
index f1eb6fee..218b2985 100644
--- a/langs/qore/Dockerfile
+++ b/langs/qore/Dockerfile
@@ -1,18 +1,18 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache bison bzip2-dev cmake curl flex g++ git gmp-dev linux-headers mpfr-dev ninja openssl-dev pcre-dev zlib-dev
+RUN apk add --no-cache bison build-base bzip2-dev curl flex gmp-dev linux-headers mpfr-dev openssl-dev pcre2-dev zlib-dev
 
-ENV VER=1.19.2
+ENV VER=2.1.2
 
-RUN curl -#L https://github.com/qorelanguage/qore/archive/refs/tags/release-$VER.tar.gz | tar xz
+RUN curl -#L https://github.com/qorelanguage/qore/releases/download/release-$VER/qore-$VER.tar.bz2 \
+  | tar xj --strip-components 1
 
-WORKDIR /qore-release-$VER/build
-
-RUN cmake .. -G Ninja             \
-    -DCMAKE_BUILD_TYPE='Release'  \
-    -DCMAKE_INSTALL_PREFIX='/usr' \
-    -DSINGLE_COMPILATION_UNIT='1' \
- && ninja install
+RUN ./configure            \
+    --disable-debug        \
+    --prefix=/usr          \
+ && make -j`nproc` install \
+ && strip /usr/bin/qore    \
+          /usr/lib/libqore.so.12
 
 FROM codegolf/lang-base
 
@@ -23,7 +23,7 @@ COPY --from=0 /usr/lib/libbz2.so.1     \
               /usr/lib/libgcc_s.so.1   \
               /usr/lib/libgmp.so.10    \
               /usr/lib/libmpfr.so.6    \
-              /usr/lib/libpcre.so.1    \
+              /usr/lib/libpcre2-8.so.0 \
               /usr/lib/libqore.so.12   \
               /usr/lib/libssl.so.3     \
               /usr/lib/libstdc++.so.6  \
diff --git a/langs/r/Dockerfile b/langs/r/Dockerfile
index 5ef1b8f2..f88ac7a9 100644
--- a/langs/r/Dockerfile
+++ b/langs/r/Dockerfile
@@ -1,13 +1,12 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base bzip2-dev curl-dev gettext-dev gfortran \
-                       pcre2-dev perl tzdata xz-dev zlib-dev
+RUN apk add --no-cache build-base bzip2-dev curl curl-dev gettext-dev \
+                       gfortran pcre2-dev perl tzdata xz-dev zlib-dev
 
-ENV VER=4.4.3
+ENV VER=4.5.1
 
-RUN wget -O- https://cran.r-project.org/src/base/R-4/R-$VER.tar.xz | tar xJ
-
-WORKDIR /R-$VER
+RUN curl -#L https://cran.r-project.org/src/base/R-4/R-$VER.tar.xz \
+  | tar xJ --strip-components 1
 
 RUN ./configure                      \
     --disable-byte-compiled-packages \
@@ -27,7 +26,7 @@ RUN strip /usr/lib/R/lib/libR.so     \
 
 WORKDIR /busybox
 
-RUN wget -O- https://busybox.net/downloads/busybox-1.36.1.tar.bz2 \
+RUN curl -#L https://busybox.net/downloads/busybox-1.36.1.tar.bz2 \
   | tar xj --strip-components 1
 
 COPY busybox-config .config
@@ -39,8 +38,8 @@ FROM codegolf/lang-base
 COPY --from=0 /busybox/_install/bin        /bin
 COPY --from=0 /lib/ld-musl-x86_64.so.1     /lib/
 COPY --from=0 /busybox/_install/usr/bin    /usr/bin
-COPY --from=0 /usr/bin/Rscript             \
-              /usr/bin/printf              /usr/bin/
+COPY --from=0 /usr/bin/printf              /usr/bin/
+COPY --from=0 /usr/bin/Rscript             /usr/bin/R
 COPY --from=0 /usr/lib/libbz2.so.1         \
               /usr/lib/libgcc_s.so.1       \
               /usr/lib/libgfortran.so.5    \
@@ -49,7 +48,8 @@ COPY --from=0 /usr/lib/libbz2.so.1         \
               /usr/lib/liblzma.so.5        \
               /usr/lib/libpcre2-8.so.0     \
               /usr/lib/libquadmath.so.0    \
-              /usr/lib/libz.so.1           /usr/lib/
+              /usr/lib/libz.so.1           \
+              /usr/lib/libzstd.so.1        /usr/lib/
 COPY --from=0 /usr/lib/R/bin/R             \
               /usr/lib/R/bin/Rscript       /usr/lib/R/bin/
 COPY --from=0 /usr/lib/R/bin/exec/R        /usr/lib/R/bin/exec/
@@ -68,6 +68,6 @@ COPY --from=0 /usr/lib/R/library/methods   /usr/lib/R/library/methods
 COPY --from=0 /usr/lib/R/library/stats     /usr/lib/R/library/stats
 COPY --from=0 /usr/lib/R/library/utils     /usr/lib/R/library/utils
 
-ENTRYPOINT ["Rscript"]
+ENTRYPOINT ["R"]
 
 CMD ["--version"]
diff --git a/langs/racket/Dockerfile b/langs/racket/Dockerfile
index 4a32fa39..b698edee 100644
--- a/langs/racket/Dockerfile
+++ b/langs/racket/Dockerfile
@@ -1,8 +1,8 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl linux-headers
 
-ENV VER=8.16
+ENV VER=8.17
 
 RUN curl -#L https://github.com/racket/racket/archive/refs/tags/v$VER.tar.gz \
   | tar xz --strip-components 1
@@ -12,8 +12,8 @@ RUN make bc -j`nproc` PKGS='' RACKETBC_SUFFIX=''
 FROM codegolf/lang-base
 
 COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /racket/bin/racket       /usr/local/bin/
-COPY --from=0 /racket/collects         /usr/local/collects
+COPY --from=0 /racket/bin/racket       /usr/bin/
+COPY --from=0 /racket/collects         /usr/collects
 
 ENTRYPOINT ["racket"]
 
diff --git a/langs/raku/Dockerfile b/langs/raku/Dockerfile
index 1f0b1c01..4ae9f1d2 100644
--- a/langs/raku/Dockerfile
+++ b/langs/raku/Dockerfile
@@ -1,8 +1,8 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base git perl
 
-ENV VER=2025.03
+ENV VER=2025.06.1
 
 RUN git clone --branch $VER https://github.com/rakudo/rakudo.git
 
diff --git a/langs/rebol/Dockerfile b/langs/rebol/Dockerfile
index 61af0f0f..2fb0cc76 100644
--- a/langs/rebol/Dockerfile
+++ b/langs/rebol/Dockerfile
@@ -1,8 +1,10 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache curl libc6-compat
 
-RUN curl https://www.rebol.com/downloads/v278/rebol-core-278-4-10.tar.gz \
+ENV VER=278
+
+RUN curl -#L https://www.rebol.com/downloads/v$VER/rebol-core-$VER-4-10.tar.gz \
   | tar xz --strip-components 1
 
 FROM codegolf/lang-base
diff --git a/langs/rexx/Dockerfile b/langs/rexx/Dockerfile
index 28eef184..7e2e5c64 100644
--- a/langs/rexx/Dockerfile
+++ b/langs/rexx/Dockerfile
@@ -1,24 +1,30 @@
-FROM alpine:3.21 AS builder
+FROM debian:bookworm-slim AS builder
 
-RUN apk add --no-cache build-base curl
+RUN apt-get update                   \
+ && DEBIAN_FRONTEND='noninteractive' \
+    apt-get install --yes gcc sysv-rc-conf wget
 
-ENV VER=3.9.6
+ENV VER=3.9.7
 
-RUN curl -#L https://sourceforge.net/projects/regina-rexx/files/regina-rexx/$VER/regina-rexx-$VER.tar.gz \
-  | tar xz --strip-components 1
+RUN wget -O libregina.deb https://sourceforge.net/projects/regina-rexx/files/regina-rexx/$VER/libregina3_$VER-1-amd64-Debian-12.deb \
+ && dpkg -i libregina.deb
 
-RUN ./configure && make && make install
+RUN wget -O regina.deb https://sourceforge.net/projects/regina-rexx/files/regina-rexx/$VER/regina-rexx_$VER-1-amd64-Debian-12.deb \
+ && dpkg -i regina.deb
 
 COPY rexx.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/rexx -s -static rexx.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/rexx -s rexx.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /usr/bin/rexx            \
-              /usr/local/bin/regina    /usr/bin/
-COPY --from=0 /libregina.so.3          /usr/lib/
+COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6          \
+              /lib/x86_64-linux-gnu/libcrypt.so.1      \
+              /lib/x86_64-linux-gnu/libdl.so.2         \
+              /lib/x86_64-linux-gnu/libpthread.so.0    /lib/
+COPY --from=0 /lib64/ld-linux-x86-64.so.2              /lib64/
+COPY --from=0 /usr/bin/regina /usr/bin/rexx            /usr/bin/
+COPY --from=0 /usr/lib/x86_64-linux-gnu/libregina.so.3 /usr/lib/
 
 ENTRYPOINT ["rexx"]
 
diff --git a/langs/rockstar/Dockerfile b/langs/rockstar/Dockerfile
index f4e4527a..ed8cbf0d 100644
--- a/langs/rockstar/Dockerfile
+++ b/langs/rockstar/Dockerfile
@@ -1,8 +1,8 @@
-FROM mcr.microsoft.com/dotnet/sdk:9.0.200-alpine3.21 AS builder
+FROM mcr.microsoft.com/dotnet/sdk:9.0.301-alpine3.22-amd64 AS builder
 
 RUN apk add --no-cache gcc musl-dev zlib-dev
 
-ENV VER=2.0.30
+ENV VER=2.0.31
 
 RUN curl -#L https://github.com/RockstarLang/rockstar/archive/refs/tags/v$VER.tar.gz \
   | tar xz --strip-components 1
@@ -11,9 +11,9 @@ RUN find Starship -name '*.csproj' -exec sed -Ei 's/\bnet\d+\b/net9/' {} + \
  && dotnet publish Starship/Rockstar -c Release -o /usr/bin \
     -p:InvariantGlobalization=true
 
-COPY rockstarwrapper.c /
+COPY rockstar.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/rockstarwrapper -s -static rockstarwrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/rockstarwrapper -s -static rockstar.c
 
 FROM codegolf/lang-base
 
diff --git a/langs/rockstar/rockstarwrapper.c b/langs/rockstar/rockstar.c
similarity index 100%
rename from langs/rockstar/rockstarwrapper.c
rename to langs/rockstar/rockstar.c
diff --git a/langs/ruby/Dockerfile b/langs/ruby/Dockerfile
index 6802e008..ff2dbbc6 100644
--- a/langs/ruby/Dockerfile
+++ b/langs/ruby/Dockerfile
@@ -1,32 +1,21 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache bison clang curl git gmp-dev make mpfr-dev texinfo
+RUN apk add --no-cache clang curl make
 
 # Also rebuild GolfScript, iogii.
-ENV VER=3.4.2
-
-RUN git clone --depth 1 --progress git://sourceware.org/git/binutils-gdb.git binutils
-
-WORKDIR /gold
-
-RUN CC='clang' CXX='clang++' \
-    ../binutils/configure    \
-    --disable-werror         \
-    --enable-gold            \
-    --enable-plugins         \
- && make -j`nproc` all-gold  \
- && mv gold/ld-new /usr/bin/ld
+ENV VER=3.4.5
 
 WORKDIR /ruby
 
 RUN curl -#L https://cache.ruby-lang.org/pub/ruby/3.4/ruby-$VER.tar.xz \
   | tar xJ --strip-components 1
 
-RUN CC='clang' ./configure \
+# Prefer clang to gcc as it produces smaller binaries with adequate stack.
+RUN ./configure            \
     --disable-install-doc  \
     --disable-jit-support  \
     --prefix=/usr          \
-    --without-gmp          \
+    --with-gcc=clang       \
  && make -j`nproc` install \
  && strip -s /usr/bin/ruby
 
diff --git a/langs/rust/Dockerfile b/langs/rust/Dockerfile
index 0439b931..6dca1905 100644
--- a/langs/rust/Dockerfile
+++ b/langs/rust/Dockerfile
@@ -1,7 +1,7 @@
 # Also bump Gleam, Hush, Uiua.
-ARG VER=1.85.1
+ARG VER=1.88.0
 
-FROM rust:$VER-alpine3.21 AS builder
+FROM rust:$VER-alpine3.22 AS builder
 
 ARG VER
 
@@ -26,7 +26,7 @@ FROM codegolf/lang-base
 
 COPY --from=0 /lib/ld-musl-x86_64.so.1                 /lib/
 COPY --from=0 /usr/bin/ld /usr/bin/rust                /usr/bin/
-COPY --from=0 /usr/lib/libbfd-2.43.1.so                \
+COPY --from=0 /usr/lib/libbfd-2.44.so                  \
               /usr/lib/libctf.so.0                     \
               /usr/lib/libgcc_s.so.1                   \
               /usr/lib/libjansson.so.4                 \
diff --git a/langs/scala/Dockerfile b/langs/scala/Dockerfile
index 286fc23b..f7b94310 100644
--- a/langs/scala/Dockerfile
+++ b/langs/scala/Dockerfile
@@ -1,48 +1,27 @@
-FROM eclipse-temurin:24_36-jdk-alpine-3.21 AS builder
-
-RUN apk add --no-cache bash curl git
-
-ENV SBTVER=1.10.7 VER=2.13.16
-
-RUN curl -#L https://github.com/sbt/sbt/releases/download/v$SBTVER/sbt-$SBTVER.tgz | tar xz
-
-RUN git clone https://github.com/scala/scala.git --branch v$VER
-
-WORKDIR /scala
-
-RUN TERM='dumb' /sbt/bin/sbt dist/mkPack
-
-FROM codegolf/lang-base
-
-COPY --from=0 /bin/bash /bin/grep /bin/uname                /bin/
-COPY --from=0 /lib/ld-musl-x86_64.so.1                      /lib/
-COPY --from=0 /opt/java/openjdk/bin/java                    /opt/java/bin/
-COPY --from=0 /opt/java/openjdk/conf/security/java.security /opt/java/conf/security/
-COPY --from=0 /opt/java/openjdk/lib/jvm.cfg                 \
-              /opt/java/openjdk/lib/libjava.so              \
-              /opt/java/openjdk/lib/libjimage.so            \
-              /opt/java/openjdk/lib/libjli.so               \
-              /opt/java/openjdk/lib/libnet.so               \
-              /opt/java/openjdk/lib/libnio.so               \
-              /opt/java/openjdk/lib/libzip.so               \
-              /opt/java/openjdk/lib/modules                 \
-              /opt/java/openjdk/lib/tzdb.dat                /opt/java/lib/
-COPY --from=0 /opt/java/openjdk/lib/server/libjvm.so        /opt/java/lib/server/
-COPY --from=0 /usr/bin/dirname /usr/bin/env                 /usr/bin/
-COPY --from=0 /usr/lib/libacl.so.1                          \
-              /usr/lib/libattr.so.1                         \
-              /usr/lib/libcrypto.so.3                       \
-              /usr/lib/libgmp.so.10                         \
-              /usr/lib/libncursesw.so.6                     \
-              /usr/lib/libreadline.so.8                     \
-              /usr/lib/libskarnet.so.2.14                   \
-              /usr/lib/libutmps.so.0.1                      /usr/lib/
-COPY --from=0 /scala/build/pack/bin/scala                   /usr/local/bin/
-COPY --from=0 /scala/build/pack/lib/scala-compiler.jar      \
-              /scala/build/pack/lib/scala-library.jar       \
-              /scala/build/pack/lib/scala-reflect.jar       /usr/local/lib/
-
-ENV JAVA_HOME='/opt/java'
+FROM alpine:3.22 AS builder
+
+RUN apk add --no-cache bash coreutils curl
+
+ENV VER=2.13.16
+
+RUN curl -#L https://github.com/scala/scala/releases/download/v$VER/scala-$VER.tgz \
+  | tar xz --directory /usr/local --strip-components 1
+
+FROM codegolf/lang-java
+
+COPY --from=0 /bin/bash /bin/grep /bin/uname    /bin/
+COPY --from=0 /usr/bin/dirname /usr/bin/env     /usr/bin/
+COPY --from=0 /usr/lib/libacl.so.1              \
+              /usr/lib/libattr.so.1             \
+              /usr/lib/libcrypto.so.3           \
+              /usr/lib/libncursesw.so.6         \
+              /usr/lib/libreadline.so.8         \
+              /usr/lib/libskarnet.so.2.14       \
+              /usr/lib/libutmps.so.0.1          /usr/lib/
+COPY --from=0 /usr/local/bin/scala              /usr/local/bin/
+COPY --from=0 /usr/local/lib/scala-compiler.jar \
+              /usr/local/lib/scala-library.jar  \
+              /usr/local/lib/scala-reflect.jar  /usr/local/lib/
 
 ENTRYPOINT ["scala"]
 
diff --git a/langs/scheme/Dockerfile b/langs/scheme/Dockerfile
index 2b0edda8..daf209b2 100644
--- a/langs/scheme/Dockerfile
+++ b/langs/scheme/Dockerfile
@@ -1,8 +1,8 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base curl
+RUN apk add --no-cache curl gcc make musl-dev
 
-ENV VER=10.1.0
+ENV VER=10.2.0
 
 RUN curl -#L https://github.com/cisco/ChezScheme/releases/download/v$VER/csv$VER.tar.gz \
   | tar xz --strip-components 1
@@ -15,9 +15,9 @@ RUN ./configure      \
 
 RUN strip /usr/bin/scheme
 
-COPY schemewrapper.c /
+COPY scheme.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/schemewrapper -s schemewrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/schemewrapper -s scheme.c
 
 FROM codegolf/lang-base
 
diff --git a/langs/scheme/schemewrapper.c b/langs/scheme/scheme.c
similarity index 100%
rename from langs/scheme/schemewrapper.c
rename to langs/scheme/scheme.c
diff --git a/langs/sed/Dockerfile b/langs/sed/Dockerfile
index 6eb81d32..b162e6fc 100644
--- a/langs/sed/Dockerfile
+++ b/langs/sed/Dockerfile
@@ -1,17 +1,21 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl
 
-RUN curl https://ftp.gnu.org/gnu/sed/sed-4.9.tar.xz | tar xJ
+ENV LDFLAGS='-static' VER=4.9
 
-RUN cd sed-4.9                                 \
- && ./configure --enable-lto LDFLAGS="-static" \
- && make                                       \
- && strip sed/sed
+RUN curl -#L https://ftp.gnu.org/gnu/sed/sed-$VER.tar.xz \
+  | tar xJ --strip-components 1
+
+RUN ./configure   \
+    --enable-lto  \
+    --prefix=/usr \
+ && make install  \
+ && strip /usr/bin/sed
 
 FROM codegolf/lang-base
 
-COPY --from=0 /sed-4.9/sed/sed /usr/bin/
+COPY --from=0 /usr/bin/sed /usr/bin/
 
 ENTRYPOINT ["sed"]
 
diff --git a/langs/sql/Dockerfile b/langs/sql/Dockerfile
index fa126900..63b08a1a 100644
--- a/langs/sql/Dockerfile
+++ b/langs/sql/Dockerfile
@@ -1,18 +1,20 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache curl gcc musl-dev
 
-RUN curl https://www.sqlite.org/2025/sqlite-autoconf-3490100.tar.gz \
-  | tar xz --strip 1
+ENV VER=3500300
+
+RUN curl -#L https://www.sqlite.org/2025/sqlite-autoconf-$VER.tar.gz \
+  | tar xz --strip-components 1
 
 COPY sql.c /
 
 RUN gcc -DSQLITE_ENABLE_MATH_FUNCTIONS -DSQLITE_THREADSAFE=0 \
-    -static -s -o sql sql.c sqlite3.c
+    -o /usr/bin/sql -s -static sql.c sqlite3.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /sql /usr/bin/
+COPY --from=0 /usr/bin/sql /usr/bin/
 
 ENTRYPOINT ["sql"]
 
diff --git a/langs/squirrel/Dockerfile b/langs/squirrel/Dockerfile
index 2587b447..baa5ab57 100644
--- a/langs/squirrel/Dockerfile
+++ b/langs/squirrel/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache clang cmake curl make patch
 
@@ -7,30 +7,29 @@ ENV VER=3.2
 RUN curl -#L https://github.com/albertodemichelis/squirrel/archive/refs/tags/v$VER.tar.gz \
   | tar xz --strip-components 1
 
-COPY argv.patch \
-     squirrel.c /
+COPY argv.patch squirrel.c /
 
-RUN patch -p 0 < argv.patch
-
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/squirrel -s -static squirrel.c
+RUN patch -p0 < argv.patch
 
 WORKDIR /build
 
-RUN cmake -DCMAKE_INSTALL_PREFIX=/usr .. \
+RUN cmake .. \
  && make install
 
-RUN strip /usr/lib/libsqstdlib.so.0 \
-          /usr/lib/libsquirrel.so.0
+RUN strip /usr/local/lib/libsqstdlib.so.0 \
+          /usr/local/lib/libsquirrel.so.0
+
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/squirrel -s /squirrel.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1  /lib/
-COPY --from=0 /usr/bin/sq               \
-              /usr/bin/squirrel         /usr/bin/
-COPY --from=0 /usr/lib/libgcc_s.so.1    \
-              /usr/lib/libsqstdlib.so.0 \
-              /usr/lib/libsquirrel.so.0 \
-              /usr/lib/libstdc++.so.6   /usr/lib/
+COPY --from=0 /lib/ld-musl-x86_64.so.1        /lib/
+COPY --from=0 /usr/bin/squirrel               \
+              /usr/local/bin/sq               /usr/bin/
+COPY --from=0 /usr/lib/libgcc_s.so.1          \
+              /usr/lib/libstdc++.so.6         \
+              /usr/local/lib/libsqstdlib.so.0 \
+              /usr/local/lib/libsquirrel.so.0 /usr/lib/
 
 ENTRYPOINT ["squirrel"]
 
diff --git a/langs/stax/Dockerfile b/langs/stax/Dockerfile
index 8f18fa13..abc5c367 100644
--- a/langs/stax/Dockerfile
+++ b/langs/stax/Dockerfile
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/dotnet/sdk:9.0.200-alpine3.21 AS builder
+FROM mcr.microsoft.com/dotnet/sdk:9.0.301-alpine3.22-amd64 AS builder
 
 RUN apk add --no-cache gcc musl-dev
 
@@ -13,9 +13,9 @@ COPY stax.csproj StaxLang.CLI.csproj
 
 RUN dotnet publish -c Release -o /out
 
-COPY staxwrapper.c /
+COPY stax.c /
 
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/staxwrapper -s -static /staxwrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/staxwrapper -s -static /stax.c
 
 FROM codegolf/lang-base
 
diff --git a/langs/stax/staxwrapper.c b/langs/stax/stax.c
similarity index 100%
rename from langs/stax/staxwrapper.c
rename to langs/stax/stax.c
diff --git a/langs/swift/Dockerfile b/langs/swift/Dockerfile
index 9e378f65..d89cf54d 100644
--- a/langs/swift/Dockerfile
+++ b/langs/swift/Dockerfile
@@ -1,12 +1,13 @@
-FROM debian:bullseye-slim AS builder
+FROM debian:bookworm-slim AS builder
 
-RUN apt-get update && apt-get install -y \
-    binutils curl libatomic1 libc6-dev libedit2 libncurses6 libsqlite3-0 libz3-4
+RUN apt-get update                   \
+ && DEBIAN_FRONTEND='noninteractive' \
+    apt-get install --yes binutils curl libatomic1 libc6-dev libedit2 libncurses6 libsqlite3-0 libz3-4
 
-ENV VER=6.0.3
+ENV VER=6.1.2
 
-RUN curl https://download.swift.org/swift-$VER-release/ubuntu2004/swift-$VER-RELEASE/swift-$VER-RELEASE-ubuntu20.04.tar.gz \
-  | tar xz --directory / --strip-components 1
+RUN curl -#L https://download.swift.org/swift-$VER-release/debian12/swift-$VER-RELEASE/swift-$VER-RELEASE-debian12.tar.gz \
+  | tar xz --strip-components 1
 
 RUN strip /usr/bin/swift /usr/bin/swift-frontend \
  && rm -r /usr/lib/swift/embedded                \
@@ -14,15 +15,24 @@ RUN strip /usr/bin/swift /usr/bin/swift-frontend \
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/x86_64-linux-gnu                     /lib/x86_64-linux-gnu
-COPY --from=0 /lib64                                    /lib64
+COPY --from=0 /lib/x86_64-linux-gnu/libc.so.6           \
+              /lib/x86_64-linux-gnu/libdl.so.2          \
+              /lib/x86_64-linux-gnu/libgcc_s.so.1       \
+              /lib/x86_64-linux-gnu/libm.so.6           \
+              /lib/x86_64-linux-gnu/libncurses.so.6     \
+              /lib/x86_64-linux-gnu/libpthread.so.0     \
+              /lib/x86_64-linux-gnu/librt.so.1          \
+              /lib/x86_64-linux-gnu/libtinfo.so.6       \
+              /lib/x86_64-linux-gnu/libutil.so.1        \
+              /lib/x86_64-linux-gnu/libz.so.1           /lib/
+COPY --from=0 /lib64/ld-linux-x86-64.so.2               /lib64/
 COPY --from=0 /usr/bin/swift /usr/bin/swift-frontend    /usr/bin/
 COPY --from=0 /usr/include                              /usr/include
-COPY --from=0 /usr/lib/clang/17/include                 /usr/lib/clang/17/include
-COPY --from=0 /usr/lib/swift                            /usr/lib/swift
 COPY --from=0 /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \
               /usr/lib/x86_64-linux-gnu/libstdc++.so.6  \
-              /usr/lib/x86_64-linux-gnu/libuuid.so.1    /usr/lib/x86_64-linux-gnu/
+              /usr/lib/x86_64-linux-gnu/libuuid.so.1    /usr/lib/
+COPY --from=0 /usr/lib/clang/17/include                 /usr/lib/clang/17/include
+COPY --from=0 /usr/lib/swift                            /usr/lib/swift
 
 ENTRYPOINT ["swift"]
 
diff --git a/langs/tcl/Dockerfile b/langs/tcl/Dockerfile
index 8b53c3fd..345f5cd2 100644
--- a/langs/tcl/Dockerfile
+++ b/langs/tcl/Dockerfile
@@ -1,23 +1,27 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache bsd-compat-headers build-base curl zlib-dev
 
-RUN curl -L https://github.com/tcltk/tcl/archive/refs/tags/core-9-0-1.tar.gz | tar xz
+ENV LDFLAGS='-static' VER=9-0-2
 
-ENV LDFLAGS=-static
+RUN curl -#L https://github.com/tcltk/tcl/archive/refs/tags/core-$VER.tar.gz \
+  | tar xz --strip-components 1
 
-RUN mv /tcl-* /tcl   \
- && cd tcl/unix      \
- && ./configure      \
+WORKDIR /unix
+
+RUN ./configure      \
     --disable-load   \
     --disable-shared \
     --enable-64-bit  \
- && make             \
- && strip tclsh
+    --prefix=/usr    \
+ && make install     \
+ && strip /usr/bin/tcl*
 
 FROM codegolf/lang-base
 
-COPY --from=0 /tcl/library/   /library/
-COPY --from=0 /tcl/unix/tclsh /usr/bin/tcl
+COPY --from=0 /library      /library
+COPY --from=0 /usr/bin/tcl* /usr/bin/tcl
 
 ENTRYPOINT ["tcl"]
+
+CMD ["--version"]
diff --git a/langs/tex/Dockerfile b/langs/tex/Dockerfile
index da093fd5..9b3c546e 100644
--- a/langs/tex/Dockerfile
+++ b/langs/tex/Dockerfile
@@ -1,44 +1,48 @@
-FROM reitzig/texlive-base:2024.2 AS builder
-
-# Download dvi-to-text
-ENV VERSION=v0.2.1
-ENV TARBALL=dvi-to-text_${VERSION}_x86_64-unknown-linux-musl.tar.xz
-# reitzig/texlive-base does not have curl
-RUN wget https://github.com/jared-hughes/dvi-to-text/releases/download/${VERSION}/${TARBALL} && \
-  tar -xJf ${TARBALL} -C /
-
-# Add a full 8-bit font to allow emitting non-ascii bytes
-COPY octet.mf /usr/local/texlive/2024/texmf-dist/fonts/source/public/cm
-# update kpathsea file database
-RUN ls -R /usr/local/texlive/2024/texmf-dist > /usr/local/texlive/2024/texmf-dist/ls-R && \
-  # remove all ligatures
-  rm -r /usr/local/texlive/2024/texmf-dist/fonts/tfm/public/cm/*.tfm && \
-  sed -i 's/ligs:=./ligs:=0/g' /usr/local/texlive/2024/texmf-dist/fonts/source/public/cm/*.mf && \
-  # load octet font
-  echo "\font\octet=octet" >> /usr/local/texlive/2024/texmf-dist/tex/plain/base/plain.tex && \
-  # apply changes
-  mktexfmt tex.fmt
-
-# Prune unneeded stuff
-RUN bash -c "rm -r /usr/local/texlive/2024/texmf-dist/{doc,source,scripts,bibtex,dvips}" && \
-  bash -c "rm -r /usr/local/texlive/2024/texmf-dist/{tex,fonts,xdvi,metafont,makeindex,mft,dvipdfmx}" && \
-  bash -c "rm -r /usr/local/texlive/2024/bin/x86_64-linuxmusl/{lua*,pdf*,*dvi*,mf-nowin,bibtex}" && \
-  bash -c "rm -r /usr/local/texlive/2024/texmf-var/web2c/{lua*,pdftex,metafont}" && \
-  rm -r /usr/local/texlive/2024/texmf-var/fonts && \
-  rm -r /usr/local/texlive/2024/tlpkg
+FROM reitzig/texlive-minimal:2025.5 AS builder
 
-FROM codegolf/lang-base
+RUN apk add --no-cache dash gcc musl-dev
+
+ENV VER=0.2.1
+
+# Download dvi-to-text.
+RUN wget -O tex.tar.xz https://github.com/jared-hughes/dvi-to-text/releases/download/v$VER/dvi-to-text_v${VER}_x86_64-unknown-linux-musl.tar.xz \
+ && tar xJf tex.tar.xz --directory /
+
+WORKDIR /usr/local/texlive/2025/texmf-dist
+
+# Add a full 8-bit font to allow emitting non-ascii bytes.
+COPY octet.mf fonts/source/public/cm
 
-COPY --from=builder /bin                      /bin
-COPY --from=builder /lib/ld-musl-x86_64.so.1  /lib/ld-musl-x86_64.so.1
-COPY --from=builder /dvi-to-text              /usr/bin/
-COPY --from=builder /usr/lib/libncursesw.so.6 \
-                    /usr/lib/libreadline.so.8 /usr/lib/
-COPY --from=builder /usr/local/bin            /usr/local/bin
-COPY --from=builder /usr/local/texlive/2024   /usr/local/texlive/2024
+# Update kpathsea file database.
+RUN ls -R . > ls-R                                           \
+  # Remove all ligatures.
+ && rm -r fonts/tfm/public/cm/*.tfm                          \
+ && sed -i 's/ligs:=./ligs:=0/g' fonts/source/public/cm/*.mf \
+  # Load octet font.
+ && echo "\font\octet=octet" >> tex/plain/base/plain.tex     \
+  # Apply changes.
+ && mktexfmt tex.fmt
+
+# Prune unneeded stuff.
+RUN bash -c "rm -r ../texmf-var/web2c/{lua*,pdftex,metafont}                              \
+                   ../bin/x86_64-linuxmusl/{lua*,pdf*,*dvi*,mf-nowin,bibtex}              \
+                   {scripts,bibtex,dvips,tex,fonts,xdvi,metafont,makeindex,mft,dvipdfmx}" \
+ && rm -r ../texmf-var/fonts ../tlpkg
+
+COPY tex.c /
+
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/tex -s /tex.c
+
+FROM codegolf/lang-base
 
-COPY tex /usr/bin/
+COPY --from=0 /usr/bin/dash             /bin/sh
+COPY --from=0 /lib/ld-musl-x86_64.so.1  /lib/
+COPY --from=0 /dvi-to-text /usr/bin/tex /usr/bin/
+COPY --from=0 /usr/lib/libncursesw.so.6 \
+              /usr/lib/libreadline.so.8 /usr/lib/
+COPY --from=0 /usr/local/bin            /usr/local/bin
+COPY --from=0 /usr/local/texlive/2025   /usr/local/texlive/2025
 
-ENTRYPOINT ["/usr/bin/tex"]
+ENTRYPOINT ["tex"]
 
-CMD ["-v"]
+CMD ["--version"]
diff --git a/langs/tex/tex b/langs/tex/tex
deleted file mode 100755
index 2367b0ba..00000000
--- a/langs/tex/tex
+++ /dev/null
@@ -1,107 +0,0 @@
-#!/bin/bash -e
-
-if [ "$1" = "-v" ]
-then
-  /usr/local/bin/tex -v
-  exit
-fi
-
-function join_by {
-  # https://stackoverflow.com/a/17841619/7481517
-  local sep=${1-} f=${2-}
-  if shift 2; then
-    printf %s "$f" "${@/#/$sep}"
-  fi
-}
-
-cd /tmp
-
-code="$1"
-shift
-
-init="\def\argc{$#}"
-if [ $# -eq 0 ]; then
-  init+='\global\def\argv#1{}'
-else
-  # separate arg0 from the rest of the args
-  arg0="$1"
-  shift
-  args=$(join_by "or" "$@")
-  # To pass in values, we need to escape some special characters
-  # All the special characters \{}%&#^_%~ are set to catcode 12 ("other")
-  # Also set the whitespace characters to catcode 12, unlike \dospecials
-  # But we still need access to some to finish the definition, so we use
-  #   byte 1 = begin group (previously {)
-  #   byte 2 = end group (previously })
-  #   byte 4 = space
-  #   byte 5 = escape (previously \)
-  #   byte 6 = parameter (previously #)
-  #   byte 7 = comment (previously %)
-  # The end of the group resets all the catcodes for future tokenizing, but it
-  # does not change the catcodes of the tokens already created
-  init+='
-{\catcode1=1\catcode2=2\catcode5=0\catcode6=6\catcode4=10\catcode7=14
-\catcode`$=12\catcode`&=12\catcode`^=12\catcode`_=12\catcode37=12
-\catcode`~=12\catcode`#=12\catcode`{=12\catcode`}=12\catcode9=12
-\catcode32=12\catcode10=12\catcode12=12\catcode13=13\catcode92=12
-'
-  # The following line reads something like
-  #   \global\def\argv#1{
-  #     \ifnum#1=0
-  #       arg0
-  #     \else
-  #       \ifcase#1 \or arg1 \or arg2 \or ... \or argN \fi
-  #     \fi
-  #   }
-  # we handle 0 separately to avoid it starting with a space:
-  #   \ifcase#1arg0\or...    doesn't read the index right
-  #   \ifcase#1 arg0\or...   starts with an space
-  #   \ifcase#1{}arg0\or...  starts with an empty group (invisible but brutal)
-  #   \ifcase#1\relax arg0\or...  starts with a \relax (invisible but brutal)
-  # no easy way directly in the \ifcase, so use the extra \ifnum wrapper
-  #   \ifnum#1=0 arg0\else\ifcase\or arg1\or arg2\fi\fi
-  # ensures that arg0 is preceded by a digit (which removes the space), or a 
-  # macro which doesn't get expanded (which removes the space)
-  #
-  # Note: If you ever come back to this, make sure it works for
-  # argv{\the\i}, argv\i, and argv{\count0}, and hole args starting with digits
-  body='globaldefargv1ifnum1=0'"$arg0"'elseifcase1or'"$args"'fifi'
-  # Replace each newline '\n' with '\x07\n\n'. The comment character deletes
-  # the first newline, so it's equivalent in most cases. The difference is when
-  # the preceding line ends with one or spaces. This construction prevents
-  # clobbering the trailing spaces.
-  body="${body//$'\n'/$'\x07\n\n'}"
-  init+="$body"
-fi
-
-# \octet enables the octet font
-# \footline={} disables page numbers
-# \parindent=0pt prevents per-paragraph indentation
-# \hsize and \vsize set the page dimensions. I set them a bit less than the
-#   maximum legal dimension which is less than 16384pt.
-# \bye closes the document (TeX doesn't handle EOF how you might expect)
-code_to_run="\octet\footline={}\parindent=0pt\hsize=16000pt\vsize=16000pt\relax
-$init
-$code
-\bye"
-
-# don't stop the bash script if /usr/local/bin/tex errors
-set +e
-
-# The randoms might not actually prevent reading the file, but they do
-# certainly help avoid grepping out the wrong line
-FILENAME="solution_${RANDOM}_${RANDOM}_${RANDOM}_${RANDOM}"
-
-echo "$code_to_run" > "$FILENAME.tex"
-
-/usr/local/bin/tex "$FILENAME.tex" >/dev/null
-
-cat "$FILENAME.log" \
-  | grep -Fv "$FILENAME." \
-  | grep -Fv "This is TeX, Version 3.141592653" \
-  | grep -Fv " [1] )" \
-  >&2
-
-if [[ -f "$FILENAME.dvi" ]]; then
-  dvi-to-text "$FILENAME.dvi"
-fi
diff --git a/langs/tex/tex.c b/langs/tex/tex.c
new file mode 100644
index 00000000..6bfecd28
--- /dev/null
+++ b/langs/tex/tex.c
@@ -0,0 +1,170 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#undef RAND_MAX
+
+#define RAND_MAX __UINT16_MAX__
+#define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
+
+char* each_join(char* arr[], int cnt, const char* sep);
+
+const char* tex = "/usr/local/bin/tex";
+
+int main(int argc, char* argv[]) {
+    if (!strcmp(argv[1], "--version")) {
+        execv(tex, argv);
+        ERR_AND_EXIT("execv");
+    }
+
+    if (chdir("/tmp"))
+        ERR_AND_EXIT("chdir");
+
+    char* code = argv[1];
+
+    memmove(&argv[1], &argv[2], argc-- * sizeof(char*));
+
+    char init[RAND_MAX+1], body[RAND_MAX+1];
+
+    if (!snprintf(init, sizeof(init), "\\def\\argc{%d}", --argc))
+        ERR_AND_EXIT("snprintf");
+
+    if (argc == 0)
+        strcat(init, "\\global\\def\\argv#1{}");
+    else {
+        // To pass in values, we need to escape some special characters.
+        // All the special characters \{}%&#^_%~ are set to catcode 12 ("other").
+        // Also set the whitespace characters to catcode 12, unlike \dospecials.
+        // But we still need access to some to finish the definition, so we use
+        //   byte 1 = begin group (previously {)
+        //   byte 2 = end group (previously })
+        //   byte 4 = space
+        //   byte 5 = escape (previously \)
+        //   byte 6 = parameter (previously #)
+        //   byte 7 = comment (previously %)
+        // The end of the group resets all the catcodes for future tokenizing, but it does not change the catcodes of the tokens already created.
+        strcat(init, "{\\catcode1=1\\catcode2=2\\catcode5=0\\catcode6=6\\catcode4=10\\catcode7=14\\catcode`$=12\\catcode`&=12\\catcode`^=12\\catcode`_=12\\catcode37=12"
+            "\\catcode`~=12\\catcode`#=12\\catcode`{=12\\catcode`}=12\\catcode9=12\\catcode32=12\\catcode10=12\\catcode12=12\\catcode13=13\\catcode92=12");
+        // The following line reads something like
+        //   \global\def\argv#1{
+        //     \ifnum#1=0
+        //       arg0
+        //     \else
+        //       \ifcase#1 \or arg1 \or arg2 \or ... \or argN \fi
+        //     \fi
+        //   }
+        // we handle 0 separately to avoid it starting with a space:
+        //   \ifcase#1arg0\or...    doesn't read the index right
+        //   \ifcase#1 arg0\or...   starts with an space
+        //   \ifcase#1{}arg0\or...  starts with an empty group (invisible but brutal)
+        //   \ifcase#1\relax arg0\or...  starts with a \relax (invisible but brutal)
+        // no easy way directly in the \ifcase, so use the extra \ifnum wrapper
+        //   \ifnum#1=0 arg0\else\ifcase\or arg1\or arg2\fi\fi
+        // ensures that arg0 is preceded by a digit (which removes the space), or a macro which doesn't get expanded (which removes the space).
+        //
+        // Note: If you ever come back to this, make sure it works for argv{\the\i}, argv\i, and argv{\count0}, and hole args starting with digits.
+        if (!snprintf(body, sizeof(body), "globaldefargv1ifnum1=0%selseifcase1or%sfifi", argv[1], each_join(&argv[1], argc, "or")))
+            ERR_AND_EXIT("snprintf");
+
+        strcat(init, body);
+    }
+    // \octet enables the octet font.
+    // \footline={} disables page numbers.
+    // \parindent=0pt prevents per-paragraph indentation.
+    // \hsize and \vsize set the page dimensions. I set them a bit less than the maximum legal dimension which is less than 16384pt.
+    // \bye closes the document (TeX doesn't handle EOF how you might expect).
+    if (!snprintf(body, sizeof(body), "\\octet\\footline={}\\parindent=0pt\\hsize=16000pt\\vsize=16000pt\\relax\n%s\n%s\n\\bye", init, code))
+        ERR_AND_EXIT("snprintf");
+
+    srand((unsigned) time(NULL));
+
+    char file[64];
+    // The randoms might not actually prevent reading the file, but they do certainly help avoid grepping out the wrong line.
+    if (!snprintf(file, sizeof(file), "solution_%u_%u_%u_%u", rand() % RAND_MAX, rand() % RAND_MAX, rand() % RAND_MAX, rand() % RAND_MAX))
+        ERR_AND_EXIT("snprintf");
+
+    char src[128];
+
+    if (!snprintf(src, sizeof(src), "%s.tex", file))
+        ERR_AND_EXIT("snprintf");
+
+    FILE* fp;
+
+    if (!(fp = fopen(src, "w")))
+        ERR_AND_EXIT("fopen");
+
+    if (fputs(body, fp)) {
+        if (fclose(fp))
+            ERR_AND_EXIT("fclose");
+
+        ERR_AND_EXIT("fputs");
+    }
+
+    if (fclose(fp))
+        ERR_AND_EXIT("fclose");
+
+    char cmd[256];
+
+    if (!snprintf(cmd, sizeof(cmd), "%s %s > /dev/null", tex, src))
+        ERR_AND_EXIT("snprintf");
+
+    system(cmd); // Here we omit `ERR_AND_EXIT` to prevent the program from terminating as soon as /usr/local/bin/tex returns an error.
+
+    char log[128];
+
+    if (!snprintf(log, sizeof(log), "%s.log", file))
+        ERR_AND_EXIT("snprintf");
+
+    if (!(fp = fopen(log, "r")))
+        ERR_AND_EXIT("fopen");
+
+    char line[4096];
+
+    while (fgets(line, sizeof(line), fp)) {
+        if (strstr(line, file) || strstr(line, "Version 3.141592653") || strstr(line, " [1] )") ||
+            (strstr(line, "=\\count") && !strstr(line, " =\\count ")))
+            continue;
+
+        if (fputs(line, stderr))
+            ERR_AND_EXIT("fputs");
+    }
+
+    if (fclose(fp))
+        ERR_AND_EXIT("fclose");
+
+    char dvi[128];
+
+    if (!snprintf(dvi, sizeof(dvi), "%s.dvi", file))
+        ERR_AND_EXIT("snprintf");
+
+    if (!access(dvi, F_OK)) {
+        if (!snprintf(cmd, sizeof(cmd), "dvi-to-text %s", dvi))
+            ERR_AND_EXIT("snprintf");
+
+        if (system(cmd))
+            ERR_AND_EXIT("system");
+    }
+}
+
+// Based on the original Bash implementation:
+// https://stackoverflow.com/a/17841619/7481517
+char* each_join(char* arr[], int cnt, const char* sep) {
+    size_t len;
+
+    for (int i = 0; i < cnt; i++)
+        len += strlen(arr[i]) + strlen(sep);
+
+    char* rt;
+
+    if (!(rt = malloc(len + 1)))
+        ERR_AND_EXIT("malloc");
+
+    for (int i = 1; i < cnt; i++) {
+        strcat(rt, arr[i]);
+        strcat(rt, sep);
+    }
+
+    return rt;
+}
diff --git a/langs/uiua/Dockerfile b/langs/uiua/Dockerfile
index 34445d7b..01f13fa7 100644
--- a/langs/uiua/Dockerfile
+++ b/langs/uiua/Dockerfile
@@ -1,14 +1,28 @@
-FROM rust:1.85.1-alpine3.21 AS builder
+FROM rust:1.88.0-slim-bookworm AS builder
 
-RUN apk add --no-cache build-base linux-headers
+RUN apt-get update                   \
+ && DEBIAN_FRONTEND='noninteractive' \
+    apt-get install --yes curl
 
-RUN cargo install uiua --root /usr --version 0.14.1 \
+ENV CRT='x86_64-unknown-linux-musl' VER=0.16.2
+
+RUN curl -#L https://github.com/uiua-lang/uiua/archive/refs/tags/$VER.tar.gz \
+  | tar xz --strip-components 1
+
+RUN rustup target add $CRT \
+ && cargo add ffi
+
+RUN cargo install         \
+    --features binary     \
+    --no-default-features \
+    --path /              \
+    --root /usr           \
+    --target $CRT         \
  && strip /usr/bin/uiua
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /usr/bin/uiua            /usr/bin/
+COPY --from=0 /usr/bin/uiua /usr/bin/
 
 ENTRYPOINT ["uiua"]
 
diff --git a/langs/umka/Dockerfile b/langs/umka/Dockerfile
new file mode 100644
index 00000000..622fe678
--- /dev/null
+++ b/langs/umka/Dockerfile
@@ -0,0 +1,22 @@
+FROM alpine:3.22 AS builder
+
+RUN apk add --no-cache curl gcc make musl-dev
+
+ENV VER=1.5.3
+
+RUN curl -#L https://github.com/vtereshkov/umka-lang/archive/refs/tags/v$VER.tar.gz \
+  | tar xz --strip-components 1
+
+RUN make -j`nproc` install
+
+COPY umka.c /
+
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/umka -s umka.c
+
+FROM codegolf/lang-base
+
+COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
+COPY --from=0 /usr/bin/umka            /usr/bin/
+COPY --from=0 /usr/local/bin/umka      /usr/local/bin/
+
+ENTRYPOINT ["umka"]
diff --git a/langs/cjam/cjam.c b/langs/umka/umka.c
similarity index 65%
rename from langs/cjam/cjam.c
rename to langs/umka/umka.c
index d65d414d..1ee168f3 100644
--- a/langs/cjam/cjam.c
+++ b/langs/umka/umka.c
@@ -5,7 +5,7 @@
 
 #define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
-const char* cjam = "/opt/java/bin/java", *code = "code.cjam";
+const char* umka = "/usr/local/bin/umka", *code = "code.um";
 
 int main(int argc, char* argv[]) {
     if (!strcmp(argv[1], "--version"))
@@ -29,15 +29,14 @@ int main(int argc, char* argv[]) {
     if (fclose(fp))
         ERR_AND_EXIT("fclose");
 
-    int cargc = argc + 3;
-    char** cargv = malloc(cargc * sizeof(char*));
-    cargv[0] = (char*) cjam;
-    cargv[1] = "-jar";
-    cargv[2] = "/cjam.jar";
-    cargv[3] = (char*) code;
-    memcpy(&cargv[4], &argv[2], (argc - 2) * sizeof(char*));
-    cargv[cargc - 1] = NULL;
+    int uargc = argc + 2;
+    char** uargv = malloc(uargc * sizeof(char*));
+    uargv[0] = (char*) umka;
+    uargv[1] = "-warn";
+    uargv[2] = (char*) code;
+    memcpy(&uargv[3], &argv[2], (argc - 2) * sizeof(char*));
+    uargv[uargc - 1] = NULL;
 
-    execv(cjam, cargv);
+    execv(umka, uargv);
     ERR_AND_EXIT("execv");
 }
diff --git a/langs/v/Dockerfile b/langs/v/Dockerfile
index 88107890..75b63c6d 100644
--- a/langs/v/Dockerfile
+++ b/langs/v/Dockerfile
@@ -1,8 +1,8 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache build-base dash gc-dev git
+RUN apk add --no-cache dash gc-dev gcc git make musl-dev
 
-ENV VER=0.4.10
+ENV VER=0.4.11
 
 WORKDIR /usr/local
 
diff --git a/langs/vala/Dockerfile b/langs/vala/Dockerfile
new file mode 100644
index 00000000..7a4eb647
--- /dev/null
+++ b/langs/vala/Dockerfile
@@ -0,0 +1,82 @@
+ARG VER=0.56
+
+FROM alpine:3.22 AS builder
+
+ARG VER
+
+RUN apk add --no-cache bison curl flex g++ gobject-introspection-dev make
+
+RUN curl -#L https://download.gnome.org/sources/vala/$VER/vala-$VER.18.tar.xz \
+  | tar xJ --strip-components 1
+
+RUN ./configure                      \
+    --disable-valadoc                \
+    --prefix=/usr                    \
+ && make -j`nproc` install           \
+ && strip /usr/lib/libvala-$VER.so.0 \
+          /usr/lib/vala-$VER/libvalaccodegen.so
+
+COPY vala.c /
+
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/vala -s vala.c
+
+FROM codegolf/lang-base
+
+ARG VER
+
+COPY --from=0 /lib/ld-musl-x86_64.so.1                   /lib/
+COPY --from=0 /usr/bin/as                                \
+              /usr/bin/cc                                \
+              /usr/bin/gcc                               \
+              /usr/bin/ld                                \
+              /usr/bin/pkg-config                        \
+              /usr/bin/vala                              \
+              /usr/bin/valac                             /usr/bin/
+COPY --from=0 /usr/include                               /usr/include
+COPY --from=0 /usr/lib/Scrt1.o                           \
+              /usr/lib/crti.o                            \
+              /usr/lib/crtn.o                            \
+              /usr/lib/libbfd-2.44.so                    \
+              /usr/lib/libc.so                           \
+              /usr/lib/libctf.so.0                       \
+              /usr/lib/libffi.so.8                       \
+              /usr/lib/libgcc_s.so                       \
+              /usr/lib/libgcc_s.so.1                     \
+              /usr/lib/libglib-2.0.so                    \
+              /usr/lib/libglib-2.0.so.0                  \
+              /usr/lib/libgmodule-2.0.so.0               \
+              /usr/lib/libgmp.so.10                      \
+              /usr/lib/libgobject-2.0.so                 \
+              /usr/lib/libgobject-2.0.so.0               \
+              /usr/lib/libintl.so                        \
+              /usr/lib/libintl.so.8                      \
+              /usr/lib/libisl.so.23                      \
+              /usr/lib/libjansson.so.4                   \
+              /usr/lib/libmpc.so.3                       \
+              /usr/lib/libmpfr.so.6                      \
+              /usr/lib/libpcre2-8.so.0                   \
+              /usr/lib/libpkgconf.so.6                   \
+              /usr/lib/libsframe.so.1                    \
+              /usr/lib/libssp_nonshared.a                \
+              /usr/lib/libvala-$VER.so.0                 \
+              /usr/lib/libz.so.1                         \
+              /usr/lib/libzstd.so.1                      /usr/lib/
+COPY --from=0 /usr/lib/gcc/*/14.2.0/crtbeginS.o          \
+              /usr/lib/gcc/*/14.2.0/crtendS.o            \
+              /usr/lib/gcc/*/14.2.0/libgcc.a             /usr/lib/gcc/x86_64-alpine-linux-musl/14.2.0/
+COPY --from=0 /usr/lib/glib-2.0/include                  /usr/lib/glib-2.0/include
+COPY --from=0 /usr/lib/pkgconfig/glib-2.0.pc             \
+              /usr/lib/pkgconfig/gobject-2.0.pc          \
+              /usr/lib/pkgconfig/libffi.pc               \
+              /usr/lib/pkgconfig/libpcre2-8.pc           /usr/lib/pkgconfig/
+COPY --from=0 /usr/lib/vala-$VER/libvalaccodegen.so      /usr/lib/vala-$VER/
+COPY --from=0 /usr/libexec/gcc/*/14.2.0/cc1              \
+              /usr/libexec/gcc/*/14.2.0/liblto_plugin.so /usr/libexec/gcc/x86_64-alpine-linux-musl/14.2.0/
+COPY --from=0 /usr/share/gir-1.0/GLib-2.0.gir            \
+              /usr/share/gir-1.0/GObject-2.0.gir         /usr/share/gir-1.0/
+COPY --from=0 /usr/share/vala-$VER/vapi/glib-2.0.vapi    \
+              /usr/share/vala-$VER/vapi/gobject-2.0.vapi /usr/share/vala-$VER/vapi/
+
+ENTRYPOINT ["vala"]
+
+CMD ["--version"]
diff --git a/langs/vala/vala.c b/langs/vala/vala.c
new file mode 100755
index 00000000..d7473fa4
--- /dev/null
+++ b/langs/vala/vala.c
@@ -0,0 +1,63 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
+
+const char* vala = "/usr/bin/valac", *code = "code.vala";
+
+int main(int argc, char* argv[]) {
+    if (!strcmp(argv[1], "--version")) {
+        execv(vala, argv);
+        ERR_AND_EXIT("execv");
+    }
+
+    if (chdir("/tmp"))
+        ERR_AND_EXIT("chdir");
+
+    FILE* fp;
+
+    if (!(fp = fopen(code, "w")))
+        ERR_AND_EXIT("fopen");
+
+    char buffer[4096];
+    ssize_t nbytes;
+
+    while ((nbytes = read(STDIN_FILENO, buffer, sizeof(buffer))))
+        if (fwrite(buffer, sizeof(char), nbytes, fp) != (size_t) nbytes)
+            ERR_AND_EXIT("fwrite");
+
+    if (fclose(fp))
+        ERR_AND_EXIT("fclose");
+
+    pid_t pid;
+
+    if (!(pid = fork())) {
+        execl(vala, vala, "--color=always", "--quiet", code, NULL);
+        ERR_AND_EXIT("execl");
+    }
+
+    int status;
+
+    waitpid(pid, &status, 0);
+
+    if (!WIFEXITED(status))
+        exit(EXIT_FAILURE);
+
+    if (WEXITSTATUS(status))
+        return WEXITSTATUS(status);
+
+    if (remove(code))
+        ERR_AND_EXIT("remove");
+
+    int vargc = argc;
+    char** vargv = malloc(vargc * sizeof(char*));
+    vargv[0] = "code";
+    memcpy(&vargv[1], &argv[2], (argc - 2) * sizeof(char*));
+    vargv[vargc - 1] = NULL;
+
+    execv("code", vargv);
+    ERR_AND_EXIT("execv");
+}
diff --git a/langs/viml/Dockerfile b/langs/viml/Dockerfile
index 43a30e51..9daf6151 100644
--- a/langs/viml/Dockerfile
+++ b/langs/viml/Dockerfile
@@ -1,8 +1,10 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base dash git ncurses-dev
 
-RUN git clone --branch v9.1.0447 --depth 1 https://github.com/vim/vim.git
+ENV VER=9.1.0447
+
+RUN git clone --branch v$VER --depth 1 https://github.com/vim/vim.git
 
 WORKDIR /vim/src
 
diff --git a/langs/vyxal/Dockerfile b/langs/vyxal/Dockerfile
index d3fac73e..36eecaee 100644
--- a/langs/vyxal/Dockerfile
+++ b/langs/vyxal/Dockerfile
@@ -1,22 +1,11 @@
-FROM eclipse-temurin:24_36-jre-alpine-3.21 AS builder
+FROM alpine:3.22 AS builder
 
-ENV VER=3.7.0
+ENV VER=3.8.0
 
-RUN wget -O vyxal.jar https://github.com/Vyxal/Vyxal/releases/download/v$VER/vyxal-$VER.jar
+RUN wget -qO vyxal.jar https://github.com/Vyxal/Vyxal/releases/download/v$VER/vyxal-$VER.jar
 
-FROM codegolf/lang-base
+FROM codegolf/lang-java
 
-COPY --from=0 /vyxal.jar                             /
-COPY --from=0 /lib/ld-musl-x86_64.so.1               /lib/
-COPY --from=0 /opt/java/openjdk/bin/java             /opt/java/bin/
-COPY --from=0 /opt/java/openjdk/lib/jvm.cfg          \
-              /opt/java/openjdk/lib/libjava.so       \
-              /opt/java/openjdk/lib/libjimage.so     \
-              /opt/java/openjdk/lib/libjli.so        \
-              /opt/java/openjdk/lib/libnet.so        \
-              /opt/java/openjdk/lib/libnio.so        \
-              /opt/java/openjdk/lib/libzip.so        \
-              /opt/java/openjdk/lib/modules          /opt/java/lib/
-COPY --from=0 /opt/java/openjdk/lib/server/libjvm.so /opt/java/lib/server/
+COPY --from=0 /vyxal.jar /
 
 CMD ["/opt/java/bin/java", "-jar", "vyxal.jar", "-h"]
diff --git a/langs/wren/Dockerfile b/langs/wren/Dockerfile
index 98e2a757..f9b0e6be 100644
--- a/langs/wren/Dockerfile
+++ b/langs/wren/Dockerfile
@@ -1,19 +1,23 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
 RUN apk add --no-cache build-base curl
 
-RUN curl -L https://github.com/wren-lang/wren-cli/archive/refs/tags/0.4.0.tar.gz \
-  | tar xz
+ENV LDFLAGS='-static' VER=0.4.0
+
+RUN curl -#L https://github.com/wren-lang/wren-cli/archive/refs/tags/$VER.tar.gz \
+  | tar xz --strip-components 1
 
 COPY stdin.patch /
 
-RUN mv wren-* wren         \
- && patch -p0 <stdin.patch \
- && LDFLAGS=-static make -C /wren/projects/make
+RUN patch -p0 < stdin.patch
+
+WORKDIR /projects/make
+
+RUN make
 
 FROM codegolf/lang-base
 
-COPY --from=0 /wren/bin/wren_cli /usr/bin/wren
+COPY --from=0 /bin/wren_cli /usr/bin/wren
 
 ENTRYPOINT ["wren"]
 
diff --git a/langs/wren/stdin.patch b/langs/wren/stdin.patch
index f0f8fbe7..c1f70d21 100644
--- a/langs/wren/stdin.patch
+++ b/langs/wren/stdin.patch
@@ -1,5 +1,5 @@
---- wren/src/cli/vm.c
-+++ wren/src/cli/vm.c
+--- src/cli/vm.c
++++ src/cli/vm.c
 @@ -303,11 +303,35 @@ static void freeVM()
  
  WrenInterpretResult runFile(const char* path)
diff --git a/langs/zig/Dockerfile b/langs/zig/Dockerfile
index 9c4beb46..520d8920 100644
--- a/langs/zig/Dockerfile
+++ b/langs/zig/Dockerfile
@@ -1,31 +1,26 @@
-FROM alpine:3.21 AS builder
+FROM alpine:3.22 AS builder
 
-RUN apk add --no-cache binutils curl gcc musl-dev
+RUN apk add --no-cache curl gcc musl-dev
 
-ENV VER=0.14.0
+ENV VER=0.14.1
 
-RUN curl -# https://ziglang.org/download/$VER/zig-linux-x86_64-$VER.tar.xz \
-  | tar xJC tmp --strip-components 1
+RUN curl -#L https://ziglang.org/download/$VER/zig-x86_64-linux-$VER.tar.xz \
+  | tar xJ --directory /usr/local --strip-components 1
 
-# Remove some platforms we don't need.
-RUN rm -r /tmp/lib/libc/darwin \
-          /tmp/lib/libc/glibc  \
-          /tmp/lib/libc/mingw  \
-          /tmp/lib/libc/wasi   \
- && strip /tmp/zig
+COPY zig.c /
 
-COPY zigwrapper.c /
-
-RUN gcc -Wall -Werror -Wextra -o /usr/bin/zigwrapper -s -static zigwrapper.c
+RUN gcc -Wall -Werror -Wextra -o /usr/bin/zig -s -static zig.c
 
 FROM codegolf/lang-base
 
-COPY --from=0 /lib/ld-musl-x86_64.so.1 /lib/
-COPY --from=0 /usr/bin/env             \
-              /usr/bin/zigwrapper      /usr/bin/
-COPY --from=0 /tmp/zig                 /usr/local/bin/
-COPY --from=0 /tmp/lib                 /usr/local/lib
+COPY --from=0 /lib/ld-musl-x86_64.so.1       /lib/
+COPY --from=0 /usr/bin/env /usr/bin/zig      /usr/bin/
+COPY --from=0 /usr/local/zig                 /usr/local/
+COPY --from=0 /usr/local/lib/compiler_rt.zig \
+              /usr/local/lib/ubsan_rt.zig    /usr/local/lib/
+COPY --from=0 /usr/local/lib/compiler_rt     /usr/local/lib/compiler_rt
+COPY --from=0 /usr/local/lib/std             /usr/local/lib/std
 
-ENTRYPOINT ["zigwrapper"]
+ENTRYPOINT ["zig"]
 
 CMD ["--version"]
diff --git a/langs/zig/zigwrapper.c b/langs/zig/zig.c
similarity index 95%
rename from langs/zig/zigwrapper.c
rename to langs/zig/zig.c
index a8c563af..c628565e 100644
--- a/langs/zig/zigwrapper.c
+++ b/langs/zig/zig.c
@@ -6,7 +6,7 @@
 
 #define ERR_AND_EXIT(msg) do { perror(msg); exit(EXIT_FAILURE); } while (0)
 
-const char* zig = "/usr/local/bin/zig", *code = "code.zig";
+const char* zig = "/usr/local/zig", *code = "code.zig";
 
 int main(int argc, char* argv[]) {
     if (!strcmp(argv[1], "--version")) {
diff --git a/latest-langs b/latest-langs
index b4a9f839..e9b32aa3 100755
--- a/latest-langs
+++ b/latest-langs
@@ -20,8 +20,8 @@ constant %paths = (
     'Arturo'       => 'github.com/arturo-lang/arturo',
     'Assembly'     => 'registry.npmjs.org/@defasm/core/latest',
     'AWK'          => 'ftp.gnu.org/gnu/gawk/?C=M;O=D',
-    'Bash'         => 'en.wikipedia.org/wiki/Bash_(Unix_shell)',
-    'BASIC'        => 'en.wikipedia.org/wiki/FreeBASIC',
+    'Bash'         => 'ftp.gnu.org/gnu/bash/?C=M;O=D',
+    'BASIC'        => 'github.com/freebasic/fbc/releases/latest',
     'Befunge'      => 'github.com/VorpalBlade/cfunge',
     'Berry'        => 'github.com/berry-lang/berry/releases/latest',
     'BQN'          => 'aplwiki.com/wiki/BQN',
@@ -69,9 +69,9 @@ constant %paths = (
     'Julia'        => 'api.github.com/repos/JuliaLang/julia/releases',
     'K'            => 'codeberg.org/ngn/k',
     'Kotlin'       => 'github.com/JetBrains/kotlin/releases/latest',
-    'Lua'          => 'en.wikipedia.org/wiki/Lua_(programming_language)',
+    'Lua'          => 'github.com/lua/lua/releases/latest',
     'Nim'          => 'en.wikipedia.org/wiki/Nim_(programming_language)',
-    'OCaml'        => 'en.wikipedia.org/wiki/OCaml',
+    'OCaml'        => 'github.com/ocaml/ocaml/releases/latest',
     'Odin'         => 'api.github.com/repos/odin-lang/Odin/releases',
     'Pascal'       => 'en.wikipedia.org/wiki/Free_Pascal',
     'Perl'         => 'endoflife.date/api/perl.json',
@@ -85,21 +85,23 @@ constant %paths = (
     'Racket'       => 'github.com/racket/racket/releases/latest',
     'Raku'         => 'github.com/rakudo/rakudo/releases/latest',
     'Rebol'        => 'en.wikipedia.org/wiki/Rebol',
-    'Rexx'         => 'sourceforge.net/projects/regina-rexx/files',
+    'Rexx'         => 'packages.gentoo.org/packages/dev-lang/regina-rexx',
     'Rockstar'     => 'github.com/RockstarLang/rockstar/releases/latest',
     'Ruby'         => 'endoflife.date/api/ruby.json',
-    'Rust'         => 'en.wikipedia.org/wiki/Rust_(programming_language)',
+    'Rust'         => 'github.com/rust-lang/rust/releases/latest',
     'Scala'        => 'github.com/scala/scala/releases/latest',
-    'Scheme'       => 'en.wikipedia.org/wiki/Chez_Scheme',
+    'Scheme'       => 'github.com/cisco/ChezScheme/releases/latest',
     'sed'          => 'ftp.gnu.org/gnu/sed/?C=M;O=D',
-    'SQL'          => 'en.wikipedia.org/wiki/SQLite',
+    'SQL'          => 'sqlite.org/index.html',
     'Squirrel'     => 'github.com/albertodemichelis/squirrel/releases/latest',
     'Stax'         => 'github.com/tomtheisen/stax/releases/latest',
     'Swift'        => 'github.com/swiftlang/swift/releases/latest',
     'Tcl'          => 'en.wikipedia.org/wiki/Tcl',
     'TeX'          => 'en.wikipedia.org/wiki/TeX',
     'Uiua'         => 'crates.io/api/v1/crates/uiua',
+    'Umka'         => 'github.com/vtereshkov/umka-lang/releases/latest',
     'V'            => 'en.wikipedia.org/wiki/V_(programming_language)',
+    'Vala'         => 'gitlab.gnome.org/GNOME/vala/-/tags',
     'VimL'         => 'en.wikipedia.org/wiki/Vim_(text_editor)',
     'Vyxal'        => 'github.com/Vyxal/Vyxal/releases/latest',
     'Wren'         => 'github.com/wren-lang/wren/releases/latest',
@@ -112,6 +114,9 @@ my %langs = from-toml 'config/data/langs.toml'.IO;
 my regex ver { \d+ ( <[.-]> \d+ )+ | <xdigit> ** 7..* }
 
 for %langs{ @langs || * }:p.sort: *.key.fc -> (:key($name), :value(%lang)) {
+    # Catch exceptions and carry on to the next lang.
+    CATCH { default { note "$name: $_" } }
+
     my $old = %lang<version> ~~ / <ver> /;
     my $new = do with %paths{$name} {
         my $res = get($_);
@@ -125,13 +130,16 @@ for %langs{ @langs || * }:p.sort: *.key.fc -> (:key($name), :value(%lang)) {
         when / 'endoflife'      / { $res.&from-json[0]<latest> }
         when / 'fennel-lang'    / { $res ~~ / 'fennel-' <(<ver>)> / }
         when / 'ftp.gnu.org'    / { $res ~~ / '-' <(<ver>)> '.tar.gz' / }
-        when / 'golfscript.com' / { $res ~~ / '(Alpha)' .+? <(<ver>)> / }
+        when / 'gentoo.org'     / { $res ~~ / 'Version' .+? <(<ver>)> / }
+        when / 'gnome.org'      / { $res ~~ / 'Release ' <(<ver>)> / }
+        when / 'golfscript.com' / { $res ~~ / 'iogii-' <(<ver>)> / }
         when / 'jmvdveer'       / { $res ~~ / 'algol68g-' <(<ver>)> / }
         when / 'lists.sr.ht'    / { $res ~~ / 'Hare ' <(<ver>)> ' release' / }
         when / 'npmjs.org'      / { $res.&from-json[0]<version> }
         when / 'picat-lang.org' / { $res ~~ / 'Version ' <(<ver>)> / }
         when / 'pypi.org'       / { $res.&from-json<info><version> }
         when / 'sourceforge'    / { $res ~~ / 'Latest' .+? <(<ver>)> / }
+        when / 'sqlite.org'     / { $res ~~ / 'Version' .+? <(<ver>)> / }
         when / 'swi-prolog'     / { $res ~~ / 'stable' .+? <(<ver>)> / }
         when / 'api.github.com' / {
             $res.&from-json.grep(!*<prerelease>).map({ .<name> ~~ / <ver> / })
diff --git a/main.go b/main.go
index 85656cf2..5ab5f3b5 100644
--- a/main.go
+++ b/main.go
@@ -61,23 +61,31 @@ func main() {
 				`INSERT INTO trophies(user_id, trophy)
 				      SELECT user_id, 'big-brother'::cheevo
 				        FROM points
-				       WHERE points >= 1984
+				       WHERE points >= 1_984
 				   UNION ALL
 				      SELECT user_id, 'its-over-9000'
 				        FROM points
-				       WHERE points > 9000
+				       WHERE points > 9_000
 				   UNION ALL
 				      SELECT user_id, 'twenty-kiloleagues'
 				        FROM points
-				       WHERE points >= 20000
+				       WHERE points >= 20_000
 				   UNION ALL
 				      SELECT user_id, 'marathon-runner'
 				        FROM points
-				       WHERE points >= 42195
+				       WHERE points >= 42_195
 				   UNION ALL
 				      SELECT user_id, '0xdead'
 				        FROM points
-				       WHERE points >= 57005
+				       WHERE points >= 57_005
+				   UNION ALL
+				      SELECT user_id, 'overflowing'
+				        FROM points
+				       WHERE points >= 65_536
+				   UNION ALL
+				      SELECT user_id, 'into-space'
+				        FROM points
+				       WHERE points >= 100_000
 				 ON CONFLICT DO NOTHING`,
 			); err != nil {
 				log.Println(err)
@@ -193,8 +201,8 @@ func populateHolesLangsTables(db *sqlx.DB) error {
 	}
 
 	insertLang, err := tx.PrepareNamed(
-		`INSERT INTO langs ( id,  experiment,  digest_trunc)
-		      VALUES       (:id, :experiment, :digest_trunc)`,
+		`INSERT INTO langs ( id,  experiment,  digest_trunc,  name)
+		      VALUES       (:id, :experiment, :digest_trunc, :name)`,
 	)
 	if err != nil {
 		return err
diff --git a/package-lock.json b/package-lock.json
index 084e937a..55b0f06b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -61,9 +61,9 @@
       }
     },
     "node_modules/@codemirror/commands": {
-      "version": "6.8.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz",
-      "integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==",
+      "version": "6.8.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.1.tgz",
+      "integrity": "sha512-KlGVYufHMQzxbdQONiLyGQDUW0itrLZwq3CcY7xpv9ZLRHqzkBSoteocBHtMCoY7/Ci4xhzSrToIeLg7FxHuaw==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/language": "^6.0.0",
@@ -73,9 +73,9 @@
       }
     },
     "node_modules/@codemirror/lang-cpp": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz",
-      "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==",
+      "version": "6.0.3",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.3.tgz",
+      "integrity": "sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/language": "^6.0.0",
@@ -126,9 +126,9 @@
       }
     },
     "node_modules/@codemirror/lang-java": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz",
-      "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.2.tgz",
+      "integrity": "sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/language": "^6.0.0",
@@ -136,9 +136,9 @@
       }
     },
     "node_modules/@codemirror/lang-javascript": {
-      "version": "6.2.3",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz",
-      "integrity": "sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==",
+      "version": "6.2.4",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.4.tgz",
+      "integrity": "sha512-0WVmhp1QOqZ4Rt6GlVGwKJN3KW7Xh4H2q8ZZNGZaP6lRdxXJzmjm4FqvmOojVj6khWJHIb9sp7U/72W7xQgqAA==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/autocomplete": "^6.0.0",
@@ -151,9 +151,9 @@
       }
     },
     "node_modules/@codemirror/lang-php": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz",
-      "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.2.tgz",
+      "integrity": "sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/lang-html": "^6.0.0",
@@ -164,9 +164,9 @@
       }
     },
     "node_modules/@codemirror/lang-python": {
-      "version": "6.1.7",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.7.tgz",
-      "integrity": "sha512-mZnFTsL4lW5p9ch8uKNKeRU3xGGxr1QpESLilfON2E3fQzOa/OygEMkaDvERvXDJWJA9U9oN/D4w0ZuUzNO4+g==",
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.2.1.tgz",
+      "integrity": "sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/autocomplete": "^6.3.2",
@@ -177,9 +177,9 @@
       }
     },
     "node_modules/@codemirror/lang-rust": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz",
-      "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.2.tgz",
+      "integrity": "sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/language": "^6.0.0",
@@ -187,9 +187,9 @@
       }
     },
     "node_modules/@codemirror/lang-sql": {
-      "version": "6.8.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz",
-      "integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==",
+      "version": "6.9.0",
+      "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.9.0.tgz",
+      "integrity": "sha512-xmtpWqKSgum1B1J3Ro6rf7nuPqf2+kJQg5SjrofCAcyCThOe0ihSktSoXfXuhQBnwx1QbmreBbLJM5Jru6zitg==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/autocomplete": "^6.0.0",
@@ -201,9 +201,9 @@
       }
     },
     "node_modules/@codemirror/language": {
-      "version": "6.11.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.0.tgz",
-      "integrity": "sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==",
+      "version": "6.11.2",
+      "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.2.tgz",
+      "integrity": "sha512-p44TsNArL4IVXDTbapUmEkAlvWs2CFQbcfc0ymDsis1kH2wh0gcY96AS29c/vp2d0y2Tquk1EDSaawpzilUiAw==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/state": "^6.0.0",
@@ -215,9 +215,9 @@
       }
     },
     "node_modules/@codemirror/legacy-modes": {
-      "version": "6.5.0",
-      "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.0.tgz",
-      "integrity": "sha512-dNw5pwTqtR1giYjaJyEajunLqxGavZqV0XRtVZyMJnNOD2HmK9DMUmuCAr6RMFGRJ4l8OeQDjpI/us+R09mQsw==",
+      "version": "6.5.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.5.1.tgz",
+      "integrity": "sha512-DJYQQ00N1/KdESpZV7jg9hafof/iBNp9h7TYo1SLMk86TWl9uDsVdho2dzd81K+v4retmK6mdC7WpuOQDytQqw==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/language": "^6.0.0"
@@ -235,9 +235,9 @@
       }
     },
     "node_modules/@codemirror/search": {
-      "version": "6.5.10",
-      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz",
-      "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==",
+      "version": "6.5.11",
+      "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.11.tgz",
+      "integrity": "sha512-KmWepDE6jUdL6n8cAAqIpRmLPBZ5ZKnicE8oGU/s3QrAVID+0VhLFrzUucVKHG5035/BSykhExDL/Xm7dHthiA==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/state": "^6.0.0",
@@ -255,9 +255,9 @@
       }
     },
     "node_modules/@codemirror/theme-one-dark": {
-      "version": "6.1.2",
-      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz",
-      "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==",
+      "version": "6.1.3",
+      "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz",
+      "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/language": "^6.0.0",
@@ -267,12 +267,13 @@
       }
     },
     "node_modules/@codemirror/view": {
-      "version": "6.36.5",
-      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.5.tgz",
-      "integrity": "sha512-cd+FZEUlu3GQCYnguYm3EkhJ8KJVisqqUsCOKedBoAt/d9c76JUUap6U0UrpElln5k6VyrEOYliMuDAKIeDQLg==",
+      "version": "6.38.1",
+      "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz",
+      "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/state": "^6.5.0",
+        "crelt": "^1.0.6",
         "style-mod": "^4.1.0",
         "w3c-keyname": "^2.2.4"
       }
@@ -299,9 +300,9 @@
       "license": "ISC"
     },
     "node_modules/@esbuild/aix-ppc64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.1.tgz",
-      "integrity": "sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz",
+      "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==",
       "cpu": [
         "ppc64"
       ],
@@ -315,9 +316,9 @@
       }
     },
     "node_modules/@esbuild/android-arm": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.1.tgz",
-      "integrity": "sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz",
+      "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==",
       "cpu": [
         "arm"
       ],
@@ -331,9 +332,9 @@
       }
     },
     "node_modules/@esbuild/android-arm64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.1.tgz",
-      "integrity": "sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz",
+      "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==",
       "cpu": [
         "arm64"
       ],
@@ -347,9 +348,9 @@
       }
     },
     "node_modules/@esbuild/android-x64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.1.tgz",
-      "integrity": "sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz",
+      "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==",
       "cpu": [
         "x64"
       ],
@@ -363,9 +364,9 @@
       }
     },
     "node_modules/@esbuild/darwin-arm64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.1.tgz",
-      "integrity": "sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz",
+      "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==",
       "cpu": [
         "arm64"
       ],
@@ -379,9 +380,9 @@
       }
     },
     "node_modules/@esbuild/darwin-x64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.1.tgz",
-      "integrity": "sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz",
+      "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==",
       "cpu": [
         "x64"
       ],
@@ -395,9 +396,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-arm64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.1.tgz",
-      "integrity": "sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz",
+      "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==",
       "cpu": [
         "arm64"
       ],
@@ -411,9 +412,9 @@
       }
     },
     "node_modules/@esbuild/freebsd-x64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.1.tgz",
-      "integrity": "sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz",
+      "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==",
       "cpu": [
         "x64"
       ],
@@ -427,9 +428,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.1.tgz",
-      "integrity": "sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz",
+      "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==",
       "cpu": [
         "arm"
       ],
@@ -443,9 +444,9 @@
       }
     },
     "node_modules/@esbuild/linux-arm64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.1.tgz",
-      "integrity": "sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz",
+      "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==",
       "cpu": [
         "arm64"
       ],
@@ -459,9 +460,9 @@
       }
     },
     "node_modules/@esbuild/linux-ia32": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.1.tgz",
-      "integrity": "sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz",
+      "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==",
       "cpu": [
         "ia32"
       ],
@@ -475,9 +476,9 @@
       }
     },
     "node_modules/@esbuild/linux-loong64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.1.tgz",
-      "integrity": "sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz",
+      "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==",
       "cpu": [
         "loong64"
       ],
@@ -491,9 +492,9 @@
       }
     },
     "node_modules/@esbuild/linux-mips64el": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.1.tgz",
-      "integrity": "sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz",
+      "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==",
       "cpu": [
         "mips64el"
       ],
@@ -507,9 +508,9 @@
       }
     },
     "node_modules/@esbuild/linux-ppc64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.1.tgz",
-      "integrity": "sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz",
+      "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==",
       "cpu": [
         "ppc64"
       ],
@@ -523,9 +524,9 @@
       }
     },
     "node_modules/@esbuild/linux-riscv64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.1.tgz",
-      "integrity": "sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz",
+      "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==",
       "cpu": [
         "riscv64"
       ],
@@ -539,9 +540,9 @@
       }
     },
     "node_modules/@esbuild/linux-s390x": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.1.tgz",
-      "integrity": "sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz",
+      "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==",
       "cpu": [
         "s390x"
       ],
@@ -555,9 +556,9 @@
       }
     },
     "node_modules/@esbuild/linux-x64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.1.tgz",
-      "integrity": "sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz",
+      "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==",
       "cpu": [
         "x64"
       ],
@@ -571,9 +572,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-arm64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.1.tgz",
-      "integrity": "sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz",
+      "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==",
       "cpu": [
         "arm64"
       ],
@@ -587,9 +588,9 @@
       }
     },
     "node_modules/@esbuild/netbsd-x64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.1.tgz",
-      "integrity": "sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz",
+      "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==",
       "cpu": [
         "x64"
       ],
@@ -603,9 +604,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-arm64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.1.tgz",
-      "integrity": "sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz",
+      "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==",
       "cpu": [
         "arm64"
       ],
@@ -619,9 +620,9 @@
       }
     },
     "node_modules/@esbuild/openbsd-x64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.1.tgz",
-      "integrity": "sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz",
+      "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==",
       "cpu": [
         "x64"
       ],
@@ -634,10 +635,26 @@
         "node": ">=18"
       }
     },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz",
+      "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/@esbuild/sunos-x64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.1.tgz",
-      "integrity": "sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz",
+      "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==",
       "cpu": [
         "x64"
       ],
@@ -651,9 +668,9 @@
       }
     },
     "node_modules/@esbuild/win32-arm64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.1.tgz",
-      "integrity": "sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz",
+      "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==",
       "cpu": [
         "arm64"
       ],
@@ -667,9 +684,9 @@
       }
     },
     "node_modules/@esbuild/win32-ia32": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.1.tgz",
-      "integrity": "sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz",
+      "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==",
       "cpu": [
         "ia32"
       ],
@@ -683,9 +700,9 @@
       }
     },
     "node_modules/@esbuild/win32-x64": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.1.tgz",
-      "integrity": "sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz",
+      "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==",
       "cpu": [
         "x64"
       ],
@@ -699,9 +716,9 @@
       }
     },
     "node_modules/@eslint-community/eslint-utils": {
-      "version": "4.5.1",
-      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz",
-      "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==",
+      "version": "4.7.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
+      "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -728,9 +745,9 @@
       }
     },
     "node_modules/@eslint/config-array": {
-      "version": "0.19.2",
-      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz",
-      "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==",
+      "version": "0.21.0",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
+      "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
@@ -743,9 +760,9 @@
       }
     },
     "node_modules/@eslint/config-array/node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -767,9 +784,9 @@
       }
     },
     "node_modules/@eslint/config-helpers": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz",
-      "integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==",
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz",
+      "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -777,9 +794,9 @@
       }
     },
     "node_modules/@eslint/core": {
-      "version": "0.12.0",
-      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz",
-      "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==",
+      "version": "0.15.1",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz",
+      "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
@@ -814,9 +831,9 @@
       }
     },
     "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -824,6 +841,16 @@
         "concat-map": "0.0.1"
       }
     },
+    "node_modules/@eslint/eslintrc/node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
     "node_modules/@eslint/eslintrc/node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -838,13 +865,16 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "9.23.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz",
-      "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==",
+      "version": "9.32.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
+      "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
       "dev": true,
       "license": "MIT",
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
       }
     },
     "node_modules/@eslint/object-schema": {
@@ -858,13 +888,13 @@
       }
     },
     "node_modules/@eslint/plugin-kit": {
-      "version": "0.2.7",
-      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz",
-      "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==",
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz",
+      "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==",
       "dev": true,
       "license": "Apache-2.0",
       "dependencies": {
-        "@eslint/core": "^0.12.0",
+        "@eslint/core": "^0.15.1",
         "levn": "^0.4.1"
       },
       "engines": {
@@ -947,9 +977,9 @@
       }
     },
     "node_modules/@humanwhocodes/retry": {
-      "version": "0.4.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz",
-      "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==",
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+      "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -984,25 +1014,25 @@
       }
     },
     "node_modules/@lezer/css": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.11.tgz",
-      "integrity": "sha512-FuAnusbLBl1SEAtfN8NdShxYJiESKw9LAFysfea1T96jD3ydBn12oYjaSG1a04BQRIUd93/0D8e5CV1cUMkmQg==",
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.0.tgz",
+      "integrity": "sha512-pBL7hup88KbI7hXnZV3PQsn43DHy6TWyzuyk2AO9UyoXcDltvIdqWKE1dLL/45JVZ+YZkHe1WVHqO6wugZZWcw==",
       "license": "MIT",
       "dependencies": {
         "@lezer/common": "^1.2.0",
         "@lezer/highlight": "^1.0.0",
-        "@lezer/lr": "^1.0.0"
+        "@lezer/lr": "^1.3.0"
       }
     },
     "node_modules/@lezer/go": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.0.tgz",
-      "integrity": "sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.1.tgz",
+      "integrity": "sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==",
       "license": "MIT",
       "dependencies": {
         "@lezer/common": "^1.2.0",
         "@lezer/highlight": "^1.0.0",
-        "@lezer/lr": "^1.0.0"
+        "@lezer/lr": "^1.3.0"
       }
     },
     "node_modules/@lezer/highlight": {
@@ -1037,9 +1067,9 @@
       }
     },
     "node_modules/@lezer/javascript": {
-      "version": "1.4.21",
-      "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz",
-      "integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==",
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.1.tgz",
+      "integrity": "sha512-ATOImjeVJuvgm3JQ/bpo2Tmv55HSScE2MTPnKRMRIPx2cLhHGyX2VnqpHhtIV1tVzIjZDbcWQm+NCTF40ggZVw==",
       "license": "MIT",
       "dependencies": {
         "@lezer/common": "^1.2.0",
@@ -1057,9 +1087,9 @@
       }
     },
     "node_modules/@lezer/php": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.2.tgz",
-      "integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==",
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.4.tgz",
+      "integrity": "sha512-D2dJ0t8Z28/G1guztRczMFvPDUqzeMLSQbdWQmaiHV7urc8NlEOnjYk9UrZ531OcLiRxD4Ihcbv7AsDpNKDRaQ==",
       "license": "MIT",
       "dependencies": {
         "@lezer/common": "^1.2.0",
@@ -1068,9 +1098,9 @@
       }
     },
     "node_modules/@lezer/python": {
-      "version": "1.1.17",
-      "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.17.tgz",
-      "integrity": "sha512-Iz0doICPko9uv2chIfSsViNSugNa4PWhxs17jtFd0ZMt+OieDq3wxtFOdmj7wtst3FWDeJkB0CxWNot0BlYixw==",
+      "version": "1.1.18",
+      "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.18.tgz",
+      "integrity": "sha512-31FiUrU7z9+d/ElGQLJFXl+dKOdx0jALlP3KEOsGTex8mvj+SoE1FgItcHWK/axkxCHGUSpqIHt6JAWfWu9Rhg==",
       "license": "MIT",
       "dependencies": {
         "@lezer/common": "^1.2.0",
@@ -1146,16 +1176,6 @@
         "@codemirror/view": "6.x.x"
       }
     },
-    "node_modules/@trysound/sax": {
-      "version": "0.2.0",
-      "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
-      "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
-      "dev": true,
-      "license": "ISC",
-      "engines": {
-        "node": ">=10.13.0"
-      }
-    },
     "node_modules/@types/diff": {
       "version": "7.0.2",
       "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz",
@@ -1164,9 +1184,9 @@
       "license": "MIT"
     },
     "node_modules/@types/estree": {
-      "version": "1.0.7",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
-      "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
       "dev": true,
       "license": "MIT"
     },
@@ -1178,21 +1198,21 @@
       "license": "MIT"
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.28.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.28.0.tgz",
-      "integrity": "sha512-lvFK3TCGAHsItNdWZ/1FkvpzCxTHUVuFrdnOGLMa0GGCFIbCgQWVk3CzCGdA7kM3qGVc+dfW9tr0Z/sHnGDFyg==",
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
+      "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.28.0",
-        "@typescript-eslint/type-utils": "8.28.0",
-        "@typescript-eslint/utils": "8.28.0",
-        "@typescript-eslint/visitor-keys": "8.28.0",
+        "@typescript-eslint/scope-manager": "8.38.0",
+        "@typescript-eslint/type-utils": "8.38.0",
+        "@typescript-eslint/utils": "8.38.0",
+        "@typescript-eslint/visitor-keys": "8.38.0",
         "graphemer": "^1.4.0",
-        "ignore": "^5.3.1",
+        "ignore": "^7.0.0",
         "natural-compare": "^1.4.0",
-        "ts-api-utils": "^2.0.1"
+        "ts-api-utils": "^2.1.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1202,22 +1222,22 @@
         "url": "https://opencollective.com/typescript-eslint"
       },
       "peerDependencies": {
-        "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
+        "@typescript-eslint/parser": "^8.38.0",
         "eslint": "^8.57.0 || ^9.0.0",
         "typescript": ">=4.8.4 <5.9.0"
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.28.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.28.0.tgz",
-      "integrity": "sha512-LPcw1yHD3ToaDEoljFEfQ9j2xShY367h7FZ1sq5NJT9I3yj4LHer1Xd1yRSOdYy9BpsrxU7R+eoDokChYM53lQ==",
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
+      "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.28.0",
-        "@typescript-eslint/types": "8.28.0",
-        "@typescript-eslint/typescript-estree": "8.28.0",
-        "@typescript-eslint/visitor-keys": "8.28.0",
+        "@typescript-eslint/scope-manager": "8.38.0",
+        "@typescript-eslint/types": "8.38.0",
+        "@typescript-eslint/typescript-estree": "8.38.0",
+        "@typescript-eslint/visitor-keys": "8.38.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -1232,35 +1252,75 @@
         "typescript": ">=4.8.4 <5.9.0"
       }
     },
+    "node_modules/@typescript-eslint/project-service": {
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
+      "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/tsconfig-utils": "^8.38.0",
+        "@typescript-eslint/types": "^8.38.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <5.9.0"
+      }
+    },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.28.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.28.0.tgz",
-      "integrity": "sha512-u2oITX3BJwzWCapoZ/pXw6BCOl8rJP4Ij/3wPoGvY8XwvXflOzd1kLrDUUUAIEdJSFh+ASwdTHqtan9xSg8buw==",
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
+      "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.28.0",
-        "@typescript-eslint/visitor-keys": "8.28.0"
+        "@typescript-eslint/types": "8.38.0",
+        "@typescript-eslint/visitor-keys": "8.38.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/tsconfig-utils": {
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
+      "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
+      "dev": true,
+      "license": "MIT",
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <5.9.0"
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.28.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.28.0.tgz",
-      "integrity": "sha512-oRoXu2v0Rsy/VoOGhtWrOKDiIehvI+YNrDk5Oqj40Mwm0Yt01FC/Q7nFqg088d3yAsR1ZcZFVfPCTTFCe/KPwg==",
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
+      "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.28.0",
-        "@typescript-eslint/utils": "8.28.0",
+        "@typescript-eslint/types": "8.38.0",
+        "@typescript-eslint/typescript-estree": "8.38.0",
+        "@typescript-eslint/utils": "8.38.0",
         "debug": "^4.3.4",
-        "ts-api-utils": "^2.0.1"
+        "ts-api-utils": "^2.1.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1275,9 +1335,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.28.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.28.0.tgz",
-      "integrity": "sha512-bn4WS1bkKEjx7HqiwG2JNB3YJdC1q6Ue7GyGlwPHyt0TnVq6TtD/hiOdTZt71sq0s7UzqBFXD8t8o2e63tXgwA==",
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
+      "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -1289,20 +1349,22 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.28.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.28.0.tgz",
-      "integrity": "sha512-H74nHEeBGeklctAVUvmDkxB1mk+PAZ9FiOMPFncdqeRBXxk1lWSYraHw8V12b7aa6Sg9HOBNbGdSHobBPuQSuA==",
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
+      "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.28.0",
-        "@typescript-eslint/visitor-keys": "8.28.0",
+        "@typescript-eslint/project-service": "8.38.0",
+        "@typescript-eslint/tsconfig-utils": "8.38.0",
+        "@typescript-eslint/types": "8.38.0",
+        "@typescript-eslint/visitor-keys": "8.38.0",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
         "minimatch": "^9.0.4",
         "semver": "^7.6.0",
-        "ts-api-utils": "^2.0.1"
+        "ts-api-utils": "^2.1.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1316,16 +1378,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.28.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.28.0.tgz",
-      "integrity": "sha512-OELa9hbTYciYITqgurT1u/SzpQVtDLmQMFzy/N8pQE+tefOyCWT79jHsav294aTqV1q1u+VzqDGbuujvRYaeSQ==",
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
+      "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.28.0",
-        "@typescript-eslint/types": "8.28.0",
-        "@typescript-eslint/typescript-estree": "8.28.0"
+        "@eslint-community/eslint-utils": "^4.7.0",
+        "@typescript-eslint/scope-manager": "8.38.0",
+        "@typescript-eslint/types": "8.38.0",
+        "@typescript-eslint/typescript-estree": "8.38.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1340,14 +1402,14 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.28.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.28.0.tgz",
-      "integrity": "sha512-hbn8SZ8w4u2pRwgQ1GlUrPKE+t2XvcCW5tTRF7j6SMYIuYG37XuzIW44JCZPa36evi0Oy2SnM664BlIaAuQcvg==",
+      "version": "8.38.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
+      "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.28.0",
-        "eslint-visitor-keys": "^4.2.0"
+        "@typescript-eslint/types": "8.38.0",
+        "eslint-visitor-keys": "^4.2.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1358,9 +1420,9 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
-      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -1371,9 +1433,9 @@
       }
     },
     "node_modules/acorn": {
-      "version": "8.14.1",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
-      "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
       "dev": true,
       "license": "MIT",
       "bin": {
@@ -1448,9 +1510,9 @@
       "license": "ISC"
     },
     "node_modules/brace-expansion": {
-      "version": "2.0.1",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
-      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1498,9 +1560,9 @@
       }
     },
     "node_modules/chart.js": {
-      "version": "4.4.8",
-      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
-      "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
+      "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
       "license": "MIT",
       "dependencies": {
         "@kurkle/color": "^0.3.0"
@@ -1520,9 +1582,9 @@
       }
     },
     "node_modules/codemirror": {
-      "version": "6.0.1",
-      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
-      "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
+      "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
       "license": "MIT",
       "dependencies": {
         "@codemirror/autocomplete": "^6.0.0",
@@ -1652,13 +1714,13 @@
       "license": "MIT"
     },
     "node_modules/commander": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
-      "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+      "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
-        "node": ">= 10"
+        "node": ">=16"
       }
     },
     "node_modules/concat-map": {
@@ -1690,9 +1752,9 @@
       }
     },
     "node_modules/css-select": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
-      "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+      "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
@@ -1707,13 +1769,13 @@
       }
     },
     "node_modules/css-tree": {
-      "version": "2.3.1",
-      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
-      "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
+      "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "mdn-data": "2.0.30",
+        "mdn-data": "2.12.2",
         "source-map-js": "^1.0.1"
       },
       "engines": {
@@ -1721,9 +1783,9 @@
       }
     },
     "node_modules/css-what": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
-      "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+      "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
       "dev": true,
       "license": "BSD-2-Clause",
       "engines": {
@@ -1770,9 +1832,9 @@
       "license": "CC0-1.0"
     },
     "node_modules/debug": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
-      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1795,9 +1857,9 @@
       "license": "MIT"
     },
     "node_modules/diff": {
-      "version": "7.0.0",
-      "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
-      "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
+      "integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
       "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.3.1"
@@ -1876,9 +1938,9 @@
       }
     },
     "node_modules/esbuild": {
-      "version": "0.25.1",
-      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
-      "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==",
+      "version": "0.25.8",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz",
+      "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==",
       "hasInstallScript": true,
       "license": "MIT",
       "bin": {
@@ -1888,31 +1950,32 @@
         "node": ">=18"
       },
       "optionalDependencies": {
-        "@esbuild/aix-ppc64": "0.25.1",
-        "@esbuild/android-arm": "0.25.1",
-        "@esbuild/android-arm64": "0.25.1",
-        "@esbuild/android-x64": "0.25.1",
-        "@esbuild/darwin-arm64": "0.25.1",
-        "@esbuild/darwin-x64": "0.25.1",
-        "@esbuild/freebsd-arm64": "0.25.1",
-        "@esbuild/freebsd-x64": "0.25.1",
-        "@esbuild/linux-arm": "0.25.1",
-        "@esbuild/linux-arm64": "0.25.1",
-        "@esbuild/linux-ia32": "0.25.1",
-        "@esbuild/linux-loong64": "0.25.1",
-        "@esbuild/linux-mips64el": "0.25.1",
-        "@esbuild/linux-ppc64": "0.25.1",
-        "@esbuild/linux-riscv64": "0.25.1",
-        "@esbuild/linux-s390x": "0.25.1",
-        "@esbuild/linux-x64": "0.25.1",
-        "@esbuild/netbsd-arm64": "0.25.1",
-        "@esbuild/netbsd-x64": "0.25.1",
-        "@esbuild/openbsd-arm64": "0.25.1",
-        "@esbuild/openbsd-x64": "0.25.1",
-        "@esbuild/sunos-x64": "0.25.1",
-        "@esbuild/win32-arm64": "0.25.1",
-        "@esbuild/win32-ia32": "0.25.1",
-        "@esbuild/win32-x64": "0.25.1"
+        "@esbuild/aix-ppc64": "0.25.8",
+        "@esbuild/android-arm": "0.25.8",
+        "@esbuild/android-arm64": "0.25.8",
+        "@esbuild/android-x64": "0.25.8",
+        "@esbuild/darwin-arm64": "0.25.8",
+        "@esbuild/darwin-x64": "0.25.8",
+        "@esbuild/freebsd-arm64": "0.25.8",
+        "@esbuild/freebsd-x64": "0.25.8",
+        "@esbuild/linux-arm": "0.25.8",
+        "@esbuild/linux-arm64": "0.25.8",
+        "@esbuild/linux-ia32": "0.25.8",
+        "@esbuild/linux-loong64": "0.25.8",
+        "@esbuild/linux-mips64el": "0.25.8",
+        "@esbuild/linux-ppc64": "0.25.8",
+        "@esbuild/linux-riscv64": "0.25.8",
+        "@esbuild/linux-s390x": "0.25.8",
+        "@esbuild/linux-x64": "0.25.8",
+        "@esbuild/netbsd-arm64": "0.25.8",
+        "@esbuild/netbsd-x64": "0.25.8",
+        "@esbuild/openbsd-arm64": "0.25.8",
+        "@esbuild/openbsd-x64": "0.25.8",
+        "@esbuild/openharmony-arm64": "0.25.8",
+        "@esbuild/sunos-x64": "0.25.8",
+        "@esbuild/win32-arm64": "0.25.8",
+        "@esbuild/win32-ia32": "0.25.8",
+        "@esbuild/win32-x64": "0.25.8"
       }
     },
     "node_modules/escape-string-regexp": {
@@ -1929,20 +1992,20 @@
       }
     },
     "node_modules/eslint": {
-      "version": "9.23.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz",
-      "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==",
+      "version": "9.32.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
+      "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.12.1",
-        "@eslint/config-array": "^0.19.2",
-        "@eslint/config-helpers": "^0.2.0",
-        "@eslint/core": "^0.12.0",
+        "@eslint/config-array": "^0.21.0",
+        "@eslint/config-helpers": "^0.3.0",
+        "@eslint/core": "^0.15.0",
         "@eslint/eslintrc": "^3.3.1",
-        "@eslint/js": "9.23.0",
-        "@eslint/plugin-kit": "^0.2.7",
+        "@eslint/js": "9.32.0",
+        "@eslint/plugin-kit": "^0.3.4",
         "@humanfs/node": "^0.16.6",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@humanwhocodes/retry": "^0.4.2",
@@ -1953,9 +2016,9 @@
         "cross-spawn": "^7.0.6",
         "debug": "^4.3.2",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^8.3.0",
-        "eslint-visitor-keys": "^4.2.0",
-        "espree": "^10.3.0",
+        "eslint-scope": "^8.4.0",
+        "eslint-visitor-keys": "^4.2.1",
+        "espree": "^10.4.0",
         "esquery": "^1.5.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
@@ -1990,9 +2053,9 @@
       }
     },
     "node_modules/eslint-scope": {
-      "version": "8.3.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz",
-      "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+      "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
@@ -2020,9 +2083,9 @@
       }
     },
     "node_modules/eslint/node_modules/brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -2031,9 +2094,9 @@
       }
     },
     "node_modules/eslint/node_modules/eslint-visitor-keys": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
-      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -2043,6 +2106,16 @@
         "url": "https://opencollective.com/eslint"
       }
     },
+    "node_modules/eslint/node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
     "node_modules/eslint/node_modules/minimatch": {
       "version": "3.1.2",
       "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2057,15 +2130,15 @@
       }
     },
     "node_modules/espree": {
-      "version": "10.3.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
-      "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==",
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+      "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
       "dev": true,
       "license": "BSD-2-Clause",
       "dependencies": {
-        "acorn": "^8.14.0",
+        "acorn": "^8.15.0",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.2.0"
+        "eslint-visitor-keys": "^4.2.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2075,9 +2148,9 @@
       }
     },
     "node_modules/espree/node_modules/eslint-visitor-keys": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
-      "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==",
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
       "dev": true,
       "license": "Apache-2.0",
       "engines": {
@@ -2308,9 +2381,9 @@
       }
     },
     "node_modules/ignore": {
-      "version": "5.3.2",
-      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
-      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+      "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -2475,9 +2548,9 @@
       "license": "MIT"
     },
     "node_modules/luxon": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.0.tgz",
-      "integrity": "sha512-WE7p0p7W1xji9qxkLYsvcIxZyfP48GuFrWIBQZIsbjCyf65dG1rv4n83HcOyEyhvzxJCrUoObCRNFgRNIQ5KNA==",
+      "version": "3.7.1",
+      "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",
+      "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==",
       "license": "MIT",
       "engines": {
         "node": ">=12"
@@ -2499,9 +2572,9 @@
       "license": "Apache-2.0"
     },
     "node_modules/mdn-data": {
-      "version": "2.0.30",
-      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
-      "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
+      "version": "2.12.2",
+      "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
+      "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
       "dev": true,
       "license": "CC0-1.0"
     },
@@ -2770,10 +2843,17 @@
         "queue-microtask": "^1.2.2"
       }
     },
+    "node_modules/sax": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+      "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/semver": {
-      "version": "7.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
-      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
       "dev": true,
       "license": "ISC",
       "bin": {
@@ -2849,25 +2929,25 @@
       }
     },
     "node_modules/svgo": {
-      "version": "3.3.2",
-      "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
-      "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
+      "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@trysound/sax": "0.2.0",
-        "commander": "^7.2.0",
+        "commander": "^11.1.0",
         "css-select": "^5.1.0",
-        "css-tree": "^2.3.1",
+        "css-tree": "^3.0.1",
         "css-what": "^6.1.0",
         "csso": "^5.0.5",
-        "picocolors": "^1.0.0"
+        "picocolors": "^1.1.1",
+        "sax": "^1.4.1"
       },
       "bin": {
-        "svgo": "bin/svgo"
+        "svgo": "bin/svgo.js"
       },
       "engines": {
-        "node": ">=14.0.0"
+        "node": ">=16"
       },
       "funding": {
         "type": "opencollective",
@@ -2914,9 +2994,9 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.8.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
-      "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
       "dev": true,
       "license": "Apache-2.0",
       "peer": true,
diff --git a/routes/admin_solutions.go b/routes/admin_solutions.go
index 808f9d0e..2217444e 100644
--- a/routes/admin_solutions.go
+++ b/routes/admin_solutions.go
@@ -71,14 +71,22 @@ func adminSolutionsRunGET(w http.ResponseWriter, r *http.Request) {
 			for s := range solutions {
 				// Run each solution up to three times.
 				for range 3 {
-					// Get the first failing (or last overall) run.
-					var run hole.Run
-					for _, r := range hole.Play(
+					runs, err := hole.Play(
 						r.Context(),
 						config.AllHoleByID[s.HoleID],
 						config.AllLangByID[s.LangID],
 						s.Code,
-					) {
+					)
+					if err != nil {
+						s.Failing = true
+						s.Stderr = err.Error()
+						s.Took = 0
+						continue
+					}
+
+					// Get the first failing (or last overall) run.
+					var run hole.Run
+					for _, r := range runs {
 						run = r
 						if !r.Pass {
 							break
diff --git a/routes/api.go b/routes/api.go
index 589845f1..e5f50f04 100644
--- a/routes/api.go
+++ b/routes/api.go
@@ -258,22 +258,22 @@ func apiSolutionsSearchGET(w http.ResponseWriter, r *http.Request) {
 		if err := db.SelectContext(
 			ctx,
 			&matches,
-			`SELECT			
-				hole, lang, scoring, count,
-				substr(code, GREATEST(start - 30, 1), LEAST(30, start - 1)) AS before,
-				substr(code, start, "end" - start) AS match,
-				substr(code, "end", 30) AS after
-			FROM
-				(SELECT code, hole, lang, scoring,
-						regexp_count(code, $2, 1, $3)       AS count,
-						regexp_instr(code, $2, 1, 1, 0, $3) AS start,
-						regexp_instr(code, $2, 1, 1, 1, $3) AS end
-				FROM solutions
-				WHERE user_id = $1
-					AND regexp_like(code, $2, $3)
-					AND (hole = $4 OR $4 IS NULL)
-					AND (lang = $5 OR $5 IS NULL)
-				LIMIT 1000)`,
+			`WITH matches AS (
+			  SELECT code, hole, lang, scoring,
+			         regexp_count(code, $2, 1, $3)       count,
+			         regexp_instr(code, $2, 1, 1, 0, $3) start,
+			         regexp_instr(code, $2, 1, 1, 1, $3) end
+			    FROM solutions
+			   WHERE user_id = $1
+			     AND regexp_like(code, $2, $3)
+			     AND (hole = $4 OR $4 IS NULL)
+			     AND (lang = $5 OR $5 IS NULL)
+			   LIMIT 1000
+			) SELECT hole, lang, scoring, count,
+			         substr(code, GREATEST(start - 30, 1), LEAST(30, start - 1)) before,
+			         substr(code, start, "end" - start)                          match,
+			         substr(code, "end", 30)                                     after
+			    FROM matches`,
 			golfer.ID, pattern, flags, hole, lang,
 		); err != nil {
 			if ctx.Err() == context.DeadlineExceeded {
diff --git a/routes/api.yml b/routes/api.yml
index e310a3a2..fbd0b9dd 100644
--- a/routes/api.yml
+++ b/routes/api.yml
@@ -1,4 +1,4 @@
-openapi: 3.1.0
+openapi: 3.1.1
 
 info:
   title: Code Golf API
diff --git a/routes/banner.go b/routes/banner.go
index 3ca0e5f5..b6e40665 100644
--- a/routes/banner.go
+++ b/routes/banner.go
@@ -12,7 +12,7 @@ import (
 	"github.com/code-golf/code-golf/pretty"
 )
 
-var nextHole = config.ExpHoleByID["set"]
+var nextHole = config.ExpHoleByID["mandelbrot"]
 
 type banner struct {
 	Body          template.HTML
diff --git a/routes/feed.go b/routes/feed.go
index 7e349432..d4c28106 100644
--- a/routes/feed.go
+++ b/routes/feed.go
@@ -25,6 +25,10 @@ func init() {
 		sha, created, id string
 		hole             bool
 	}{
+		{"4caf324", "2025-07-01 00:00:04", "mandelbrot", true},
+		{"5920cfd", "2025-06-01 00:00:13", "scrambled-sort", true},
+		{"273405d", "2025-05-01 00:00:34", "star-wars-gpt", true},
+		{"ef914b6", "2025-03-31 23:00:46", "set", true},
 		{"689be06", "2025-03-01 00:00:44", "partition-numbers", true},
 		{"fcf2c4a", "2025-01-31 00:00:01", "tic-tac-toe", true},
 		{"dddcae6", "2025-01-12 19:04:35", "gleam", false},
diff --git a/routes/golfer_cheevos.go b/routes/golfer_cheevos.go
index 4ed26b8c..ad5b30eb 100644
--- a/routes/golfer_cheevos.go
+++ b/routes/golfer_cheevos.go
@@ -91,6 +91,11 @@ func golferCheevosGET(w http.ResponseWriter, r *http.Request) {
 		cheevo       string
 		holes, langs []any
 	}{
+		{
+			cheevo: "alphabet-soup",
+			holes:  []any{"scrambled-sort"},
+			langs:  []any{"c", "d", "j", "k", "r", "v"},
+		},
 		{
 			cheevo: "archivist",
 			holes:  []any{"isbn"},
diff --git a/routes/golfer_code_search.go b/routes/golfer_code_search.go
index f2bd2a2d..287c9f85 100644
--- a/routes/golfer_code_search.go
+++ b/routes/golfer_code_search.go
@@ -2,26 +2,9 @@ package routes
 
 import (
 	"net/http"
-
-	"github.com/code-golf/code-golf/config"
 )
 
 // GET /golfer/search
 func golferSearchGET(w http.ResponseWriter, r *http.Request) {
-	data := struct {
-		Langs map[string]string
-		Holes map[string]string
-	}{
-		Langs: make(map[string]string),
-		Holes: make(map[string]string),
-	}
-
-	for k, v := range config.AllLangByID {
-		data.Langs[k] = v.Name
-	}
-	for k, v := range config.AllHoleByID {
-		data.Holes[k] = v.Name
-	}
-
-	render(w, r, "golfer/code-search", data, "Solution search")
+	render(w, r, "golfer/code-search", nil, "Solution search")
 }
diff --git a/routes/golfer_profile.go b/routes/golfer_profile.go
index 1f49ad75..23718ae0 100644
--- a/routes/golfer_profile.go
+++ b/routes/golfer_profile.go
@@ -41,16 +41,16 @@ func golferGET(w http.ResponseWriter, r *http.Request) {
 			Country            *config.Country
 			Name               string
 		}
-		Langs          []*config.Lang
+		Langs []struct {
+			Lang                          *config.Lang
+			RankBytes, RankChars, RankMin *int
+		}
 		OAuthProviders map[string]*oauth.Config
-		Trophies       map[string]map[string]int
 		Wall           []row
 	}{
 		CategoryOverview: map[string]map[string]int{"bytes": {}, "chars": {}},
 		Connections:      oauth.GetConnections(db, golfer.ID, true),
-		Langs:            config.LangList,
 		OAuthProviders:   oauth.Providers,
-		Trophies:         map[string]map[string]int{},
 		Wall:             make([]row, 0, limit),
 	}
 
@@ -164,7 +164,8 @@ rows:
 		data.CategoryOverview[cat.Scoring][cat.Category] = cat.Points
 	}
 
-	rows, err = db.Query(
+	if err := db.Select(
+		&data.Langs,
 		`WITH ranks AS (
 		    SELECT user_id, scoring, lang,
 		           RANK() OVER (PARTITION BY scoring, lang
@@ -172,30 +173,17 @@ rows:
 		      FROM rankings
 		     WHERE NOT experimental
 		  GROUP BY user_id, scoring, lang
-		) SELECT lang, scoring, rank FROM ranks WHERE user_id = $1`,
+		) SELECT lang,
+		         any_value(rank) FILTER (WHERE scoring = 'bytes') rank_bytes,
+		         any_value(rank) FILTER (WHERE scoring = 'chars') rank_chars,
+		         min(rank)                                        rank_min
+		    FROM ranks
+		    JOIN langs ON lang = id
+		   WHERE user_id = $1
+		GROUP BY lang
+		ORDER BY rank_min, any_value(name)`,
 		golfer.ID,
-	)
-	if err != nil {
-		panic(err)
-	}
-	defer rows.Close()
-
-	for rows.Next() {
-		var lang, scoring string
-		var rank int
-
-		if err := rows.Scan(&lang, &scoring, &rank); err != nil {
-			panic(err)
-		}
-
-		if _, ok := data.Trophies[lang]; !ok {
-			data.Trophies[lang] = map[string]int{}
-		}
-
-		data.Trophies[lang][scoring] = rank
-	}
-
-	if err := rows.Err(); err != nil {
+	); err != nil {
 		panic(err)
 	}
 
diff --git a/routes/golfer_solution.go b/routes/golfer_solution.go
index 204439b5..749349f1 100644
--- a/routes/golfer_solution.go
+++ b/routes/golfer_solution.go
@@ -116,7 +116,10 @@ func golferSolutionPOST(w http.ResponseWriter, r *http.Request) {
 		Timeout bool          `json:"timeout"`
 	}
 
-	runs := h.Play(ctx, hole, lang, code)
+	runs, err := h.Play(ctx, hole, lang, code)
+	if err != nil {
+		panic(err)
+	}
 	subsetRuns := make([]SubsetRun, len(runs))
 
 	overallPass := true
diff --git a/routes/legacy_api.go b/routes/legacy_api.go
index fa64037a..7ca2e9b2 100644
--- a/routes/legacy_api.go
+++ b/routes/legacy_api.go
@@ -96,7 +96,10 @@ func solutionPOST(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	runs := hole.Play(r.Context(), holeObj, langObj, in.Code)
+	runs, err := hole.Play(r.Context(), holeObj, langObj, in.Code)
+	if err != nil {
+		panic(err)
+	}
 
 	out := struct {
 		Cheevos     []config.Cheevo     `json:"cheevos"`
@@ -191,11 +194,6 @@ func solutionPOST(w http.ResponseWriter, r *http.Request) {
 			panic(err)
 		}
 
-		// For now don't show any popups for experimental solutions.
-		if experimental {
-			out.RankUpdates = []Golfer.RankUpdate{}
-		}
-
 		recordUpdates := make([]Golfer.RankUpdate, 0, 2)
 
 		for _, rank := range out.RankUpdates {
@@ -217,6 +215,11 @@ func solutionPOST(w http.ResponseWriter, r *http.Request) {
 			go discord.LogNewRecord(golfer, holeObj, langObj, recordUpdates, db)
 		}
 
+		// For now don't show any popups for experimental solutions.
+		if experimental {
+			out.RankUpdates = []Golfer.RankUpdate{}
+		}
+
 		// Cheevos.
 		if experimental {
 			if c := golfer.Earn(db, "black-box-testing"); c != nil {
@@ -251,7 +254,7 @@ func solutionPOST(w http.ResponseWriter, r *http.Request) {
 						out.Cheevos = append(out.Cheevos, *c)
 					}
 				}
-			case "star-wars-opening-crawl":
+			case "star-wars-gpt", "star-wars-opening-crawl":
 				if month == time.May && day == 4 {
 					if c := golfer.Earn(db, "may-the-4ᵗʰ-be-with-you"); c != nil {
 						out.Cheevos = append(out.Cheevos, *c)
@@ -281,6 +284,12 @@ func solutionPOST(w http.ResponseWriter, r *http.Request) {
 						out.Cheevos = append(out.Cheevos, *c)
 					}
 				}
+			case "τ":
+				if month == time.June && day == 28 {
+					if c := golfer.Earn(db, "how-about-second-pi"); c != nil {
+						out.Cheevos = append(out.Cheevos, *c)
+					}
+				}
 			}
 
 			if in.Lang == "viml" && golfer.Settings["hole"]["editor-keymap"] == "vim" {
@@ -340,7 +349,7 @@ func solutionPOST(w http.ResponseWriter, r *http.Request) {
 
 // GET /mini-rankings/{hole}/{lang}/{scoring:bytes|chars}/{view:top|me|following}
 func apiMiniRankingsGET(w http.ResponseWriter, r *http.Request) {
-	limit := 7
+	limit := 50
 	if r.FormValue("long") == "1" {
 		limit = 99
 	}
diff --git a/routes/rankings_holes.go b/routes/rankings_holes.go
index 196853af..69d802e4 100644
--- a/routes/rankings_holes.go
+++ b/routes/rankings_holes.go
@@ -25,12 +25,12 @@ func rankingsHolesGET(w http.ResponseWriter, r *http.Request) {
 		Pager                                 *pager.Pager
 		Recent                                bool
 		Rows                                  []struct {
-			Country                             *config.Country
-			Holes, Rank, Points, Strokes, Total int
-			Lang                                *config.Lang
-			Name                                string
-			OtherStrokes                        *int
-			Submitted                           time.Time
+			Country                                  *config.Country
+			Holes, Points, Rank, Row, Strokes, Total int
+			Lang                                     *config.Lang
+			Name                                     string
+			OtherStrokes                             *int
+			Submitted                                time.Time
 		}
 	}{
 		HoleID:  param(r, "hole"),
@@ -133,12 +133,13 @@ func rankingsHolesGET(w http.ResponseWriter, r *http.Request) {
 			          other_strokes    other_strokes,
 			          points           points,
 			          rank_overall     rank,
+			          row_overall      row,
 			          strokes          strokes,
 			          submitted        submitted,
 			          COUNT(*) OVER()  total
 			     FROM rankings
 			     JOIN users ON user_id = id
-			    WHERE hole = $1 AND scoring = $2 AND NOT experimental_lang
+			    WHERE hole = $1 AND scoring = $2
 			 ORDER BY rank_overall, submitted
 			    LIMIT $3 OFFSET $4`
 
@@ -150,6 +151,7 @@ func rankingsHolesGET(w http.ResponseWriter, r *http.Request) {
 			          other_strokes    other_strokes,
 			          points_for_lang  points,
 			          rank             rank,
+			          row              row,
 			          strokes          strokes,
 			          submitted        submitted,
 			          COUNT(*) OVER()  total
diff --git a/routes/rankings_medals.go b/routes/rankings_medals.go
index f4006b91..1f68d98a 100644
--- a/routes/rankings_medals.go
+++ b/routes/rankings_medals.go
@@ -15,10 +15,10 @@ func rankingsMedalsGET(w http.ResponseWriter, r *http.Request) {
 		HoleID, LangID, Scoring  string
 		Pager                    *pager.Pager
 		Rows                     []struct {
-			Country                       *config.Country
-			Diamond, Gold, Silver, Bronze int
-			Name                          string
-			Rank, Total                   int
+			Country                                *config.Country
+			Unicorn, Diamond, Gold, Silver, Bronze int
+			Name                                   string
+			Rank, Total                            int
 		}
 	}{
 		HoleID:  param(r, "hole"),
@@ -42,6 +42,7 @@ func rankingsMedalsGET(w http.ResponseWriter, r *http.Request) {
 		&data.Rows,
 		`WITH counts AS (
 		    SELECT user_id,
+		           COUNT(*) FILTER (WHERE medal = 'unicorn') unicorn,
 		           COUNT(*) FILTER (WHERE medal = 'diamond') diamond,
 		           COUNT(*) FILTER (WHERE medal = 'gold'   ) gold,
 		           COUNT(*) FILTER (WHERE medal = 'silver' ) silver,
@@ -54,7 +55,7 @@ func rankingsMedalsGET(w http.ResponseWriter, r *http.Request) {
 		) SELECT RANK() OVER(
 		             ORDER BY gold DESC, diamond DESC, silver DESC, bronze DESC
 		         ),
-		         diamond, gold, silver, bronze,
+		         unicorn, diamond, gold, silver, bronze,
 		         country_flag country, login name, COUNT(*) OVER() total
 		    FROM counts
 		    JOIN users ON id = user_id
@@ -95,7 +96,7 @@ func rankingsMedalsGET(w http.ResponseWriter, r *http.Request) {
 		description += " in " + data.Scoring
 	}
 
-	description += "."
+	description += ". Ranked by golds, then diamonds, then silvers, then bronzes."
 
 	render(w, r, "rankings/medals", data, "Rankings: Medals", description)
 }
diff --git a/routes/render.go b/routes/render.go
index 6bf1144a..63b9ef3a 100644
--- a/routes/render.go
+++ b/routes/render.go
@@ -39,7 +39,9 @@ func render(w http.ResponseWriter, r *http.Request, name string, data ...any) {
 		LogInURL, Name, Nonce, Path, Theme string
 		Golfer                             *golfer.Golfer
 		GolferInfo                         *golfer.GolferInfo
+		Holes                              map[string][]string
 		JS                                 []string
+		Langs                              map[string][]string
 		Location                           *time.Location
 		Nav                                *config.Navigaton
 		Request                            *http.Request
@@ -52,7 +54,9 @@ func render(w http.ResponseWriter, r *http.Request, name string, data ...any) {
 		Description: "Code Golf is a game designed to let you show off your code-fu by solving problems in the least number of characters.",
 		Golfer:      theGolfer,
 		GolferInfo:  session.GolferInfo(r),
+		Holes:       make(map[string][]string),
 		JS:          []string{config.Assets["js/base.tsx"]},
+		Langs:       make(map[string][]string),
 		Name:        name,
 		Nonce:       rand.Text(),
 		Path:        r.URL.Path,
@@ -62,6 +66,14 @@ func render(w http.ResponseWriter, r *http.Request, name string, data ...any) {
 		Title:       "Code Golf",
 	}
 
+	// Mapping lang & hole IDs to a list of known names (the first is the primary one)
+	for k, v := range config.AllLangByID {
+		args.Langs[k] = []string{v.Name, k}
+	}
+	for k, v := range config.AllHoleByID {
+		args.Holes[k] = append([]string{v.Name, k}, v.Aliases...)
+	}
+
 	if g := args.GolferInfo; g != nil && g.About != "" {
 		args.Description = g.About
 	}
diff --git a/routes/routes.go b/routes/routes.go
index 8fa07672..051b7c18 100644
--- a/routes/routes.go
+++ b/routes/routes.go
@@ -145,6 +145,7 @@ func Router(db *sqlx.DB) http.Handler {
 		r.Get("/scores/{hole}/{lang}/{scoring}/{page}", scoresGET)
 		r.Post("/solution", solutionPOST)
 		r.Get("/stats", statsGET)
+		r.Get("/stats/{page:cheevos}", statsCheevosGET)
 		r.Get("/stats/{page:countries}", statsCountriesGET)
 		r.Get("/stats/{page:golfers}", statsGolfersGET)
 		r.Get("/stats/{page:(?:holes|langs)}", statsTableGET)
diff --git a/routes/stats.go b/routes/stats.go
index cea84523..28a8a0d1 100644
--- a/routes/stats.go
+++ b/routes/stats.go
@@ -49,6 +49,30 @@ func statsGET(w http.ResponseWriter, r *http.Request) {
 	render(w, r, "stats", data, "Statistics")
 }
 
+// GET /stats/{page:cheevos}
+func statsCheevosGET(w http.ResponseWriter, r *http.Request) {
+	type row struct {
+		Cheevo        *config.Cheevo
+		Golfers, Rank int
+		Percent       string
+	}
+
+	var data []row
+	if err := session.Database(r).Select(
+		&data,
+		` SELECT RANK() OVER (ORDER BY COUNT(*) DESC)             rank,
+		         trophy                                           cheevo,
+		         COUNT(*)                                         golfers,
+		         ROUND(COUNT(*) / SUM(COUNT(*)) OVER () * 100, 2) percent
+		    FROM trophies
+		GROUP BY trophy`,
+	); err != nil {
+		panic(err)
+	}
+
+	render(w, r, "stats", data, "Statistics: Achievements")
+}
+
 // GET /stats/{page:countries}
 func statsCountriesGET(w http.ResponseWriter, r *http.Request) {
 	type row struct {
@@ -94,6 +118,7 @@ func statsGolfersGET(w http.ResponseWriter, r *http.Request) {
 	var data []struct {
 		Count, Sum int
 		Date       time.Time
+		PerDay     string
 	}
 
 	if err := session.Database(r).Select(
@@ -115,9 +140,11 @@ func statsGolfersGET(w http.ResponseWriter, r *http.Request) {
 		           COUNT(*)
 		      FROM earnt_golfers
 		  GROUP BY date
-		) SELECT * FROM first_golfer
+		) SELECT *, 0 per_day FROM first_golfer
 		   UNION ALL
-		  SELECT *, SUM(count) OVER (ORDER BY date) FROM counts`,
+		  SELECT *, SUM(count) OVER (ORDER BY date),
+		         ROUND(count / EXTRACT(doy FROM date), 2) per_day
+		    FROM counts`,
 	); err != nil {
 		panic(err)
 	}
diff --git a/run-lang.c b/run-lang.c
index 329725ae..40d16b85 100644
--- a/run-lang.c
+++ b/run-lang.c
@@ -213,7 +213,7 @@ int main(__attribute__((unused)) int argc, char *argv[]) {
         ALLOW(pwritev),         // 296
         ALLOW(read),            // 0
         ALLOW(readv),           // 19
-        ALLOW(sendfile),        // 40 (Used by cat in wrappers, TODO remove)
+        // ALLOW(sendfile),     // 40
         ALLOW(write),           // 1
         ALLOW(writev),          // 20
 
diff --git a/sql/a-schema.sql b/sql/a-schema.sql
index b523dd36..a1597cdd 100644
--- a/sql/a-schema.sql
+++ b/sql/a-schema.sql
@@ -2,23 +2,24 @@ CREATE EXTENSION citext;
 CREATE EXTENSION hstore;
 
 CREATE TYPE cheevo AS ENUM (
-    '0xdead', 'aged-like-fine-wine', 'alchemist', 'archivist',
-    'assembly-required', 'bakers-dozen', 'big-brother', 'biohazard',
-    'bird-is-the-word', 'black-box-testing', 'blackjack', 'bullseye',
-    'busy-beaver', 'caffeinated', 'centenarian', 'cobowl', 'cunning-linguist',
-    'dammit-janet', 'different-strokes', 'disappearing-act', 'dont-panic',
-    'double-slit-experiment', 'elephpant-in-the-room', 'emergency-room',
-    'evil-scheme', 'fish-n-chips', 'fore', 'forty-winks', 'go-forth',
-    'gone-in-60-holes', 'happy-birthday-code-golf', 'happy-go-lucky',
-    'hello-world', 'hextreme-agony', 'hugs-and-kisses', 'inception',
-    'independence-day', 'interview-ready', 'its-over-9000', 'jeweler',
-    'just-kidding', 'like-comment-subscribe', 'marathon-runner',
+    '0xdead', 'aged-like-fine-wine', 'alchemist', 'alphabet-soup',
+    'archivist', 'assembly-required', 'bakers-dozen', 'big-brother',
+    'biohazard', 'bird-is-the-word', 'black-box-testing', 'blackjack',
+    'bullseye', 'busy-beaver', 'caffeinated', 'centenarian', 'cobowl',
+    'cunning-linguist', 'dammit-janet', 'different-strokes',
+    'disappearing-act', 'dont-panic', 'double-slit-experiment',
+    'elephpant-in-the-room', 'emergency-room', 'evil-scheme', 'fish-n-chips',
+    'fore', 'forty-winks', 'go-forth', 'gone-in-60-holes',
+    'happy-birthday-code-golf', 'happy-go-lucky', 'hello-world',
+    'hextreme-agony', 'how-about-second-pi', 'hugs-and-kisses', 'inception',
+    'independence-day', 'interview-ready', 'into-space', 'its-over-9000',
+    'jeweler', 'just-kidding', 'like-comment-subscribe', 'marathon-runner',
     'mary-had-a-little-lambda', 'may-the-4ᵗʰ-be-with-you',
     'my-god-its-full-of-stars', 'neunundneunzig-luftballons', 'off-the-grid',
-    'omniglot', 'omniglutton', 'ouroboros', 'pangramglot', 'patches-welcome',
-    'phileas-fogg', 'pi-day', 'polyglot', 'polyglutton', 'real-programmers',
-    'right-on', 'rm-rf', 'rtfm', 'rule-34', 's-box-360', 'slowcoach',
-    'smörgåsbord', 'solve-quine', 'sounds-quite-nice', 'takeout',
+    'omniglot', 'omniglutton', 'ouroboros', 'overflowing', 'pangramglot',
+    'patches-welcome', 'phileas-fogg', 'pi-day', 'polyglot', 'polyglutton',
+    'real-programmers', 'right-on', 'rm-rf', 'rtfm', 'rule-34', 's-box-360',
+    'slowcoach', 'smörgåsbord', 'solve-quine', 'sounds-quite-nice', 'takeout',
     'the-watering-hole', 'tim-toady', 'tl-dr', 'twelvetide',
     'twenty-kiloleagues', 'under-pressure', 'up-to-eleven', 'vampire-byte',
     'x-factor', 'x86'
@@ -34,12 +35,12 @@ CREATE TYPE hole AS ENUM (
     'arabic-to-roman', 'arithmetic-numbers', 'arrows', 'ascending-primes',
     'ascii-table', 'billiards', 'brainfuck', 'calendar',
     'card-number-validation', 'catalan-numbers', 'catalans-constant',
-    'christmas-trees', 'collatz', 'connect-four', 'css-colors', 'css-grid',
-    'cubes', 'day-of-week', 'dfa-simulator', 'diamonds', 'divisors',
-    'ellipse-perimeters', 'emirp-numbers', 'emirp-numbers-long', 'emojify',
-    'evil-numbers', 'evil-numbers-long', 'factorial-factorisation',
-    'farey-sequence', 'fibonacci', 'fizz-buzz', 'flags',
-    'floyd-steinberg-dithering', 'foo-fizz-buzz-bar',
+    'christmas-trees', 'collatz', 'connect-four', 'css-colors',
+    'css-colors-inverse', 'css-grid', 'cubes', 'day-of-week', 'dfa-simulator',
+    'diamonds', 'divisors', 'ellipse-perimeters', 'emirp-numbers',
+    'emirp-numbers-long', 'emojify', 'evil-numbers', 'evil-numbers-long',
+    'factorial-factorisation', 'farey-sequence', 'fibonacci', 'fizz-buzz',
+    'flags', 'floyd-steinberg-dithering', 'foo-fizz-buzz-bar',
     'forsyth-edwards-notation', 'fractions', 'game-of-life',
     'gijswijts-sequence', 'gray-code-decoder', 'gray-code-encoder',
     'happy-numbers', 'happy-numbers-long', 'hexagonal-spiral', 'hexdump',
@@ -54,19 +55,20 @@ CREATE TYPE hole AS ENUM (
     'odious-numbers', 'odious-numbers-long', 'ordinal-numbers',
     'p-adic-expansion', 'palindromemordnilap', 'pangram-grep',
     'partition-numbers', 'pascals-triangle', 'pernicious-numbers',
-    'pernicious-numbers-long', 'poker', 'polyominoes', 'prime-numbers',
-    'prime-numbers-long', 'proximity-grid', 'qr-decoder', 'qr-encoder',
-    'quadratic-formula', 'quine', 'recamán', 'repeating-decimals',
-    'reverse-polish-notation', 'reversi', 'rijndael-s-box',
-    'rock-paper-scissors-spock-lizard', 'roman-to-arabic', 'rot13',
-    'rule-110', 'scrambled-alphabetization', 'semiprime-numbers', 'set',
+    'pernicious-numbers-long', 'poker', 'polygon-triangulations',
+    'polyominoes', 'prime-numbers', 'prime-numbers-long', 'proximity-grid',
+    'qr-decoder', 'qr-encoder', 'quadratic-formula', 'quine', 'recamán',
+    'repeating-decimals', 'reverse-polish-notation', 'reversi',
+    'rijndael-s-box', 'rock-paper-scissors-spock-lizard', 'roman-to-arabic',
+    'rot13', 'rule-110', 'scrambled-sort', 'semiprime-numbers', 'set',
     'seven-segment', 'si-units', 'sierpiński-triangle', 'smith-numbers',
     'snake', 'spelling-numbers', 'sphenic-numbers', 'star-wars-gpt',
     'star-wars-opening-crawl', 'sudoku', 'sudoku-fill-in', 'ten-pin-bowling',
-    'tic-tac-toe', 'time-distance', 'tongue-twisters', 'transpose-sentence',
-    'trinomial-triangle', 'turtle', 'tutorial', 'united-states',
-    'vampire-numbers', 'van-eck-sequence', 'zeckendorf-representation',
-    'zodiac-signs', 'γ', 'λ', 'π', 'τ', 'φ', '√2', '𝑒'
+    'tic-tac-toe', 'time-distance', 'tongue-twisters', 'topological-sort',
+    'transpose-sentence', 'trinomial-triangle', 'turtle', 'tutorial',
+    'united-states', 'vampire-numbers', 'van-eck-sequence',
+    'zeckendorf-representation', 'zodiac-signs', 'γ', 'λ', 'π', 'τ', 'φ',
+    '√2', '𝑒'
 );
 
 CREATE TYPE idea_category AS ENUM ('cheevo', 'hole', 'lang', 'other');
@@ -82,8 +84,8 @@ CREATE TYPE lang AS ENUM (
     'lua', 'nim', 'ocaml', 'odin', 'pascal', 'perl', 'php', 'picat',
     'powershell', 'prolog', 'python', 'qore', 'r', 'racket', 'raku', 'rebol',
     'rexx', 'rockstar', 'ruby', 'rust', 'scala', 'scheme', 'sed', 'sql',
-    'squirrel', 'stax', 'swift', 'tcl', 'tex', 'uiua', 'v', 'viml', 'vyxal',
-    'wren', 'zig'
+    'squirrel', 'stax', 'swift', 'tcl', 'tex', 'uiua', 'umka', 'v', 'vala',
+    'viml', 'vyxal', 'wren', 'zig'
 );
 
 CREATE TYPE medal AS ENUM ('unicorn', 'diamond', 'gold', 'silver', 'bronze');
@@ -173,9 +175,10 @@ CREATE UNLOGGED TABLE holes (
 
 -- Ditto for config/data/langs.toml.
 CREATE UNLOGGED TABLE langs (
-    id           lang  NOT NULL PRIMARY KEY,
-    experiment   int   NOT NULL,
-    digest_trunc bytea NOT NULL
+    id           lang   NOT NULL PRIMARY KEY,
+    experiment   int    NOT NULL,
+    digest_trunc bytea  NOT NULL,
+    name         citext NOT NULL
 );
 
 CREATE TABLE notes (
diff --git a/sql/b-earn-cheevos.sql b/sql/b-earn-cheevos.sql
index 07a9bfc6..952c391e 100644
--- a/sql/b-earn-cheevos.sql
+++ b/sql/b-earn-cheevos.sql
@@ -49,17 +49,23 @@ BEGIN
     IF hole = 'game-of-life' AND lang = 'elixir' THEN
         earned := earn(earned, 'alchemist', user_id); END IF;
 
+    -- 🥣 Alphabet Soup
+    SELECT COUNT(*) >= 3 INTO found FROM UNNEST(langs_for_hole)
+     WHERE unnest IN ('c', 'd', 'j', 'k', 'r', 'v');
+    IF hole = 'scrambled-sort' AND found THEN
+        earned := earn(earned, 'alphabet-soup', user_id); END IF;
+
     -- 📚 Archivist
     SELECT COUNT(*) >= 3 INTO found FROM UNNEST(langs_for_hole)
      WHERE unnest IN ('basic', 'cobol', 'common-lisp', 'forth', 'fortran');
     IF hole = 'isbn' AND found THEN
         earned := earn(earned, 'archivist', user_id); END IF;
 
-    -- 🪛 Assembly Required.
+    -- 🪛 Assembly Required
     IF hole = 'seven-segment' AND lang = 'assembly' THEN
         earned := earn(earned, 'assembly-required', user_id); END IF;
 
-    -- 🐦 Bird Is the Word.
+    -- 🐦 Bird Is the Word
     SELECT COUNT(*) >= 3 INTO found FROM UNNEST(langs_for_hole)
      WHERE unnest IN ('awk', 'prolog', 'sql', 'swift', 'tcl', 'wren');
     IF hole = 'levenshtein-distance' AND found THEN
diff --git a/svg/05ab1e.svg b/svg/05ab1e.svg
index 0c42c77d..aacce9c0 100644
--- a/svg/05ab1e.svg
+++ b/svg/05ab1e.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 400 141.085"><path d="M206.202 56.589v36.434h3.876c2.861 0 3.875-.824 3.875-3.147 0-3.113.05-3.111 4.552.218 11.164 8.254 27.469 4.407 33.821-7.979 13.018-25.388-9.17-52.799-32.582-40.252l-5.791 3.104V32.561c0-12.199-.064-12.406-3.875-12.406h-3.876zM46.372 39.682c-17.302 7.189-19.063 55.829-2.46 67.97 10.347 7.566 27.372 3.313 34.383-8.589 4.983-8.462 4.57-41.459-.634-50.544-5.929-10.349-18.86-14.001-31.289-8.837m238.128 2.16c-1.269 5.779-7.542 10.871-13.391 10.871-3.409 0-4.442.722-4.442 3.101 0 2.725 1.033 3.101 8.527 3.101h8.527v51.163h9.302V37.209h-3.753c-2.905 0-3.982 1.046-4.77 4.633m44.369-1.711c-15.708 8.37-17.456 38.152-2.851 48.552 12.561 8.944 38.323.243 38.323-12.943 0-3.335-9.408-1.244-10.793 2.398-5.256 13.824-27.967 8.412-27.967-6.665v-4.806h38.76v-5.098c0-18.184-19.482-29.958-35.472-21.438m-233.432.567c-.324 1.066-1.389 7.519-2.366 14.341s-2.163 14.67-2.635 17.442c-1.1 6.456 3.705 6.982 10.622 1.162 14.649-12.326 32.59 4.932 24.222 23.3-5.422 11.898-24.982 9.847-28.195-2.956-.618-2.461-2.358-3.972-5.208-4.519-5.521-1.06-5.563-.985-3.578 6.386 5.181 19.239 36.437 21.496 45.841 3.311 10.48-20.268-9.538-42.223-31.043-34.047-3.532 1.343-3.979 1.186-3.318-1.163.421-1.493 1.218-5.157 1.771-8.141 2.13-11.478 1.142-10.853 17.159-10.853 13.591 0 14.624-.219 14.624-3.101 0-3.804-36.749-4.932-37.896-1.162m64.407.471c-6.255 2.715-9.339 6.373-9.407 11.157-.073 5.125 7.245 4.892 9.217-.294 4.331-11.391 24.134-8.967 22.988 2.814-.438 4.503-1.018 4.939-8.223 6.198-20.945 3.658-27.132 7.496-27.132 16.835 0 13.661 15.961 20.87 30.169 13.626 6.017-3.068 6.019-3.068 8.229.087 3.209 4.582 11.792 3.603 12.504-1.427.433-3.059.005-3.471-2.713-2.608-3.148.999-3.228.563-3.228-17.612 0-10.634-.758-20.3-1.765-22.511-3.609-7.92-19.39-11.147-30.639-6.265M66.653 47.81c11.222 11.222 7.524 52.302-5.107 56.744-11.49 4.041-18.501-5.422-19.406-26.192C40.872 49.3 53.013 34.169 66.653 47.81m283.854.133c2.664 2.664 4.532 6.178 4.532 8.527v3.995H340.31c-16.072 0-17.18-.805-12.565-9.132 4.828-8.711 15.806-10.346 22.762-3.39m-112.083-.562c10.698 7.493 10.792 31.012.154 38.463-12.7 8.896-27.241-7.623-23.068-26.208 2.627-11.703 14.577-18.094 22.914-12.255m-55.478 26.767c0 11.181-15.571 18.808-23.076 11.303-6.349-6.348-2.993-13.284 7.234-14.951 3.17-.516 6.811-1.16 8.09-1.431 6.968-1.475 7.752-.961 7.752 5.079m-34.109 31.278v4.652h105.427v-9.303H148.837zm165.892 0v4.652h51.162v-9.303h-51.162"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 13 13"><text xml:space="preserve" x="-.384" y="9.314" style="font-style:normal;font-weight:400;font-size:7.55934px;line-height:1.25;font-family:sans-serif;fill:currentColor;fill-opacity:1;stroke:none;stroke-width:.264583"><tspan x="-.384" y="9.314" style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-family:Verdana;-inkscape-font-specification:Verdana;stroke-width:.264583">abe</tspan></text></svg>
diff --git a/svg/umka.svg b/svg/umka.svg
new file mode 100644
index 00000000..b54bcb52
--- /dev/null
+++ b/svg/umka.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 237.7 237.7"><path d="M51.837.45c-3.787 0-7.588 1.6-10.238 4.8l-35.2 43.2c-5.7 7-.698 17.4 8.302 17.4l69.199-1c11 0 17.1-11.8 10.1-20.4L62 5.25c-2.6-3.2-6.375-4.8-10.163-4.8m136.662.775c-3.85 0-7.7 1.575-10.4 4.725L142.3 48.05c-5.8 6.8-.7 17 8.4 17l72.7 1.599c11.2 0 16.1-8.499 7.3-19.899l-31.8-40.8c-2.7-3.15-6.55-4.725-10.4-4.725M81.4 70.75l-61 .6c-7.6 0-12.8 1.3-15.4 3.7S.901 81.45.701 86.95.4 103.25.4 119.05s.2 34 .5 54.3 6.2 36 17.5 47.2c11.5 11.1 28.5 16.7 51 16.7 22.4 0 44.6-8.401 66.4-25.101v.3c.6 7.6 2.2 13 4.6 16.2 2.5 3.2 7.3 4.8 14.5 4.8h62c7.2 0 12-1.6 14.3-4.8 2.4-3.1 4-9.899 4.8-20.299.9-10.4 1.3-29.8 1.3-58.3s-.701-49.601-2.201-63.401c-.4-3.6-2.1-6.7-5.1-9.5-3-2.7-7-4.199-12.1-4.399-17.8-.9-30.4-1.3-37.8-1.3s-14.898.3-22.398.8-12.702 2.099-15.702 4.899c-3 2.7-4.8 5.9-5.4 9.5-1.3 10.2-1.898 29.201-1.898 57.201v19.4q-2.55 8.55-8.4 13.5c-3.9 3.3-8.101 4.899-12.401 4.899s-7.8-1.599-10.6-4.799c-2.7-3.2-4.1-7.9-4.1-14l.3-77.5c0-4.9-1.3-8.5-4-10.9-2.6-2.5-7.3-3.7-14.1-3.7"/></svg>
diff --git a/svg/vala.svg b/svg/vala.svg
new file mode 100644
index 00000000..c68890bc
--- /dev/null
+++ b/svg/vala.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 512 512"><path d="M19.27 512C8.626 512 0 503.373 0 492.73V19.27C0 8.626 8.627 0 19.27 0h473.46C503.374 0 512 8.627 512 19.27v473.46c0 10.643-8.627 19.27-19.27 19.27zm248.32-409.305c-39.762-3.602-83.167 4.483-131.508 41.97-51.75 40.133-56.773 116.458 7.406 113.07-13.76-26.054-9.481-101.96 59.079-126.676l7.033 298.354 62.904.01 121.338-325.456h-30.494l-91.913 267.436"/></svg>
diff --git a/svgo.config.js b/svgo.config.js
index cd893762..986d4a31 100644
--- a/svgo.config.js
+++ b/svgo.config.js
@@ -1,6 +1,10 @@
 export default {
     js2svg: { finalNewline: true  },
     plugins: [
+        {
+            name: 'convertTransform',
+            active: false,
+        },
         {
             name: 'preset-default',
             // We need top-level IDs for <use href="#foo"/> to work.
diff --git a/t/examples.t b/t/examples.t
index 336abaaa..2758b998 100644
--- a/t/examples.t
+++ b/t/examples.t
@@ -19,7 +19,7 @@ for 'config/data/langs.toml'.IO.&from-toml.map({
             # Some langs print noise to STDERR, so don't test for no output.
             is $got<stdout>, $exp, 'Stdout';
             is $got<stderr>,   '', 'Stderr'
-                if $lang ne any < factor kotlin tex vyxal >;
+                if $lang ne any < factor kotlin vyxal >;
         }
     }
 }
diff --git a/t/lang-features.t b/t/lang-features.t
index e9ce91b5..3bc883d3 100644
--- a/t/lang-features.t
+++ b/t/lang-features.t
@@ -15,6 +15,14 @@ is post-solution(|.value)<runs>[0]<stderr>, '', .key for
 like post-solution(:lang<j> :code('echo JVERSION'))<runs>[0]<stdout>,
     / '/j64/linux' /, 'J engine is baseline AMD 64 (no AVX 512)';
 
+# Null byte in solution.
+is-deeply post-solution( :code(qq:to/CODE/) )<runs>[0]<pass stderr>:p,
+        say "Fizz" x \$_ %% 3 ~ "Buzz" x \$_ %% 5 || \$_ for 1 .. 100;
+        # Null byte in comment = \x00
+    CODE
+    ( :!pass :stderr('Solutions must not contain a literal null byte.') ),
+    'Null byte in solution fails';
+
 # Trivial Tex Quine.
 my $code = "Trivial\n";
 my $err  = ｢Quine in TeX must have at least one '\' character.｣;
diff --git a/utils/update_sql_from_api.go b/utils/update_sql_from_api.go
index 5c48f0aa..1ecaf255 100644
--- a/utils/update_sql_from_api.go
+++ b/utils/update_sql_from_api.go
@@ -27,9 +27,10 @@ import (
 
 // Create a string containing the given number of characters and UTF-8 encoded bytes.
 func makeCode(chars *int, bytes int) (result string) {
+	// Support assembly solutions without chars, just return empty string as
+	// the source byte length isn't the same as the DB byte length.
 	if chars == nil {
-		// Support assembly solutions without chars.
-		return strings.Repeat("a", bytes)
+		return ""
 	}
 
 	delta := bytes - *chars
@@ -187,9 +188,9 @@ func main() {
 
 		if _, err := tx.Exec(
 			`INSERT INTO solutions (  bytes,     chars,    code, hole, lang,
-			                        scoring, submitted, user_id)
+			                        scoring, submitted, user_id, lang_digest)
 			                VALUES (     $1,        $2,      $3,   $4,   $5,
-			                             $6,        $7,      $8)
+			                             $6,        $7,      $8,   '')
 			ON CONFLICT            (user_id, hole, lang, scoring)
 			  DO UPDATE SET bytes = excluded.bytes,
 			                chars = excluded.chars,
diff --git a/views/html/feeds.html b/views/html/feeds.html
index 68cbdcac..7c17997e 100644
--- a/views/html/feeds.html
+++ b/views/html/feeds.html
@@ -5,7 +5,7 @@
 
     <p>
         New languages are released in a fairly ad-hoc manner. New holes target
-        a monthy release schedule with new holes appearing on the first of the
+        a monthly release schedule with new holes appearing on the first of the
         month. Exceptions can occur, for example the simpler
         <a href=day-of-week>Day of Week</a> hole was released mid-month.
 
diff --git a/views/html/golfer/code-search.html b/views/html/golfer/code-search.html
index a9ae33fc..1059bc1b 100644
--- a/views/html/golfer/code-search.html
+++ b/views/html/golfer/code-search.html
@@ -1,9 +1,6 @@
 {{ template "header" . }}
 {{ template "golfer" . }}
 
-<script id=holes type=application/json>{{ .Data.Holes }}</script>
-<script id=langs type=application/json>{{ .Data.Langs }}</script>
-
 <main>
     <div id=search>
         <input id=searchInput autofocus/>
diff --git a/views/html/golfer/profile.html b/views/html/golfer/profile.html
index 7db95d09..5c3c693b 100644
--- a/views/html/golfer/profile.html
+++ b/views/html/golfer/profile.html
@@ -80,31 +80,18 @@
         <canvas></canvas>
     </section>
 
-    {{ with len .Data.Trophies }}
+    {{ with .Data.Langs }}
         <section class=icons>
-            <h2>{{ . }} / {{ len $.Data.Langs }} Languages</h2>
-    {{ range $.Data.Langs }}
-        {{ $trophy := index $.Data.Trophies .ID }}
-        {{ if $trophy }}
-            {{ $bytes := index $trophy "bytes" }}
-            {{ $chars := index $trophy "chars" }}
-
-            {{/* Lowest non-zero rank */}}
-            {{ $rank := $bytes }}
-            {{ if and $chars (or (not $bytes) (lt $chars $bytes)) }}
-                {{ $rank = $chars }}
-            {{ end }}
-
-            <a class="rank-{{ $rank }}"
-                href="/rankings/holes/all/{{ .ID }}/bytes"
-               title="{{ .Name }}: {{/* */}}
-                {{- with $bytes }}{{ . }}{{ ord . }} in Bytes{{ end }}
-                {{- if and $bytes $chars }}, {{ end }}
-                {{- with $chars }}{{ . }}{{ ord . }} in Chars{{ end }}.">
-                {{ svg .ID }}
+            <h2>{{ len . }} / {{ $.GolferInfo.LangsTotal }} Languages</h2>
+        {{ range . }}
+            <a class="rank-{{ .RankMin }}"
+                href="/rankings/holes/all/{{ .Lang.ID }}/bytes"
+               title="{{ .Lang.Name }}: {{/* */}}
+                {{- .RankBytes }}{{ ord .RankBytes }} in Bytes
+                {{- with .RankChars }}, {{ . }}{{ ord . }} in Chars{{ end }}.">
+                {{ svg .Lang.ID }}
             </a>
         {{ end }}
-    {{ end }}
         </section>
     {{ end }}
 
diff --git a/views/html/golfer/solution.html b/views/html/golfer/solution.html
index 13d9a175..0ab04925 100644
--- a/views/html/golfer/solution.html
+++ b/views/html/golfer/solution.html
@@ -15,11 +15,11 @@
 {{ if .Data.Rank }}
     <p>
         <a href="/rankings/holes/{{ .Data.Hole.ID }}/{{ .Data.Lang.ID }}/{{ .Data.Scoring -}}
-            ?page={{ page .Data.Row }}">
+            ?page={{ page .Data.Row }}&row={{ .Data.Row }}">
             {{ comma .Data.Rank }}<sup>{{ ord .Data.Rank }}</sup> in {{ .Data.Lang.Name }}</a>
         and
         <a href="/rankings/holes/{{ .Data.Hole.ID }}/all/{{ .Data.Scoring -}}
-            ?page={{ page .Data.RowOverall }}">
+            ?page={{ page .Data.RowOverall }}&row={{ .Data.RowOverall }}">
             {{ comma .Data.RankOverall }}<sup>{{ ord .Data.RankOverall }}</sup> overall</a>.
 {{ end }}
 
diff --git a/views/html/header.html b/views/html/header.html
index aa3c7397..d494b0df 100644
--- a/views/html/header.html
+++ b/views/html/header.html
@@ -30,6 +30,9 @@
 <meta property=og:title        content="{{ .Title }}">
 <meta property=og:url          content="https://code.golf{{ with .Path }}{{ . }}{{ end }}">
 
+<script id=langs type=application/json>{{ .Langs }}</script>
+<script id=holes type=application/json>{{ .Holes }}</script>
+
 {{ range .CSS -}}
     <link rel=stylesheet href="{{ .Path }}" {{- with .Media }} media="{{ . }}" {{- end }}>
 {{ end }}
@@ -63,6 +66,16 @@
 {{- end -}}
 </script>
 
+<dialog id=search-nav-dialog>
+    <form id=search-nav-form >
+        <div>
+            <input placeholder="Search holes, #langs, @golfers..." id=search-nav-input autocomplete=off />
+            <ul id=search-nav-results></ul>
+        </div>
+        <div class=hint>Ctrl+P again to print, arrows/tab to navigate results, enter to confirm</div>
+    </form>
+</dialog>
+
 <header id=site-header>
     <nav>
         <a {{ if eq .Path "/" }} class=on {{ end }}
diff --git a/views/html/hole-details.html b/views/html/hole-details.html
index 1c534d08..21bf8c0c 100644
--- a/views/html/hole-details.html
+++ b/views/html/hole-details.html
@@ -22,7 +22,7 @@
 {{ if .Data.Hole.CaseFold }}
     <div class=info>The output will be checked case-insensitively.</div>
 {{ end }}
-{{ if .Data.Hole.ItemDelimiter }}
+{{ if .Data.Hole.MultisetItemDelimiter }}
     <div class=info>The output judge is order agnostic. The expected output shown is one of many valid outputs.</div>
 {{ end }}
 {{ with .Data.Hole.Data }}
diff --git a/views/html/hole-info.html b/views/html/hole-info.html
index acefa80d..7cdb31de 100644
--- a/views/html/hole-info.html
+++ b/views/html/hole-info.html
@@ -7,10 +7,10 @@
             syscalls</a> to write output.
     </div>
 {{ if eq .Data.Hole.ID "quine" }}
-    <div class="hide info golfscript kotlin r">
+    <div class="hide info cjam golfscript kotlin r racket">
         Implicit output is disabled for this hole.
     </div>
-    <div class="hide info k">
+    <div class="hide info 05ab1e jq k stax uiua">
         Implicit output is disabled for this hole. Code must end with a newline.
     </div>
     <div class="hide info powershell">
diff --git a/views/html/hole-json.html b/views/html/hole-json.html
index b6107098..87179d33 100644
--- a/views/html/hole-json.html
+++ b/views/html/hole-json.html
@@ -3,7 +3,7 @@
 </script>
 
 <script id=case-fold type=application/json>{{ .Data.Hole.CaseFold }}</script>
-<script id=langs     type=application/json>{{ .Data.Langs         }}</script>
+<script id=languages type=application/json>{{ .Data.Langs         }}</script>
 <script id=solutions type=application/json>{{ .Data.Solutions     }}</script>
 
 <script id=has-notes type=application/json>
diff --git a/views/html/hole.html b/views/html/hole.html
index cc4d794a..8eb8e8da 100644
--- a/views/html/hole.html
+++ b/views/html/hole.html
@@ -9,29 +9,34 @@
     </details>
     <nav class=tabs id=picker data-style='{{ setting .Golfer "hole" "lang-picker-style" }}'></nav>
     <nav class=tabs id=solutionPicker></nav>
-    <section>
-        <header>
-            <div id=strokes>0 bytes, 0 chars</div>
-            <a class=hide href id=restoreLink>Restore solution</a>
-        </header>
-        <header class=wide>
-            <nav class=tabs id=scoringTabs><a>Bytes</a><a href>Chars</a></nav>
-            <a href id=allLink>All</a>
-        </header>
-        <div id=editor></div>
-        <table class="nowrap-second wide" id=scores><tr><td></table>
-        <nav class="tabs wide" id=rankingsView>
-            <a {{ if ne .Data.RankingsView "top" }} href {{ end }}>
-                {{ svg "trophy-fill" }} Top
-            </a>
-            <a {{ if ne .Data.RankingsView "me" }} href {{ end }}>
-                {{ svg "person-fill" }} Me
-            </a>
-            <a {{ if ne .Data.RankingsView "following" }} href {{ end }}>
-                {{ svg "people-fill" }} Following
-            </a>
-        </nav>
-    </section>
+    <div id=editor-and-rankings>
+        <div id=editor>
+            <header>
+                <div id=strokes>0 bytes, 0 chars</div>
+                <a class=hide href id=restoreLink>Restore solution</a>
+            </header>
+        </div>
+        <div id=rankings class=wide>
+            <header>
+                <nav class=tabs id=scoringTabs><a>Bytes</a><a href>Chars</a></nav>
+                <a href id=allLink>All</a>
+            </header>
+            <div id=scores-wrapper>
+                <table class=nowrap-second id=scores><tr><td></table>
+            </div>
+            <nav class=tabs id=rankingsView>
+                <a {{ if ne .Data.RankingsView "top" }} href {{ end }}>
+                    {{ svg "trophy-fill" }} Top
+                </a>
+                <a {{ if ne .Data.RankingsView "me" }} href {{ end }}>
+                    {{ svg "person-fill" }} Me
+                </a>
+                <a {{ if ne .Data.RankingsView "following" }} href {{ end }}>
+                    {{ svg "people-fill" }} Following
+                </a>
+            </nav>
+        </div>
+    </div>
     {{ template "hole-info" . }}
     <div id=run>
     {{ if .Golfer }}
diff --git a/views/html/rankings/holes.html b/views/html/rankings/holes.html
index 3dab1194..1d6a0dfe 100644
--- a/views/html/rankings/holes.html
+++ b/views/html/rankings/holes.html
@@ -33,8 +33,10 @@
     {{ $name := "" }}
     {{ with .Golfer }}{{ $name = .Name }}{{ end }}
 
+    {{ $row := atoi (.Request.FormValue "row") }}
+
     {{ range .Data.Rows }}
-        <tr {{ if eq $name .Name }} class=me {{ end }}>
+        <tr {{ if or (eq $name .Name) (and $row (eq $row .Row)) }} class=me {{ end }}>
             <td>{{ comma .Rank }}<sup>{{ ord .Rank }}</sup>
             <td>{{ template "golfer-link" . }}
             {{ if eq $.Data.HoleID "all" }}<td class=right>{{ .Holes }}{{ end }}
diff --git a/views/html/rankings/medals.html b/views/html/rankings/medals.html
index 2d02606b..b34a2294 100644
--- a/views/html/rankings/medals.html
+++ b/views/html/rankings/medals.html
@@ -9,10 +9,11 @@
             <tr>
                 <th>#
                 <th>Golfer
-                <th class=right title="Gold medals">🥇
-                <th class=right title="Diamonds (uncontested gold medals)">💎
-                <th class=right title="Silver medals">🥈
-                <th class=right title="Bronze medals">🥉
+                <th class="right wide" title="Unicorns (uncontested solves)">🦄
+                <th class="right wide" title="Diamonds (uncontested gold medals)">💎
+                <th class=right        title="Gold medals">🥇
+                <th class=right        title="Silver medals">🥈
+                <th class=right        title="Bronze medals">🥉
         <tbody>
         {{ $name := "" }}
         {{ with .Golfer }}{{ $name = .Name }}{{ end }}
@@ -21,10 +22,11 @@
             <tr {{ if eq $name .Name }} class=me {{ end }}>
                 <td>{{ comma .Rank }}<sup>{{ ord .Rank }}</sup>
                 <td>{{ template "golfer-link" . }}
-                <td class=right> {{ comma .Gold    }}
-                <td class=right>({{ comma .Diamond }})
-                <td class=right> {{ comma .Silver  }}
-                <td class=right> {{ comma .Bronze  }}
+                <td class="right wide">{{ comma .Unicorn }}
+                <td class="right wide">{{ comma .Diamond }}
+                <td class=right       >{{ comma .Gold    }}
+                <td class=right       >{{ comma .Silver  }}
+                <td class=right       >{{ comma .Bronze  }}
         {{ end }}
     </table>
 
diff --git a/views/html/settings.html b/views/html/settings.html
index 0f8e7f95..d18b13cd 100644
--- a/views/html/settings.html
+++ b/views/html/settings.html
@@ -17,8 +17,12 @@
     {{ if gt (len .Options) 3 }}
         <select name="{{ .ID }}">
         {{ range .Options }}
-            <option value="{{ .ID }}" {{ if eq $value .ID }} selected {{ end }}>
-                {{ .Name }}
+            {{ if .Group }}
+                <optgroup label="{{ .Name }}">
+            {{ else }}
+                <option value="{{ .ID }}" {{ if eq $value .ID }} selected {{ end }}>
+                    {{ .Name }}
+            {{ end }}
         {{ end }}
         </select>
     {{ else if eq (len .Options) 0 }}
diff --git a/views/html/stats.html b/views/html/stats.html
index 7501bfba..1a877130 100644
--- a/views/html/stats.html
+++ b/views/html/stats.html
@@ -7,13 +7,13 @@
 
 {{ if eq .Title "Statistics" }}
     <div id=overview>
-        <div>
-            <h2>Cheevos</h2>
+        <a href=stats/cheevos>
+            <h2>Achievements</h2>
             <span>
                 {{ .Data.Cheevos }}
                 <small>({{ comma .Data.CheevosEarned }} earned)</small>
             </span>
-        </div>
+        </a>
 
         <a href=stats/countries><h2>Countries</h2>{{ .Data.Countries }}</a>
         <a href=stats/golfers><h2>Golfers</h2>{{ comma .Data.Golfers }}</a>
@@ -46,6 +46,28 @@
             </span>
         </div>
     </div>
+{{ else if eq .Title "Statistics: Achievements" }}
+    <table class="nowrap-second sticky">
+        <thead>
+            <tr>
+                <th>#
+                <th>Achievement
+                <th class=right>Golfers
+                <th class=right>Percent
+        <tbody>
+        {{ range .Data }}
+            <tr>
+                <td>{{ .Rank }}<sup>{{ ord .Rank }}</sup>
+                <td>
+                    <a href="/rankings/cheevos/{{ .Cheevo.ID }}"
+                        title="{{ .Cheevo.Name }} - {{ .Cheevo.Description }}">
+                        {{ .Cheevo.Emoji }}
+                        <span>{{ .Cheevo.Name }}</span>
+                    </a>
+                <td class=right>{{ comma .Golfers }}
+                <td class=right>{{ .Percent }}%
+        {{ end }}
+    </table>
 {{ else if eq .Title "Statistics: Countries" }}
     <table class="nowrap-second sticky">
         <thead>
@@ -85,12 +107,14 @@
                 <tr>
                     <th>Year
                     <th class=right>Golfers
-                    <th class=right>∑
+                    <th class=right>Per Day
+                    <th class=right>Total
             <tbody>
             {{ range slice .Data 1 }}
                 <tr>
                     <td>{{ .Date.Year }}
                     <td class=right>{{ comma .Count }}
+                    <td class=right>{{ .PerDay }}
                     <td class=right>{{ comma .Sum }}
             {{ end }}
         </table>
diff --git a/views/views.go b/views/views.go
index 78de9be0..39cb8279 100644
--- a/views/views.go
+++ b/views/views.go
@@ -7,6 +7,7 @@ import (
 	"io/fs"
 	"net/http"
 	"net/url"
+	"strconv"
 	"strings"
 
 	"github.com/code-golf/code-golf/config"
@@ -19,6 +20,7 @@ import (
 var views embed.FS
 
 var tmpl = template.New("").Funcs(template.FuncMap{
+	"atoi":      func(s string) int { i, _ := strconv.Atoi(s); return i },
 	"bytes":     pretty.Bytes,
 	"comma":     pretty.Comma,
 	"dec":       func(i int) int { return i - 1 },



Test patch:
diff --git a/config/holes_test.go b/config/holes_test.go
index f858b8be..ab843967 100644
--- a/config/holes_test.go
+++ b/config/holes_test.go
@@ -4,18 +4,18 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/SeerUK/assert"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestHoleSynopses(t *testing.T) {
 	for _, hole := range AllHoleList {
 		t.Run(hole.Name, func(t *testing.T) {
-			assert.True(t, hole.Synopsis != "", "Has a synopsis")
+			assert.NotEmpty(t, hole.Synopsis, "Has a synopsis")
 
 			assert.True(t, strings.HasSuffix(hole.Synopsis, "."),
 				"Synopsis ends in a period")
 
-			assert.False(t, strings.ContainsRune(hole.Synopsis, '\n'),
+			assert.NotContains(t, hole.Synopsis, "\n",
 				"Synopsis spans exactly one line")
 
 			assert.False(t, strings.ContainsAny(hole.Synopsis, `"'`),
diff --git a/pretty/pretty_test.go b/pretty/pretty_test.go
index f26acf14..19ef0e45 100644
--- a/pretty/pretty_test.go
+++ b/pretty/pretty_test.go
@@ -2,9 +2,10 @@ package pretty
 
 import (
 	"html/template"
-	"strings"
 	"testing"
 	"time"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestBytes(t *testing.T) {
@@ -16,9 +17,7 @@ func TestBytes(t *testing.T) {
 		{"1.0 KiB", 1024},
 		{"1.0 MiB", 1024 * 1024},
 	} {
-		if got := Bytes(tt.b); got != tt.want {
-			t.Errorf("Bytes(%v) = %v; want %v", tt.b, got, tt.want)
-		}
+		assert.Equal(t, tt.want, Bytes(tt.b))
 	}
 }
 
@@ -35,9 +34,7 @@ func TestComma(t *testing.T) {
 		{"123,456", 123456},
 		{"1,234,567", 1234567},
 	} {
-		if got := Comma(tt.i); got != tt.want {
-			t.Errorf("Comma(%v) = %v; want %v", tt.i, got, tt.want)
-		}
+		assert.Equal(t, tt.want, Comma(tt.i))
 	}
 }
 
@@ -60,9 +57,7 @@ func TestOrdinal(t *testing.T) {
 		{"th", 112},
 		{"th", 113},
 	} {
-		if got := Ordinal(tt.i); got != tt.want {
-			t.Errorf("Ordinal(%v) = %v; want %v", tt.i, got, tt.want)
-		}
+		assert.Equal(t, tt.want, Ordinal(tt.i))
 	}
 }
 
@@ -74,13 +69,6 @@ func TestTime(t *testing.T) {
 		{"a min ago", time.Now().UTC()},
 		{"7 Jun 1989", time.Date(1989, time.June, 7, 0, 0, 0, 0, time.UTC)},
 	} {
-		got := Time(tt.t)
-
-		// Extract the bit between the <time> tags.
-		got = got[strings.IndexByte(string(got), '>')+1 : len(got)-len("</time>")]
-
-		if got != tt.want {
-			t.Errorf("Ordinal(%v) = %v; want %v", tt.t, got, tt.want)
-		}
+		assert.Contains(t, Time(tt.t), ">"+tt.want+"<")
 	}
 }
diff --git a/routes/callback_test.go b/routes/callback_test.go
index 464ae11c..532412a7 100644
--- a/routes/callback_test.go
+++ b/routes/callback_test.go
@@ -5,7 +5,7 @@ import (
 	"net/http/httptest"
 	"testing"
 
-	"github.com/SeerUK/assert"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestCallbackGETNoCode(t *testing.T) {
diff --git a/routes/log-out_test.go b/routes/log-out_test.go
index 828f22c6..dc19230c 100644
--- a/routes/log-out_test.go
+++ b/routes/log-out_test.go
@@ -5,7 +5,7 @@ import (
 	"net/http/httptest"
 	"testing"
 
-	"github.com/SeerUK/assert"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestLogOutGET(t *testing.T) {
diff --git a/routes/random_test.go b/routes/random_test.go
index 11e86506..c718fa8d 100644
--- a/routes/random_test.go
+++ b/routes/random_test.go
@@ -5,7 +5,7 @@ import (
 	"net/http/httptest"
 	"testing"
 
-	"github.com/SeerUK/assert"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestRandomGET(t *testing.T) {