diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 78e2435355596db24964423962bfc40166001a44..e7061f0c03f61d35491684a92410b7d3a0291b96 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,10 +1,18 @@
 image: node:latest
 
-pages:
-  script:
+default:
+  before_script:
     - apt-get update
     - apt-get -y install openjdk-11-jdk
     - npm install
+
+test:
+  script:
+    - npm install
+    - npm run test
+
+pages:
+  script:
     - npm run tailwind
     - npx shadow-cljs release main
 
diff --git a/package-lock.json b/package-lock.json
index 9cacf7115114e5d755d9dcc1e543bf98eeb82d24..aaf8887c48ebb39e3e10b8797d2667287235a705 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -5,27 +5,27 @@
   "requires": true,
   "dependencies": {
     "@babel/code-frame": {
-      "version": "7.14.5",
-      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz",
-      "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==",
+      "version": "7.16.7",
+      "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz",
+      "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==",
       "dev": true,
       "requires": {
-        "@babel/highlight": "^7.14.5"
+        "@babel/highlight": "^7.16.7"
       }
     },
     "@babel/helper-validator-identifier": {
-      "version": "7.15.7",
-      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz",
-      "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==",
+      "version": "7.16.7",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz",
+      "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==",
       "dev": true
     },
     "@babel/highlight": {
-      "version": "7.14.5",
-      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz",
-      "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==",
+      "version": "7.16.7",
+      "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz",
+      "integrity": "sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw==",
       "dev": true,
       "requires": {
-        "@babel/helper-validator-identifier": "^7.14.5",
+        "@babel/helper-validator-identifier": "^7.16.7",
         "chalk": "^2.0.0",
         "js-tokens": "^4.0.0"
       },
@@ -204,25 +204,19 @@
       }
     },
     "autoprefixer": {
-      "version": "10.3.6",
-      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.6.tgz",
-      "integrity": "sha512-3bDjTfF0MfZntwVCSd18XAT2Zndufh3Mep+mafbzdIQEeWbncVRUVDjH8/EPANV9Hq40seJ24QcYAyhUsFz7gQ==",
+      "version": "10.4.2",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz",
+      "integrity": "sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ==",
       "dev": true,
       "requires": {
-        "browserslist": "^4.17.1",
-        "caniuse-lite": "^1.0.30001260",
-        "fraction.js": "^4.1.1",
-        "nanocolors": "^0.2.8",
+        "browserslist": "^4.19.1",
+        "caniuse-lite": "^1.0.30001297",
+        "fraction.js": "^4.1.2",
         "normalize-range": "^0.1.2",
-        "postcss-value-parser": "^4.1.0"
+        "picocolors": "^1.0.0",
+        "postcss-value-parser": "^4.2.0"
       }
     },
-    "balanced-match": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
-      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
-      "dev": true
-    },
     "base64-js": {
       "version": "1.5.1",
       "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -241,16 +235,6 @@
       "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==",
       "dev": true
     },
-    "brace-expansion": {
-      "version": "1.1.11",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
-      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
-      "dev": true,
-      "requires": {
-        "balanced-match": "^1.0.0",
-        "concat-map": "0.0.1"
-      }
-    },
     "braces": {
       "version": "3.0.2",
       "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
@@ -359,16 +343,16 @@
       }
     },
     "browserslist": {
-      "version": "4.17.2",
-      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.17.2.tgz",
-      "integrity": "sha512-jSDZyqJmkKMEMi7SZAgX5UltFdR5NAO43vY0AwTpu4X3sGH7GLLQ83KiUomgrnvZRCeW0yPPnKqnxPqQOER9zQ==",
+      "version": "4.19.1",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz",
+      "integrity": "sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A==",
       "dev": true,
       "requires": {
-        "caniuse-lite": "^1.0.30001261",
-        "electron-to-chromium": "^1.3.854",
+        "caniuse-lite": "^1.0.30001286",
+        "electron-to-chromium": "^1.4.17",
         "escalade": "^3.1.1",
-        "nanocolors": "^0.2.12",
-        "node-releases": "^1.1.76"
+        "node-releases": "^2.0.1",
+        "picocolors": "^1.0.0"
       }
     },
     "buffer": {
@@ -394,12 +378,6 @@
       "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=",
       "dev": true
     },
-    "bytes": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
-      "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
-      "dev": true
-    },
     "callsites": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -413,9 +391,9 @@
       "dev": true
     },
     "caniuse-lite": {
-      "version": "1.0.30001264",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001264.tgz",
-      "integrity": "sha512-Ftfqqfcs/ePiUmyaySsQ4PUsdcYyXG2rfoBVsk3iY1ahHaJEw65vfb7Suzqm+cEkwwPIv/XWkg27iCpRavH4zA==",
+      "version": "1.0.30001299",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001299.tgz",
+      "integrity": "sha512-iujN4+x7QzqA2NCSrS5VUy+4gLmRd4xv6vbBBsmfVqTx8bLAD8097euLqQgKxSVLvxjSDcvF1T/i9ocgnUFexw==",
       "dev": true
     },
     "chalk": {
@@ -465,16 +443,6 @@
         "safe-buffer": "^5.0.1"
       }
     },
-    "color": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/color/-/color-4.0.1.tgz",
-      "integrity": "sha512-rpZjOKN5O7naJxkH2Rx1sZzzBgaiWECc6BYXjeCE6kF0kcASJYbUq02u7JqIHwCb/j3NhV+QhRL2683aICeGZA==",
-      "dev": true,
-      "requires": {
-        "color-convert": "^2.0.1",
-        "color-string": "^1.6.0"
-      }
-    },
     "color-convert": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -490,28 +458,6 @@
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "dev": true
     },
