diff --git a/README.md b/README.md
index 3acbd9a2826b6e1948d92e7f18c3c6074617de2e..f2a4499b08e472b32cc6aeeff98f70b25577c346 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,7 @@ TermLayout is _not_ a Web browser: it has no facilities for navigating links. It
 
 If you are using TermLayout with an annotator generated by Annotator Generator, you might also be interested in `tmux-annotator.sh` which sets up tmux with a “hotkey” to annotate the current screen and display the result in TermLayout.
 
-Options for Web Adjuster v0.2798
+Options for Web Adjuster v0.2799
 ============
 
 General options
diff --git a/adjuster.py b/adjuster.py
index 69fd37a7adf5231f1d0d48ad14a50b8f9a89156d..0e5e96f26e60bf0e66afb589358f3e8481bb159a 100644
--- a/adjuster.py
+++ b/adjuster.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python2
+# (only --markdown-options etc can be run in Python 3)
 
-program_name = "Web Adjuster v0.2798 (c) 2012-19 Silas S. Brown"
+program_name = "Web Adjuster v0.2799 (c) 2012-19 Silas S. Brown"
 
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -42,7 +43,7 @@ twoline_program_name = program_name+"\nLicensed under the Apache License, Versio
 # --------------------------------------------------
 if '--split-files' in sys.argv:
     if '--autopep8' in sys.argv:
-        print "autopep8",__file__
+        print ("autopep8 "+__file__)
         d=os.popen("autopep8 '"+__file__.replace("'","'\"'\"'")+"'").read()
         assert "\n\n" in d, "check you have autopep8 command"
     else: d=open(__file__).read()
@@ -60,14 +61,14 @@ if '--split-files' in sys.argv:
         except: continue # e.g. before top.py
         assert not fname in filesDone,"Duplicate "+fname
         filesDone.add(fname)
-        print "Writing src/"+fname
+        print ("Writing src/"+fname)
         out = open(fname,"w")
         if not fname=="top.py":
             out.write("#@file: "+fname+"\n"+apache)
         out.write(contents)
         if not fname=="end.py": out.write("\n")
         Makefile.write(fname+" ")
-    print "Writing src/Makefile"
+    print ("Writing src/Makefile")
     Makefile.write("\n\n../adjuster.py: $(Files)\n\tcat $(Files) | grep -v '^#[+]# ' > $@\n")
     raise SystemExit
 
@@ -76,26 +77,30 @@ if '--split-files' in sys.argv:
 # Basic Tornado import (or not if generating the website)
 # --------------------------------------------------
 
+def asStr(u):
+    if type("")==bytes: return u.encode('utf-8') # Python 2
+    else: return u # Python 3
+
 if '--version' in sys.argv:
     # no imports needed other than "sys" ("os" for above)
     # (If this code has been run through autopep8, many
     # imports might have been moved to the very top anyway,
     # but at least it still won't depend on tornado just
     # to print the version or the options as HTML.)
-    print twoline_program_name ; raise SystemExit
+    print (twoline_program_name) ; raise SystemExit
 elif '--html-options' in sys.argv or '--markdown-options' in sys.argv:
     # for updating the website
     # (these options are not included in the help text)
     tornado=inDL=False ; html = '--html-options' in sys.argv
-    if html: print "<h3>Options for "+program_name[:program_name.index("(c)")].strip()+"</h3>"
-    else: print "Options for "+program_name[:program_name.index("(c)")].strip()+"\n============\n"
+    if html: print ("<h3>Options for "+program_name[:program_name.index("(c)")].strip()+"</h3>")
+    else: print ("Options for "+program_name[:program_name.index("(c)")].strip()+"\n============\n")
     def heading(h):
         global inDL
         if html:
-            if inDL: print "</dl>"
-            print "<h4>"+h+"</h4>"
-            print "<dl>"
-        else: print h+"\n"+'-'*len(h)+'\n'
+            if inDL: print ("</dl>")
+            print ("<h4>"+h+"</h4>")
+            print ("<dl>")
+        else: print (h+"\n"+'-'*len(h)+'\n')
         inDL = True
     def define(name,default=None,help="",multiple=False):
         if default or default==False:
@@ -114,8 +119,8 @@ elif '--html-options' in sys.argv or '--markdown-options' in sys.argv:
         for w in ["lot","not","all","Important","between","any"]:
             if html: help=re.sub("(?<![A-Za-z])"+w.upper()+"(?![A-Za-z])","<strong>"+w+"</strong>",help)
             else: help=re.sub("(?<![A-Za-z])"+w.upper()+"(?![A-Za-z])","**"+w+"**",help)
-        if html: print "<dt><kbd>--"+name+"</kbd>"+amp(default)+"</dt><dd>"+help.replace(" - ","---")+"</dd>"
-        else: print "`--"+name+"` "+default+"\n: "+help.replace(" - ","---").replace("---",u'\u2014'.encode('utf-8'))+"\n"
+        if html: print ("<dt><kbd>--"+name+"</kbd>"+amp(default)+"</dt><dd>"+help.replace(" - ","---")+"</dd>")
+        else: print ("`--"+name+"` "+default+"\n: "+help.replace(" - ","---").replace("---",asStr(u'\u2014'))+"\n")
 else: # normal run: go ahead with Tornado import
     import tornado
     from tornado.httpclient import AsyncHTTPClient,HTTPError
