diff --git a/2021/README.md b/2021/README.md
index 52bb9049992e5bbf6f451dce949fc549071cdbf1..cd2668e400cb1f9bea7e808ca1889ffddd9aca72 100644
--- a/2021/README.md
+++ b/2021/README.md
@@ -114,3 +114,10 @@ either side?
 - We'll go for a finite element approach.
 
 Hmm. Part 2 takes a few seconds, but no surprise there -- it's now O(n^5).
+
+## Day 10
+
+This looks like a job for a stack!
+
+Update: Yes, yes it was a job for a stack. Some maps simplified matters but
+were not essential.
diff --git a/2021/src/main/java/edu/unl/cse/bohn/Main.java b/2021/src/main/java/edu/unl/cse/bohn/Main.java
index c1a7573dd7ffa665d51df86b0f5fc22fc329fc95..875e4b596eeab33d66f361eb9a25d3ac23ef04c5 100644
--- a/2021/src/main/java/edu/unl/cse/bohn/Main.java
+++ b/2021/src/main/java/edu/unl/cse/bohn/Main.java
@@ -8,7 +8,7 @@ public class Main {
     public static final String defaultApiKey = "aoc";
 
     private static String getUserInput(String prompt, String defaultValue, Scanner scanner) {
-        System.out.print(prompt + "[" + defaultValue + "]: ");
+        System.out.print(prompt + " [" + defaultValue + "]: ");
         String userInput = scanner.nextLine();
         return userInput.equals("") ? defaultValue : userInput;
     }
diff --git a/2021/src/main/java/edu/unl/cse/bohn/year2021/Day10.java b/2021/src/main/java/edu/unl/cse/bohn/year2021/Day10.java
new file mode 100644
index 0000000000000000000000000000000000000000..bd850389dff7fe5cc93ffd8152c569105f687a5c
--- /dev/null
+++ b/2021/src/main/java/edu/unl/cse/bohn/year2021/Day10.java
@@ -0,0 +1,103 @@
+package edu.unl.cse.bohn.year2021;
+
+import edu.unl.cse.bohn.Puzzle;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Stack;
+
+@SuppressWarnings("unused")
+public class Day10 extends Puzzle {
+    private final Map<Character, Character> chunkPartners;
+    private final Map<Character, Long> corruptionScores;
+    private final Map<Character, Long> completionScores;
+    private final Set<String> corruptedLines;
+
+    public Day10(boolean isProductionReady) {
+        super(isProductionReady);
+        sampleData = """
+                [({(<(())[]>[[{[]{<()<>>
+                [(()[<>])]({[<{<<[]>>(
+                {([(<{}[<>[]}>{[]{[(<()>
+                (((({<>}<{<{<>}{[]{[]{}
+                [[<[([]))<([[{}[[()]]]
+                [{[{({}]{}}([{[{{{}}([]
+                {<[[]]>}<{[{[{[]{()[[[]
+                [<(<(<(<{}))><([]([]()
+                <{([([[(<>()){}]>(<<{{
+                <{([{{}}[<[[[<>{}]]]>[]]""";
+        chunkPartners = new HashMap<>();
+        chunkPartners.put('(', ')');
+        chunkPartners.put('[', ']');
+        chunkPartners.put('{', '}');
+        chunkPartners.put('<', '>');
+        corruptionScores = new HashMap<>();
+        corruptionScores.put(')', 3L);
+        corruptionScores.put(']', 57L);
+        corruptionScores.put('}', 1197L);
+        corruptionScores.put('>', 25137L);
+        completionScores = new HashMap<>();
+        completionScores.put(')', 1L);
+        completionScores.put(']', 2L);
+        completionScores.put('}', 3L);
+        completionScores.put('>', 4L);
+        corruptedLines = new HashSet<>();
+    }
+
+    @Override
+    public long computePart1(List<String> data) {
+        Long syntaxErrorScore = 0L;
+        Set<Character> chunkOpeners = chunkPartners.keySet();
+        for (String line : data) {
+            Stack<Character> openChunks = new Stack<>();
+            boolean errorFound = false;
+            int i = 0;
+            while (!errorFound && i < line.length()) {
+                char c = line.charAt(i++);
+                if (chunkOpeners.contains(c)) {
+                    openChunks.push(c);
+                } else {
+                    char partner = openChunks.pop();
+                    if (c != chunkPartners.get(partner)) {
+                        errorFound = true;
+                        corruptedLines.add(line);
+                        syntaxErrorScore += corruptionScores.get(c);
+                    }
+                }
+            }
+        }
+        return syntaxErrorScore;
+    }
+
+    @Override
+    public long computePart2(List<String> data) {
+        Set<String> uncorruptedLines = new HashSet<>(data);
+        uncorruptedLines.removeAll(corruptedLines);
+        List<Long> lineScores = new LinkedList<>();
+        Set<Character> chunkOpeners = chunkPartners.keySet();
+        for (String line : uncorruptedLines) {
+            long lineScore = 0L;
+            Stack<Character> openChunks = new Stack<>();
+            for (char c : line.toCharArray()) {
+                if (chunkOpeners.contains(c)) {
+                    openChunks.push(c);
+                } else {
+                    openChunks.pop();
+                }
+            }
+            while (!openChunks.empty()) {
+                char c = openChunks.pop();
+                lineScore = lineScore * 5 + completionScores.get(chunkPartners.get(c));
+            }
+            if (lineScore > 0) {
+                lineScores.add(lineScore);
+            }
+        }
+        lineScores.sort(null);
+        return lineScores.get(lineScores.size() / 2);
+    }
+}