diff --git a/Cache.hs b/Cache.hs
--- a/Cache.hs
+++ b/Cache.hs
@@ -81,7 +81,7 @@
 makeRequest saddr@(ServerAddr host prt) request =
     handle (return . ServerError . (show::SomeException -> String)) $ do
         connect host (show prt) makeRequest'
-            `catchIO` const (return $ ServerError $ "Cannot connect to "++saddrStr saddr++"!")
+            `catchIO` (\e -> return $ ServerError $ "Cannot connect to "++saddrStr saddr++"! "++show e)
     where
         makeRequest' (sock,_) = do
             send sock . BL.toStrict $ encode request
diff --git a/MainState.hs b/MainState.hs
--- a/MainState.hs
+++ b/MainState.hs
@@ -76,6 +76,8 @@
         ok <- initUI
         if ok then m >>= (endUI >>).return.Just else return Nothing
 
+type TutorialLevel = Maybe Int
+
 -- | this could be neatened using GADTs
 data MainState
     = PlayState
@@ -87,7 +89,7 @@
         , psGameStateMoveStack :: [(GameState, PlayerMove)]
         , psUndoneStack        :: [PlayerMove]
         , psTitle              :: Maybe String
-        , psTutLevel           :: Maybe Int
+        , psTutLevel           :: TutorialLevel
         , psIsSub              :: Bool
         , psSaved              :: Bool
         , psMarks              :: Map Char MainState
@@ -152,6 +154,10 @@
     InitState {}   -> IMInit
     MetaState {}   -> IMMeta
 
+getTutLevel :: MainState -> TutorialLevel
+getTutLevel s@(PlayState {}) = psTutLevel s
+getTutLevel _                = Nothing
+
 newPlayState (frame,st) pms title tutLev sub saved = PlayState st frame [] False False [] pms title tutLev sub saved Map.empty
 newReplayState st soln title = ReplayState st [] soln [] title Map.empty
 newEditState (frame,st) msoln mpath = EditState st [] [] frame mpath
@@ -182,9 +188,13 @@
     } deriving (Eq, Ord, Show, Read)
 initTutProgress = TutProgress False 1 Nothing
 
-wrenchOnlyTutLevel, noUndoTutLevel :: Maybe Int -> Bool
+wrenchOnlyTutLevel, noUndoTutLevel, keysTutLevel, conflictsTutLev :: TutorialLevel -> Bool
 wrenchOnlyTutLevel = (`elem` (Just <$> [1..4]))
 noUndoTutLevel = (`elem` (Just <$> [1..7]))