@@ -483,11 +488,11 @@ define("errorHTML",default="Adjuster error has been logged",help="What to say wh
 define("logDebug",default=False,help="Write debugging messages (to standard error if in the foreground, or to the logs if in the background). Use as an alternative to --logging=debug if you don't also want debug messages from other Tornado modules. On Unix you may also toggle this at runtime by sending SIGUSR1 to the process(es).") # see debuglog()
 # and continuing into the note below:
 if not tornado:
-    if html: print "</dl>"
+    if html: print ("</dl>")
     end = "Tornado-provided logging options are not listed above because they might vary across Tornado versions; run <kbd>python adjuster.py --help</kbd> to see a full list of the ones available on your setup. They typically include <kbd>log_file_max_size</kbd>, <kbd>log_file_num_backups</kbd>, <kbd>log_file_prefix</kbd> and <kbd>log_to_stderr</kbd>."
     # and --logging=debug, but that may generate a lot of entries from curl_httpclient
-    if html: print end
-    else: print end.replace("<kbd>","`").replace("</kbd>","`")
+    if html: print (end)
+    else: print (end.replace("<kbd>","`").replace("</kbd>","`"))
     raise SystemExit
 
 #@file: import2-other.py
@@ -744,7 +749,7 @@ def parse_command_line(final):
     else:
         rest=tornado.options.parse_command_line(final=final)
     if rest: errExit("Unrecognised command-line argument '%s'" % rest[0]) # maybe they missed a '--' at the start of an option: don't want result to be ignored without anyone noticing
-  except tornado.options.Error,e: optErr(e.message)
+  except tornado.options.Error as e: optErr(e.message)
 def optErr(m):
     if "PhantomJS" in m: m += " (try --js_interpreter=PhantomJS instead?)" # old option was --PhantomJS
     errExit(m)
@@ -754,7 +759,7 @@ def parse_config_file(cfg):
     if not tornado.options.parse_config_file.func_defaults: # Tornado 2.x
         tornado.options.parse_config_file(cfg)
     else: tornado.options.parse_config_file(cfg,final=False)
-  except tornado.options.Error,e: optErr(e.message)
+  except tornado.options.Error as e: optErr(e.message)
 def check_config_file(cfg):
     # Tornado doesn't catch capitalisation and spelling errors etc by default
     try:
@@ -1814,7 +1819,7 @@ def listen_on_port(application,port,address,browser,core="all",**kwargs):
     if port in port_randomise: return
     for portTry in [5,4,3,2,1,0]:
       try: return h.bind(port,address)
-      except socket.error, e:
+      except socket.error as e:
         if is_sslHelp:
             # We had better not time.sleep() here trying
             # to open, especially not if multicore: don't
@@ -1893,7 +1898,7 @@ def workaround_raspbian7_IPv6_bug():
     if hasattr(socket, "AI_ADDRCONFIG"): flags |= socket.AI_ADDRCONFIG
     for af,socktype,proto,r1,r2 in socket.getaddrinfo(None,options.port,socket.AF_UNSPEC,socket.SOCK_STREAM,0,flags):
         try: socket.socket(af,socktype,proto)
-        except socket.error, e:
+        except socket.error as e:
             if "family not supported" in e.strerror:
                 options.address = "0.0.0.0" # use IPv4 only
                 return
@@ -2102,14 +2107,14 @@ def wrapped_readUntilClose(s,onLast,onChunk):
 def writeAndClose(stream,data):
     # This helper function is needed for CONNECT and own_server handling because, contrary to Tornado docs, some Tornado versions (e.g. 2.3) send the last data packet in the FIRST callback of IOStream's read_until_close
     if data:
-        if debug_connections: print "Writing",myRepr(data),"to",peerName(stream.socket),"and closing it"
+        if debug_connections: print ("Writing "+myRepr(data)+" to "+peerName(stream.socket)+" and closing it")
         try: stream.write(data,lambda *args:True)
         except: pass # ignore errors like client disconnected
     if not stream.closed():
         try: stream.close()
         except: pass
 def writeOrError(opposite,name,stream,data):
-    if debug_connections: print "Writing",myRepr(data),"to",peerName(stream.socket)
+    if debug_connections: print ("Writing "+myRepr(data)+" to "+peerName(stream.socket))
     try: stream.write(data)
     except:
         if name and not hasattr(stream,"writeOrError_already_complained"): logging.error("Error writing data to "+name)
@@ -2176,8 +2181,8 @@ class WebdriverWrapper:
         if options.logDebug:
           try:
             for e in self.theWebDriver.get_log('browser'):
-                print "webdriver log:",e['message']
-          except: print "webdriver log exception"
+                print ("webdriver log: "+e['message'])
+          except: print ("webdriver log exception")
     def execute_script(self,script): self.theWebDriver.execute_script(script)
     def click_id(self,clickElementID): self.theWebDriver.find_element_by_id(clickElementID).click()
     def click_xpath(self,xpath): self.theWebDriver.find_element_by_xpath(xpath).click()
@@ -2233,7 +2238,7 @@ def webdriverWrapper_receiver(pipe,timeoutLock):
             return pipe.close()
         if cmd=="EOF": return pipe.close()
         try: ret,exc = getattr(w,cmd)(*args), None
-        except Exception, e:
+        except Exception as e:
             p = find_adjuster_in_traceback()
             if p: # see if we can add it to the message (note p will start with ", " so no need to add a space before it)
               try:
@@ -3502,7 +3507,7 @@ document.forms[0].i.focus()
             usr = s.recv(1024).strip()
             if usr.split(':')[-1]==myUsername: return True
             else: logging.error("ident server didn't confirm username: rejecting this connection")
-        except Exception,e: logging.error("Trouble connecting to ident server (%s): rejecting this connection" % repr(e))
+        except Exception as e: logging.error("Trouble connecting to ident server (%s): rejecting this connection" % repr(e))
         self.set_status(401)
         if usr: self.write(usr+": ")
         self.write("Connection from wrong account (ident check failed)\n")
@@ -4328,8 +4333,8 @@ def httpfetch(url,**kwargs):
     if kwargs.get('proxy_host',None) and kwargs.get('proxy_port',None): req.set_proxy("http://"+kwargs['proxy_host']+':'+kwargs['proxy_port'],"http")
     r = None
     try: resp = urllib2.build_opener(DoNotRedirect).open(req,timeout=60)
-    except urllib2.HTTPError, e: resp = e
-    except Exception, e: resp = r = wrapResponse(str(e)) # could be anything, especially if urllib2 has been overridden by a 'cloud' provider
+    except urllib2.HTTPError as e: resp = e
+    except Exception as e: resp = r = wrapResponse(str(e)) # could be anything, especially if urllib2 has been overridden by a 'cloud' provider
     if r==None: r = wrapResponse(resp.read(),resp.info(),resp.getcode())
     kwargs['callback'](r)
 def wrapResponse(body,info={},code=500):
@@ -4772,11 +4777,11 @@ def find_text_in_HTML(htmlStr): # returns a codeTextList; encodes entities in ut
     err=""
     try:
         parser.feed(htmlStr) ; parser.close()
-    except UnicodeDecodeError, e:
+    except UnicodeDecodeError as e:
         # sometimes happens in parsing the start of a tag in duff HTML (possibly emitted by a duff htmlFilter if we're currently picking out text for the renderer)
         try: err="UnicodeDecodeError at bytes %d-%d: %s" % (e.start,e.end,e.reason)
         except: err = "UnicodeDecodeError"
-    except HTMLParseError, e: # rare?
+    except HTMLParseError as e: # rare?
         try: err="HTMLParseError: "+e.msg+" at "+str(e.lineno)+":"+str(e.offset) # + ' after '+repr(htmlStr[parser.lastCodeStart:])
         except: err = "HTMLParseError"
         logging.info("WARNING: find_text_in_HTML finishing early due to "+err)
@@ -4894,7 +4899,7 @@ def get_httpequiv_charset(htmlStr):
         parser.feed(htmlStr) ; parser.close()
     except UnicodeDecodeError: pass
     except HTMLParseError: pass
-    except Finished,e: return e.charset,e.tagStart,e.tagEnd
+    except Finished as e: return e.charset,e.tagStart,e.tagEnd
     return None,None,None
 
 def get_and_remove_httpequiv_charset(body):
@@ -5330,7 +5335,7 @@ def htmlFind(html,markup):
     def blankOut(m): return " "*(m.end()-m.start())
     return re.sub("<!--.*?-->",blankOut,html,flags=re.DOTALL).lower().find(markup) # TODO: improve efficiency of this? (blankOut doesn't need to go through the entire document)
 
-def html_additions(html,(cssToAdd,attrsToAdd),slow_CSS_switch,cookieHostToSet,jsCookieString,canRender,cookie_host,is_password_domain,IsEdge,addHtmlFilterOptions,htmlFilterOutput):
+def html_additions(html,toAdd,slow_CSS_switch,cookieHostToSet,jsCookieString,canRender,cookie_host,is_password_domain,IsEdge,addHtmlFilterOptions,htmlFilterOutput):
     # Additions to make to HTML only (not on HTML embedded in JSON)
     # called from doResponse2 if do_html_process is set
     if html.startswith("<?xml"): link_close = " /"
@@ -5343,6 +5348,7 @@ def html_additions(html,(cssToAdd,attrsToAdd),slow_CSS_switch,cookieHostToSet,js
     if set_window_onerror: headAppend += r"""<script><!--
 window.onerror=function(msg,url,line){alert(msg); return true}
 --></script>"""
+    cssToAdd,attrsToAdd = toAdd
     if cssToAdd:
         # do this BEFORE options.headAppend, because someone might want to refer to it in a script in options.headAppend (although bodyPrepend is a better place to put 'change the href according to screen size' scripts, as some Webkit-based browsers don't make screen size available when processing the HEAD of the 1st document in the session)
         if options.cssName: