diff --git a/_includes/head.html b/_includes/head.html
index 18f6f2c..e18be61 100644
--- a/_includes/head.html
+++ b/_includes/head.html
@@ -5,7 +5,29 @@
{% if page.title %}{{ page.title | escape }}{% else %}{{ site.title | escape }}{% endif %}
-
+
+
+
+
+
+
diff --git a/_includes/header.html b/_includes/header.html
new file mode 100644
index 0000000..c23172a
--- /dev/null
+++ b/_includes/header.html
@@ -0,0 +1,35 @@
+
diff --git a/_sass/_syntax-dark.scss b/_sass/_syntax-dark.scss
new file mode 100644
index 0000000..05c078f
--- /dev/null
+++ b/_sass/_syntax-dark.scss
@@ -0,0 +1,120 @@
+// Dark syntax colors: output of `bundle exec rougify style github.dark --scope .highlight` (rouge 4.7.0).
+// Included from dark-theme.scss inside the dark-theme mixin.
+@mixin syntax-dark {
+.highlight table td { padding: 5px; }
+.highlight table pre { margin: 0; }
+.highlight, .highlight .w {
+ color: #c9d1d9;
+ background-color: #161b22;
+}
+.highlight .k, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt, .highlight .kv {
+ color: #ff7b72;
+}
+.highlight .gr {
+ color: #f0f6fc;
+}
+.highlight .gd {
+ color: #ffdcd7;
+ background-color: #67060c;
+}
+.highlight .nb {
+ color: #ffa657;
+}
+.highlight .nc {
+ color: #ffa657;
+}
+.highlight .no {
+ color: #ffa657;
+}
+.highlight .nn {
+ color: #ffa657;
+}
+.highlight .sr {
+ color: #7ee787;
+}
+.highlight .na {
+ color: #7ee787;
+}
+.highlight .nt {
+ color: #7ee787;
+}
+.highlight .gi {
+ color: #aff5b4;
+ background-color: #033a16;
+}
+.highlight .ges {
+ font-weight: bold;
+ font-style: italic;
+}
+.highlight .kc {
+ color: #79c0ff;
+}
+.highlight .l, .highlight .ld, .highlight .m, .highlight .mb, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .il, .highlight .mo, .highlight .mx {
+ color: #79c0ff;
+}
+.highlight .sb {
+ color: #79c0ff;
+}
+.highlight .bp {
+ color: #79c0ff;
+}
+.highlight .ne {
+ color: #79c0ff;
+}
+.highlight .nl {
+ color: #79c0ff;
+}
+.highlight .py {
+ color: #79c0ff;
+}
+.highlight .nv, .highlight .vc, .highlight .vg, .highlight .vi, .highlight .vm {
+ color: #79c0ff;
+}
+.highlight .o, .highlight .ow {
+ color: #79c0ff;
+}
+.highlight .gh {
+ color: #1f6feb;
+ font-weight: bold;
+}
+.highlight .gu {
+ color: #1f6feb;
+ font-weight: bold;
+}
+.highlight .s, .highlight .sa, .highlight .sc, .highlight .dl, .highlight .sd, .highlight .s2, .highlight .se, .highlight .sh, .highlight .sx, .highlight .s1, .highlight .ss {
+ color: #a5d6ff;
+}
+.highlight .nd {
+ color: #d2a8ff;
+}
+.highlight .nf, .highlight .fm {
+ color: #d2a8ff;
+}
+.highlight .err {
+ color: #f0f6fc;
+ background-color: #8e1519;
+}
+.highlight .c, .highlight .ch, .highlight .cd, .highlight .cm, .highlight .cp, .highlight .cpf, .highlight .c1, .highlight .cs {
+ color: #8b949e;
+}
+.highlight .gl {
+ color: #8b949e;
+}
+.highlight .gt {
+ color: #8b949e;
+}
+.highlight .ni {
+ color: #c9d1d9;
+}
+.highlight .si {
+ color: #c9d1d9;
+}
+.highlight .ge {
+ color: #c9d1d9;
+ font-style: italic;
+}
+.highlight .gs {
+ color: #c9d1d9;
+ font-weight: bold;
+}
+}
\ No newline at end of file
diff --git a/_sass/dark-theme.scss b/_sass/dark-theme.scss
new file mode 100644
index 0000000..38bfdd6
--- /dev/null
+++ b/_sass/dark-theme.scss
@@ -0,0 +1,205 @@
+// Dark theme (issue #117).
+//
+// Minima 2.5 compiles its colors into the CSS and runs lighten()/darken() on
+// them, so its Sass variables can't be swapped for CSS custom properties.
+// Dark mode is therefore a set of override rules, scoped so the light CSS is
+// left exactly as minima emits it.
+//
+// Which theme applies is decided by `data-theme` on , set before first
+// paint by the script in _includes/head.html and changed by assets/js/theme.js.
+// Without JavaScript there is no `data-theme`, and the media query at the
+// bottom follows the OS setting instead.
+
+@import "syntax-dark";
+
+// Every foreground/background pair below meets WCAG AA (4.5:1) or better.
+$dark-background-color: #1b1b1d; // also the theme-color meta value in dark
+$dark-text-color: #e3e3e3; // 13.4:1
+$dark-brand-color: #6ea8fe; // 7.1:1
+$dark-visited-color: #8fb0e0;
+$dark-grey-color: #a0a0a0; // 6.6:1, post meta and footer
+$dark-grey-color-light: #3a3a3d; // borders
+$dark-grey-color-dark: #c8c8c8; // site title, 10.3:1
+$dark-code-background: #161b22; // matches rouge github.dark
+
+@mixin dark-theme {
+ color-scheme: dark;
+
+ body {
+ color: $dark-text-color;
+ background-color: $dark-background-color;
+ }
+
+ a {
+ color: $dark-brand-color;
+
+ &:visited {
+ color: $dark-visited-color;
+ }
+
+ &:hover {
+ color: $dark-text-color;
+ }
+ }
+
+ blockquote {
+ color: $dark-grey-color;
+ border-left-color: $dark-grey-color-light;
+ }
+
+ pre,
+ code {
+ border-color: $dark-grey-color-light;
+ background-color: $dark-code-background;
+ }
+
+ hr {
+ border-color: $dark-grey-color-light;
+ }
+
+ .svg-icon {
+ fill: $dark-grey-color;
+ }
+
+ table {
+ color: #d0d0d0;
+ border-color: $dark-grey-color-light;
+
+ tr:nth-child(even) {
+ background-color: #232326;
+ }
+
+ th {
+ background-color: #26262a;
+ border-color: #444448;
+ border-bottom-color: #55555a;
+ }
+
+ td {
+ border-color: $dark-grey-color-light;
+ }
+ }
+
+ .site-header {
+ border-top-color: $dark-grey-color-dark;
+ border-bottom-color: $dark-grey-color-light;
+ }
+
+ .site-title {
+ &,
+ &:visited {
+ color: $dark-grey-color-dark;
+ }
+ }
+
+ .site-nav {
+ .page-link,
+ .theme-toggle {
+ color: $dark-text-color;
+ }
+
+ @include media-query($on-palm) {
+ background-color: $dark-background-color;
+ border-color: $dark-grey-color-light;
+
+ .menu-icon > svg {
+ fill: $dark-grey-color-dark;
+ }
+ }
+ }
+
+ .site-footer {
+ border-top-color: $dark-grey-color-light;
+ }
+
+ .footer-col-wrapper,
+ .post-meta {
+ color: $dark-grey-color;
+ }
+
+ @include syntax-dark;
+
+ // github.dark leaves these two unset, which would keep minima's light
+ // colors (#888, #555) on the dark code background.
+ .highlight .go {
+ color: #8b949e;
+ }
+
+ .highlight .gp {
+ color: #c9d1d9;
+ }
+}
+
+html[data-theme="dark"] {
+ @include dark-theme;
+}
+
+// Keeps native controls and scrollbars light when a visitor picks Light on a
+// dark OS; the color-scheme meta would otherwise let the browser follow the OS.
+html[data-theme="light"] {
+ color-scheme: light;
+}
+
+// No JavaScript: no data-theme attribute, so follow the OS setting.
+@media (prefers-color-scheme: dark) {
+ html:not([data-theme]) {
+ @include dark-theme;
+ }
+}
+
+/**
+ * Theme toggle button (in _includes/header.html)
+ */
+.theme-toggle {
+ padding: 0;
+ border: 0;
+ background: none;
+ color: $text-color;
+ font: inherit;
+ line-height: $base-line-height;
+ cursor: pointer;
+ vertical-align: middle;
+
+ // The class rule would otherwise beat the UA's [hidden] { display: none }.
+ &[hidden] {
+ display: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid currentColor;
+ outline-offset: 2px;
+ border-radius: 3px;
+ }
+
+ @include media-query($on-palm) {
+ margin-left: 20px;
+ padding: 5px 10px;
+ }
+}
+
+.theme-icon {
+ display: none;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+ vertical-align: middle;
+}
+
+.theme-icon-fill {
+ fill: currentColor;
+}
+
+html[data-theme-pref="light"] .theme-icon-light,
+html[data-theme-pref="dark"] .theme-icon-dark,
+html[data-theme-pref="system"] .theme-icon-system {
+ display: inline-block;
+}
+
+// Colors fade only while theme.js has this class on (just after a
+// click), so page loads and navigation never animate.
+html.theme-transition,
+html.theme-transition * {
+ transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease !important;
+}
diff --git a/assets/js/theme.js b/assets/js/theme.js
new file mode 100644
index 0000000..5ca46cb
--- /dev/null
+++ b/assets/js/theme.js
@@ -0,0 +1,72 @@
+// Theme toggle (issue #117). The inline script in _includes/head.html has
+// already set data-theme before paint; this file wires up the header button
+// and keeps the page in step with the OS setting and with other open tabs.
+(function () {
+ var STORAGE_KEY = "theme";
+ var ORDER = ["light", "dark", "system"];
+ var LABELS = { light: "Light", dark: "Dark", system: "System" };
+ // Browser toolbar color on mobile. Light keeps the value the site always had.
+ var THEME_COLORS = { light: "#424242", dark: "#1b1b1d" };
+
+ var root = document.documentElement;
+ var darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
+ var button = document.getElementById("theme-toggle");
+ var themeColorMeta = document.getElementById("theme-color-meta");
+
+ function readPref() {
+ var pref;
+ try { pref = localStorage.getItem(STORAGE_KEY); } catch (e) {}
+ return ORDER.indexOf(pref) === -1 ? "system" : pref;
+ }
+
+ function savePref(pref) {
+ try { localStorage.setItem(STORAGE_KEY, pref); } catch (e) {}
+ }
+
+ // Held in memory too, so the toggle still cycles when storage is blocked.
+ var currentPref = readPref();
+
+ function applyTheme(pref) {
+ currentPref = pref;
+ var theme = pref === "system" ? (darkQuery.matches ? "dark" : "light") : pref;
+ root.setAttribute("data-theme", theme);
+ root.setAttribute("data-theme-pref", pref);
+ if (themeColorMeta) themeColorMeta.setAttribute("content", THEME_COLORS[theme]);
+ if (button) {
+ var label = "Theme: " + LABELS[pref];
+ button.setAttribute("aria-label", label);
+ button.setAttribute("title", label);
+ }
+ }
+
+ // Fade colors for a click only; see html.theme-transition in dark-theme.scss.
+ var transitionTimer;
+ function withTransition() {
+ root.classList.add("theme-transition");
+ clearTimeout(transitionTimer);
+ transitionTimer = setTimeout(function () {
+ root.classList.remove("theme-transition");
+ }, 300);
+ }
+
+ if (button) {
+ button.addEventListener("click", function () {
+ var next = ORDER[(ORDER.indexOf(currentPref) + 1) % ORDER.length];
+ savePref(next);
+ withTransition();
+ applyTheme(next);
+ });
+ button.hidden = false;
+ }
+
+ darkQuery.addEventListener("change", function () {
+ if (currentPref === "system") applyTheme("system");
+ });
+
+ // Another tab changed the preference.
+ window.addEventListener("storage", function (event) {
+ if (event.key === STORAGE_KEY) applyTheme(readPref());
+ });
+
+ applyTheme(currentPref);
+})();
diff --git a/assets/main.scss b/assets/main.scss
index 12f3a0b..a1c7e08 100644
--- a/assets/main.scss
+++ b/assets/main.scss
@@ -3,6 +3,7 @@
---
@import "minima";
+@import "dark-theme";
// The About page portrait. `cover` keeps the face centred whatever aspect
// ratio the source photo has, so the circle never squashes it.