+keysTutLevel (Just n) = n >= 5
+keysTutLevel _        = False
+conflictsTutLev (Just n) = n == 11
+conflictsTutLev _        = False
 
 data InitLock = InitLock
     { initLockName    :: String
diff --git a/NEWS b/NEWS
deleted file mode 100644
--- a/NEWS
+++ /dev/null
@@ -1,108 +0,0 @@
-This is an abbreviated summary; see the git log for gory details.
-
-0.9:
-    Switch from SDL1 to SDL2.
-    Animate force conflicts.
-    Rework default keybindings and click-to-move.
-    Allow adjusting sound volume.
-    Add tutorial lock and 3 initiation locks.
-    Replace inefficient Read-based serialisation of user info with Binary.
-0.8.2:
-    Show esteem with symbols as well as colour (thanks Locria Cyber)
-    Fix relative esteem calculation display
-0.8.1:
-    Revise tutorial and initiation levels REP and GAP
-    Disallow overextended/compressed springs
-    Allow editing spring tension by turning
-    Add compact solution format
-    Revert scoring change introduced in 0.6
-    Add --datadir option
-    Fix loading partial initiation solutions
-    Fix server locking
-0.8:
-    Add single-player introductory subgame ("initiation").
-    Improve UI variously.
-0.7.2.3:
-    Hopefully work around curses build failures
-0.7.2.1:
-    Support >=ghc-8.6.5
-0.7.2:
-    Support ghc-8.0
-    Use Argon2 for server-side password hashing
-    Tweak graphics and text
-0.7.1.1:
-    Make server compatible with feed-1.0.0 (thanks constatinus)
-    Avoid DOS-reserved codenames (thanks constatinus)
-0.7.1:
-    Rework tutorial and intro.
-    Animate movement.
-    Prevent dragging wrench off-path from generating a move.
-    Various UI improvements.
-0.7.0.1:
-    Fix compile error on older ghc versions.
-0.7:
-    Encrypt passwords when communicating with server
-        (in previous versions, they were salted and hashed but not encrypted).
-    Clean out solutions when a lock becomes public.
-    Various UI improvements, particularly for curses mode.
-
-0.6.2:
-    Fix obscure but actual bug in core game physics:
-        wrenches were being blocked when they shouldn't be.
-        See TST:C on the main server for an example - it can be solved only in
-        intricacy versions prior to this one.
-    Extra help text screen for editing first lock.
-    Cycle through all possibilities when placing blocks in editor.
-
-0.6.1:
-    Fix stupid bug preventing registration via keyboard command.
-
-0.6:
-    New scoring system - you don't get the point for a solution if the lock
-	owner has read your note.
-    Server optionally notifies users by email when their locks are solved.
-    Server produces RSS feeds.
-    Save solutions-in-progress of locks and tutorial.
-    Indicate when there's a pending network request, and allow cancellation.
-    Support ghc-7.10.
-    Support OS X (thanks Kevin Eaves).
-    Further tweaks to UI and tutorials.
-
-0.5:
-    Adjustments to graphics, tutorial, and metagame UI, to increase clarity.
-    Concurrency on server; no more freezes while it checks a solution.
-    Misc optimisations.
-    Fix sound lag on Windows.
-    
-0.4:
-    Sound effects (thanks linley).
-    Animate to show the two physics phases (thanks dormir).
-    Help screens.
-    Help text for buttons and metagame.
-    Deneutralised metagame terminology.
-    Tutorial tweaked.
-    Various UI improvements (thanks Quicksand-S and dormir)
-
-0.3:
-    Mouse control.
-    Improvements to curses UI - handle resizing; show keys; allow rebinding.
-    Shortened tutorial drastically. Other levels now in 'tutorial-extra/'.
-    Improve handling inaccessible servers.
-    Added background.
-    First version uploaded to hackage.
-    Scoring tweak: empty slots are accessed iff all non-empty slots are.
-    Detailed game info in README.
-    Hover text for UI buttons.
-    Marks.
-    Lock testing in edit mode.
-    Significant bugs fixed:
-	editor, lockfile reader, and validity checker were not in accordance
-	    on what is allowed as the root of a spring.
-	SDL UI was redrawing the screen on every mouse movement.
-	curses UI wasn't refreshing properly on async flag.
-	stale notes weren't getting refreshed.
-	retired display got stuck on changing codename.
-	you couldn't move into the bolthole with keyboard commands.
-    
-0.2:
-    First public release
diff --git a/NEWS.md b/NEWS.md
new file mode 100644
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,118 @@
+This is an abbreviated summary; see the git log for gory details.
+
+# 0.9
+* Switch from SDL1 to SDL2.
+* Animate force conflicts.
+* Rework default keybindings and click-to-move.
+* Allow adjusting sound volume.
+* Add tutorial lock and 3 initiation locks.
+* Replace inefficient Read-based serialisation of user info with Binary.
+
+# 0.8.2
+* Show esteem with symbols as well as colour (thanks Locria Cyber)
+* Fix relative esteem calculation display
+
+# 0.8.1
+* Revise tutorial and initiation levels REP and GAP
+* Disallow overextended/compressed springs
+* Allow editing spring tension by turning
+* Add compact solution format
+* Revert scoring change introduced in 0.6
+* Add --datadir option
+* Fix loading partial initiation solutions
+* Fix server locking
+
+# 0.8
+* Add single-player introductory subgame ("initiation").
+* Improve UI variously.
+
+# 0.7.2.3
+* Hopefully work around curses build failures
+
+# 0.7.2.1
+* Support >=ghc-8.6.5
+
+# 0.7.2
+* Support ghc-8.0
+* Use Argon2 for server-side password hashing
+* Tweak graphics and text
+
+# 0.7.1.1
+* Make server compatible with feed-1.0.0 (thanks constatinus)
+* Avoid DOS-reserved codenames (thanks constatinus)
+
+# 0.7.1
+* Rework tutorial and intro.
+* Animate movement.
+* Prevent dragging wrench off-path from generating a move.
+* Various UI improvements.
+
+# 0.7.0.1
+* Fix compile error on older ghc versions.
+
+# 0.7
+* Encrypt passwords when communicating with server
+    (in previous versions, they were salted and hashed but not encrypted).
+* Clean out solutions when a lock becomes public.
+* Various UI improvements, particularly for curses mode.
+
+# 0.6.2
+* Fix obscure but actual bug in core game physics:
+    wrenches were being blocked when they shouldn't be.
+    See TST:C on the main server for an example - it can be solved only in
+    intricacy versions prior to this one.
+* Extra help text screen for editing first lock.
+* Cycle through all possibilities when placing blocks in editor.
+
+# 0.6.1
+* Fix stupid bug preventing registration via keyboard command.
+
+# 0.6
+* New scoring system - you don't get the point for a solution if the lock
+owner has read your note.
+* Server optionally notifies users by email when their locks are solved.
+* Server produces RSS feeds.
+* Save solutions-in-progress of locks and tutorial.
+* Indicate when there's a pending network request, and allow cancellation.
+* Support ghc-7.10.
+* Support OS X (thanks Kevin Eaves).
+* Further tweaks to UI and tutorials.
+
+# 0.5
+* Adjustments to graphics, tutorial, and metagame UI, to increase clarity.
+* Concurrency on server; no more freezes while it checks a solution.
+* Misc optimisations.
+* Fix sound lag on Windows.
+
+# 0.4
+* Sound effects (thanks linley).
+* Animate to show the two physics phases (thanks dormir).
+* Help screens.
+* Help text for buttons and metagame.
+* Deneutralised metagame terminology.
+* Tutorial tweaked.
+* Various UI improvements (thanks Quicksand-S and dormir)
+
+# 0.3
+* Mouse control.
+* Improvements to curses UI - handle resizing; show keys; allow rebinding.
+* Shortened tutorial drastically. Other levels now in 'tutorial-extra/'.
+* Improve handling inaccessible servers.
+* Added background.
+* First version uploaded to hackage.
+* Scoring tweak: empty slots are accessed iff all non-empty slots are.
+* Detailed game info in README.
+* Hover text for UI buttons.
+* Marks.
+* Lock testing in edit mode.
+* Significant bugs fixed:
+* editor, lockfile reader, and validity checker were not in accordance
+    on what is allowed as the root of a spring.
+* SDL UI was redrawing the screen on every mouse movement.
+* curses UI wasn't refreshing properly on async flag.
+* stale notes weren't getting refreshed.
+* retired display got stuck on changing codename.
+* you couldn't move into the bolthole with keyboard commands.
+
+# 0.2
+* First public release
diff --git a/README b/README
deleted file mode 100644
--- a/README
+++ /dev/null
@@ -1,157 +0,0 @@
-# Intricacy
-A game of competitive puzzle-design.
-
-https://mbays.sdf.org/intricacy/
-
-## Configuration
-You can change keybindings in SDL mode by right-clicking on the corresponding
-button and pressing a key.
-
-Config files are saved in `~/.intricacy/` on unixoids, or something like
-`...\Application Data\intricacy\` on windows. In particular, the locks you
-create in the lock editor are saved in there (and you can organise them into
-directories by including (back)slashs in the lock name). You can edit them in
-a text editor if you want - see AsciiLock.hs for what the characters mean.
-
-If you want to cheat your way past initiation, change "False" to "True" in
-`metagame.conf`.
-
-
-## Rough description of the game mechanics
-* Locks consist of blocks, pivots and balls. Blocks can be connected to other
-    blocks by springs. Pivots have arms. When picking a lock, you control two
-    tools - a hook, which acts as a mobile one-armed pivot, and a wrench,
-    which acts as a mobile block with momentum.
-* To open a lock, the "bolthole" (the area in the top-right) must be empty.
-* Each turn, the tools push and rotate according to the player's commands, and
-    then each spring which is compressed or extended beyond its natural length
-    pushes/pulls on the block at its end.
-* If two forces try to move a piece in two different directions, or try to
-    move two pieces into the same hex, one or both is blocked.
-* If a pivot/hook is trying to turn and there's a piece in the way of one of
-    its arms, it will at first try to push the piece away - but if that force
-    is blocked, it will try instead to pull the piece round with the arm as it
-    turns.
-* If two springs are trying to push two blocks into the same hex, they will
-    generally both be blocked. However, if one of the forces is also pushing
-    against a fixed piece such as a tool, it won't block the other force.
-
-
-## Full details of the metagame mechanics
-(Where by the 'game' I mean the lock-picking bit, and by the 'metagame' I mean
-the bit with the 3-letter codenames and the three lock slots and notes and so
-on. If you haven't seen anything of the kind... keep solving those locks!)
-
-Scoring is always relative - each player has a score relative to each other
-player. That score is the number of the second player's lock slots to which
-the first player has access, minus the number of the first player's lock slots
-to which the second player has access.
-
-A player accesses a lock slot when one of the following holds
-    * the player has solved the lock in the slot, and declared the solution;
-    * the player has read three notes on the lock in the slot;
-    * there's no lock in the slot, and the player has accessed all the slots
-	which do have locks in them.
-
-A player reads every note in every lock the player accesses. When a lock is
-replaced ('retired'), each note secured by the lock becomes 'public', and is
-read by every player.
-
-Note this means that once three notes on a lock become public, every player
-accesses the lock. The lock is then 'public' (and its owner should replace
-it!).
-
-To declare a solution, you must secure a note on it behind a non-public lock
-in one of your three lock slots. Once the note is placed, it can't be moved.
-
-
-## Full details of the game mechanics
-Springs are directed; one of the blocks it is connected to is the 'root', the
-other the 'end'. A block is stationary if it is not the end of any spring. The
-directed graph whose nodes are the blocks and whose edges are the springs is
-required to be acyclic. A spring may also be rooted in a pivot.
-
-I will now attempt to describe in full excruciating detail the game physics,
-i.e. the algorithm used to determine what happens on a turn. You shouldn't
-need to read this to play the game! Experimenting and turning on blockage
-annotations should be enough.
-
-The following description corresponds to the code in Physics.hs. The algorithm
-is the result of an extended process of experiment and iterated
-simplification; see notes/game in the source distribution if you're perverse
-enough to want to read a scattered stream-of-consciousness account of the
-process.
-
-Each turn is separated into two phases. In the first phase, the forces arising
-from the player's move are 'resolved', possibly resulting in some movement. In
-the second phase, forces arising from stretched or compressed springs are
-resolved, possibly resulting in some movement.
-
-Here we abuse physics terminology, and use the term 'force' to refer to either
-a directional force in the usual sense, which we call a 'push', or a
-rotational force, which we call a 'torque'. A force is always on a piece, in
-some direction. Only the obvious six hex directions and the two obvious
-rotational directions occur; the magnitude is always 1. 
-
-Player moves result in forces on the tools in the obvious way. A wrench which
-is moving gets a push in that direction each turn (sorry, Newton!). A spring
-which is not at its relaxed length results in a push on the end piece.
-
-To _resolve_ these initial forces:
-    * each initial force is 'propagated to a force group'
-    * if a group is 'inconsistent' with another group: if one of the groups is
-        'dominated' by the other, it is 'blocked'; else both are blocked
-    * each force in each unblocked force group is 'applied'
-
-To _apply_ a push: move the piece in the given direction.
-To _apply_ a torque: rotate the piece in the given direction.
-
-To _propagate_ a force:
-    * If applying the force would result in an overlap with another piece,
-	push that piece - but if it's an arm, twist the pivot instead when
-	that makes sense.
-    * If the force is a torque and an arm is pushing on another piece, we have
-	a special rule: if the obvious push ends up propagating to a resisted
-	force (see below), then it is replaced with a 'clawing' push. e.g. in
-	the following situation:
-	    \ O #
-	     o
-	if the pivot is twisted clockwise, the ball will first be pushed east,
-	but when that force propagates to a push on the stationary block to
-	its east which is resisted, processing will back up and a push on the
-	ball southeast will be tried instead.
-    * A spring connecting two blocks transmits a push on one block to the
-	other unless the direction is such that the push is tending to
-        compress resp. extend the spring and the spring is currently extended 
-        resp. compressed, or the force is a player force and the spring isn't 
-        already fully compressed resp. extended.
-
-A force is _resisted_ if it is a push on a pivot or on a block which isn't the
-    end of a spring, or it's a torque on a block, or it's a force on a hook
-    which isn't already getting that force this phase as the result of a
-    player move, or it's a force on a wrench which isn't a push in the
-    direction it's already moving.
-
-To _propagate an initial force to a force group_:
-    * Propagate the force as above, then recursively propagate the resulting
-	forces, checking for resistance as we go. The force group consists of
-	all the resulting propagated forces.
-    * On resistance: back up to try clawing as described above if appropriate,
-	else consider the whole force group resisted - so the resulting force
-	group is empty.
-
-Two forces are _inconsistent_ when
-    * they act on the same piece in different directions, or
-    * applying both at once results in overlapping pieces, or
-    * applying both at once results in an overextended/overcompressed spring.
-
-Two force groups are _inconsistent_ iff there is some force from the first
-    which is inconsistent with some force from the second.
-
-A force group _dominates_ another iff their initial forces are spring forces,
-    and the root of the first spring is an ancestor of the root of the second
-    spring, where an ancestor of a block B is recursively defined to be a block
-    which is the root of a spring which ends at B, or an ancestor of such a
-    block.
-
--- mbays@sdf.org 2013
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,157 @@
+# Intricacy
+A game of competitive puzzle-design.
+
+https://mbays.sdf.org/intricacy/
+
+## Configuration
+You can change keybindings in SDL mode by right-clicking on the corresponding
+button and pressing a key.
+
+Config files are saved in `~/.intricacy/` on unixoids, or something like
+`...\Application Data\intricacy\` on windows. In particular, the locks you
+create in the lock editor are saved in there (and you can organise them into
+directories by including (back)slashs in the lock name). You can edit them in
+a text editor if you want - see AsciiLock.hs for what the characters mean.
+
+If you want to cheat your way past initiation, change "False" to "True" in
+`metagame.conf`.
+
+
+## Rough description of the game mechanics
+* Locks consist of blocks, pivots and balls. Blocks can be connected to other
+    blocks by springs. Pivots have arms. When picking a lock, you control two
+    tools - a hook, which acts as a mobile one-armed pivot, and a wrench,
+    which acts as a mobile block with momentum.
+* To open a lock, the "bolthole" (the area in the top-right) must be empty.
+* Each turn, the tools push and rotate according to the player's commands, and
+    then each spring which is compressed or extended beyond its natural length
+    pushes/pulls on the block at its end.
+* If two forces try to move a piece in two different directions, or try to
+    move two pieces into the same hex, one or both is blocked.
+* If a pivot/hook is trying to turn and there's a piece in the way of one of
+    its arms, it will at first try to push the piece away - but if that force
+    is blocked, it will try instead to pull the piece round with the arm as it
+    turns.
+* If two springs are trying to push two blocks into the same hex, they will
+    generally both be blocked. However, if one of the forces is also pushing
+    against a fixed piece such as a tool, it won't block the other force.
+
+
+## Full details of the metagame mechanics
+(Where by the 'game' I mean the lock-picking bit, and by the 'metagame' I mean
+the bit with the 3-letter codenames and the three lock slots and notes and so
+on. If you haven't seen anything of the kind... keep solving those locks!)
+
+Scoring is always relative - each player has a score relative to each other
+player. That score is the number of the second player's lock slots to which
+the first player has access, minus the number of the first player's lock slots
+to which the second player has access.
+
+A player accesses a lock slot when one of the following holds
+    * the player has solved the lock in the slot, and declared the solution;
+    * the player has read three notes on the lock in the slot;
+    * there's no lock in the slot, and the player has accessed all the slots
+	which do have locks in them.
+
+A player reads every note in every lock the player accesses. When a lock is
+replaced ('retired'), each note secured by the lock becomes 'public', and is
+read by every player.
+
+Note this means that once three notes on a lock become public, every player
+accesses the lock. The lock is then 'public' (and its owner should replace
+it!).
+
+To declare a solution, you must secure a note on it behind a non-public lock
+in one of your three lock slots. Once the note is placed, it can't be moved.
+
+
+## Full details of the game mechanics
+Springs are directed; one of the blocks it is connected to is the 'root', the
+other the 'end'. A block is stationary if it is not the end of any spring. The
+directed graph whose nodes are the blocks and whose edges are the springs is
+required to be acyclic. A spring may also be rooted in a pivot.
+
+I will now attempt to describe in full excruciating detail the game physics,
+i.e. the algorithm used to determine what happens on a turn. You shouldn't
+need to read this to play the game! Experimenting and turning on blockage
+annotations should be enough.
+
+The following description corresponds to the code in Physics.hs. The algorithm
+is the result of an extended process of experiment and iterated
+simplification; see notes/game in the source distribution if you're perverse
+enough to want to read a scattered stream-of-consciousness account of the
+process.
+
+Each turn is separated into two phases. In the first phase, the forces arising
+from the player's move are 'resolved', possibly resulting in some movement. In
+the second phase, forces arising from stretched or compressed springs are
+resolved, possibly resulting in some movement.
+
+Here we abuse physics terminology, and use the term 'force' to refer to either
+a directional force in the usual sense, which we call a 'push', or a
+rotational force, which we call a 'torque'. A force is always on a piece, in
+some direction. Only the obvious six hex directions and the two obvious
+rotational directions occur; the magnitude is always 1. 
+
+Player moves result in forces on the tools in the obvious way. A wrench which
+is moving gets a push in that direction each turn (sorry, Newton!). A spring
+which is not at its relaxed length results in a push on the end piece.
+
+To _resolve_ these initial forces:
+    * each initial force is 'propagated to a force group'
+    * if a group is 'inconsistent' with another group: if one of the groups is
+        'dominated' by the other, it is 'blocked'; else both are blocked
+    * each force in each unblocked force group is 'applied'
+
+To _apply_ a push: move the piece in the given direction.
+To _apply_ a torque: rotate the piece in the given direction.
+
+To _propagate_ a force:
+    * If applying the force would result in an overlap with another piece,
+	push that piece - but if it's an arm, twist the pivot instead when
+	that makes sense.
+    * If the force is a torque and an arm is pushing on another piece, we have
+	a special rule: if the obvious push ends up propagating to a resisted
+	force (see below), then it is replaced with a 'clawing' push. e.g. in
+	the following situation:
+	    \ O #
+	     o
+	if the pivot is twisted clockwise, the ball will first be pushed east,
+	but when that force propagates to a push on the stationary block to
+	its east which is resisted, processing will back up and a push on the
+	ball southeast will be tried instead.
+    * A spring connecting two blocks transmits a push on one block to the
+	other unless the direction is such that the push is tending to
+        compress resp. extend the spring and the spring is currently extended 
+        resp. compressed, or the force is a player force and the spring isn't 
+        already fully compressed resp. extended.
+
+A force is _resisted_ if it is a push on a pivot or on a block which isn't the
+    end of a spring, or it's a torque on a block, or it's a force on a hook
+    which isn't already getting that force this phase as the result of a
+    player move, or it's a force on a wrench which isn't a push in the
+    direction it's already moving.
+
+To _propagate an initial force to a force group_:
+    * Propagate the force as above, then recursively propagate the resulting
+	forces, checking for resistance as we go. The force group consists of
+	all the resulting propagated forces.
+    * On resistance: back up to try clawing as described above if appropriate,
+	else consider the whole force group resisted - so the resulting force
+	group is empty.
+
+Two forces are _inconsistent_ when
+    * they act on the same piece in different directions, or
+    * applying both at once results in overlapping pieces, or
+    * applying both at once results in an overextended/overcompressed spring.
+
+Two force groups are _inconsistent_ iff there is some force from the first
+    which is inconsistent with some force from the second.
+
+A force group _dominates_ another iff their initial forces are spring forces,
+    and the root of the first spring is an ancestor of the root of the second
+    spring, where an ancestor of a block B is recursively defined to be a block
+    which is the root of a spring which ends at B, or an ancestor of such a
+    block.
+
+-- mbays@sdf.org 2013
diff --git a/SDL2RenderCache.hs b/SDL2RenderCache.hs
--- a/SDL2RenderCache.hs
+++ b/SDL2RenderCache.hs
@@ -298,9 +298,9 @@
         sequence_ [ pixelR (FVec (1/3 + i/4) (-1/4)) (bright white) | i <- [-1..1] ]
 
 renderGlyph (VolumeButton vol) = do
-    sequence_ [ arcR (FVec (-2/3) 0) r (-20) 20
-            (if vol > 0 then bright green else dim red)
-        | r <- [1/3] <> [2/3 | vol > 16 || vol == 0] <> [1 | vol > 32 || vol == 0 ] ]
+    sequence_ [ arcR (FVec (-2/3) 0) ((fi r+1)/3) (-20) 20
+            (if vol > 16*r then bright green else dim red)
+        | r <- [0..2] ]
     unless (vol > 0) $
         aaLineR (innerCorner hw) (innerCorner $ neg hw) $ dim red
 
diff --git a/SDL2UI.hs b/SDL2UI.hs
--- a/SDL2UI.hs
+++ b/SDL2UI.hs
@@ -53,6 +53,7 @@
 import           InputMode
 import           KeyBindings
 import           Lock
+import           MainState
 import           Maxlocksize
 import           Metagame
 import           Mundanities
@@ -403,9 +404,16 @@
     helpOfSelectableFiltered _ s                    = helpOfSelectable s
 
 
-data UIOptButton a = UIOptButton { getUIOpt :: UIOptions->a, setUIOpt :: a->UIOptions->UIOptions,
-    uiOptVals :: [a], uiOptPos :: HexVec, uiOptGlyph :: a->Glyph, uiOptDescr :: a->String,
-    uiOptModes :: [InputMode], onSet :: Maybe (a -> UIM ()) }
+data UIOptButton a = UIOptButton
+    { getUIOpt      :: UIOptions->a
+    , setUIOpt      :: a->UIOptions->UIOptions
+    , uiOptVals     :: [a]
+    , uiOptPos      :: HexVec
+    , uiOptGlyph    :: a->Glyph
+    , uiOptDescr    :: a->String
+    , uiOptModes    :: [InputMode]
+    , uiOptSideText :: a -> TutorialLevel -> Maybe (String, HexVec)
+    , onSet         :: Maybe (a -> UIM ()) }
 
 -- non-uniform type, so can't use a list...
 uiOB1 = UIOptButton useFiveColouring (\v o -> o {useFiveColouring=v}) [True,False]
@@ -413,6 +421,7 @@
         (\v -> if v then "Adjacent pieces get different colours" else
         "Pieces are coloured according to type")
         [] -- disabled
+        (const $ const Nothing)
         Nothing
 uiOB2 = UIOptButton showBlocks (\v o -> o {showBlocks=v}) [ShowBlocksBlocking,ShowBlocksAll,ShowBlocksNone]
         (periphery 0 +^ 2 *^ hu +^ 2 *^ neg hv) ShowBlocksButton
@@ -420,7 +429,9 @@
             ShowBlocksBlocking -> "Showing conflicting forces"
             ShowBlocksAll      -> "Showing conflicting forces and movements they prevent"
             ShowBlocksNone     -> "Not showing conflicts")
-        [IMPlay, IMReplay] Nothing
+        [IMPlay, IMReplay]
+        (\a tutLev -> if a == ShowBlocksNone && conflictsTutLev tutLev then Just ("Conflicts -->", 14*^neg hu) else Nothing)
+        Nothing
 uiOB3P = UIOptButton ((!? IMPlay) . whsButtons) (\v o -> o { whsButtons = Map.alter (const v) IMPlay $ whsButtons o })
         [Nothing, Just WHSSelected, Just WHSHook, Just WHSWrench]
         (periphery 3 +^ 3 *^ hv) WhsButtonsButton
@@ -430,23 +441,25 @@
                 WHSSelected -> "selected piece; right-click on buttons to rebind"
                 WHSWrench   -> "wrench; right-click on buttons to rebind"
                 WHSHook     -> "hook; right-click on buttons to rebind")
-        [IMPlay] Nothing
+        [IMPlay]
+        (\a tutLev -> if isNothing a && keysTutLevel tutLev then Just ("<-- KEYS", 3*^hu) else Nothing)
+        Nothing
 uiOB3E = UIOptButton ((!? IMEdit) . whsButtons) (\v o -> o { whsButtons = Map.alter (const v) IMEdit $ whsButtons o })
         [Nothing, Just WHSSelected]
         (periphery 3 +^ 3 *^ hv) WhsEditButtonsButton
         (\case
             Nothing -> "Click to show (and rebind) keyboard control buttons."
             Just _ -> "Showing buttons for controlling cursor / selected piece; right-click on buttons to rebind")
-        [IMEdit] Nothing
+        [IMEdit] (const $ const Nothing) Nothing
 uiOB4 = UIOptButton showButtonText (\v o -> o {showButtonText=v}) [True,False]
         (periphery 0 +^ 2 *^ hu +^ 3 *^ hv) ShowButtonTextButton
         (\v -> if v then "Help text enabled" else
         "Help text disabled")
-        [IMPlay, IMEdit, IMReplay, IMMeta, IMInit] Nothing
+        [IMPlay, IMEdit, IMReplay, IMMeta, IMInit] (const $ const Nothing) Nothing
 uiOB5 = UIOptButton fullscreen (\v o -> o {fullscreen=v}) [True,False]
         (periphery 0 +^ 4 *^ hu +^ 2 *^ hv) FullscreenButton
         (\v -> if v then "Fullscreen mode active" else "Windowed mode active")
-        [IMPlay, IMEdit, IMReplay, IMMeta, IMInit] (Just onFullScreenChange)
+        [IMPlay, IMEdit, IMReplay, IMMeta, IMInit] (const $ const Nothing) (Just onFullScreenChange)
 uiOB6 = UIOptButton soundVolume (\v o -> o {soundVolume=v}) [64,0,16,32]
         (periphery 0 +^ 4 *^ hu +^ hv) VolumeButton
         (\case
@@ -454,29 +467,36 @@
             v | v >= 32 -> "Sound effects at medium volume"
             0           -> "Sound effects disabled"
             _           -> "Sound effects at low volume")
-        [IMPlay, IMEdit, IMReplay] $ Just (`setVolume` AllChannels)
+        [IMPlay, IMEdit, IMReplay] (const $ const Nothing) $ Just (`setVolume` AllChannels)
 
-drawUIOptionButtons :: InputMode -> UIM ()
-drawUIOptionButtons mode = do
-    drawUIOptionButton mode uiOB1
-    drawUIOptionButton mode uiOB2
-    drawUIOptionButton mode uiOB3P
-    drawUIOptionButton mode uiOB3E
-    drawUIOptionButton mode uiOB4
-    drawUIOptionButton mode uiOB5
+drawUIOptionButtons :: InputMode -> TutorialLevel -> UIM ()
+drawUIOptionButtons mode tutLev = do
+    drawUIOptionButton mode tutLev uiOB1
+    drawUIOptionButton mode tutLev uiOB2
+    drawUIOptionButton mode tutLev uiOB3P
+    drawUIOptionButton mode tutLev uiOB3E
+    drawUIOptionButton mode tutLev uiOB4
+    drawUIOptionButton mode tutLev uiOB5
 #ifdef SOUND
-    drawUIOptionButton mode uiOB6
+    drawUIOptionButton mode tutLev uiOB6
 #endif
-drawUIOptionButton im b = when (im `elem` uiOptModes b) $ do
+drawUIOptionButton im tutLev b = when (im `elem` uiOptModes b) $ do
     value <- gets $ getUIOpt b . uiOptions
     renderToMain $ mapM_ (\g -> drawAtRel g (uiOptPos b))
         [HollowGlyph $ obscure purple, uiOptGlyph b value]
+    maybe (pure ()) drawSideText $ uiOptSideText b value tutLev
+    where
+    drawSideText (s,dv) = do
+        smallFont <- gets dispFontSmall
+        renderToMain $ withFont smallFont $ recentreAt (uiOptPos b) $ rescaleRender (1/4) $
+            renderStrColAtLeft white s dv
+
 describeUIOptionButton :: UIOptButton a -> MaybeT UIM String
 describeUIOptionButton b = do
     value <- gets $ getUIOpt b . uiOptions
     return $ uiOptDescr b value
 -- XXX: hand-hacking lenses...
-toggleUIOption (UIOptButton getopt setopt vals _ _ _ _ monSet) = do
+toggleUIOption (UIOptButton getopt setopt vals _ _ _ _ _ monSet) = do
     value <- gets $ getopt . uiOptions
     let value' = cycle vals !! max 0 (1 + fromMaybe 0 (elemIndex value vals))
     modifyUIOptions $ setopt value'
diff --git a/SDL2UIMInstance.hs b/SDL2UIMInstance.hs
--- a/SDL2UIMInstance.hs
+++ b/SDL2UIMInstance.hs
@@ -61,12 +61,13 @@
         lift $ clearButtons >> clearSelectables
         s <- get
         let mode = ms2im s
+            tutLev = getTutLevel s
         lift waitFrame
         drawMainState' s
         lift . drawTitle =<< getTitle
         lift $ do
             drawButtons mode
-            drawUIOptionButtons mode
+            drawUIOptionButtons mode tutLev
             updateHoverStr mode
             drawMsgLine
             drawShortMouseHelp mode s
diff --git a/intricacy.cabal b/intricacy.cabal
--- a/intricacy.cabal
+++ b/intricacy.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               intricacy
-version:            0.9.0.0
+version:            0.9.1.0
 license:            GPL-3.0-or-later
 license-file:       COPYING
 maintainer:         mbays@sdf.org
@@ -33,9 +33,9 @@
 extra-source-files:
     Main_stub.h
 extra-doc-files:
-    README
+    README.md
     BUILD
-    NEWS
+    NEWS.md
     tutorial-extra/*.lock
     tutorial-extra/README
 
diff --git a/tutorial/05-hook.text b/tutorial/05-hook.text
--- a/tutorial/05-hook.text
+++ b/tutorial/05-hook.text
@@ -1,1 +1,1 @@
-turn hook with mouse wheel (or keys) to pull springs aside
+turn hook with mouse wheel or keys to pull springs aside