-    "color-string": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.6.0.tgz",
-      "integrity": "sha512-c/hGS+kRWJutUBEngKKmk4iH3sD59MBkoxVapS/0wgpCz2u7XsNloxknyvBhzwEs1IbV36D9PwqLPJ2DTu3vMA==",
-      "dev": true,
-      "requires": {
-        "color-name": "^1.0.0",
-        "simple-swizzle": "^0.2.2"
-      }
-    },
-    "commander": {
-      "version": "6.2.1",
-      "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
-      "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
-      "dev": true
-    },
-    "concat-map": {
-      "version": "0.0.1",
-      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
-      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
-      "dev": true
-    },
     "console-browserify": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
@@ -607,18 +553,6 @@
         "randomfill": "^1.0.3"
       }
     },
-    "css-color-names": {
-      "version": "0.0.4",
-      "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
-      "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=",
-      "dev": true
-    },
-    "css-unit-converter": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/css-unit-converter/-/css-unit-converter-1.1.2.tgz",
-      "integrity": "sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==",
-      "dev": true
-    },
     "cssesc": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -690,9 +624,9 @@
       "dev": true
     },
     "electron-to-chromium": {
-      "version": "1.3.857",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.857.tgz",
-      "integrity": "sha512-a5kIr2lajm4bJ5E4D3fp8Y/BRB0Dx2VOcCRE5Gtb679mXIME/OFhWler8Gy2ksrf8gFX+EFCSIGA33FB3gqYpg==",
+      "version": "1.4.46",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.46.tgz",
+      "integrity": "sha512-UtV0xUA/dibCKKP2JMxOpDtXR74zABevuUEH4K0tvduFSIoxRVcYmQsbB51kXsFTX8MmOyWMt8tuZAlmDOqkrQ==",
       "dev": true
     },
     "elliptic": {
@@ -731,14 +665,6 @@
       "dev": true,
       "requires": {
         "is-arrayish": "^0.2.1"
-      },
-      "dependencies": {
-        "is-arrayish": {
-          "version": "0.2.1",
-          "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-          "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
-          "dev": true
-        }
       }
     },
     "escalade": {
@@ -770,9 +696,9 @@
       }
     },
     "fast-glob": {
-      "version": "3.2.7",
-      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz",
-      "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==",
+      "version": "3.2.11",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz",
+      "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==",
       "dev": true,
       "requires": {
         "@nodelib/fs.stat": "^2.0.2",
@@ -812,26 +738,9 @@
       }
     },
     "fraction.js": {
-      "version": "4.1.1",
-      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz",
-      "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==",
-      "dev": true
-    },
-    "fs-extra": {
-      "version": "10.0.0",
-      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz",
-      "integrity": "sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.2.0",
-        "jsonfile": "^6.0.1",
-        "universalify": "^2.0.0"
-      }
-    },
-    "fs.realpath": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
-      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.2.tgz",
+      "integrity": "sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA==",
       "dev": true
     },
     "fsevents": {
@@ -847,20 +756,6 @@
       "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
       "dev": true
     },
-    "glob": {
-      "version": "7.2.0",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
-      "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
-      "dev": true,
-      "requires": {
-        "fs.realpath": "^1.0.0",
-        "inflight": "^1.0.4",
-        "inherits": "2",
-        "minimatch": "^3.0.4",
-        "once": "^1.3.0",
-        "path-is-absolute": "^1.0.0"
-      }
-    },
     "glob-parent": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -870,12 +765,6 @@
         "is-glob": "^4.0.3"
       }
     },
-    "graceful-fs": {
-      "version": "4.2.8",
-      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz",
-      "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==",
-      "dev": true
-    },
     "has": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -939,12 +828,6 @@
         }
       }
     },
-    "hex-color-regex": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
-      "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==",
-      "dev": true
-    },
     "hmac-drbg": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@@ -956,24 +839,6 @@
         "minimalistic-crypto-utils": "^1.0.1"
       }
     },
-    "hsl-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz",
-      "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=",
-      "dev": true
-    },
-    "hsla-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz",
-      "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=",
-      "dev": true
-    },
-    "html-tags": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz",
-      "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==",
-      "dev": true
-    },
     "https-browserify": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
@@ -986,15 +851,6 @@
       "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
       "dev": true
     },
-    "import-cwd": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
-      "integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==",
-      "dev": true,
-      "requires": {
-        "import-from": "^3.0.0"
-      }
-    },
     "import-fresh": {
       "version": "3.3.0",
       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -1005,33 +861,6 @@
         "resolve-from": "^4.0.0"
       }
     },
-    "import-from": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz",
-      "integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==",
-      "dev": true,
-      "requires": {
-        "resolve-from": "^5.0.0"
-      },
-      "dependencies": {
-        "resolve-from": {
-          "version": "5.0.0",
-          "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
-          "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
-          "dev": true
-        }
-      }
-    },
-    "inflight": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
-      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
-      "dev": true,
-      "requires": {
-        "once": "^1.3.0",
-        "wrappy": "1"
-      }
-    },
     "inherits": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
@@ -1039,9 +868,9 @@
       "dev": true
     },
     "is-arrayish": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
-      "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+      "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
       "dev": true
     },
     "is-binary-path": {
@@ -1053,24 +882,10 @@
         "binary-extensions": "^2.0.0"
       }
     },
