Add dark theme
authorTheSaminator <TheSaminator@users.noreply.github.com>
Sun, 22 May 2022 18:13:34 +0000 (14:13 -0400)
committerTheSaminator <TheSaminator@users.noreply.github.com>
Sun, 22 May 2022 18:13:34 +0000 (14:13 -0400)
12 files changed:
src/jvmMain/kotlin/starshipfights/auth/providers.kt
src/jvmMain/kotlin/starshipfights/auth/utils.kt
src/jvmMain/kotlin/starshipfights/data/auth/user_sessions.kt
src/jvmMain/kotlin/starshipfights/info/view_tpl.kt
src/jvmMain/kotlin/starshipfights/info/views_gdpr.kt
src/jvmMain/kotlin/starshipfights/info/views_user.kt
src/jvmMain/resources/static/images/background-dark.jpg [new file with mode: 0644]
src/jvmMain/resources/static/images/external-link-dark.svg [new file with mode: 0644]
src/jvmMain/resources/static/images/external-link.svg
src/jvmMain/resources/static/images/panel-dark.svg [new file with mode: 0644]
src/jvmMain/resources/static/init.js
src/jvmMain/resources/static/style.css

index 668f23a67eed17343438d8d93638ee1692969c4a..94c935f0068ff1a745e19e81e49c0684ae27327b 100644 (file)
@@ -25,6 +25,7 @@ import starshipfights.CurrentConfiguration
 import starshipfights.DiscordLogin
 import starshipfights.data.Id
 import starshipfights.data.admiralty.*
+import starshipfights.data.auth.PreferredTheme
 import starshipfights.data.auth.User
 import starshipfights.data.auth.UserSession
 import starshipfights.data.createNonce
@@ -112,7 +113,8 @@ interface AuthProvider {
                                                showUserStatus = form["showstatus"] == "yes",
                                                logIpAddresses = form["logaddress"] == "yes",
                                                profileName = form["name"]?.takeIf { it.isNotBlank() && it.length <= PROFILE_NAME_MAX_LENGTH } ?: redirect("/me/manage" + withErrorMessage("Invalid name - must not be blank, must be at most $PROFILE_NAME_MAX_LENGTH characters")),
-                                               profileBio = form["bio"]?.takeIf { it.isNotBlank() && it.length <= PROFILE_BIO_MAX_LENGTH } ?: redirect("/me/manage" + withErrorMessage("Invalid bio - must not be blank, must be at most $PROFILE_BIO_MAX_LENGTH characters"))
+                                               profileBio = form["bio"]?.takeIf { it.isNotBlank() && it.length <= PROFILE_BIO_MAX_LENGTH } ?: redirect("/me/manage" + withErrorMessage("Invalid bio - must not be blank, must be at most $PROFILE_BIO_MAX_LENGTH characters")),
+                                               preferredTheme = form["theme"]?.uppercase()?.takeIf { it in PreferredTheme.values().map { it.name } }?.let { PreferredTheme.valueOf(it) } ?: currentUser.preferredTheme
                                        )
                                        User.put(newUser)
                                        
