 git-arr            |   2 +-
 git.py             |  70 ++++++++++++++-------
 static/git-arr.css | 180 +++++++++++++++++++++++++++++++++++++++++------------
 static/syntax.css  | 146 +++++++++++++++++++++++++++++++++++++------
 utils.py           |  55 +++++++++-------
 views/blob.html    |   2 +
 views/commit.html  |   2 +
 views/index.html   |   3 +-
 views/summary.html |  14 +++--
 9 files changed, 364 insertions(+), 110 deletions(-)

diff --git a/git-arr b/git-arr
index b109f23..337912a 100755
--- a/git-arr
+++ b/git-arr
@@ -186,7 +186,7 @@ bottle.app.push(app)
 
 def with_utils(f):
     """Decorator to add the utilities to the return value.
-    
+
     Used to wrap functions that return dictionaries which are then passed to
     templates.
     """
diff --git a/git.py b/git.py
index 3fa160c..8402c8c 100644
--- a/git.py
+++ b/git.py
@@ -6,6 +6,7 @@ command line tool directly, so please be careful with using untrusted
 parameters.
 """
 
+import functools
 import sys
 import io
 import subprocess
@@ -199,7 +200,8 @@ class Repo:
         """Returns a GitCommand() on our path."""
         return GitCommand(self.path, cmd)
 
-    def for_each_ref(self, pattern=None, sort=None, count=None):
+    @functools.lru_cache
+    def _for_each_ref(self, pattern=None, sort=None, count=None):
         """Returns a list of references."""
         cmd = self.cmd("for-each-ref")
         if sort:
@@ -209,26 +211,36 @@ class Repo:
         if pattern:
             cmd.arg(pattern)
 
+        refs = []
         for l in cmd.run():
             obj_id, obj_type, ref = l.split()
-            yield obj_id, obj_type, ref
-
-    def branches(self, sort="-authordate"):
-        """Get the (name, obj_id) of the branches."""
-        refs = self.for_each_ref(pattern="refs/heads/", sort=sort)
-        for obj_id, _, ref in refs:
-            yield ref[len("refs/heads/") :], obj_id
+            refs.append((obj_id, obj_type, ref))
+        return refs
 
+    @functools.cache
     def branch_names(self):
         """Get the names of the branches."""
-        return (name for name, _ in self.branches())
-
+        refs = self._for_each_ref(pattern="refs/heads/", sort="-authordate")
+        return [ref[len("refs/heads/") :] for _, _, ref in refs]
+
+    @functools.cache
+    def main_branch(self):
+        """Get the name of the main branch."""
+        bs = self.branch_names()
+        for branch in ["master", "main"]:
+            if branch in bs:
+                return branch
+        if bs:
+            return bs[0]
+        return None
+
+    @functools.cache
     def tags(self, sort="-taggerdate"):
         """Get the (name, obj_id) of the tags."""
-        refs = self.for_each_ref(pattern="refs/tags/", sort=sort)
-        for obj_id, _, ref in refs:
-            yield ref[len("refs/tags/") :], obj_id
+        refs = self._for_each_ref(pattern="refs/tags/", sort=sort)
+        return [(ref[len("refs/tags/") :], obj_id) for obj_id, _, ref in refs]
 
+    @functools.lru_cache
     def commit_ids(self, ref, limit=None):
         """Generate commit ids."""
         cmd = self.cmd("rev-list")
@@ -238,9 +250,9 @@ class Repo:
         cmd.arg(ref)
         cmd.arg("--")
 
-        for l in cmd.run():
-            yield l.rstrip("\n")
+        return [l.rstrip("\n") for l in cmd.run()]
 
+    @functools.lru_cache
     def commit(self, commit_id):
         """Return a single commit."""
         cs = list(self.commits(commit_id, limit=1))
@@ -248,11 +260,11 @@ class Repo:
             return None
         return cs[0]
 
-    def commits(self, ref, limit=None, offset=0):
+    @functools.lru_cache
+    def commits(self, ref, limit, offset=0):
         """Generate commit objects for the ref."""
         cmd = self.cmd("rev-list")
-        if limit:
-            cmd.max_count = limit + offset
+        cmd.max_count = limit + offset
 
         cmd.header = None
 
@@ -261,6 +273,7 @@ class Repo:
 
         info_buffer = ""
         count = 0
+        commits = []
         for l in cmd.run():
             if "\0" in l:
                 pre, post = l.split("\0", 1)
@@ -268,7 +281,7 @@ class Repo:
 
                 count += 1
                 if count > offset:
-                    yield Commit.from_str(self, info_buffer)
+                    commits.append(Commit.from_str(self, info_buffer))
 
                 # Start over.
                 info_buffer = post
@@ -278,8 +291,11 @@ class Repo:
         if info_buffer:
             count += 1
             if count > offset:
-                yield Commit.from_str(self, info_buffer)
+                commits.append(Commit.from_str(self, info_buffer))
+
+        return commits
 
+    @functools.lru_cache
     def diff(self, ref):
         """Return a Diff object for the ref."""
         cmd = self.cmd("diff-tree")
@@ -295,6 +311,7 @@ class Repo:
 
         return Diff.from_str(cmd.run())
 
+    @functools.lru_cache
     def refs(self):
         """Return a dict of obj_id -> ref."""
         cmd = self.cmd("show-ref")
@@ -308,10 +325,12 @@ class Repo:
 
         return r
 
+    @functools.lru_cache
     def tree(self, ref):
         """Returns a Tree instance for the given ref."""
         return Tree(self, ref)
 
+    @functools.lru_cache
     def blob(self, path, ref):
         """Returns a Blob instance for the given path."""
         cmd = self.cmd("cat-file")
@@ -329,9 +348,10 @@ class Repo:
 
         return Blob(out.read()[: int(head)])
 
+    @functools.cache
     def last_commit_timestamp(self):
         """Return the timestamp of the last commit."""
-        refs = self.for_each_ref(
+        refs = self._for_each_ref(
             pattern="refs/heads/", sort="-committerdate", count=1
         )
         for obj_id, _, _ in refs:
@@ -515,12 +535,13 @@ class Diff:
 
 
 class Tree:
-    """ A git tree."""
+    """A git tree."""
 
     def __init__(self, repo: Repo, ref: str):
         self.repo = repo
         self.ref = ref
 
+    @functools.lru_cache
     def ls(
         self, path, recursive=False
     ) -> Iterable[Tuple[str, smstr, Optional[int]]]:
@@ -537,6 +558,7 @@ class Tree:
         else:
             cmd.arg(path)
 
+        files = []
         for l in cmd.run():
             _mode, otype, _oid, size, name = l.split(None, 4)
             if size == "-":
@@ -553,7 +575,9 @@ class Tree:
 
             # We use a smart string for the name, as it's often tricky to
             # manipulate otherwise.
-            yield otype, smstr(name), size
+            files.append((otype, smstr(name), size))
+
+        return files
 
 
 class Blob:
diff --git a/static/git-arr.css b/static/git-arr.css
index 0ef1961..57a415b 100644
--- a/static/git-arr.css
+++ b/static/git-arr.css
@@ -1,15 +1,53 @@
-
 /*
  * git-arr style sheet
  */
+:root {
+    --body-bg: white;
+    --text-fg: black;
+    --h1-bg: #ddd;
+    --hr-bg: #e3e3e3;
+    --text-lowcontrast-fg: grey;
+    --a-fg: #800;
+    --a-explicit-fg: #038;
+    --table-hover-bg: #eee;
+    --head-bg: #88ff88;
+    --tag-bg: #ffff88;
+    --age-fg0: darkgreen;
+    --age-fg1: green;
+    --age-fg2: seagreen;
+    --diff-added-fg: green;
+    --diff-deleted-fg: red;
+}
+
+@media (prefers-color-scheme: dark) {
+    :root {
+        --body-bg: #121212;
+        --text-fg: #c9d1d9;
+        --h1-bg: #2f2f2f;
+        --hr-bg: #e3e3e3;
+        --text-lowcontrast-fg: grey;
+        --a-fg: #d4b263;
+        --a-explicit-fg: #44b4ec;
+        --table-hover-bg: #313131;
+        --head-bg: #020;
+        --tag-bg: #333000;
+        --age-fg0: #51a552;
+        --age-fg1: #468646;
+        --age-fg2: #2f722f;
+        --diff-added-fg: #00A000;
+        --diff-deleted-fg: #A00000;
+    }
+}
 
 body {
     font-family: sans-serif;
     padding: 0 1em 1em 1em;
+    color: var(--text-fg);
+    background: var(--body-bg);
 }
 
 h1 {
-    background: #ddd;
+    background: var(--h1-bg);
     padding: 0.3em;
 }
 
@@ -21,26 +59,29 @@ h2, h3 {
 
 hr {
     border: none;
-    background-color: #e3e3e3;
+    background-color: var(--hr-bg);
     height: 1px;
 }
 
+
 /* By default, use implied links, more discrete for increased readability. */
 a {
     text-decoration: none;
-    color: black;
+    color: var(--text-fg);
 }
+
 a:hover {
-    text-decoration: underline;
-    color: #800;
+    color: var(--a-fg);
 }
 
+
 /* Explicit links */
 a.explicit {
-    color: #038;
+    color: var(--a-explicit-fg);
 }
+
 a.explicit:hover, a.explicit:active {
-    color: #880000;
+    color: var(--a-fg);
 }
 
 
@@ -48,22 +89,27 @@ a.explicit:hover, a.explicit:active {
 table.nice {
     text-align: left;
 }
+
 table.nice td {
     padding: 0.15em 0.5em;
 }
+
 table.nice td.links {
 }
+
 table.nice td.main {
     min-width: 10em;
 }
+
 table.nice tr:hover {
-    background: #eee;
+    background: var(--table-hover-bg);
 }
 
+
 /* Table for commits. */
 table.commits td.date {
     font-style: italic;
-    color: gray;
+    color: var(--text-lowcontrast-fg);
 }
 
 @media (min-width: 600px) {
@@ -71,106 +117,143 @@ table.commits td.date {
         min-width: 32em;
     }
 }
+
 table.commits td.author {
-    color: gray;
+    color: var(--text-lowcontrast-fg);
 }
 
+
 /* Table for commit information. */
 table.commit-info tr:hover {
     background: inherit;
 }
+
 table.commit-info td {
     vertical-align: top;
 }
+
 table.commit-info span.date, span.email {
-    color: gray;
+    color: var(--text-lowcontrast-fg);
 }
 
+
 /* Reference annotations. */
 span.refs {
     margin: 0px 0.5em;
     padding: 0px 0.25em;
-    border: solid 1px gray;
+    border: solid 1px var(--text-lowcontrast-fg);
 }
+
 span.head {
-    background-color: #88ff88;
+    background-color: var(--head-bg);
 }
+
 span.tag {
-    background-color: #ffff88;
+    background-color: var(--tag-bg);
 }
 
+
 /* Projects table */
 table.projects td.name a {
-    color: #038;
+    color: var(--a-explicit-fg);
+}
+
+
+/* Truncate long descriptions based on the viewport width. */
+table.projects td.desc {
+    max-width: 50vw;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    white-space: nowrap;
 }
 
+
 /* Age of an object.
  * Note this is hidden by default as we rely on javascript to show it. */
 span.age {
     display: none;
-    color: gray;
+    color: var(--text-lowcontrast-fg);
     font-size: smaller;
 }
 
 span.age-band0 {
-    color: darkgreen;
+    color: var(--age-fg0);
 }
 
 span.age-band1 {
-    color: green;
+    color: var(--age-fg1);
 }
 
 span.age-band2 {
-    color: seagreen;
+    color: var(--age-fg2);
 }
 
+
 /* Toggable titles */
 div.toggable-title {
     font-weight: bold;
     margin-bottom: 0.3em;
 }
 
+pre {
+    /* Sometimes, <pre> elements (commit messages, diffs, blobs) have very
+     * long lines. In those case, use automatic overflow, which will
+     * introduce a horizontal scroll bar for this element only (more
+     * comfortable than stretching the page, which is the default). */
+    overflow: auto;
+}
+
+
 /* Commit message and diff. */
 pre.commit-message {
     font-size: large;
-    padding: 0.2em 2em;
+    padding: 0.2em 0.5em;
 }
+
 pre.diff-body {
     /* Note this is only used as a fallback if pygments is not available. */
 }
+
 table.changed-files {
     font-family: monospace;
 }
+
 table.changed-files span.lines-added {
-    color: green;
+    color: var(--diff-added-fg);
 }
+
 table.changed-files span.lines-deleted {
-    color: red;
+    color: var(--diff-deleted-fg);
 }
 
+
 /* Pagination. */
 div.paginate {
     padding-bottom: 1em;
 }
 
 div.paginate span.inactive {
-    color: gray;
+    color: var(--text-lowcontrast-fg);
 }
 
+
 /* Directory listing. */
 @media (min-width: 600px) {
     table.ls td.name {
         min-width: 20em;
     }
 }
+
 table.ls {
     font-family: monospace;
     font-size: larger;
 }
+
 table.ls tr.blob td.size {
-    color: gray;
+    color: var(--text-lowcontrast-fg);
 }
 
+
 /* Blob. */
 pre.blob-body {
     /* Note this is only used as a fallback if pygments is not available. */
@@ -184,60 +267,79 @@ table.blob-binary pre {
 table.blob-binary .offset {
     text-align: right;
     font-size: x-small;
-    color: darkgray;
-    border-right: 1px solid #eee;
+    color: var(--text-lowcontrast-fg);
+    border-right: 1px solid var(--text-lowcontrast-fg);
 }
 
 table.blob-binary tr.etc {
     text-align: center;
 }
 
+
 /* Pygments overrides. */
+div.colorized-src {
+    font-size: larger;
+}
+
+div.colorized-src .source_code {
+    /* Ignore pygments style's background. */
+    background: var(--body-bg);
+}
+
+td.code > div.source_code {
+    /* This is a workaround, in pygments 2.11 there's a bug where the wrapper
+     * div is inside the table, so we need to override the descendant (because
+     * the style sets it on ".source_code" and the most specific value wins).
+     * Once we no longer support 2.11, we can remove this. */
+    background: var(--body-bg);
+}
+
 div.linenodiv {
     padding-right: 0.5em;
-    font-size: larger;  /* must match div.source_code */
 }
+
 div.linenodiv a {
-    color: gray;
-}
-div.source_code {
-    background: inherit;
-    font-size: larger;
+    color: var(--text-lowcontrast-fg);
 }
 
+
 /* Repository information table. */
 table.repo_info tr:hover {
     background: inherit;
 }
+
 table.repo_info td.category {
     font-weight: bold;
     /* So we can copy-paste rows and preserve spaces, useful for the row:
-     *   git clone | url
-     */
+     *   git clone | url */
     white-space: pre-wrap;
 }
+
 table.repo_info td {
     vertical-align: top;
 }
 
 span.ctrlchr {
-    color: gray;
+    color: var(--text-lowcontrast-fg);
     padding: 0 0.2ex 0 0.1ex;
-    margin:  0 0.2ex 0 0.1ex;
+    margin: 0 0.2ex 0 0.1ex;
 }
 
+
 /*
  * Markdown overrides
  */
 
 /* Colored links (same as explicit links above) */
 div.markdown a {
-    color: #038;
+    color: var(--a-explicit-fg);
 }
+
 div.markdown a:hover, div.markdown a:active {
-    color: #880000;
+    color: var(--a-fg);
 }
 
+
 /* Restrict max width for readability */
 div.markdown {
     max-width: 55em;
diff --git a/static/syntax.css b/static/syntax.css
index 097e4d2..f8f1919 100644
--- a/static/syntax.css
+++ b/static/syntax.css
@@ -1,30 +1,37 @@
 
 /* CSS for syntax highlighting.
- * Generated by pygments (what we use for syntax highlighting):
+ * Generated by pygments (what we use for syntax highlighting).
  *
- * $ pygmentize -S default -f html -a .source_code
+ * Light mode:  pygmentize -S default -f html -a .source_code
  */
 
+pre { line-height: 125%; }
+td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
 .source_code .hll { background-color: #ffffcc }
-.source_code  { background: #f8f8f8; }
-.source_code .c { color: #408080; font-style: italic } /* Comment */
+.source_code { background: #f8f8f8; }
+.source_code .c { color: #3D7B7B; font-style: italic } /* Comment */
 .source_code .err { border: 1px solid #FF0000 } /* Error */
 .source_code .k { color: #008000; font-weight: bold } /* Keyword */
 .source_code .o { color: #666666 } /* Operator */
-.source_code .cm { color: #408080; font-style: italic } /* Comment.Multiline */
-.source_code .cp { color: #BC7A00 } /* Comment.Preproc */
-.source_code .c1 { color: #408080; font-style: italic } /* Comment.Single */
-.source_code .cs { color: #408080; font-style: italic } /* Comment.Special */
+.source_code .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
+.source_code .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
+.source_code .cp { color: #9C6500 } /* Comment.Preproc */
+.source_code .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
+.source_code .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
+.source_code .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
 .source_code .gd { color: #A00000 } /* Generic.Deleted */
 .source_code .ge { font-style: italic } /* Generic.Emph */
-.source_code .gr { color: #FF0000 } /* Generic.Error */
+.source_code .gr { color: #E40000 } /* Generic.Error */
 .source_code .gh { color: #000080; font-weight: bold } /* Generic.Heading */
-.source_code .gi { color: #00A000 } /* Generic.Inserted */
-.source_code .go { color: #808080 } /* Generic.Output */
+.source_code .gi { color: #008400 } /* Generic.Inserted */
+.source_code .go { color: #717171 } /* Generic.Output */
 .source_code .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
 .source_code .gs { font-weight: bold } /* Generic.Strong */
 .source_code .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
-.source_code .gt { color: #0040D0 } /* Generic.Traceback */
+.source_code .gt { color: #0044DD } /* Generic.Traceback */
 .source_code .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
 .source_code .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
 .source_code .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
@@ -33,38 +40,139 @@
 .source_code .kt { color: #B00040 } /* Keyword.Type */
 .source_code .m { color: #666666 } /* Literal.Number */
 .source_code .s { color: #BA2121 } /* Literal.String */
-.source_code .na { color: #7D9029 } /* Name.Attribute */
+.source_code .na { color: #687822 } /* Name.Attribute */
 .source_code .nb { color: #008000 } /* Name.Builtin */
 .source_code .nc { color: #0000FF; font-weight: bold } /* Name.Class */
 .source_code .no { color: #880000 } /* Name.Constant */
 .source_code .nd { color: #AA22FF } /* Name.Decorator */
-.source_code .ni { color: #999999; font-weight: bold } /* Name.Entity */
-.source_code .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
+.source_code .ni { color: #717171; font-weight: bold } /* Name.Entity */
+.source_code .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
 .source_code .nf { color: #0000FF } /* Name.Function */
-.source_code .nl { color: #A0A000 } /* Name.Label */
+.source_code .nl { color: #767600 } /* Name.Label */
 .source_code .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
 .source_code .nt { color: #008000; font-weight: bold } /* Name.Tag */
 .source_code .nv { color: #19177C } /* Name.Variable */
 .source_code .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
 .source_code .w { color: #bbbbbb } /* Text.Whitespace */
+.source_code .mb { color: #666666 } /* Literal.Number.Bin */
 .source_code .mf { color: #666666 } /* Literal.Number.Float */
 .source_code .mh { color: #666666 } /* Literal.Number.Hex */
 .source_code .mi { color: #666666 } /* Literal.Number.Integer */
 .source_code .mo { color: #666666 } /* Literal.Number.Oct */
+.source_code .sa { color: #BA2121 } /* Literal.String.Affix */
 .source_code .sb { color: #BA2121 } /* Literal.String.Backtick */
 .source_code .sc { color: #BA2121 } /* Literal.String.Char */
+.source_code .dl { color: #BA2121 } /* Literal.String.Delimiter */
 .source_code .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
 .source_code .s2 { color: #BA2121 } /* Literal.String.Double */
-.source_code .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
+.source_code .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
 .source_code .sh { color: #BA2121 } /* Literal.String.Heredoc */
-.source_code .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
+.source_code .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
 .source_code .sx { color: #008000 } /* Literal.String.Other */
-.source_code .sr { color: #BB6688 } /* Literal.String.Regex */
+.source_code .sr { color: #A45A77 } /* Literal.String.Regex */
 .source_code .s1 { color: #BA2121 } /* Literal.String.Single */
 .source_code .ss { color: #19177C } /* Literal.String.Symbol */
 .source_code .bp { color: #008000 } /* Name.Builtin.Pseudo */
+.source_code .fm { color: #0000FF } /* Name.Function.Magic */
 .source_code .vc { color: #19177C } /* Name.Variable.Class */
 .source_code .vg { color: #19177C } /* Name.Variable.Global */
 .source_code .vi { color: #19177C } /* Name.Variable.Instance */
+.source_code .vm { color: #19177C } /* Name.Variable.Magic */
 .source_code .il { color: #666666 } /* Literal.Number.Integer.Long */
 
+/*
+ * Dark mode:  pygmentize -S native -f html -a .source_code
+ */
+
+@media (prefers-color-scheme: dark) {
+
+pre { line-height: 125%; }
+td.linenos .normal { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+span.linenos { color: #aaaaaa; background-color: transparent; padding-left: 5px; padding-right: 5px; }
+td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
+.source_code .hll { background-color: #404040 }
+.source_code { background: #202020; color: #d0d0d0 }
+.source_code .c { color: #ababab; font-style: italic } /* Comment */
+.source_code .err { color: #a61717; background-color: #e3d2d2 } /* Error */
+.source_code .esc { color: #d0d0d0 } /* Escape */
+.source_code .g { color: #d0d0d0 } /* Generic */
+.source_code .k { color: #6ebf26; font-weight: bold } /* Keyword */
+.source_code .l { color: #d0d0d0 } /* Literal */
+.source_code .n { color: #d0d0d0 } /* Name */
+.source_code .o { color: #d0d0d0 } /* Operator */
+.source_code .x { color: #d0d0d0 } /* Other */
+.source_code .p { color: #d0d0d0 } /* Punctuation */
+.source_code .ch { color: #ababab; font-style: italic } /* Comment.Hashbang */
+.source_code .cm { color: #ababab; font-style: italic } /* Comment.Multiline */
+.source_code .cp { color: #cd2828; font-weight: bold } /* Comment.Preproc */
+.source_code .cpf { color: #ababab; font-style: italic } /* Comment.PreprocFile */
+.source_code .c1 { color: #ababab; font-style: italic } /* Comment.Single */
+.source_code .cs { color: #e50808; font-weight: bold; background-color: #520000 } /* Comment.Special */
+.source_code .gd { color: #d22323 } /* Generic.Deleted */
+.source_code .ge { color: #d0d0d0; font-style: italic } /* Generic.Emph */
+.source_code .gr { color: #d22323 } /* Generic.Error */
+.source_code .gh { color: #ffffff; font-weight: bold } /* Generic.Heading */
+.source_code .gi { color: #589819 } /* Generic.Inserted */
+.source_code .go { color: #cccccc } /* Generic.Output */
+.source_code .gp { color: #aaaaaa } /* Generic.Prompt */
+.source_code .gs { color: #d0d0d0; font-weight: bold } /* Generic.Strong */
+.source_code .gu { color: #ffffff; text-decoration: underline } /* Generic.Subheading */
+.source_code .gt { color: #d22323 } /* Generic.Traceback */
+.source_code .kc { color: #6ebf26; font-weight: bold } /* Keyword.Constant */
+.source_code .kd { color: #6ebf26; font-weight: bold } /* Keyword.Declaration */
+.source_code .kn { color: #6ebf26; font-weight: bold } /* Keyword.Namespace */
+.source_code .kp { color: #6ebf26 } /* Keyword.Pseudo */
+.source_code .kr { color: #6ebf26; font-weight: bold } /* Keyword.Reserved */
+.source_code .kt { color: #6ebf26; font-weight: bold } /* Keyword.Type */
+.source_code .ld { color: #d0d0d0 } /* Literal.Date */
+.source_code .m { color: #51b2fd } /* Literal.Number */
+.source_code .s { color: #ed9d13 } /* Literal.String */
+.source_code .na { color: #bbbbbb } /* Name.Attribute */
+.source_code .nb { color: #2fbccd } /* Name.Builtin */
+.source_code .nc { color: #71adff; text-decoration: underline } /* Name.Class */
+.source_code .no { color: #40ffff } /* Name.Constant */
+.source_code .nd { color: #ffa500 } /* Name.Decorator */
+.source_code .ni { color: #d0d0d0 } /* Name.Entity */
+.source_code .ne { color: #bbbbbb } /* Name.Exception */
+.source_code .nf { color: #71adff } /* Name.Function */
+.source_code .nl { color: #d0d0d0 } /* Name.Label */
+.source_code .nn { color: #71adff; text-decoration: underline } /* Name.Namespace */
+.source_code .nx { color: #d0d0d0 } /* Name.Other */
+.source_code .py { color: #d0d0d0 } /* Name.Property */
+.source_code .nt { color: #6ebf26; font-weight: bold } /* Name.Tag */
+.source_code .nv { color: #40ffff } /* Name.Variable */
+.source_code .ow { color: #6ebf26; font-weight: bold } /* Operator.Word */
+.source_code .w { color: #666666 } /* Text.Whitespace */
+.source_code .mb { color: #51b2fd } /* Literal.Number.Bin */
+.source_code .mf { color: #51b2fd } /* Literal.Number.Float */
+.source_code .mh { color: #51b2fd } /* Literal.Number.Hex */
+.source_code .mi { color: #51b2fd } /* Literal.Number.Integer */
+.source_code .mo { color: #51b2fd } /* Literal.Number.Oct */
+.source_code .sa { color: #ed9d13 } /* Literal.String.Affix */
+.source_code .sb { color: #ed9d13 } /* Literal.String.Backtick */
+.source_code .sc { color: #ed9d13 } /* Literal.String.Char */
+.source_code .dl { color: #ed9d13 } /* Literal.String.Delimiter */
+.source_code .sd { color: #ed9d13 } /* Literal.String.Doc */
+.source_code .s2 { color: #ed9d13 } /* Literal.String.Double */
+.source_code .se { color: #ed9d13 } /* Literal.String.Escape */
+.source_code .sh { color: #ed9d13 } /* Literal.String.Heredoc */
+.source_code .si { color: #ed9d13 } /* Literal.String.Interpol */
+.source_code .sx { color: #ffa500 } /* Literal.String.Other */
+.source_code .sr { color: #ed9d13 } /* Literal.String.Regex */
+.source_code .s1 { color: #ed9d13 } /* Literal.String.Single */
+.source_code .ss { color: #ed9d13 } /* Literal.String.Symbol */
+.source_code .bp { color: #2fbccd } /* Name.Builtin.Pseudo */
+.source_code .fm { color: #71adff } /* Name.Function.Magic */
+.source_code .vc { color: #40ffff } /* Name.Variable.Class */
+.source_code .vg { color: #40ffff } /* Name.Variable.Global */
+.source_code .vi { color: #40ffff } /* Name.Variable.Instance */
+.source_code .vm { color: #40ffff } /* Name.Variable.Magic */
+.source_code .il { color: #51b2fd } /* Literal.Number.Integer.Long */
+
+/* Dark mode - my overrides, because the defaults are too bright. */
+
+.source_code .gh { color: rgb(189, 193, 198); }
+.source_code .gu { color: rgb(189, 193, 198); }
+}
+
diff --git a/utils.py b/utils.py
index 9c2c45a..9fb6544 100644
--- a/utils.py
+++ b/utils.py
@@ -9,6 +9,14 @@ try:
     from pygments import highlight  # type: ignore
     from pygments import lexers  # type: ignore
     from pygments.formatters import HtmlFormatter  # type: ignore
+
+    _html_formatter = HtmlFormatter(
+        encoding="utf-8",
+        cssclass="source_code",
+        linenos="table",
+        anchorlinenos=True,
+        lineanchors="line",
+    )
 except ImportError:
     pygments = None
 
@@ -19,6 +27,7 @@ except ImportError:
     markdown = None
 
 import base64
+import functools
 import mimetypes
 import string
 import os.path
@@ -32,6 +41,7 @@ def shorten(s: str, width=60):
     return s[:57] + "..."
 
 
+@functools.lru_cache
 def can_colorize(s: str):
     """True if we can colorize the string, False otherwise."""
     if pygments is None:
@@ -77,6 +87,7 @@ def can_embed_image(repo, fname):
     )
 
 
+@functools.lru_cache
 def colorize_diff(s: str) -> str:
     lexer = lexers.DiffLexer(encoding="utf-8")
     formatter = HtmlFormatter(encoding="utf-8", cssclass="source_code")
@@ -84,6 +95,7 @@ def colorize_diff(s: str) -> str:
     return highlight(s, lexer, formatter)
 
 
+@functools.lru_cache
 def colorize_blob(fname, s: str) -> str:
     try:
         lexer = lexers.guess_lexer_for_filename(fname, s, encoding="utf-8")
@@ -98,24 +110,7 @@ def colorize_blob(fname, s: str) -> str:
             except lexers.ClassNotFound:
                 pass
 
-    formatter = HtmlFormatter(
-        encoding="utf-8",
-        cssclass="source_code",
-        linenos="table",
-        anchorlinenos=True,
-        lineanchors="line",
-    )
-
-    return highlight(s, lexer, formatter)
-
-
-def markdown_blob(s: str) -> str:
-    extensions = [
-        "markdown.extensions.fenced_code",
-        "markdown.extensions.tables",
-        RewriteLocalLinksExtension(),
-    ]
-    return markdown.markdown(s, extensions=extensions)
+    return highlight(s, lexer, _html_formatter)
 
 
 def embed_image_blob(fname: str, image_data: bytes) -> str:
@@ -126,11 +121,13 @@ def embed_image_blob(fname: str, image_data: bytes) -> str:
     )
 
 
+@functools.lru_cache
 def is_binary(b: bytes):
     # Git considers a blob binary if NUL in first ~8KB, so do the same.
     return b"\0" in b[:8192]
 
 
+@functools.lru_cache
 def hexdump(s: bytes):
     graph = string.ascii_letters + string.digits + string.punctuation + " "
     b = s.decode("latin1")
@@ -177,7 +174,23 @@ if markdown:
             tag.set("href", new_target)
 
     class RewriteLocalLinksExtension(markdown.Extension):
-        def extendMarkdown(self, md, md_globals):
-            md.treeprocessors.add(
-                "RewriteLocalLinks", RewriteLocalLinks(), "_end"
+        def extendMarkdown(self, md):
+            md.treeprocessors.register(
+                RewriteLocalLinks(), "RewriteLocalLinks", 1000
             )
+
+    _md_extensions = [
+        "markdown.extensions.fenced_code",
+        "markdown.extensions.tables",
+        RewriteLocalLinksExtension(),
+    ]
+
+    @functools.lru_cache
+    def markdown_blob(s: str) -> str:
+        return markdown.markdown(s, extensions=_md_extensions)
+
+else:
+
+    @functools.lru_cache
+    def markdown_blob(s: str) -> str:
+        raise RuntimeError("markdown_blob() called without markdown support")
diff --git a/views/blob.html b/views/blob.html
index f1c6ad0..67ac11c 100644
--- a/views/blob.html
+++ b/views/blob.html
@@ -77,7 +77,9 @@
 {{!markdown_blob(blob.utf8_content)}}
 </div>
 % elif can_colorize(blob.utf8_content):
+<div class="colorized-src">
 {{!colorize_blob(fname.raw, blob.utf8_content)}}
+</div>
 % else:
 <pre class="blob-body">
 {{blob.utf8_content}}
diff --git a/views/commit.html b/views/commit.html
index e21ff04..f753377 100644
--- a/views/commit.html
+++ b/views/commit.html
@@ -56,7 +56,9 @@
 <hr/>
 
 % if can_colorize(c.diff.body):
+<div class="colorized-src">
 {{!colorize_diff(c.diff.body)}}
+</div>
 % else:
 <pre class="diff-body">
 {{c.diff.body}}
diff --git a/views/index.html b/views/index.html
index a1d7aa4..91517db 100644
--- a/views/index.html
+++ b/views/index.html
@@ -20,7 +20,8 @@
     % for repo in sorted(repos.values(), key = lambda r: r.name):
     <tr>
         <td class="name"><a href="r/{{repo.name}}/">{{repo.name}}</a></td>
-        <td class="desc"><a href="r/{{repo.name}}/">{{repo.info.desc}}</a></td>
+        <td class="desc"><a href="r/{{repo.name}}/" title="{{repo.info.desc}}">
+            {{repo.info.desc}}</a></td>
         <td><span class="age">{{repo.last_commit_timestamp()}}</span></td>
     </tr>
     %end
diff --git a/views/summary.html b/views/summary.html
index d3388e6..9b180b8 100644
--- a/views/summary.html
+++ b/views/summary.html
@@ -35,20 +35,22 @@
 <hr/>
 % end
 
-% if "master" in repo.branch_names():
+% if repo.main_branch():
 <div class="toggable-title" onclick="toggle('commits')">
-  <a href="b/master/">commits (master)</a>
+  <a href="b/{{repo.main_branch()}}/">commits ({{repo.main_branch()}})</a>
 </div>
-%     kwargs = dict(repo = repo, start_ref = "refs/heads/master",
+%     kwargs = dict(repo = repo,
+%                   start_ref = "refs/heads/" + repo.main_branch(),
 %                   limit = repo.info.commits_in_summary,
 %                   shorten = shorten, repo_root = ".", offset = 0)
 %     include commit-list **kwargs
 <hr/>
 <div class="toggable-title" onclick="toggle('ls')">
-  <a href="b/master/t/">tree (master)</a>
+  <a href="b/{{repo.main_branch()}}/t/">tree ({{repo.main_branch()}})</a>
 </div>
-%     kwargs = dict(repo = repo, tree=repo.tree("master"),
-%                   treeroot="b/master/t", dirname=smstr.from_url(""))
+%     kwargs = dict(repo = repo, tree=repo.tree(repo.main_branch()),
+%                   treeroot="b/" + repo.main_branch() + "/t",
+%                   dirname=smstr.from_url(""))
 %     include tree-list **kwargs
 <hr/>
 % end