-    "is-color-stop": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz",
-      "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=",
-      "dev": true,
-      "requires": {
-        "css-color-names": "^0.0.4",
-        "hex-color-regex": "^1.1.0",
-        "hsl-regex": "^1.0.0",
-        "hsla-regex": "^1.0.0",
-        "rgb-regex": "^1.0.1",
-        "rgba-regex": "^1.0.0"
-      }
-    },
     "is-core-module": {
-      "version": "2.7.0",
-      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.7.0.tgz",
-      "integrity": "sha512-ByY+tjCciCr+9nLryBYcSD50EOGWt95c7tIsKTG1J2ixKKXPvF7Ej3AVd+UfDydAJom3biBGDBALaO79ktwgEQ==",
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz",
+      "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==",
       "dev": true,
       "requires": {
         "has": "^1.0.3"
@@ -1120,38 +935,16 @@
       "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
       "dev": true
     },
-    "jsonfile": {
-      "version": "6.1.0",
-      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
-      "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
-      "dev": true,
-      "requires": {
-        "graceful-fs": "^4.1.6",
-        "universalify": "^2.0.0"
-      }
-    },
     "lilconfig": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz",
-      "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==",
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz",
+      "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==",
       "dev": true
     },
     "lines-and-columns": {
-      "version": "1.1.6",
-      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz",
-      "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
-      "dev": true
-    },
-    "lodash": {
-      "version": "4.17.21",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
-      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
-      "dev": true
-    },
-    "lodash.topath": {
-      "version": "4.5.2",
-      "resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
-      "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=",
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+      "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
       "dev": true
     },
     "loose-envify": {
@@ -1219,48 +1012,18 @@
       "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=",
       "dev": true
     },
-    "minimatch": {
-      "version": "3.0.4",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
-      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
-      "dev": true,
-      "requires": {
-        "brace-expansion": "^1.1.7"
-      }
-    },
     "minimist": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
       "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
       "dev": true
     },
-    "modern-normalize": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/modern-normalize/-/modern-normalize-1.1.0.tgz",
-      "integrity": "sha512-2lMlY1Yc1+CUy0gw4H95uNN7vjbpoED7NNRSBHE25nWfLBdmMzFCsPshlzbxHz+gYMcBEUN8V4pU16prcdPSgA==",
-      "dev": true
-    },
-    "nanocolors": {
-      "version": "0.2.12",
-      "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.12.tgz",
-      "integrity": "sha512-SFNdALvzW+rVlzqexid6epYdt8H9Zol7xDoQarioEFcFN0JHo4CYNztAxmtfgGTVRCmFlEOqqhBpoFGKqSAMug==",
-      "dev": true
-    },
     "nanoid": {
-      "version": "3.1.28",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.28.tgz",
-      "integrity": "sha512-gSu9VZ2HtmoKYe/lmyPFES5nknFrHa+/DT9muUFWFMi6Jh9E1I7bkvlQ8xxf1Kos9pi9o8lBnIOkatMhKX/YUw==",
+      "version": "3.1.32",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.32.tgz",
+      "integrity": "sha512-F8mf7R3iT9bvThBoW4tGXhXFHCctyCiUUPrWF8WaTqa3h96d9QybkSeba43XVOOE3oiLfkVDe4bT8MeGmkrTxw==",
       "dev": true
     },
-    "node-emoji": {
-      "version": "1.11.0",
-      "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz",
-      "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==",
-      "dev": true,
-      "requires": {
-        "lodash": "^4.17.21"
-      }
-    },
     "node-libs-browser": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz",
@@ -1293,9 +1056,9 @@
       }
     },
     "node-releases": {
-      "version": "1.1.77",
-      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz",
-      "integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==",
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz",
+      "integrity": "sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA==",
       "dev": true
     },
     "normalize-path": {
@@ -1321,15 +1084,6 @@
       "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
       "dev": true
     },
-    "once": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
-      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
-      "dev": true,
-      "requires": {
-        "wrappy": "1"
-      }
-    },
     "os-browserify": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
@@ -1382,12 +1136,6 @@
       "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==",
       "dev": true
     },
-    "path-is-absolute": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
-      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
-      "dev": true
-    },
     "path-parse": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -1418,41 +1166,45 @@
       "resolved": "https://registry.npmjs.org/phosphor-react/-/phosphor-react-1.4.0.tgz",
       "integrity": "sha512-K2/ZtkigHV/li5qfYPqY5fA7MLkWM52tW9ghBCyJc7JS5ykI/WD61JC8kFWaZ6yqlEHXPUJLHr+p+5uGsBnHmQ=="
     },
+    "picocolors": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+      "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+      "dev": true
+    },
     "picomatch": {
-      "version": "2.3.0",
-      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
-      "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==",
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
       "dev": true
     },
     "postcss": {
-      "version": "8.3.8",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.8.tgz",
-      "integrity": "sha512-GT5bTjjZnwDifajzczOC+r3FI3Cu+PgPvrsjhQdRqa2kTJ4968/X9CUce9xttIB0xOs5c6xf0TCWZo/y9lF6bA==",
+      "version": "8.4.5",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz",
+      "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==",
       "dev": true,
       "requires": {
-        "nanocolors": "^0.2.2",
-        "nanoid": "^3.1.25",
-        "source-map-js": "^0.6.2"
+        "nanoid": "^3.1.30",
+        "picocolors": "^1.0.0",
+        "source-map-js": "^1.0.1"
       }
     },
     "postcss-js": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-3.0.3.tgz",
-      "integrity": "sha512-gWnoWQXKFw65Hk/mi2+WTQTHdPD5UJdDXZmX073EY/B3BWnYjO4F4t0VneTCnCGQ5E5GsCdMkzPaTXwl3r5dJw==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz",
+      "integrity": "sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ==",
       "dev": true,
       "requires": {
-        "camelcase-css": "^2.0.1",
-        "postcss": "^8.1.6"
+        "camelcase-css": "^2.0.1"
       }
     },
     "postcss-load-config": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.0.tgz",
-      "integrity": "sha512-ipM8Ds01ZUophjDTQYSVP70slFSYg3T0/zyfII5vzhN6V57YSxMgG5syXuwi5VtS8wSf3iL30v0uBdoIVx4Q0g==",
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.1.tgz",
+      "integrity": "sha512-c/9XYboIbSEUZpiD1UQD0IKiUe8n9WHYV7YFe7X7J+ZwCsEKkUJSFWjS9hBU1RR9THR7jMXst8sxiqP0jjo2mg==",
       "dev": true,
       "requires": {
-        "import-cwd": "^3.0.0",
-        "lilconfig": "^2.0.3",
+        "lilconfig": "^2.0.4",
         "yaml": "^1.10.2"
       }
     },