@@ -445,7 +447,7 @@ object TestAuthProvider : AuthProvider {
                                if (call.getUserSession() != null)
                                        redirect("/me")
                                
-                               call.respondHtml(HttpStatusCode.OK, page("Authentication Test", call.standardNavBar(), CustomSidebar {
+                               call.respondHtml(HttpStatusCode.OK, call.page("Authentication Test", call.standardNavBar(), CustomSidebar {
                                        p {
                                                +"This instance does not have Discord OAuth login set up. As a fallback, this authentication mode is used for testing only."
                                        }
@@ -529,7 +531,7 @@ class ProductionAuthProvider(private val discordLogin: DiscordLogin) : AuthProvi
        override fun installRouting(conf: Routing) {
                with(conf) {
                        get("/login") {
-                               call.respondHtml(HttpStatusCode.OK, page("Login with Discord", call.standardNavBar()) {
+                               call.respondHtml(HttpStatusCode.OK, call.page("Login with Discord", call.standardNavBar()) {
                                        section {
                                                p {
                                                        style = "text-align:center"
index fb7b2401365d2f33b150e34124bcc1babc03284b..8f18d426460db2cbf8bfd8e9990f5e9e882d58b6 100644 (file)
@@ -36,11 +36,18 @@ suspend fun User.updated() = copy(
        lastActivity = Instant.now()
 ).also { User.put(it) }
 
-suspend fun ApplicationCall.getUserSession() = request.userAgent()?.let { sessions.get<Id<UserSession>>()?.resolve(it) }?.renewed(request.origin.remoteHost)
+val UserAndSessionAttribute = AttributeKey<Pair<UserSession?, User?>>("SfUserAndSession")
 
-suspend fun ApplicationCall.getUser() = getUserSession()?.user?.let { User.get(it) }?.updated()
+suspend fun ApplicationCall.getUserSession() = getUserAndSession().first
 
-suspend fun ApplicationCall.getUserAndSession() = getUserSession()?.let { it to User.get(it.user)?.updated() } ?: (null to null)
+suspend fun ApplicationCall.getUser() = getUserAndSession().second
+
+suspend fun ApplicationCall.getUserAndSession() = request.pipeline.attributes.getOrNull(UserAndSessionAttribute)
+       ?: request.userAgent()?.let { sessions.get<Id<UserSession>>()?.resolve(it) }
+               ?.renewed(request.origin.remoteHost)
+               ?.let { it to User.get(it.user)?.updated() }
+               ?.also { request.pipeline.attributes.put(UserAndSessionAttribute, it) }
+       ?: (null to null)
 
 object UserSessionIdSerializer : SessionSerializer<Id<UserSession>> {
        override fun serialize(session: Id<UserSession>): String {
index 7a21fc0e59d118e44031d6cb376d056e4262d722..c602df31deed0d9e15e961201d94e3d8c427a64e 100644 (file)
@@ -24,6 +24,8 @@ data class User(
        val profileName: String,
        val profileBio: String,
        
+       val preferredTheme: PreferredTheme = PreferredTheme.SYSTEM,
+       
        val registeredAt: @Contextual Instant,
        val lastActivity: @Contextual Instant,
        val showUserStatus: Boolean,
@@ -46,6 +48,10 @@ data class User(
        })
 }
 
+enum class PreferredTheme {
+       SYSTEM, LIGHT, DARK;
+}
+
 enum class UserStatus {
        AVAILABLE, IN_MATCHMAKING, READY_FOR_BATTLE, IN_BATTLE
 }
index 1df1584b36bbfa257d206e31994b8d0da503e94c..d46317df0e1a4bb0ed042bf9419b9b9f64c317fa 100644 (file)
@@ -1,68 +1,81 @@
 package starshipfights.info
 
+import io.ktor.application.*
 import kotlinx.html.*
+import starshipfights.auth.getUser
+import starshipfights.data.auth.PreferredTheme
 
-fun page(pageTitle: String? = null, navBar: List<NavItem>? = null, sidebar: Sidebar? = null, content: SECTIONS.() -> Unit): HTML.() -> Unit = {
-       head {
-               meta(charset = "utf-8")
-               meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
+suspend fun ApplicationCall.page(pageTitle: String? = null, navBar: List<NavItem>? = null, sidebar: Sidebar? = null, content: SECTIONS.() -> Unit): HTML.() -> Unit {
+       val theme = getUser()?.preferredTheme
+       
+       return {
+               when (theme) {
+                       PreferredTheme.LIGHT -> "light"
+                       PreferredTheme.DARK -> "dark"
+                       else -> null
+               }?.let { attributes["data-theme"] = it }
                
-               link(rel = "icon", type = "image/svg+xml", href = "/static/images/icon.svg")
-               link(rel = "preconnect", href = "https://fonts.googleapis.com")
-               link(rel = "preconnect", href = "https://fonts.gstatic.com") { attributes["crossorigin"] = "anonymous" }
-               link(rel = "stylesheet", href = "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Jetbrains+Mono:wght@400;600;800&display=swap")
-               link(rel = "stylesheet", href = "/static/style.css")
-               
-               title {
-                       +"Starship Fights"
-                       pageTitle?.let { +" | $it" }
+               head {
+                       meta(charset = "utf-8")
+                       meta(name = "viewport", content = "width=device-width, initial-scale=1.0")
+                       
+                       link(rel = "icon", type = "image/svg+xml", href = "/static/images/icon.svg")
+                       link(rel = "preconnect", href = "https://fonts.googleapis.com")
+                       link(rel = "preconnect", href = "https://fonts.gstatic.com") { attributes["crossorigin"] = "anonymous" }
+                       link(rel = "stylesheet", href = "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&family=Jetbrains+Mono:wght@400;600;800&display=swap")
+                       link(rel = "stylesheet", href = "/static/style.css")
+                       
+                       title {
+                               +"Starship Fights"
+                               pageTitle?.let { +" | $it" }
+                       }
                }
-       }
-       body {
-               div { id = "bg" }
-               
-               navBar?.let { nb ->
-                       nav(classes = "desktop") {
-                               div(classes = "list") {
-                                       for (ni in nb) {
-                                               div(classes = "item") {
-                                                       ni.displayIn(this)
+               body {
+                       div { id = "bg" }
+                       
+                       navBar?.let { nb ->
+                               nav(classes = "desktop") {
+                                       div(classes = "list") {
+                                               for (ni in nb) {
+                                                       div(classes = "item") {
+                                                               ni.displayIn(this)
+                                                       }
                                                }
                                        }
                                }
                        }
-               }
-               
-               sidebar?.let {
-                       aside(classes = "desktop") {
-                               it.displayIn(this)
-                       }
-               }
-               
-               main {
+                       
                        sidebar?.let {
-                               aside(classes = "mobile") {
+                               aside(classes = "desktop") {
                                        it.displayIn(this)
                                }
                        }
                        
-                       with(sectioned()) {
-                               content()
-                       }
-                       
-                       navBar?.let { nb ->
-                               nav(classes = "mobile") {
-                                       div(classes = "list") {
-                                               for (ni in nb) {
-                                                       div(classes = "item") {
-                                                               ni.displayIn(this)
+                       main {
+                               sidebar?.let {
+                                       aside(classes = "mobile") {
+                                               it.displayIn(this)
+                                       }
+                               }
+                               
+                               with(sectioned()) {
+                                       content()
+                               }
+                               
+                               navBar?.let { nb ->
+                                       nav(classes = "mobile") {
+                                               div(classes = "list") {
+                                                       for (ni in nb) {
+                                                               div(classes = "item") {
+                                                                       ni.displayIn(this)
+                                                               }
                                                        }
                                                }
                                        }
                                }
                        }
+                       
+                       script(src = "/static/init.js") {}
                }
-               
-               script(src = "/static/init.js") {}
        }
 }
index 1e8598f988872275133132588adca908486b9732..323ab7eb3f5a0ae6ced941da03b33a2b49401b0c 100644 (file)
@@ -76,6 +76,7 @@ suspend fun ApplicationCall.privateInfo(): String {
                appendLine("Profile bio: \"\"\"")
                appendLine(user.profileBio)
                appendLine("\"\"\"")
+               appendLine("Display theme: ${user.preferredTheme}")
                appendLine("")
                appendLine("## Activity data")
                appendLine("Registered at: ${user.registeredAt}")
index 7f1bed64a2760fba1c4c2a125203cb364ee3947c..4a795503281cc735ae24d7872f8056534e39187e 100644 (file)
@@ -175,6 +175,41 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit {
                                        
                                        +currentUser.profileBio
                                }
+                               h3 {
+                                       +"Display Theme"
+                               }
+                               label {
+                                       radioInput(name = "theme") {
+                                               id = "system-theme"
+                                               value = "system"
+                                               required = true
+                                               checked = currentUser.preferredTheme == PreferredTheme.SYSTEM
+                                       }
+                                       +Entities.nbsp
+                                       +"System Choice"
+                               }
+                               br
+                               label {
+                                       radioInput(name = "theme") {
+                                               id = "light-theme"
+                                               value = "light"
+                                               required = true
+                                               checked = currentUser.preferredTheme == PreferredTheme.LIGHT
+                                       }
+                                       +Entities.nbsp
+                                       +"Light Theme"
+                               }
+                               br
+                               label {
+                                       radioInput(name = "theme") {
+                                               id = "dark-theme"
+                                               value = "dark"
+                                               required = true
+                                               checked = currentUser.preferredTheme == PreferredTheme.DARK
+                                       }
+                                       +Entities.nbsp
+                                       +"Dark Theme"
+                               }
                                h3 {
                                        +"Privacy Settings"
                                }
@@ -222,6 +257,9 @@ suspend fun ApplicationCall.manageUserPage(): HTML.() -> Unit {
                                        value = "Accept Changes"
                                }
                        }
+                       script {
+                               unsafe { +"window.sfThemeChoice = true;" }
+                       }
                }
                section {
                        h2 { +"Logged-In Sessions" }
diff --git a/src/jvmMain/resources/static/images/background-dark.jpg b/src/jvmMain/resources/static/images/background-dark.jpg
new file mode 100644 (file)
index 0000000..fffc212
Binary files /dev/null and b/src/jvmMain/resources/static/images/background-dark.jpg differ
diff --git a/src/jvmMain/resources/static/images/external-link-dark.svg b/src/jvmMain/resources/static/images/external-link-dark.svg
new file mode 100644 (file)
index 0000000..32461b5
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+               xmlns="http://www.w3.org/2000/svg"
+               version="1.1"
+               viewBox="1 0 12 12"
+               height="16"
+               width="16">
+       <path
+                       style="fill:#6699ff;fill-opacity:1"
+                       id="path12"
+                       d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"/>
+       <path
+                       style="fill:#5588ee;fill-opacity:1"
+                       id="path10"
+                       d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"/>
+       <path
+                       style="fill:#4477dd;fill-opacity:1"
+                       id="path14"
+                       d="M4.082 5.51c0-.621.621-.621.621-.621 1.864.621 3.107 1.864 3.728 3.728 0 0 0 .621-.62.621-1.245-1.864-1.866-2.485-3.73-3.728z"/>
+</svg>
index e56fae71d66ded822bf51fb8504c65ccd9862eb3..1d8102fe69be21be9bc7dea1ae1dc05f0a6ed948 100644 (file)
@@ -6,11 +6,11 @@
                height="16"
                width="16">
        <path
-                       style="fill:#2255bb;fill-opacity:1"
+                       style="fill:#003399;fill-opacity:1"
                        id="path12"
                        d="M7.002 3.01h-5v8h8v-5h-1v4h-6v-6h4z"/>
        <path
-                       style="fill:#4477dd;fill-opacity:1"
+                       style="fill:#3366cc;fill-opacity:1"
                        id="path10"
                        d="M5.002 1.01h7v7l-2-2-3 2v-1l3-2.25 1 1V2.01h-3.75l1 1-2.25 3h-1l2-3z"/>
        <path
diff --git a/src/jvmMain/resources/static/images/panel-dark.svg b/src/jvmMain/resources/static/images/panel-dark.svg
new file mode 100644 (file)
index 0000000..7ac4c0f
--- /dev/null
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+               xmlns="http://www.w3.org/2000/svg"
+               version="1.1"
+               viewBox="0 0 64 64"
+               height="64px"
+               width="64px">
+       <path
+                       d="M 24,8 A 16,16 0 0 1 8,24 v 16 A 16,16 0 0 1 24,56 H 40 A 16,16 0 0 1 56,40 V 24 A 16,16 0 0 1 40,8 Z"
+                       style="fill:#333333;opacity:0.9;stroke:none"/>
+       <path
+                       d="M 20 4 L 20 8 C 20 14.67 14.67 20 8 20 L 4 20 L 4 44 L 8 44 C 14.67 44 20 49.33 20 56 L 20 60 L 44 60 L 44 56 C 44 49.33 49.33 44 56 44 L 60 44 L 60 20 L 56 20 C 49.33 20 44 14.67 44 8 L 44 4 L 20 4 z M 24 8 L 40 8 A 16 16 0 0 0 56 24 L 56 40 A 16 16 0 0 0 40 56 L 24 56 A 16 16 0 0 0 8 40 L 8 24 A 16 16 0 0 0 24 8 z"
+                       style="fill:#99ccff;opacity:1;fill-rule:evenodd;stroke:none"/>
+</svg>
index 2a39b4713b5e89cf1942dea1e2412b0edca5cbea..c926b6e5c45a403e39d1de4d4f89363b4f560f3f 100644 (file)
                if (!window.sfFactionSelect) return;
 
                const factionInputs = document.getElementsByName("faction");
+               const sexInputs = document.getElementsByName("sex");
                for (const factionInput of factionInputs) {
                        factionInput.addEventListener("click", () => {
-                               const sexInputs = document.getElementsByName("sex");
                                if (factionInput.hasAttribute("data-force-gender")) {
                                        const forceGender = factionInput.getAttribute("data-force-gender");
                                        for (const sexInput of sexInputs) {
                        };
                }
        });
+
+       window.addEventListener("load", function () {
+               // Generate random admiral names
+               if (!window.sfThemeChoice) return;
+
+               const themeChoices = document.getElementsByName("theme");
+               for (let themeChoice of themeChoices) {
+                       const theme = themeChoice.value;
+                       themeChoice.addEventListener("click", () => {
+                               document.documentElement.setAttribute("data-theme", theme);
+                       });
+               }
+       });
 })();
index c317111816cf2e2580e31ed7fd9583493beebe92..7c8ae885e742399e802ccc45af644e54a61639f1 100644 (file)
@@ -10,11 +10,256 @@ html {
        --h1-size: 1.6em;
        --h2-size: 1.4em;
        --h3-size: 1.2em;
+
+       /***************
+       * color params *
+       ***************/
+
+       --selection-fg: #eee;
+       --selection-bg: rgba(51, 102, 204, 0.6);
+
+       --h1-border: #888;
+       --h1-shadow: #444;
+       --h1-backgr: #aaa;
+
+       --h2-border: #666;
+
+       --h3-unline: #888;
+
+       --list-a-fg: #369;
+       --list-a-h-fg: #fff;
+       --list-a-h-bg: #69c;
+
+       --a-fg: #36c;
+       --a-v-fg: #63c;
+
+       --tbl-border: #036;
+       --tbl-backgr: #eee;
+
+       --input-bg: #bbb;
+       --input-ul: #222;
+       --input-fg: #024;
+       --input-f-bg: #777;
+
+       --err-bg: #e77;
+       --err-fg: #422;
+       --err-ul: #211;
+
+       --btn-fg: #bdf;
+       --btn-bg: #06c;
+       --btn-h-bg: #05a;
+       --btn-a-bg: #048;
+
+       --evil-btn-fg: #fcc;
+       --evil-btn-bg: #c33;
+       --evil-btn-h-bg: #a22;
+       --evil-btn-a-bg: #811;
+
+       --btn-na-bg: #444;
+       --btn-na-fg: #bbb;
+
+       --iframe-border: #036;
+
+       /*************
+       * url params *
+       *************/
+
+       --panel: url("/static/images/panel.svg");
+       --bgimg: url("/static/images/background.jpg");
+       --extln: url("/static/images/external-link.svg");
+}
+
+html[data-theme="dark"] {
+       color: #ddd;
+       background-color: #555;
+
+       /***************
+       * color params *
+       ***************/
+
+       --selection-fg: #111;
+       --selection-bg: rgba(102, 153, 255, 0.9);
+
+       --h1-border: #333;
+       --h1-shadow: #bbb;
+       --h1-backgr: #111;
+
+       --h2-border: #999;
+
+       --h3-unline: #777;
+
+       --list-a-fg: #9cf;
+       --list-a-h-fg: #333;
+       --list-a-h-bg: #9cf;
+
+       --a-fg: #69f;
+       --a-v-fg: #96f;
+
+       --tbl-border: #9cf;
+       --tbl-backgr: #111;
+
+       --input-bg: #444;
+       --input-ul: #ddd;
+       --input-fg: #bdf;
+       --input-f-bg: #888;
+
+       --err-bg: #844;
+       --err-fg: #fcc;
+       --err-ul: #422;
+
+       --btn-fg: #024;
+       --btn-bg: #39f;
+       --btn-h-bg: #7bf;
+       --btn-a-bg: #bdf;
+
+       --evil-btn-fg: #411;
+       --evil-btn-bg: #d33;
+       --evil-btn-h-bg: #e66;
+       --evil-btn-a-bg: #f99;
+
+       --btn-na-bg: #bbb;
+       --btn-na-fg: #444;
+
+       --iframe-border: #9cf;
+
+       /*************
+       * url params *
+       *************/
+
+       --panel: url("/static/images/panel-dark.svg");
+       --bgimg: url("/static/images/background-dark.jpg");
+       --extln: url("/static/images/external-link-dark.svg");
+}
+
+@media only screen and (prefers-color-scheme: dark) {
+       html[data-theme="light"] {
+               color: #222;
+               background-color: #aaa;
+
+               /***************
+               * color params *
+               ***************/
+
+               --selection-fg: #eee;
+               --selection-bg: rgba(51, 102, 204, 0.6);
+
+               --h1-border: #888;
+               --h1-shadow: #444;
+               --h1-backgr: #aaa;
+
+               --h2-border: #666;
+
+               --h3-unline: #888;
+
+               --list-a-fg: #369;
+               --list-a-h-fg: #fff;
+               --list-a-h-bg: #69c;
+
+               --a-fg: #36c;
+               --a-v-fg: #63c;
+
+               --tbl-border: #036;
+               --tbl-backgr: #eee;
+
+               --input-bg: #bbb;
+               --input-ul: #222;
+               --input-fg: #024;
+               --input-f-bg: #777;
+
+               --err-bg: #e77;
+               --err-fg: #422;
+               --err-ul: #211;
+
+               --btn-fg: #bdf;
+               --btn-bg: #06c;
+               --btn-h-bg: #05a;
+               --btn-a-bg: #048;
+
+               --evil-btn-fg: #fcc;
+               --evil-btn-bg: #c33;
+               --evil-btn-h-bg: #a22;
+               --evil-btn-a-bg: #811;
+
+               --btn-na-bg: #444;
+               --btn-na-fg: #bbb;
+
+               --iframe-border: #036;
+
+               /*************
+               * url params *
+               *************/
+
+               --panel: url("/static/images/panel.svg");
+               --bgimg: url("/static/images/background.jpg");
+               --extln: url("/static/images/external-link.svg");
+       }
+
+       html {
+               color: #ddd;
+               background-color: #555;
+
+               /***************
+               * color params *
+               ***************/
+
+               --selection-fg: #111;
+               --selection-bg: rgba(102, 153, 255, 0.9);
+
+               --h1-border: #333;
+               --h1-shadow: #bbb;
+               --h1-backgr: #111;
+
+               --h2-border: #999;
+
+               --h3-unline: #777;
+
+               --list-a-fg: #9cf;
+               --list-a-h-fg: #333;
+               --list-a-h-bg: #9cf;
+
+               --a-fg: #69f;
+               --a-v-fg: #96f;
+
+               --tbl-border: #9cf;
+               --tbl-backgr: #111;
+
+               --input-bg: #444;
+               --input-ul: #ddd;
+               --input-fg: #bdf;
+               --input-f-bg: #888;
+
+               --err-bg: #844;
+               --err-fg: #fcc;
+               --err-ul: #422;
+
+               --btn-fg: #024;
+               --btn-bg: #39f;
+               --btn-h-bg: #5af;
+               --btn-a-bg: #7bf;
+
+               --evil-btn-fg: #411;
+               --evil-btn-bg: #d33;
+               --evil-btn-h-bg: #e66;
+               --evil-btn-a-bg: #f99;
+
+               --btn-na-bg: #bbb;
+               --btn-na-fg: #444;
+
+               --iframe-border: #9cf;
+
+               /*************
+               * url params *
+               *************/
+
+               --panel: url("/static/images/panel-dark.svg");
+               --bgimg: url("/static/images/background-dark.jpg");
+               --extln: url("/static/images/external-link-dark.svg");
+       }
 }
 
 ::selection {
-       background-color: rgb(51, 102, 204, 0.6);
-       color: #eee;
+       background-color: var(--selection-bg);
+       color: var(--selection-fg);
 }
 
 div#bg {
@@ -31,25 +276,25 @@ h1, h2 {
 }
 
 h1 {
-       border: 0.1875rem solid #888;
-       box-shadow: inset 0 0 0 0.25rem #444;
+       border: 0.1875rem solid var(--h1-border);
+       box-shadow: inset 0 0 0 0.25rem var(--h1-shadow);
        padding: 0.3125rem;
 
-       background-color: #aaa;
+       background-color: var(--h1-backgr);
        font-variant: small-caps;
        font-size: var(--h1-size);
        font-weight: 800;
 }
 
 h2 {
-       border-bottom: 0.125rem solid #666;
+       border-bottom: 0.125rem solid var(--h2-border);
        font-size: var(--h2-size);
        font-weight: 600;
 }
 
 h3 {
        text-decoration: underline;
-       text-decoration-color: #888;
+       text-decoration-color: var(--h3-unline);
        font-size: var(--h3-size);
        font-weight: 400;
 }
@@ -60,7 +305,7 @@ h3 {
 
 /*noinspection CssOverwrittenProperties*/
 main > section, main > nav.mobile, main > aside.mobile {
-       border-image-source: url("/static/images/panel.svg");
+       border-image-source: var(--panel);
        border-image-slice: 40% fill;
        border-image-width: 1em;
        border-width: 1em;
@@ -98,7 +343,7 @@ aside.mobile img {
                top: 0;
                left: 0;
 
-               background-image: url("/static/images/background.jpg");
+               background-image: var(--bgimg);
                background-attachment: fixed;
                background-position: center;
                background-size: cover;
@@ -112,7 +357,7 @@ aside.mobile img {
 
        /*noinspection CssOverwrittenProperties*/
        main > section, nav.desktop, aside.desktop {
-               border-image-source: url("/static/images/panel.svg");
+               border-image-source: var(--panel);
                border-image-slice: 40% fill;
                border-image-width: 2em;
                border-width: 2em;
@@ -210,26 +455,26 @@ div.list > div.item > a {
        text-align: center;
 
        border-radius: 0.3em;
-       color: #369;
+       color: var(--list-a-fg);
        text-decoration: none;
 }
 
 div.list > div.item > a:visited {
-       color: #369;
+       color: var(--list-a-fg);
 }
 
 div.list > div.item > a:hover {
-       color: #fff;
-       background-color: #69c;
+       color: var(--list-a-h-fg);
+       background-color: var(--list-a-h-bg);
 }
 
 a {
-       color: #36c;
+       color: var(--a-fg);
        text-decoration: none;
 }
 
 a:visited {
-       color: #63c;
+       color: var(--a-v-fg);
 }
 
 a:hover {
@@ -237,15 +482,14 @@ a:hover {
 }
 
 a[href^="http://"]::after, a[href^="https://"]::after {
-       content: url(/static/images/external-link.svg);
+       content: var(--extln);
        height: 1em;
 }
 
 table {
        table-layout: fixed;
        border-collapse: collapse;
-       border: 0.125rem solid #036;
-       background-color: #ccc;
+       border: 0.125rem solid var(--tbl-border);
 
        width: 100%;
 }
@@ -261,8 +505,8 @@ table + table tr:first-child th {
 }
 
 td {
-       border: 0.125rem solid #036;
-       background-color: #eee;
+       border: 0.125rem solid var(--tbl-border);
+       background-color: var(--tbl-backgr);
        font-size: 0.7em;
        padding: 0.15em 0;
 
@@ -271,8 +515,8 @@ td {
 }
 
 th {
-       border: 0.125rem solid #036;
-       background-color: #036;
+       border: 0.125rem solid var(--tbl-border);
+       background-color: var(--tbl-border);
        padding: 0.15em 0;
 
        text-align: center;
@@ -282,7 +526,7 @@ th {
        font-size: 0.8em;
        font-variant: small-caps;
        font-weight: 700;
-       color: #eee;
+       color: var(--tbl-backgr);
 }
 
 input[type=text],
@@ -290,11 +534,11 @@ input[type=password],
 input[type=email],
 textarea {
        box-sizing: border-box;
-       background-color: #aaa;
+       background-color: var(--input-bg);
        border: none;
-       border-bottom: 0.1em solid #222;
+       border-bottom: 0.1em solid var(--input-ul);
 
-       color: #024;
+       color: var(--input-fg);
        font-size: 1.5em;
 
        width: 100%;
@@ -310,23 +554,23 @@ input[type=password]:focus,
 input[type=email]:focus,
 textarea:focus {
        outline: none;
-       background-color: #888;
+       background-color: var(--input-f-bg);
 }
 
 input[type=text]:invalid,
 input[type=password]:invalid,
 input[type=email]:invalid,
 textarea:invalid {
-       color: #422;
-       background-color: #e77;
-       border-bottom-color: #211;
+       color: var(--err-fg);
+       background-color: var(--err-bg);
+       border-bottom-color: var(--err-ul);
 }
 
 input[type=submit] {
-       background-color: #06c;
+       background-color: var(--btn-bg);
        border: none;
        border-radius: 0.3em;
-       color: #bdf;
+       color: var(--btn-fg);
        cursor: pointer;
        display: block;
 
@@ -336,18 +580,18 @@ input[type=submit] {
 }
 
 input[type=submit]:hover {
-       background-color: #05a;
+       background-color: var(--btn-h-bg);
 }
 
 input[type=submit]:active {
-       background-color: #048;
+       background-color: var(--btn-a-bg);
 }
 
 input[type=submit].evil {
-       background-color: #c33;
+       background-color: var(--evil-btn-bg);
        border: none;
        border-radius: 0.3em;
-       color: #fcc;
+       color: var(--evil-btn-fg);
        cursor: pointer;
        display: block;
 
@@ -357,19 +601,20 @@ input[type=submit].evil {
 }
 
 input[type=submit].evil:hover {
-       background-color: #a22;
+       background-color: var(--evil-btn-h-bg);
 }
 
 input[type=submit].evil:active {
-       background-color: #811;
+       background-color: var(--evil-btn-a-bg);
 }
 
 input[type=submit]:disabled,
 input[type=submit].evil:disabled {
-       background-color: #444;
+       background-color: var(--btn-na-bg);
+       color: var(--btn-na-fg);
        cursor: not-allowed;
 }
 
 iframe {
-       border-color: #036;
+       border-color: var(--iframe-border);
 }