@@ -1466,9 +1218,9 @@
       }
     },
     "postcss-selector-parser": {
-      "version": "6.0.6",
-      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz",
-      "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==",
+      "version": "6.0.8",
+      "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.8.tgz",
+      "integrity": "sha512-D5PG53d209Z1Uhcc0qAZ5U3t5HagH3cxu+WLZ22jt3gLUpXM4eXXfiO14jiDWST3NNooX/E8wISfOhZ9eIjGTQ==",
       "dev": true,
       "requires": {
         "cssesc": "^3.0.0",
@@ -1476,15 +1228,9 @@
       }
     },
     "postcss-value-parser": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
-      "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
-      "dev": true
-    },
-    "pretty-hrtime": {
-      "version": "1.0.3",
-      "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
-      "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
       "dev": true
     },
     "process": {
@@ -1527,18 +1273,6 @@
       "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
       "dev": true
     },
-    "purgecss": {
-      "version": "4.0.3",
-      "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-4.0.3.tgz",
-      "integrity": "sha512-PYOIn5ibRIP34PBU9zohUcCI09c7drPJJtTDAc0Q6QlRz2/CHQ8ywGLdE7ZhxU2VTqB7p5wkvj5Qcm05Rz3Jmw==",
-      "dev": true,
-      "requires": {
-        "commander": "^6.0.0",
-        "glob": "^7.0.0",
-        "postcss": "^8.2.1",
-        "postcss-selector-parser": "^6.0.2"
-      }
-    },
     "querystring": {
       "version": "0.2.0",
       "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
@@ -1654,32 +1388,15 @@
       "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==",
       "dev": true
     },
-    "reduce-css-calc": {
-      "version": "2.1.8",
-      "resolved": "https://registry.npmjs.org/reduce-css-calc/-/reduce-css-calc-2.1.8.tgz",
-      "integrity": "sha512-8liAVezDmUcH+tdzoEGrhfbGcP7nOV4NkGE3a74+qqvE7nt9i4sKLGBuZNOnpI4WiGksiNPklZxva80061QiPg==",
-      "dev": true,
-      "requires": {
-        "css-unit-converter": "^1.1.1",
-        "postcss-value-parser": "^3.3.0"
-      },
-      "dependencies": {
-        "postcss-value-parser": {
-          "version": "3.3.1",
-          "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
-          "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==",
-          "dev": true
-        }
-      }
-    },
     "resolve": {
-      "version": "1.20.0",
-      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
-      "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
+      "version": "1.21.0",
+      "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz",
+      "integrity": "sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA==",
       "dev": true,
       "requires": {
-        "is-core-module": "^2.2.0",
-        "path-parse": "^1.0.6"
+        "is-core-module": "^2.8.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
       }
     },
     "resolve-from": {
@@ -1694,27 +1411,6 @@
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
       "dev": true
     },
-    "rgb-regex": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz",
-      "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=",
-      "dev": true
-    },
-    "rgba-regex": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz",
-      "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=",
-      "dev": true
-    },
-    "rimraf": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
-      "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
-      "dev": true,
-      "requires": {
-        "glob": "^7.1.3"
-      }
-    },
     "ripemd160": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
@@ -1791,15 +1487,6 @@
       "integrity": "sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==",
       "dev": true
     },
-    "simple-swizzle": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
-      "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=",
-      "dev": true,
-      "requires": {
-        "is-arrayish": "^0.3.1"
-      }
-    },
     "source-map": {
       "version": "0.5.7",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -1807,9 +1494,9 @@
       "dev": true
     },
     "source-map-js": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz",
-      "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==",
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.1.tgz",
+      "integrity": "sha512-4+TN2b3tqOCd/kaGRJ/sTYA0tR0mdXx26ipdolxcwtJVqEnqNYvlCAt1q3ypy4QMlYus+Zh34RNtYLoq2oQ4IA==",
       "dev": true
     },
     "source-map-support": {
@@ -1862,44 +1549,38 @@
         "has-flag": "^4.0.0"
       }
     },
+    "supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+      "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+      "dev": true
+    },
     "tailwindcss": {
-      "version": "2.2.16",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.16.tgz",
-      "integrity": "sha512-EireCtpQyyJ4Xz8NYzHafBoy4baCOO96flM0+HgtsFcIQ9KFy/YBK3GEtlnD+rXen0e4xm8t3WiUcKBJmN6yjg==",
+      "version": "3.0.15",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.15.tgz",
+      "integrity": "sha512-bT2iy7FtjwgsXik4ZoJnHXR+SRCiGR1W95fVqpLZebr64m4ahwUwRbIAc5w5+2fzr1YF4Ct2eI7dojMRRl8sVQ==",
       "dev": true,
       "requires": {
         "arg": "^5.0.1",
-        "bytes": "^3.0.0",
         "chalk": "^4.1.2",
         "chokidar": "^3.5.2",
-        "color": "^4.0.1",
+        "color-name": "^1.1.4",
         "cosmiconfig": "^7.0.1",
         "detective": "^5.2.0",
         "didyoumean": "^1.2.2",
         "dlv": "^1.1.3",
         "fast-glob": "^3.2.7",
-        "fs-extra": "^10.0.0",
-        "glob-parent": "^6.0.1",
-        "html-tags": "^3.1.0",
-        "is-color-stop": "^1.1.0",
-        "is-glob": "^4.0.1",
-        "lodash": "^4.17.21",
-        "lodash.topath": "^4.5.2",
-        "modern-normalize": "^1.1.0",
-        "node-emoji": "^1.11.0",
+        "glob-parent": "^6.0.2",
+        "is-glob": "^4.0.3",
         "normalize-path": "^3.0.0",
         "object-hash": "^2.2.0",
-        "postcss-js": "^3.0.3",
+        "postcss-js": "^4.0.0",
         "postcss-load-config": "^3.1.0",
         "postcss-nested": "5.0.6",
-        "postcss-selector-parser": "^6.0.6",
-        "postcss-value-parser": "^4.1.0",
-        "pretty-hrtime": "^1.0.3",
-        "purgecss": "^4.0.3",
+        "postcss-selector-parser": "^6.0.8",
+        "postcss-value-parser": "^4.2.0",
         "quick-lru": "^5.1.1",
-        "reduce-css-calc": "^2.1.8",
-        "resolve": "^1.20.0",
-        "tmp": "^0.2.1"
+        "resolve": "^1.21.0"
       }
     },
     "timers-browserify": {
@@ -1911,15 +1592,6 @@
         "setimmediate": "^1.0.4"
       }
     },
-    "tmp": {
-      "version": "0.2.1",
-      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
-      "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
-      "dev": true,
-      "requires": {
-        "rimraf": "^3.0.0"
-      }
-    },
     "to-arraybuffer": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
@@ -1941,12 +1613,6 @@
       "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=",
       "dev": true
     },
-    "universalify": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
-      "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
-      "dev": true
-    },
     "url": {
       "version": "0.11.0",
       "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
@@ -2003,12 +1669,6 @@
         "isexe": "^2.0.0"
       }
     },
-    "wrappy": {
-      "version": "1.0.2",
-      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
-      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
-      "dev": true
-    },
     "ws": {
       "version": "7.5.5",
       "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
diff --git a/package.json b/package.json
index e6d19148e52bd77f8d227b067edaccc2539d0c9e..5cbc77ec4e5b118141a5e9cee53cb74228c1213b 100644
--- a/package.json
+++ b/package.json
@@ -12,10 +12,10 @@
   "author": "",
   "license": "ISC",
   "devDependencies": {
-    "autoprefixer": "^10.3.6",
-    "postcss": "^8.3.8",
+    "autoprefixer": "^10.4.2",
+    "postcss": "^8.4.5",
     "shadow-cljs": "^2.15.10",
-    "tailwindcss": "^2.2.16"
+    "tailwindcss": "^3.0.15"
   },
   "dependencies": {
     "phosphor-react": "^1.4.0",
diff --git a/scripts/lint b/scripts/lint
new file mode 100755
index 0000000000000000000000000000000000000000..e4b3ac7e3fc6d3b2026a61ca8e90851440bf5b68
--- /dev/null
+++ b/scripts/lint
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+set -euxo pipefail
+
+DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
+clj-kondo --lint "$DIR/../src"
diff --git a/shadow-cljs.edn b/shadow-cljs.edn
index 162ebe6f2bb95240401ff45c3d61029d3904c541..9e88bfd25083818ed2cc8a902d1b9cbaca6b5403 100644
--- a/shadow-cljs.edn
+++ b/shadow-cljs.edn
@@ -2,7 +2,8 @@
 
  :dependencies
  [[reagent "1.1.0"]
-  [re-frame "1.2.0"]]
+  [re-frame "1.2.0"]
+  [metosin/malli "0.7.5"]]
 
  :dev-http {8080 "public"}
 
diff --git a/src/star_vibe_lab/components.cljs b/src/star_vibe_lab/components.cljs
index f49a2f5654c263ff225aebe4c572f5f6f411c981..abf77d0b9016507d913eadde0575ceef6ec76118 100644
--- a/src/star_vibe_lab/components.cljs
+++ b/src/star_vibe_lab/components.cljs
@@ -38,9 +38,8 @@
 (defn- key-component [white? w pressed?-atom]
   (let [pressed? @pressed?-atom]
     [:div
-     {:class [(if pressed? "bg-red-500" "bg-white")
+     {:class [(if pressed? "bg-red-500" (if white? "bg-white" "bg-gray-900"))
               (when-not pressed? "hover:bg-black")
-              (when-not white? "bg-gray-900")
               "border-r-2"
               "border-b-2"
               "border-gray-800"]
@@ -156,7 +155,7 @@
   "Display `main-component` with a button that slides in `side-component`.
 
   The button is located top right and the component slides right-to-left."
-  [main-component side-component side-height]
+  [_main-component _side-component _side-height]
   (let [side-open? (r/atom false)]
     (fn [main-component side-component side-height]
       [:div {:class ["flex"]}
diff --git a/src/star_vibe_lab/core.cljs b/src/star_vibe_lab/core.cljs
index ceec73a001f5f31848f3518aea7154ec42ab6b03..342dfcf0e7a5bca432aa42f9877f8fbfb2ec068e 100644
--- a/src/star_vibe_lab/core.cljs
+++ b/src/star_vibe_lab/core.cljs
@@ -1,178 +1,23 @@
 (ns star-vibe-lab.core
   (:require
-   [clojure.set :as set]
-   [reagent.core :as r]
    [reagent.dom :as rdom]
    [re-frame.core :as rf]
    [star-vibe-lab.audio :as audio]
-   [star-vibe-lab.info :as info]
    [star-vibe-lab.components :as components]
-   [star-vibe-lab.util :as util]))
+   [star-vibe-lab.re-frame.core]))
 
-;;; Misc
-(def kbd->semitone (zipmap ["a" "w" "s" "e" "d" "f" "t" "g" "y" "h" "u" "j"]
-                           (range)))
-
-(defn midi-note->frequency [note]
-  (* 440 (js/Math.pow 2 (/ (- note 69) 12))))
-
-(defn kbd->midi-note [kbd]
-  "Return corresponding midi note number from kbd.
-
-If `kbd` not `kbd->semitone` return nil."
-  (when-let [semitone (get kbd->semitone kbd)]
-    (+ semitone @(rf/subscribe [:virtual-keyboard/first-note]))))
-
-;;; rf fxs
-(rf/reg-fx
- :attack-sound
- (fn [frequency]
-   (audio/attack-sound! frequency)))
-
-(rf/reg-fx
- :release-sound
- (fn [frequency]
-   (audio/release-sound! frequency)))
-
-;;; rf queries
-(rf/reg-sub
- :pressed-notes
- (fn [db _]
-   (:pressed-notes db)))
-
-(rf/reg-sub
- :current-chord
- :<- [:pressed-notes]
- (fn [pressed-notes _]
-   (if (>= (count pressed-notes) 3)
-     (some-> (get (info/generate-chord-recognition-map)
-                  (info/normalize-chord (sort (seq pressed-notes))))
-             (info/format-chord-info)))))
-
-(rf/reg-sub
- :virtual-keyboard/first-note
- (fn [db _]
-   (get-in db [:virtual-keyboard :first-note])))
-
-(rf/reg-sub
- :virtual-keyboard/note-down?
- :<- [:pressed-notes]
- :<- [:virtual-keyboard/first-note]
- (fn [[pressed-notes first-note] [_ semitone]]
-   (contains? pressed-notes (+ semitone first-note))))
-
-(rf/reg-sub
- :virtual-keyboard/mouse-down-note
- (fn [db _]
-   (get-in db [:virtual-keyboard :mouse-down-note])))
-
-(rf/reg-sub
- :chord-info/chord
- (fn [db _]
-   (get-in db [:chord-info :chord])))
-
-(rf/reg-sub
- :chord-info/key
- (fn [db _]
-   (get-in db [:chord-info :key])))
-
-(rf/reg-sub
- :chord-info/key-active?
- :<- [:chord-info/key]
- :<- [:chord-info/chord]
- (fn [[key chord] [_ nth-key]]
-   (->> chord
-        (get info/chords)
-        (map (partial + (util/note-to-semitone key))) ;; Offset the chord
-        set
-        (#(boolean (% nth-key))))))
-
-;;; rf event handlers
-(rf/reg-event-db
- :initialize
- (fn [_ _]
-   {:pressed-notes #{}
-
-    ;; On-screen keyboard state
-    :virtual-keyboard
-    {;; The midi note to which corresponds the first key, C4 in this case
-     :first-note 60
-     ;; Current note that is currently held with mouse
-     :mouse-down-note nil}
-
-    ;; Options related to the chord info component
-    :chord-info
-    {:chord :diminished-seventh
-     :key "C"}}))
-
-(rf/reg-event-db
- :chord-info/change-chord
- (fn [db [_ chord]]
-   (assoc-in db [:chord-info :chord] chord)))
-
-(rf/reg-event-db
- :chord-info/change-key
- (fn [db [_ key]]
-   (assoc-in db [:chord-info :key] key)))
-
-(rf/reg-event-fx
- :keydown
- (fn [cofx [_ kbd]]
-   (when-let [note (kbd->midi-note kbd)]
-     {:db (:db cofx)
-      :dispatch [:attack-note note]})))
-
-(rf/reg-event-fx
- :keyup
- (fn [cofx [_ kbd]]
-   (when-let [note (kbd->midi-note kbd)]
-     {:db (:db cofx)
-      :dispatch [:release-note note]})))
-
-;;; `:mouse-attack-note` and `:mouse-release-note` exist for better mouse UX in
-;;; virtual keyboard (e.g. to be able to play by sliding held mouse)
-(rf/reg-event-fx
- :mouse-attack-note
- (fn [cofx [_ note]]
-   {:db (assoc-in (:db cofx) [:virtual-keyboard :mouse-down-note] note)
-    :fx [(when-let [attacked-note
-                    (get-in cofx [:db :virtual-keyboard :mouse-down-note])]
-           [:dispatch [:release-note attacked-note]])
-         [:dispatch [:attack-note note]]]}))
-
-(rf/reg-event-fx
- :mouse-release-note
- (fn [cofx [_ note]]
-   {:db (assoc-in (:db cofx) [:virtual-keyboard :mouse-down-note] nil)
-    :dispatch [:release-note note]}))
-
-(rf/reg-event-fx
- :attack-note
- (fn [cofx [_ note]]
-   {:db (update (:db cofx) :pressed-notes #(conj % note))
-    :attack-sound (midi-note->frequency note)}))
-
-(rf/reg-event-fx
- :release-note
- (fn [cofx [_ note]]
-   {:db (update (:db cofx) :pressed-notes #(disj % note))
-    :release-sound (midi-note->frequency note)}))
-
-;;; Main
 (defn render []
   (rdom/render [components/root-component] (js/document.getElementById "app")))
 
-(defn run-reloadable []
-  (render)
-  (audio/reload!))
-
 (defn ^:dev/after-load reload! []
   (rf/clear-subscription-cache!)
-  (run-reloadable))
+  (render)
+  (audio/reload!))
 
 (defn init []
+  (star-vibe-lab.re-frame.core/init!)
   (rf/dispatch-sync [:initialize])
-  (run-reloadable)
+  (reload!)
   (js/window.addEventListener
    "keydown"
    (fn [obj]
diff --git a/src/star_vibe_lab/info.cljs b/src/star_vibe_lab/info.cljs
index 99c8659cb5e53e41910cea4e8bd99e4ce08f6708..09460d249953557abf43a1e946a67ab1939861c3 100644
--- a/src/star_vibe_lab/info.cljs
+++ b/src/star_vibe_lab/info.cljs
@@ -9,7 +9,7 @@
 
 (s/def ::chord (s/and vector? #(>= (count %) 3)))
 (s/def ::inversion nat-int?)
-(s/def ::chord-info (s/keys ::chord ::inversion))
+(s/def ::chord-info (s/keys :req [::chord ::inversion]))
 
 (def chords
   "Chords defined relative to semitone."
@@ -42,7 +42,7 @@
            (loop [i 0]
              (if (> (nth chord i) (nth chord (inc i)))
                (inc i)
-               (if (< (inc i) (- (count chord) 1))
+               (when (< (inc i) (- (count chord) 1))
                  (recur (inc i)))))]
     (inflate-chord
      (vec (map-indexed (fn [i val]
diff --git a/src/star_vibe_lab/re_frame/core.cljs b/src/star_vibe_lab/re_frame/core.cljs
new file mode 100644
index 0000000000000000000000000000000000000000..bbb3728eae335acdf79f878466cd9049683dff58
--- /dev/null
+++ b/src/star_vibe_lab/re_frame/core.cljs
@@ -0,0 +1,14 @@
+(ns star-vibe-lab.re-frame.core
+  "Main namespace of `star-vibe-lab.re-frame.*`.
+
+  Outer namespaces should only use this namespace in `star-vibe-lab.re-frame`."
+  (:require
+   [re-frame.core :as rf]
+   [star-vibe-lab.re-frame.fxs :as fxs]
+   [star-vibe-lab.re-frame.subscriptions :as subscriptions]))
+
+(defn init!
+  []
+  (rf/clear-global-interceptor)
+  (fxs/init!)
+  (subscriptions/init!))
diff --git a/src/star_vibe_lab/re_frame/db.cljs b/src/star_vibe_lab/re_frame/db.cljs
new file mode 100644
index 0000000000000000000000000000000000000000..89f726649e4a95b0fa85645f83c9793c6c7b6efb
--- /dev/null
+++ b/src/star_vibe_lab/re_frame/db.cljs
@@ -0,0 +1,44 @@
+(ns star-vibe-lab.re-frame.db
+  (:require
+   [malli.core :as m]
+   [re-frame.core :as rf]))
+
+(def initial-state
+  {:pressed-notes #{}
+
+   ;; On-screen keyboard state
+   :virtual-keyboard
+   {;; The midi note to which corresponds the first key, C4 in this case
+    :first-note 60
+    ;; Current note that is currently held with mouse
+    :mouse-down-note nil}
+
+   ;; Options related to the chord info component
+   :chord-info
+   {:chord :diminished-seventh
+    :key "C"}})
+
+(def schema
+  [:map {:registry {::midi-note pos-int?}}
+   [:pressed-notes
+    [:set ::midi-note]]
+   [:virtual-keyboard
+    [:map
+     [:first-note ::midi-note]
+     [:mouse-down-note [:maybe ::midi-note]]]]
+   [:chord-info
+    [:map
+     [:chord :keyword]
+     [:key :string]]]])
+
+(def spec-interceptor
+  (rf/->interceptor
+   :id :check-spec
+   :after (fn [context]
+             context
+             (let [db (-> context :effects :db)]
+               (if-not (m/validate schema db)
+                 (assoc-in context [:effects :log]
+                           (->> (m/explain schema db)
+                                (str "Schema failed: ")))
+                 context)))))
diff --git a/src/star_vibe_lab/re_frame/fxs.cljs b/src/star_vibe_lab/re_frame/fxs.cljs
new file mode 100644
index 0000000000000000000000000000000000000000..ab101d4399cb7ad6d1108b5edc1ed90a8298a871
--- /dev/null
+++ b/src/star_vibe_lab/re_frame/fxs.cljs
@@ -0,0 +1,82 @@
+(ns star-vibe-lab.re-frame.fxs
+  (:require
+   [re-frame.core :as rf]
+   [star-vibe-lab.audio :as audio]
+   [star-vibe-lab.re-frame.db :as db]
+   [star-vibe-lab.util :as util]))
+
+(defn init!
+  []
+  (rf/reg-event-db
+   :initialize
+   (fn [_ _]
+     (rf/reg-global-interceptor db/spec-interceptor)
+     db/initial-state))
+
+  (rf/reg-event-db
+   :chord-info/change-chord
+   (fn [db [_ chord]]
+     (assoc-in db [:chord-info :chord] chord)))
+
+  (rf/reg-event-db
+   :chord-info/change-key
+   (fn [db [_ key]]
+     (assoc-in db [:chord-info :key] key)))
+
+  (rf/reg-event-fx
+   :keydown
+   (fn [cofx [_ kbd]]
+     (when-let [note @(rf/subscribe [:virtual-keyboard/kbd->midi-note kbd])]
+       {:db (:db cofx)
+        :dispatch [:attack-note note]})))
+
+  (rf/reg-event-fx
+   :keyup
+   (fn [cofx [_ kbd]]
+     (when-let [note @(rf/subscribe [:virtual-keyboard/kbd->midi-note kbd])]
+       {:db (:db cofx)
+        :dispatch [:release-note note]})))
+
+;;; `:mouse-attack-note` and `:mouse-release-note` exist for better mouse UX in
+;;; virtual keyboard (e.g. to be able to play by sliding held mouse)
+  (rf/reg-event-fx
+   :mouse-attack-note
+   (fn [cofx [_ note]]
+     {:db (assoc-in (:db cofx) [:virtual-keyboard :mouse-down-note] note)
+      :fx [(when-let [attacked-note
+                      (get-in cofx [:db :virtual-keyboard :mouse-down-note])]
+             [:dispatch [:release-note attacked-note]])
+           [:dispatch [:attack-note note]]]}))
+
+  (rf/reg-event-fx
+   :mouse-release-note
+   (fn [cofx [_ note]]
+     {:db (assoc-in (:db cofx) [:virtual-keyboard :mouse-down-note] nil)
+      :dispatch [:release-note note]}))
+
+  (rf/reg-event-fx
+   :attack-note
+   (fn [cofx [_ note]]
+     {:db (update (:db cofx) :pressed-notes #(conj % note))
+      :attack-sound (util/midi-note->frequency note)}))
+
+  (rf/reg-event-fx
+   :release-note
+   (fn [cofx [_ note]]
+     {:db (update (:db cofx) :pressed-notes #(disj % note))
+      :release-sound (util/midi-note->frequency note)}))
+
+  (rf/reg-fx
+   :attack-sound
+   (fn [frequency]
+     (audio/attack-sound! frequency)))
+
+  (rf/reg-fx
+   :release-sound
+   (fn [frequency]
+     (audio/release-sound! frequency)))
+
+  (rf/reg-fx
+   :log
+   (fn [msg]
+     (println (str "LOG: " msg)))))
diff --git a/src/star_vibe_lab/re_frame/subscriptions.cljs b/src/star_vibe_lab/re_frame/subscriptions.cljs
new file mode 100644
index 0000000000000000000000000000000000000000..3846c413d3f8611b2d7025d4a9d0aeef9b35edd6
--- /dev/null
+++ b/src/star_vibe_lab/re_frame/subscriptions.cljs
@@ -0,0 +1,66 @@
+(ns star-vibe-lab.re-frame.subscriptions
+  (:require
+   [re-frame.core :as rf]
+   [star-vibe-lab.info :as info]
+   [star-vibe-lab.util :as util]))
+
+(defn init!
+  []
+  (rf/reg-sub
+   :pressed-notes
+   (fn [db _]
+     (:pressed-notes db)))
+
+  (rf/reg-sub
+   :current-chord
+   :<- [:pressed-notes]
+   (fn [pressed-notes _]
+     (when (>= (count pressed-notes) 3)
+       (some-> (get (info/generate-chord-recognition-map)
+                    (info/normalize-chord (sort (seq pressed-notes))))
+               (info/format-chord-info)))))
+
+  (rf/reg-sub
+   :virtual-keyboard/first-note
+   (fn [db _]
+     (get-in db [:virtual-keyboard :first-note])))
+
+  (rf/reg-sub
+   :virtual-keyboard/note-down?
+   :<- [:pressed-notes]
+   :<- [:virtual-keyboard/first-note]
+   (fn [[pressed-notes first-note] [_ semitone]]
+     (contains? pressed-notes (+ semitone first-note))))
+
+  (rf/reg-sub
+   :virtual-keyboard/kbd->midi-note
+   :<- [:virtual-keyboard/first-note]
+   (fn [first-note [_ kbd]]
+     (when-let [semitone (get util/kbd->semitone kbd)]
+       (+ semitone first-note))))
+
+  (rf/reg-sub
+   :virtual-keyboard/mouse-down-note
+   (fn [db _]
+     (get-in db [:virtual-keyboard :mouse-down-note])))
+
+  (rf/reg-sub
+   :chord-info/chord
+   (fn [db _]
+     (get-in db [:chord-info :chord])))
+
+  (rf/reg-sub
+   :chord-info/key
+   (fn [db _]
+     (get-in db [:chord-info :key])))
+
+  (rf/reg-sub
+   :chord-info/key-active?
+   :<- [:chord-info/key]
+   :<- [:chord-info/chord]
+   (fn [[key chord] [_ nth-key]]
+     (->> chord
+          (get info/chords)
+          (map (partial + (util/note-to-semitone key))) ;; Offset the chord
+          set
+          (#(boolean (% nth-key)))))))
diff --git a/src/star_vibe_lab/util.cljs b/src/star_vibe_lab/util.cljs
index 49bed836268f5ecc4dd1a6ad5df0a1d43248518f..8f0a397cbe4aff1bec46c20dcd6c56491fac222a 100644
--- a/src/star_vibe_lab/util.cljs
+++ b/src/star_vibe_lab/util.cljs
@@ -5,14 +5,17 @@
    [goog.string :as gstring]
    [goog.string.format]))
 
-(defn with-key [component key]
+(defn with-key
+  [component key]
   (with-meta component {:key key}))
 
-(defn add-keys [& components]
+(defn add-keys
+  [& components]
   (map with-key components (range)))
 
-(defn wrap-attr [component m]
+(defn wrap-attr
   "Merge attributes (such as class and style) from `m` into component."
+  [component m]
   (when-not (and (vector? component)
                  (>= (count component) 1))
     (throw (js/Error. (str "Component " component " is malformed!"))))
@@ -35,12 +38,19 @@
   (when-not (s/valid? spec x)
     (throw (s/explain spec x))))
 
+(defn format
+  [fmt & args]
+  (apply gstring/format fmt args))
+
+(def kbd->semitone (zipmap ["a" "w" "s" "e" "d" "f" "t" "g" "y" "h" "u" "j"]
+                           (range)))
+
 (defn note-to-semitone
   [note]
   (-> ["C" "C#" "D" "D#" "E" "F" "F#" "G" "G#" "A" "A#" "B"]
       (zipmap (range))
       (get note)))
 
-(defn format
-  [fmt & args]
-  (apply gstring/format fmt args))
+(defn midi-note->frequency
+  [note]
+  (* 440 (js/Math.pow 2 (/ (- note 69) 12))))
diff --git a/tailwind.config.js b/tailwind.config.js
index af37146e0ae2caa50c4d3851e93efb6b73e0f3d4..b1d4ee9757d29d1c583311d7c18e33fe2ae8f729 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,13 +1,9 @@
 module.exports = {
-  purge: [
+  content: [
     'src/**/*.cljs'
   ],
-  darkMode: false, // or 'media' or 'class'
   theme: {
     extend: {},
   },
-  variants: {
-    extend: {},
-  },
   plugins: [],
 }